Obsah
1. Komunikace se sloupcovými databázemi z jazyka Go: Parquet soubory (dokončení)
2. Rychlost zápisu vs. rychlost čtení z Parquet souborů
3. Čtení ze sloupcové databáze po záznamech je pomalé!
5. Vliv velikosti bloku na rychlost čtení dat
7. Konstantní počet gorutin pro čtení a jejich vliv na rychlost zpracování Parquet souborů
8. Vyhodnocení výsledků – použití jedné resp. 100 gorutin při čtení
9. Odvození počtu gorutin od velikosti bloku
10. Vyhodnocení výsledků – odvození počtu gorutin od velikosti bloku
11. Čtení hodnot z vybraného sloupce
12. Použití indexu sloupce při čtení
13. Zjištění počtu aktivních a neaktivních uživatelů
14. Výsledky – čtení a zpracování dat z jednoho sloupce
15. Specifikace čteného sloupce jeho jménem (cestou)
16. Rychlost přečtení všech údajů z jediného sloupce
18. Pomocné skripty pro tvorbu grafů
19. Repositář s demonstračními příklady
1. Komunikace se sloupcovými databázemi z jazyka Go: Parquet soubory (dokončení)
V dnešním článku, který svým zaměřením přímo navazuje na článek předchozí, dokončíme popis práce s Parquet soubory v programovacím jazyku Go. Zaměříme se na dvě oblasti. Jednou z nich je rychlost zápisu a čtení z Parquet souborů, protože k tomuto formátu se většinou uchylujeme ve chvíli, kdy je zapotřebí zajistit velmi rychlý přístup k jednotlivým sloupcům (a pouhé tvrzení „sloupcové databáze jsou rychlé“ pochopitelně v praxi neobstojí a musí se dokázat, za jakých předpokladů platí). A právě čtení jednotlivých sloupců je druhým důležitým tématem, kterým se dnes budeme zabývat.
2. Rychlost zápisu vs. rychlost čtení z Parquet souborů
Na konci předchozího článku jsme si uvedli dva příklady, které slouží pro jednoduché zjištění, jak dlouho trvá vytvoření a naplnění nového Parquet souboru daty a jak rychle je naopak možné tato data přečíst. Pro vytváření souboru, resp. jednotlivých záznamů, použijeme knihovnu faker, která nám vygeneruje pseudonáhodná data:
package main import ( "log" "math/rand" "os" "time" "github.com/bxcodec/faker/v3" "github.com/xitongsys/parquet-go/parquet" "github.com/xitongsys/parquet-go/writer" ) const defaultOutputFile = "flat.parquet" // Record represents one record stored in Parquet file type Record struct { ID uint64 `parquet:"name=id, type=UINT_64, encoding=PLAIN"` Name string `parquet:"name=name, type=UTF8, encoding=PLAIN_DICTIONARY"` Surname string `parquet:"name=surname, type=UTF8, encoding=PLAIN"` Email string `parquet:"name=email, type=UTF8, encoding=PLAIN"` Active bool `parquet:"name=active, type=BOOLEAN"` Color string `parquet:"name=color, type=UTF8, encoding=PLAIN_DICTIONARY"` } func generateColor() string { var colors []string = []string{ "black", "blue", "red", "magenta", "green", "cyan", "yellow", "white", } return colors[rand.Int()%len(colors)] } func writeRecords(pw *writer.ParquetWriter, n int) { // create report structure to be stored in Parquet file record := Record{} for i := 0; i < n; i++ { record.ID = uint64(i) record.Name = faker.FirstName() record.Surname = faker.LastName() record.Email = faker.Email() record.Active = i%2 == 0 record.Color = generateColor() // write the record structure into Parquet file err := pw.Write(record) if err != nil { log.Println("Write into Parquet error", err) } } } // stopWrite function stop writing into Parquet file func stopWrite(pw *writer.ParquetWriter) { err := pw.WriteStop() // most write errors are caught at this time if err != nil { log.Println("WriteStop error", err) } } func createAndWriteIntoParquetFile(filename string, records int, compression parquet.CompressionCodec) { t1 := time.Now() w, err := os.Create(filename) if err != nil { log.Println("Can't create local file", err) return } defer w.Close() // initialize Parquet file writer pw, err := writer.NewParquetWriterFromWriter(w, new(Record), 1) if err != nil { log.Println("Can't create parquet writer", err) return } pw.RowGroupSize = 128 * 1024 * 1024 //128M pw.CompressionType = compression defer stopWrite(pw) writeRecords(pw, records) log.Println("Write Finished") // compute and print duration t2 := time.Now() since := time.Since(t1) log.Println("Start time: ", t1) log.Println("End time: ", t2) log.Println("Duration: ", since) } func main() { createAndWriteIntoParquetFile("1000000records_compression_none.parquet", 1000000, parquet.CompressionCodec_UNCOMPRESSED) createAndWriteIntoParquetFile("1000000records_compression_snappy.parquet", 1000000, parquet.CompressionCodec_SNAPPY) createAndWriteIntoParquetFile("1000000records_compression_gzip.parquet", 1000000, parquet.CompressionCodec_GZIP) }
Časy trvání zápisu do ramdisku s využitím obou komprimačních algoritmů (první zápis nepoužívá žádný algoritmus):
2020/11/14 16:22:21 Write Finished 2020/11/14 16:22:21 Start time: 2020-11-14 16:22:18.382414375 +0100 CET m=+0.001018949 2020/11/14 16:22:21 End time: 2020-11-14 16:22:21.496799932 +0100 CET m=+3.115404464 2020/11/14 16:22:21 Duration: 3.114385625s 2020/11/14 16:22:24 Write Finished 2020/11/14 16:22:24 Start time: 2020-11-14 16:22:21.52651968 +0100 CET m=+3.145124247 2020/11/14 16:22:24 End time: 2020-11-14 16:22:24.81071525 +0100 CET m=+6.429319812 2020/11/14 16:22:24 Duration: 3.284195685s 2020/11/14 16:22:28 Write Finished 2020/11/14 16:22:28 Start time: 2020-11-14 16:22:24.835851362 +0100 CET m=+6.454455962 2020/11/14 16:22:28 End time: 2020-11-14 16:22:28.88592985 +0100 CET m=+10.504534394 2020/11/14 16:22:28 Duration: 4.050078532s
Taktéž jsme si uvedli příklad sloužící pro otestování rychlosti čtení po jednotlivých záznamech. Jedná se o nejpomalejší možný způsob čtení, protože zde vůbec nevyužijeme výhod poskytovaných sloupcovou databází:
// This tool is able to read all records stored in selected Parquet file. // Currently, only records with the structure `Record` is read correctly. Name // of input Parquet file needs to be selected from command line. package main import ( "log" "time" "github.com/xitongsys/parquet-go-source/local" "github.com/xitongsys/parquet-go/reader" "github.com/xitongsys/parquet-go/source" ) // Record represents one record stored in Parquet file type Record struct { ID uint64 `parquet:"name=id, type=UINT_64, encoding=PLAIN"` Name string `parquet:"name=name, type=UTF8, encoding=PLAIN_DICTIONARY"` Surname string `parquet:"name=surname, type=UTF8, encoding=PLAIN"` Email string `parquet:"name=email, type=UTF8, encoding=PLAIN"` Active bool `parquet:"name=active, type=BOOLEAN"` Color string `parquet:"name=color, type=UTF8, encoding=PLAIN_DICTIONARY"` } // closeReader tries to close the given Parquet file reader func closeReader(reader source.ParquetFile) { err := reader.Close() if err != nil { log.Println("close reader:", err) } } func readParquetFile(fileName string) { t1 := time.Now() const parallelNumber = 1 // construct the file reader and try to open the Parquet file for // reading fileReader, err := local.NewLocalFileReader(fileName) if err != nil { log.Fatal("Can't open file", err) return } // fileReader needs to be closed properly defer closeReader(fileReader) // initialize Parquet file reader parquetReader, err := reader.NewParquetReader(fileReader, new(Record), parallelNumber) if err != nil { log.Fatal("Can't create parquet reader", err) return } // parquetReader needs to be stopped defer parquetReader.ReadStop() readRecords(parquetReader) // compute and print duration t2 := time.Now() since := time.Since(t1) log.Println("Start time: ", t1) log.Println("End time: ", t2) log.Println("Duration: ", since) } func readRecords(parquetReader *reader.ParquetReader) { recordCount := int(parquetReader.GetNumRows()) log.Println("Records to read", recordCount) record := make([]Record, 1) records := 0 // try to read and display all records for i := 0; i < recordCount; i++ { // try to read record err := parquetReader.Read(&record) if err != nil { log.Println("Read error", err) continue } else { records++ } } log.Println("Read", records, "records") } func main() { readParquetFile("1000000records_compression_none.parquet") readParquetFile("1000000records_compression_snappy.parquet") readParquetFile("1000000records_compression_gzip.parquet") }
Podívejme se nyní na dosažené výsledky. Rychlost čtení (po jednotlivých záznamech) je mnohem pomalejší, než samotný zápis do sloupcové databáze:
2020/11/14 16:46:53 Records to read 1000000 2020/11/14 16:47:17 Read 1000000 records 2020/11/14 16:47:17 Start time: 2020-11-14 16:46:53.80851109 +0100 CET m=+0.000895204 2020/11/14 16:47:17 End time: 2020-11-14 16:47:17.695641899 +0100 CET m=+23.888025988 2020/11/14 16:47:17 Duration: 23.887130935s 2020/11/14 16:47:17 Records to read 1000000 2020/11/14 16:47:41 Read 1000000 records 2020/11/14 16:47:41 Start time: 2020-11-14 16:47:17.695696876 +0100 CET m=+23.888080959 2020/11/14 16:47:41 End time: 2020-11-14 16:47:41.460809934 +0100 CET m=+47.653194032 2020/11/14 16:47:41 Duration: 23.765113146s 2020/11/14 16:47:41 Records to read 1000000 2020/11/14 16:48:05 Read 1000000 records 2020/11/14 16:48:05 Start time: 2020-11-14 16:47:41.460860147 +0100 CET m=+47.653244228 2020/11/14 16:48:05 End time: 2020-11-14 16:48:05.50961075 +0100 CET m=+71.701994837 2020/11/14 16:48:05 Duration: 24.048750743s
3. Čtení ze sloupcové databáze po záznamech je pomalé!
Výsledky získané z obou předchozích příkladů a shrnuté do jediné tabulky ukazují, že čtení po jednotlivých záznamech je skutečně pomalé:
# | Komprimace | Zápis | Čtení |
---|---|---|---|
1 | None | 3.15 | 23.88s |
2 | Snappy | 3.30s | 23.76s |
3 | GZIP | 4.07s | 24.04s |
4. Čtení dat po blocích
Pokud z nějakého důvodu potřebujete skutečně zpracovávat data po záznamech a nikoli po sloupcích, je obecně rychlejší použít čtení po celých blocích. Je to vlastně velmi jednoduché. Postačuje tento kus kódu…
recordCount := int(parquetReader.GetNumRows()) record := make([]Record, 1) // try to read and display all records for i := 0; i < recordCount; i++ { // try to read record err := parquetReader.Read(&record) if err != nil { log.Println("Read error", err) continue } else { // zde se může se záznamem pracovat } }
…nahradit za čtení po větších blocích, jejichž délku si můžete určit (až pochopitelně na poslední blok, který bude obecně menší):
recordCount := int(parquetReader.GetNumRows()) records := make([]Record, blockSize) readRecords := 0 // try to read and display all records for readRecords < recordCount { // try to read record err := parquetReader.Read(&records) if err != nil { log.Println("Read error", err) continue } else { readRecords += len(records) // zde se může se záznamy v řezu pracovat } } // log.Println("Read", readRecords, "records")
5. Vliv velikosti bloku na rychlost čtení dat
Nyní tedy umíme pracovat s daty po větších blocích. Jaký je však vliv velikosti bloku na rychlost čtení? To je obecně velmi důležitá informace, protože nutnost alokovat velké bloky může mít negativní vliv na paměťové nároky a/nebo i na rychlost celé aplikace. Můžeme se pokusit provést malé měření a velikost bloku postupně zvětšovat od jednoho záznamu až po 1000 (resp. přesněji maxBlockSize) záznamů. Po každém zvětšení bloku opět přečteme všechny záznamy a zaznamenáme celkový čas:
// This tool is able to read all records stored in selected Parquet file. // Currently, only records with the structure `Record` is read correctly. Name // of input Parquet file needs to be selected from command line. package main import ( "encoding/csv" "log" "os" "strconv" "time" "github.com/xitongsys/parquet-go-source/local" "github.com/xitongsys/parquet-go/reader" "github.com/xitongsys/parquet-go/source" ) // maximum block size for reading Parquet files by blocks const maxBlockSize = 1000 // Record represents one record stored in Parquet file type Record struct { ID uint64 `parquet:"name=id, type=UINT_64, encoding=PLAIN"` Name string `parquet:"name=name, type=UTF8, encoding=PLAIN_DICTIONARY"` Surname string `parquet:"name=surname, type=UTF8, encoding=PLAIN"` Email string `parquet:"name=email, type=UTF8, encoding=PLAIN"` Active bool `parquet:"name=active, type=BOOLEAN"` Color string `parquet:"name=color, type=UTF8, encoding=PLAIN_DICTIONARY"` } // closeReader tries to close the given Parquet file reader func closeReader(reader source.ParquetFile) { err := reader.Close() if err != nil { log.Println("close reader:", err) } } func readParquetFile(fileName string, blockSize int) { // construct the file reader and try to open the Parquet file for // reading fileReader, err := local.NewLocalFileReader(fileName) if err != nil { log.Fatal("Can't open file", err) return } // fileReader needs to be closed properly defer closeReader(fileReader) // initialize Parquet file reader parquetReader, err := reader.NewParquetReader(fileReader, new(Record), 1) if err != nil { log.Fatal("Can't create parquet reader", err) return } // parquetReader needs to be stopped defer parquetReader.ReadStop() readRecords(parquetReader, blockSize) } func readRecords(parquetReader *reader.ParquetReader, blockSize int) { recordCount := int(parquetReader.GetNumRows()) // log.Println("Records to read", recordCount) records := make([]Record, blockSize) readRecords := 0 // try to read and display all records for readRecords < recordCount { // try to read record err := parquetReader.Read(&records) if err != nil { log.Println("Read error", err) continue } else { readRecords += len(records) } } // log.Println("Read", readRecords, "records") } func main() { // create and open new CSV file csvFile, err := os.Create("durations.csv") if err != nil { log.Fatal("Create CSV file", err) } defer csvFile.Close() // initialize CSV writer csvWriter := csv.NewWriter(csvFile) defer csvWriter.Flush() csvWriter.Write([]string{"Block size", "Time to read"}) for blockSize := 1; blockSize <= maxBlockSize; blockSize++ { t1 := time.Now() readParquetFile("1000000records_compression_none.parquet", blockSize) // compute and print duration since := time.Since(t1) log.Printf("Block size: %d Duration: %d\n", blockSize, since) // write duration into CSV file csvWriter.Write([]string{strconv.Itoa(blockSize), strconv.Itoa(int(since))}) } }
6. Vyhodnocení výsledků
Z grafu, který je možné ze získaných dat vykreslit, je patrné, že čím větší je velikost bloku, tím kratší je celková doba nutná pro načtení všech záznamů. Na konci grafu jsou vidět „zákmity“ způsobené činností automatického správce paměti:
Obrázek 1: Vliv velikosti bloku (počet záznamů čtených jedinou operací) na rychlost načtení.
7. Konstantní počet gorutin pro čtení a jejich vliv na rychlost zpracování Parquet souborů
Při inicializaci objektu, který čtení z Parquet souborů realizuje, je možné specifikovat počet gorutin, v nichž je prováděno vlastní čtení. Prozatím jsme počet gorutin měli nastaven na hodnotu 1:
// initialize Parquet file reader parquetReader, err := reader.NewParquetReader(fileReader, new(Record), 1)
Tento počet je však možné měnit a celkový počet gorutin bude mít vliv na rychlost čtení. Zda se jedná o záporný či spíše kladný vliv, se pokusíme zjistit v dalším příkladu, který těchto gorutin bude vyžadovat přesně 100 (konstanta readers), což je mimochodem mnohem více, než počet procesorových jader (osm fyzických jader tvářících se jako šestnáct jader logických):
// This tool is able to read all records stored in selected Parquet file. // Currently, only records with the structure `Record` is read correctly. Name // of input Parquet file needs to be selected from command line. package main import ( "encoding/csv" "log" "os" "strconv" "time" "github.com/xitongsys/parquet-go-source/local" "github.com/xitongsys/parquet-go/reader" "github.com/xitongsys/parquet-go/source" ) // maximum block size for reading Parquet files by blocks const maxBlockSize = 1000 const readers = 100 // Record represents one record stored in Parquet file type Record struct { ID uint64 `parquet:"name=id, type=UINT_64, encoding=PLAIN"` Name string `parquet:"name=name, type=UTF8, encoding=PLAIN_DICTIONARY"` Surname string `parquet:"name=surname, type=UTF8, encoding=PLAIN"` Email string `parquet:"name=email, type=UTF8, encoding=PLAIN"` Active bool `parquet:"name=active, type=BOOLEAN"` Color string `parquet:"name=color, type=UTF8, encoding=PLAIN_DICTIONARY"` } // closeReader tries to close the given Parquet file reader func closeReader(reader source.ParquetFile) { err := reader.Close() if err != nil { log.Println("close reader:", err) } } func readParquetFile(fileName string, blockSize int) { // construct the file reader and try to open the Parquet file for // reading fileReader, err := local.NewLocalFileReader(fileName) if err != nil { log.Fatal("Can't open file", err) return } // fileReader needs to be closed properly defer closeReader(fileReader) // initialize Parquet file reader parquetReader, err := reader.NewParquetReader(fileReader, new(Record), readers) if err != nil { log.Fatal("Can't create parquet reader", err) return } // parquetReader needs to be stopped defer parquetReader.ReadStop() readRecords(parquetReader, blockSize) } func readRecords(parquetReader *reader.ParquetReader, blockSize int) { recordCount := int(parquetReader.GetNumRows()) // log.Println("Records to read", recordCount) records := make([]Record, blockSize) readRecords := 0 // try to read and display all records for readRecords < recordCount { // try to read record err := parquetReader.Read(&records) if err != nil { log.Println("Read error", err) continue } else { readRecords += len(records) } } // log.Println("Read", readRecords, "records") } func main() { // create and open new CSV file csvFile, err := os.Create("durations.csv") if err != nil { log.Fatal("Create CSV file", err) } defer csvFile.Close() // initialize CSV writer csvWriter := csv.NewWriter(csvFile) defer csvWriter.Flush() csvWriter.Write([]string{"Block size", "Time to read"}) for blockSize := 1; blockSize <= maxBlockSize; blockSize++ { t1 := time.Now() readParquetFile("1000000records_compression_none.parquet", blockSize) // compute and print duration since := time.Since(t1) log.Printf("Block size: %d Duration: %d\n", blockSize, since) // write duration into CSV file csvWriter.Write([]string{strconv.Itoa(blockSize), strconv.Itoa(int(since))}) } }
8. Vyhodnocení výsledků – použití jedné resp. 100 gorutin při čtení
Časy běhu závislé na velikosti bloku budou v tomto případě odlišné:
Obrázek 2: Vliv velikosti bloku (počet záznamů čtených jedinou operací) na rychlost načtení při použití 100 gorutin.
Zajímavější je však porovnání s předchozím měřením s jedinou gorutinou:
Obrázek 3: Vliv počtu gorutin a současně i velikosti bloku na rychlost čtení.
Z grafu je patrné, že pokud je velikost bloku větší než počet gorutin, je výhodnější druhá možnost. Ovšem problém nastává při čtení po kratších blocích, kdy větší počet gorutin paradoxně vede ke zpomalení čtení (a to dokonce o celý řád!). Měli bychom tudíž přijít s lepším řešením a nějakým způsobem svázat počet gorutin s velikostí bloku.
9. Odvození počtu gorutin od velikosti bloku
V některých materiálech o knihovně určené pro čtení Parquet souborů se setkáme s tvrzením, že velikost bloku by měla odpovídat počtu gorutin, v nichž běží načítací rutina. Zda je toto tvrzení pravdivé, popř. pro jaké velikosti bloků je pravdivé, si ověříme v dalším demonstračním příkladu:
// This tool is able to read all records stored in selected Parquet file. // Currently, only records with the structure `Record` is read correctly. Name // of input Parquet file needs to be selected from command line. package main import ( "encoding/csv" "log" "os" "strconv" "time" "github.com/xitongsys/parquet-go-source/local" "github.com/xitongsys/parquet-go/reader" "github.com/xitongsys/parquet-go/source" ) // maximum block size for reading Parquet files by blocks const maxBlockSize = 1000 // Record represents one record stored in Parquet file type Record struct { ID uint64 `parquet:"name=id, type=UINT_64, encoding=PLAIN"` Name string `parquet:"name=name, type=UTF8, encoding=PLAIN_DICTIONARY"` Surname string `parquet:"name=surname, type=UTF8, encoding=PLAIN"` Email string `parquet:"name=email, type=UTF8, encoding=PLAIN"` Active bool `parquet:"name=active, type=BOOLEAN"` Color string `parquet:"name=color, type=UTF8, encoding=PLAIN_DICTIONARY"` } // closeReader tries to close the given Parquet file reader func closeReader(reader source.ParquetFile) { err := reader.Close() if err != nil { log.Println("close reader:", err) } } func readParquetFile(fileName string, blockSize int) { // construct the file reader and try to open the Parquet file for // reading fileReader, err := local.NewLocalFileReader(fileName) if err != nil { log.Fatal("Can't open file", err) return } // fileReader needs to be closed properly defer closeReader(fileReader) // initialize Parquet file reader parquetReader, err := reader.NewParquetReader(fileReader, new(Record), int64(blockSize)) if err != nil { log.Fatal("Can't create parquet reader", err) return } // parquetReader needs to be stopped defer parquetReader.ReadStop() readRecords(parquetReader, blockSize) } func readRecords(parquetReader *reader.ParquetReader, blockSize int) { recordCount := int(parquetReader.GetNumRows()) // log.Println("Records to read", recordCount) records := make([]Record, blockSize) readRecords := 0 // try to read and display all records for readRecords < recordCount { // try to read record err := parquetReader.Read(&records) if err != nil { log.Println("Read error", err) continue } else { readRecords += len(records) } } // log.Println("Read", readRecords, "records") } func main() { // create and open new CSV file csvFile, err := os.Create("durations.csv") if err != nil { log.Fatal("Create CSV file", err) } defer csvFile.Close() // initialize CSV writer csvWriter := csv.NewWriter(csvFile) defer csvWriter.Flush() csvWriter.Write([]string{"Block size", "Time to read"}) for blockSize := 1; blockSize <= maxBlockSize; blockSize++ { t1 := time.Now() readParquetFile("1000000records_compression_none.parquet", blockSize) // compute and print duration since := time.Since(t1) log.Printf("Block size: %d Duration: %d\n", blockSize, since) // write duration into CSV file csvWriter.Write([]string{strconv.Itoa(blockSize), strconv.Itoa(int(since))}) } }
10. Vyhodnocení výsledků – odvození počtu gorutin od velikosti bloku
Nejprve se podívejme na vliv doby načtení 1000000 záznamů v blocích o velikosti od jednoho záznamu až po tisíc záznamů. Počet gorutin přesně odpovídá velikosti bloku:
Obrázek 4: Čtení po blocích rozdílné velikosti s rozdílným počtem gorutin.
Vidíme, že doporučení, aby se počet gorutin odvozoval od velikosti bloku, není zcela dobré. Ještě více je to patrné z následujícího grafu:
Obrázek 5: Čtení po blocích rozdílné velikosti s rozdílným počtem gorutin. Porovnání se čtením v jediné gorutině.
Doporučení by tedy mělo znít spíše takto: počet gorutin by měl odpovídat počtu logických procesorových jader. Pouze v případě, že je zapotřebí číst po jednotlivých záznamech, použijte jedinou gorutinu.
11. Čtení hodnot z vybraného sloupce
Sloupcové databáze jsou optimalizovány na to, aby se data načítala a zpracovávala po jednotlivých sloupcích. Parquet soubory nejsou výjimkou, takže i v knihovně parquet-go nalezneme dvě metody určené pro čtení hodnot z vybraného sloupce:
# | Funkce | Stručný popis |
---|---|---|
1 | ReadColumnByIndex | čtení ze sloupce vybraného pomocí indexu |
2 | ReadColumnByPath | čtení ze sloupce vybraného cestou |
fileReader, err := local.NewLocalFileReader(fileName) // fileReader needs to be closed properly defer closeReader(fileReader) // initialize Parquet file reader parquetColumnReader, err := reader.NewParquetColumnReader(fileReader, parallelNumber)
12. Použití indexu sloupce při čtení
Pro načtení jediné hodnoty ze sloupce s indexem columnIndex se použije metoda ReadColumnByIndex, které se předá index sloupce (čísluje se od nuly) a počet načítaných hodnot:
data, _, _, err := parquetReader.ReadColumnByIndex(int64(columnIndex), 1) if err != nil { log.Println("Read error", err) continue }
Typ vrácené hodnoty je řez (slice) hodnot implementujících prázdné rozhraní – jinými slovy se jedná o kolekci libovolných hodnot. O přetypování se musí postarat samotný program, a to například následovně:
if data[0].(bool) { activeCount++ } else { inactiveCount++ }
13. Zjištění počtu aktivních a neaktivních uživatelů
V dalším demonstračním příkladu se zjišťuje počet aktivních a neaktivních uživatelů. Co to znamená? Z databáze se strukturou:
ID uint64 `parquet:"name=id, type=UINT_64, encoding=PLAIN"` Name string `parquet:"name=name, type=UTF8, encoding=PLAIN_DICTIONARY"` Surname string `parquet:"name=surname, type=UTF8, encoding=PLAIN"` Email string `parquet:"name=email, type=UTF8, encoding=PLAIN"` Active bool `parquet:"name=active, type=BOOLEAN"` Color string `parquet:"name=color, type=UTF8, encoding=PLAIN_DICTIONARY"`
budeme zpracovávat pouze sloupec Active, jehož index je roven čtyřem:
const activeColumnIndex = 4
Příklad, který načte a zpracuje údaje z tohoto sloupce, by mohl vypadat následovně:
// This tool is able to read all records stored in selected Parquet file. // Currently, only records with the structure `Record` is read correctly. Name // of input Parquet file needs to be selected from command line. package main import ( "log" "time" "github.com/xitongsys/parquet-go-source/local" "github.com/xitongsys/parquet-go/reader" "github.com/xitongsys/parquet-go/source" ) // Record represents one record stored in Parquet file type Record struct { ID uint64 `parquet:"name=id, type=UINT_64, encoding=PLAIN"` Name string `parquet:"name=name, type=UTF8, encoding=PLAIN_DICTIONARY"` Surname string `parquet:"name=surname, type=UTF8, encoding=PLAIN"` Email string `parquet:"name=email, type=UTF8, encoding=PLAIN"` Active bool `parquet:"name=active, type=BOOLEAN"` Color string `parquet:"name=color, type=UTF8, encoding=PLAIN_DICTIONARY"` } const activeColumnIndex = 4 // closeReader tries to close the given Parquet file reader func closeReader(reader source.ParquetFile) { err := reader.Close() if err != nil { log.Println("close reader:", err) } } func readParquetFile(fileName string) { t1 := time.Now() const parallelNumber = 1 // construct the file reader and try to open the Parquet file for // reading fileReader, err := local.NewLocalFileReader(fileName) if err != nil { log.Fatal("Can't open file", err) return } // fileReader needs to be closed properly defer closeReader(fileReader) // initialize Parquet file reader parquetColumnReader, err := reader.NewParquetColumnReader(fileReader, parallelNumber) if err != nil { log.Fatal("Can't create parquet column reader", err) return } // parquetReader needs to be stopped defer parquetColumnReader.ReadStop() readColumnData(parquetColumnReader, activeColumnIndex) // compute and print duration t2 := time.Now() since := time.Since(t1) log.Println("Start time: ", t1) log.Println("End time: ", t2) log.Println("Duration: ", since) } func readColumnData(parquetReader *reader.ParquetReader, columnIndex int) { valuesCount := int(parquetReader.GetNumRows()) log.Println("Values to read", valuesCount) activeCount := 0 inactiveCount := 0 values := 0 // try to read and display all records for i := 0; i < valuesCount; i++ { // try to read value data, _, _, err := parquetReader.ReadColumnByIndex(int64(columnIndex), 1) if err != nil { log.Println("Read error", err) continue } else { values++ } if data[0].(bool) { activeCount++ } else { inactiveCount++ } } log.Println("Read", values, "values", "active", activeCount, "inactive", inactiveCount) } func main() { readParquetFile("1000000records_compression_none.parquet") readParquetFile("1000000records_compression_snappy.parquet") readParquetFile("1000000records_compression_gzip.parquet") }
14. Výsledky – čtení a zpracování dat z jednoho sloupce
Nezávisle na tom, který soubor (každý používá jiný komprimační algoritmus) je zpracováván, by se mělo vypočítat 500000 aktivních a 500000 neaktivních uživatelů, protože těmito hodnotami byla databáze naplněna:
2020/11/17 19:18:08 Values to read 1000000 2020/11/17 19:18:08 Read 1000000 values active 500000 inactive 500000 2020/11/17 19:18:08 Start time: 2020-11-17 19:18:08.01666144 +0100 CET m=+0.000954929 2020/11/17 19:18:08 End time: 2020-11-17 19:18:08.598879555 +0100 CET m=+0.583173023 2020/11/17 19:18:08 Duration: 582.218211ms 2020/11/17 19:18:08 Values to read 1000000 2020/11/17 19:18:09 Read 1000000 values active 500000 inactive 500000 2020/11/17 19:18:09 Start time: 2020-11-17 19:18:08.598904304 +0100 CET m=+0.583197768 2020/11/17 19:18:09 End time: 2020-11-17 19:18:09.145461682 +0100 CET m=+1.129755154 2020/11/17 19:18:09 Duration: 546.557462ms 2020/11/17 19:18:09 Values to read 1000000 2020/11/17 19:18:09 Read 1000000 values active 500000 inactive 500000 2020/11/17 19:18:09 Start time: 2020-11-17 19:18:09.145480412 +0100 CET m=+1.129773875 2020/11/17 19:18:09 End time: 2020-11-17 19:18:09.743330813 +0100 CET m=+1.727624282 2020/11/17 19:18:09 Duration: 597.850522ms
15. Specifikace čteného sloupce jeho jménem (cestou)
V dalším – již předposledním – demonstračním příkladu je ukázáno, jak lze specifikovat sloupec cestou. Zde je situace nepatrně složitější, protože v cestě není uveden jen název sloupce, ale i případné jméno souboru obsahujícího tento sloupec (teoreticky je totiž možné tabulku rozdělit do většího množství souborů):
const activeColumnPath = "parquet_go_root.active"
Čtení potom může vypadat následovně:
for i := 0; i < valuesCount; i++ { // try to read value data, _, _, err := parquetReader.ReadColumnByPath(activeColumnPath, 1) if err != nil { log.Println("Read error", err) continue } else { // ... } if data[0].(bool) { activeCount++ } else { inactiveCount++ } }
Úplný zdrojový kód tohoto demonstračního příkladu:
// This tool is able to read all records stored in selected Parquet file. // Currently, only records with the structure `Record` is read correctly. Name // of input Parquet file needs to be selected from command line. package main import ( "log" "time" "github.com/xitongsys/parquet-go-source/local" "github.com/xitongsys/parquet-go/reader" "github.com/xitongsys/parquet-go/source" ) // Record represents one record stored in Parquet file type Record struct { ID uint64 `parquet:"name=id, type=UINT_64, encoding=PLAIN"` Name string `parquet:"name=name, type=UTF8, encoding=PLAIN_DICTIONARY"` Surname string `parquet:"name=surname, type=UTF8, encoding=PLAIN"` Email string `parquet:"name=email, type=UTF8, encoding=PLAIN"` Active bool `parquet:"name=active, type=BOOLEAN"` Color string `parquet:"name=color, type=UTF8, encoding=PLAIN_DICTIONARY"` } const activeColumnPath = "parquet_go_root.active" // closeReader tries to close the given Parquet file reader func closeReader(reader source.ParquetFile) { err := reader.Close() if err != nil { log.Println("close reader:", err) } } func readParquetFile(fileName string) { t1 := time.Now() const parallelNumber = 1 // construct the file reader and try to open the Parquet file for // reading fileReader, err := local.NewLocalFileReader(fileName) if err != nil { log.Fatal("Can't open file", err) return } // fileReader needs to be closed properly defer closeReader(fileReader) // initialize Parquet file reader parquetColumnReader, err := reader.NewParquetColumnReader(fileReader, parallelNumber) if err != nil { log.Fatal("Can't create parquet column reader", err) return } // parquetReader needs to be stopped defer parquetColumnReader.ReadStop() readColumnData(parquetColumnReader, activeColumnPath) // compute and print duration t2 := time.Now() since := time.Since(t1) log.Println("Start time: ", t1) log.Println("End time: ", t2) log.Println("Duration: ", since) } func readColumnData(parquetReader *reader.ParquetReader, columnPath string) { valuesCount := int(parquetReader.GetNumRows()) log.Println("Values to read", valuesCount) activeCount := 0 inactiveCount := 0 values := 0 // try to read and display all records for i := 0; i < valuesCount; i++ { // try to read value data, _, _, err := parquetReader.ReadColumnByPath(columnPath, 1) if err != nil { log.Println("Read error", err) continue } else { values++ } if data[0].(bool) { activeCount++ } else { inactiveCount++ } } log.Println("Read", values, "values", "active", activeCount, "inactive", inactiveCount) } func main() { readParquetFile("1000000records_compression_none.parquet") readParquetFile("1000000records_compression_snappy.parquet") readParquetFile("1000000records_compression_gzip.parquet") }
16. Rychlost přečtení všech údajů z jediného sloupce
I když je čtení dat z jediného sloupce (podle očekávání) rychlejší, než zpracování databáze po řádcích, je možné ho urychlit čtením po větších blocích, což je postup, který již velmi dobře známe. Pro naše účely tento postup nepatrně upravíme:
for readValues < valuesCount { // try to read value data, _, _, err := parquetReader.ReadColumnByIndex(int64(columnIndex), int64(blockSize)) if err != nil { log.Println("Read error", err) continue } for _, active := range data { if active.(bool) { activeCount++ } else { inactiveCount++ } } }
V dnešním posledním příkladu se přečte celý sloupec s využitím bloků proměnné délky a současně i při použití jedné, osmi, šestnácti, popř. 32 gorutin, v jejichž kódu je realizován kód pro čtení:
// This tool is able to read all records stored in selected Parquet file. // Currently, only records with the structure `Record` is read correctly. Name // of input Parquet file needs to be selected from command line. package main import ( "encoding/csv" "fmt" "log" "os" "strconv" "time" "github.com/xitongsys/parquet-go-source/local" "github.com/xitongsys/parquet-go/reader" "github.com/xitongsys/parquet-go/source" ) // maximum block size for reading Parquet files by blocks const maxBlockSize = 100 // Record represents one record stored in Parquet file type Record struct { ID uint64 `parquet:"name=id, type=UINT_64, encoding=PLAIN"` Name string `parquet:"name=name, type=UTF8, encoding=PLAIN_DICTIONARY"` Surname string `parquet:"name=surname, type=UTF8, encoding=PLAIN"` Email string `parquet:"name=email, type=UTF8, encoding=PLAIN"` Active bool `parquet:"name=active, type=BOOLEAN"` Color string `parquet:"name=color, type=UTF8, encoding=PLAIN_DICTIONARY"` } const activeColumnIndex = 4 // closeReader tries to close the given Parquet file reader func closeReader(reader source.ParquetFile) { err := reader.Close() if err != nil { log.Println("close reader:", err) } } func readParquetFile(fileName string, blockSize int, readers int) { // construct the file reader and try to open the Parquet file for // reading fileReader, err := local.NewLocalFileReader(fileName) if err != nil { log.Fatal("Can't open file", err) return } // fileReader needs to be closed properly defer closeReader(fileReader) // initialize Parquet file reader parquetColumnReader, err := reader.NewParquetColumnReader(fileReader, int64(readers)) if err != nil { log.Fatal("Can't create parquet column reader", err) return } // parquetReader needs to be stopped defer parquetColumnReader.ReadStop() readColumnData(parquetColumnReader, activeColumnIndex, blockSize) } func readColumnData(parquetReader *reader.ParquetReader, columnIndex int, blockSize int) { valuesCount := int(parquetReader.GetNumRows()) activeCount := 0 inactiveCount := 0 readValues := 0 // try to read and display all records for readValues < valuesCount { // try to read value data, _, _, err := parquetReader.ReadColumnByIndex(int64(columnIndex), int64(blockSize)) if err != nil { log.Println("Read error", err) continue } else { readValues += len(data) } for _, active := range data { if active.(bool) { activeCount++ } else { inactiveCount++ } } } log.Println("Read", readValues, "values", "active", activeCount, "inactive", inactiveCount) } func main() { var readers []int = []int{1, 8, 16, 32} for _, numReaders := range readers { // create and open new CSV file csvFileName := fmt.Sprintf("durations-%d-readers.csv", numReaders) csvFile, err := os.Create(csvFileName) if err != nil { log.Fatal("Create CSV file", err) } defer csvFile.Close() // initialize CSV writer csvWriter := csv.NewWriter(csvFile) defer csvWriter.Flush() csvWriter.Write([]string{"Block size", "Time to read"}) for blockSize := 1; blockSize <= maxBlockSize; blockSize++ { t1 := time.Now() readParquetFile("1000000records_compression_none.parquet", blockSize, numReaders) // compute and print duration since := time.Since(t1) log.Printf("Block size: %d Readers: %d Duration: %d\n", blockSize, numReaders, since) // write duration into CSV file csvWriter.Write([]string{strconv.Itoa(blockSize), strconv.Itoa(int(since))}) } } }
17. Výsledky měření rychlosti
Podívejme se nyní na dosažené časy. Z nich je patrné, že i relativně malá velikost bloku (řekněme šedesát prvků) vede ke znatelnému urychlení načítání hodnot ze sloupce:
Obrázek 6: Rychlost čtení údajů z jediného sloupce pro různé velikosti bloků a různý počet gorutin.
18. Pomocné skripty pro tvorbu grafů
Jen pro úplnost si uveďme, jaké skripty byly použity pro přípravu grafů pro dnešní článek.
První skript načte CSV soubor s dvojicí sloupců – velikost bloku a čas přečtení všech záznamů, popř. hodnot z Parquet souboru. Z těchto údajů vytvoří jednoduchý graf, který je zobrazen a současně i uložen (rastrový obrázek PNG + vektorová kresba SVG). Využívá se možností knihovny Matplotlib:
#!/usr/bin/env python3 import sys import csv import matplotlib.pyplot as plt # Check if command line argument is specified (it is mandatory). if len(sys.argv) < 2: print("Usage:") print(" read-by-blocks-chart.py input_file.csv") print("Example:") print(" read-by-blocks-chart.py durations.csv") sys.exit(1) # First command line argument should contain name of input CSV. input_csv = sys.argv[1] # Try to open the CSV file specified. with open(input_csv) as csv_input: # And open this file as CSV csv_reader = csv.reader(csv_input) # Skip header next(csv_reader, None) # Read all rows from the provided CSV file durations = [(int(row[0]), int(row[1])) for row in csv_reader] # Create new graph x = [i[0] for i in durations] y = [i[1] for i in durations] plt.plot(x, y, "b") # Title of a graph plt.title("Reading by block with size N") # Add a label to x-axis plt.xlabel("Block size") # Add a label to y-axis plt.ylabel("Duration time [ns]") # Set the plot layout plt.tight_layout() # And save the plot into raster format and vector format as well plt.savefig("read-by-block-time.png") plt.savefig("read-by-block-time.svg") # Try to show the plot on screen plt.show()
Druhý skript je velmi podobný skriptu prvnímu, ovšem odlišnost spočívá v tom, že načte dva CSV soubory a vykreslí graf s dvojicí průběhů, které je tak možné snadno porovnat:
#!/usr/bin/env python3 import sys import csv import matplotlib.pyplot as plt def read_csv(filename): # Try to open the CSV file specified. with open(filename) as csv_input: # And open this file as CSV csv_reader = csv.reader(csv_input) # Skip header next(csv_reader, None) # Read all rows from the provided CSV file durations = [(int(row[0]), int(row[1])) for row in csv_reader] # Create new graph x = [i[0] for i in durations] y = [i[1] for i in durations] return x, y # Check if command line argument is specified (it is mandatory). if len(sys.argv) < 3: print("Usage:") print(" read-by-blocks-charts.py input_file.csv input_file.csv") print("Example:") print(" read-by-blocks-charts.py durations-1.csv durations-100.csv") sys.exit(1) # First command line argument should contain name of input CSV. input_csv_1 = sys.argv[1] input_csv_2 = sys.argv[2] x1, y1 = read_csv(input_csv_1) x2, y2 = read_csv(input_csv_2) plt.plot(x1, y1, "b", label="1 reader goroutine") plt.plot(x2, y2, "r", label="100 reader goroutines") # Title of a graph plt.title("Reading by block with size N") # Add a label to x-axis plt.xlabel("Block size") # Add a label to y-axis plt.ylabel("Duration time [ns]") # Add a legend plt.legend() # Set the plot layout plt.tight_layout() # And save the plot into raster format and vector format as well plt.savefig("read-by-block-time.png") plt.savefig("read-by-block-time.svg") # Try to show the plot on screen plt.show()
19. Repositář s demonstračními příklady
Zdrojové kódy všech dnes použitých demonstračních příkladů byly uloženy do nového Git repositáře, který je dostupný na adrese https://github.com/tisnik/go-root (stále na GitHubu :-). V případě, že nebudete chtít klonovat celý repositář (ten je ovšem – alespoň prozatím – velmi malý, dnes má přibližně stovku kilobajtů), můžete namísto toho použít odkazy na jednotlivé demonstrační příklady, které naleznete v následující tabulce:
# | Příklad | Stručný popis | Cesta |
---|---|---|---|
1 | 01-write-performance-by-records | měření rychlosti zápisu do Parquet souborů po záznamech | https://github.com/tisnik/go-root/blob/master/article68/09-write-performance/01-write-performance-by-records |
2 | 02-read-performance-by-records | měření rychlosti čtení z Parquet souborů po záznamech | https://github.com/tisnik/go-root/blob/master/article69/02-read-performance-by-records |
3 | 03-read-performance-by-blocks | měření rychlosti čtení z Parquet souborů po blocích | https://github.com/tisnik/go-root/blob/master/article69/03-read-performance-by-blocks |
4 | 04-write-performance-by-records-pprof | měření rychlosti čtení z Parquet souborů po záznamech + informace z profileru | https://github.com/tisnik/go-root/blob/master/article69/04-write-performance-by-records-pprof |
5 | 05-plot-read-performance-by-blocks | vytvoření CSV souboru s údaji o rychlosti čtení z Parquet souboru | https://github.com/tisnik/go-root/blob/master/article69/05-plot-read-performance-by-blocks |
6 | 06-plot-read-performance-by-blocks-100-readers | čtení z Parquet souborů s využitím 100 gorutin | https://github.com/tisnik/go-root/blob/master/article69/06-plot-read-performance-by-blocks-100-readers |
7 | 07-plot-read-performance-by-block-N-readers | čtení z Parquet souborů s využitím proměnného počtu gorutin | https://github.com/tisnik/go-root/blob/master/article69/07-plot-read-performance-by-block-N-readers |
8 | 08-read-performance-by-column-index | čtení hodnot ze sloupce vybraného jeho indexem | https://github.com/tisnik/go-root/blob/master/article69/08-read-performance-by-column-index |
9 | 09-read-performance-by-column-path | čtení hodnot ze sloupce vybraného „cestou“ | https://github.com/tisnik/go-root/blob/master/article69/09-read-performance-by-column-path |
10 | 10-plot-read-performance-by-column-index | změření rychlosti čtení hodnot z vybraného sloupce | https://github.com/tisnik/go-root/blob/master/article69/10-plot-read-performance-by-column-index |
Pomocné nástroje:
# | Skript | Stručný popis | Cesta |
---|---|---|---|
1 | read-by-blocks-chart.py | vytvoření grafu rychlosti načítání dat v závislosti na velikosti bloku | https://github.com/tisnik/go-root/blob/master/tools/read-by-blocks-chart.py |
2 | read-by-blocks-charts.py | vytvoření grafů s větším množstvím průběhů | https://github.com/tisnik/go-root/blob/master/tools/read-by-blocks-charts.py |
Výsledky všech měření ve formě CSV souborů:
20. Odkazy na Internetu
- Několik poznámek ke sloupcovým databázím
https://www.root.cz/clanky/nekolik-poznamek-ke-sloupcovym-databazim/ - Column-oriented DBMS (Wikipedia)
https://en.wikipedia.org/wiki/Column-oriented_DBMS - Extract, transform, load (ETL)
https://en.wikipedia.org/wiki/Extract,_transform,_load - Top 9 column-oriented databases
https://www.predictiveanalyticstoday.com/top-wide-columnar-store-databases/ - Apache Parquet
https://parquet.apache.org/ - Parquet format
https://github.com/apache/parquet-format - Processing parquet files in Golang
https://dev.to/eminetto/processing-parquet-files-in-golang-1nni - Processing parquet files in Golang
https://eltonminetto.dev/en/post/2019–12–09-parquet-golang/ - Converting CSV files to Parquet with Go
https://mungingdata.com/go/csv-to-parquet/ - Balíček parquet-go
https://github.com/xitongsys/parquet-go - Balíček parquet
https://github.com/parsyl/parquet - Dokumentace k balíčku parquet-go
https://godoc.org/github.com/xitongsys/parquet-go - Parquet File Format Hadoop
https://acadgild.com/blog/parquet-file-format-hadoop - What is Apache Parquet and why you should use it
https://www.upsolver.com/blog/apache-parquet-why-use - Structure Of Parquet File Format
https://www.ellicium.com/parquet-file-format-structure/ - Parquet File with Example
https://commandstech.com/parquet-with-example/ - Faker
https://github.com/bxcodec/faker/ - Apache ORC – the smallest, fastest columnar storage for Hadoop workloads
https://orc.apache.org/ - Apache Parquet (Wikipedia)
https://en.wikipedia.org/wiki/Apache_Parquet - Apache ORC (Wikipedia)
https://en.wikipedia.org/wiki/Apache_ORC - MonetDB
https://www.monetdb.org/ - Future of Column-Oriented Data Processing with Arrow & Parquet by Julien Le Dem | DataEngConf NY '16
https://www.youtube.com/watch?v=6lCVKMQR8Dw - Data Architecture 101 for Your Business
https://www.youtube.com/watch?v=ArzohefZLE4 - Functional Data Engineering – A Set of Best Practices | Lyft
https://www.youtube.com/watch?v=4Spo2QRTz1k - Go Data Structures: Binary Search Tree
https://flaviocopes.com/golang-data-structure-binary-search-tree/ - Gobs of data
https://blog.golang.org/gobs-of-data - Formát BSON
http://bsonspec.org/ - Golang Guide: A List of Top Golang Frameworks, IDEs & Tools
https://blog.intelligentbee.com/2017/08/14/golang-guide-list-top-golang-frameworks-ides-tools/ - Stránky projektu MinIO
https://min.io/ - MinIO Quickstart Guide
https://docs.min.io/docs/minio-quickstart-guide.html - MinIO Go Client API Reference
https://docs.min.io/docs/golang-client-api-reference - MinIO Python Client API Reference
https://docs.min.io/docs/python-client-api-reference.html - Performance at Scale: MinIO Pushes Past 1.4 terabits per second with 256 NVMe Drives
https://blog.min.io/performance-at-scale-minio-pushes-past-1–3-terabits-per-second-with-256-nvme-drives/ - Benchmarking MinIO vs. AWS S3 for Apache Spark
https://blog.min.io/benchmarking-apache-spark-vs-aws-s3/ - MinIO Client Quickstart Guide
https://docs.min.io/docs/minio-client-quickstart-guide.html - Analýza kvality zdrojových kódů Minia
https://goreportcard.com/report/github.com/minio/minio - This is MinIO
https://www.youtube.com/watch?v=vF0lQh0XOCs - Running MinIO Standalone
https://www.youtube.com/watch?v=dIQsPCHvHoM - „Amazon S3 Compatible Storage in Kubernetes“ – Rob Girard, Principal Tech Marketing Engineer, Minio
https://www.youtube.com/watch?v=wlpn8K0jJ4U - Metric types
https://prometheus.io/docs/concepts/metric_types/ - Histograms with Prometheus: A Tale of Woe
http://linuxczar.net/blog/2017/06/15/prometheus-histogram-2/ - Why are Prometheus histograms cumulative?
https://www.robustperception.io/why-are-prometheus-histograms-cumulative - Histograms and summaries
https://prometheus.io/docs/practices/histograms/ - Instrumenting Golang server in 5 min
https://medium.com/@gsisimogang/instrumenting-golang-server-in-5-min-c1c32489add3 - Semantic Import Versioning in Go
https://www.aaronzhuo.com/semantic-import-versioning-in-go/ - Sémantické verzování
https://semver.org/ - Getting started with Go modules
https://medium.com/@fonseka.live/getting-started-with-go-modules-b3dac652066d - Create projects independent of $GOPATH using Go Modules
https://medium.com/mindorks/create-projects-independent-of-gopath-using-go-modules-802260cdfb51o - Anatomy of Modules in Go
https://medium.com/rungo/anatomy-of-modules-in-go-c8274d215c16 - Modules
https://github.com/golang/go/wiki/Modules - Go Modules Tutorial
https://tutorialedge.net/golang/go-modules-tutorial/ - Module support
https://golang.org/cmd/go/#hdr-Module_support - Go Lang: Memory Management and Garbage Collection
https://vikash1976.wordpress.com/2017/03/26/go-lang-memory-management-and-garbage-collection/ - Golang Internals, Part 4: Object Files and Function Metadata
https://blog.altoros.com/golang-part-4-object-files-and-function-metadata.html - A StreamLike, Immutable, Lazy Loading and smart Golang Library to deal with slices
https://github.com/wesovilabs/koazee - Handling Sparse Files on Linux
https://www.systutorials.com/136652/handling-sparse-files-on-linux/ - Gzip (Wikipedia)
https://en.wikipedia.org/wiki/Gzip - Deflate
https://en.wikipedia.org/wiki/DEFLATE - Rozhraní io.ByteReader
https://golang.org/pkg/io/#ByteReader - Rozhraní io.RuneReader
https://golang.org/pkg/io/#RuneReader - Rozhraní io.ByteScanner
https://golang.org/pkg/io/#ByteScanner - Rozhraní io.RuneScanner
https://golang.org/pkg/io/#RuneScanner - Rozhraní io.Closer
https://golang.org/pkg/io/#Closer - Rozhraní io.Reader
https://golang.org/pkg/io/#Reader - Rozhraní io.Writer
https://golang.org/pkg/io/#Writer - Typ Strings.Reader
https://golang.org/pkg/strings/#Reader - VACUUM (SQL)
https://www.sqlite.org/lang_vacuum.html - VACUUM (Postgres)
https://www.postgresql.org/docs/8.4/sql-vacuum.html - The Go Programming Language (home page)
https://golang.org/ - GoDoc
https://godoc.org/ - Go (programming language), Wikipedia
https://en.wikipedia.org/wiki/Go_(programming_language) - Go Books (kniha o jazyku Go)
https://github.com/dariubs/GoBooks - The Go Programming Language Specification
https://golang.org/ref/spec - Go: the Good, the Bad and the Ugly
https://bluxte.net/musings/2018/04/10/go-good-bad-ugly/ - Package builtin
https://golang.org/pkg/builtin/ - The Little Go Book (další kniha)
https://github.com/dariubs/GoBooks - The Go Programming Language by Brian W. Kernighan, Alan A. A. Donovan
https://www.safaribooksonline.com/library/view/the-go-programming/9780134190570/ebook_split010.html - Learning Go
https://www.miek.nl/go/ - Go Bootcamp
http://www.golangbootcamp.com/ - Programming in Go: Creating Applications for the 21st Century (další kniha o jazyku Go)
http://www.informit.com/store/programming-in-go-creating-applications-for-the-21st-9780321774637 - Introducing Go (Build Reliable, Scalable Programs)
http://shop.oreilly.com/product/0636920046516.do - Learning Go Programming
https://www.packtpub.com/application-development/learning-go-programming - The Go Blog
https://blog.golang.org/ - Getting to Go: The Journey of Go's Garbage Collector
https://blog.golang.org/ismmkeynote - Go (programovací jazyk, Wikipedia)
https://cs.wikipedia.org/wiki/Go_(programovac%C3%AD_jazyk) - Installing Go on the Raspberry Pi
https://dave.cheney.net/2012/09/25/installing-go-on-the-raspberry-pi - How the Go runtime implements maps efficiently (without generics)
https://dave.cheney.net/2018/05/29/how-the-go-runtime-implements-maps-efficiently-without-generics - Niečo málo o Go – Golang (slovensky)
http://golangsk.logdown.com/ - How Many Go Developers Are There?
https://research.swtch.com/gophercount - Modern garbage collection: A look at the Go GC strategy
https://blog.plan99.net/modern-garbage-collection-911ef4f8bd8e - Go GC: Prioritizing low latency and simplicity
https://blog.golang.org/go15gc - Is Golang a good language for embedded systems?
https://www.quora.com/Is-Golang-a-good-language-for-embedded-systems - How to use databases with Golang
https://hackernoon.com/how-to-work-with-databases-in-golang-33b002aa8c47