Strömmande överföring av kolonndata med Apache Arrow

ÖversĂ€ttningen av artikeln Ă€r sĂ€rskilt utarbetad för studenter i kursen „Data Engineer“.

Strömmande överföring av kolonndata med Apache Arrow

Under de senaste veckorna har vi med Nong Li lagt till Apache Arrow ett binÀrt strömmande format, som kompletterar det redan befintliga formatet för random access/IPD. Vi har implementationer pÄ Java och C++ samt bindningar för Python. I den hÀr artikeln kommer jag att förklara hur formatet fungerar och visa hur man kan uppnÄ mycket hög genomströmning för DataFrame pandas.

Strömning av kolumndata

En vanlig frÄga som jag fÄr frÄn anvÀndare av Arrow Àr den höga kostnaden för att överföra stora uppsÀttningar tabulÀr data frÄn format som Àr rad- eller postorienterade till kolumnformat. För flera gigabyte dataset kan transponering i minnet eller pÄ disk vara en oöverkomlig uppgift.

För strömning av data, oavsett om de ursprungliga data Àr i rad- eller kolumnformat, kvarstÄr ett alternativ att skicka smÄ paket av rader, dÀr varje paket innehÄller en kolumnlayout.

I Apache Arrow kallas en samling av kolumnarrayer i minnet, som representerar ett chank av en tabell, för en postbatch (record batch). För att representera en enda databasstruktur av en logisk tabell kan flera postbatcher samlas.

I det befintliga random access-formatet skriver vi metadata som innehÄller tabellens schema och blockens placering i slutet av filen, vilket gör att du mycket enkelt kan vÀlja vilken postbatch eller kolumn som helst frÄn datasetet. I det strömmande formatet skickar vi en serie meddelanden: schemat, följt av en eller flera postbatcher.

Olika format ser ungefÀr ut som det som visas i denna bild:

Strömmande överföring av kolonndata med Apache Arrow

Strömning av data i PyArrow: tillÀmpning

För att visa hur detta fungerar kommer jag att skapa ett exempel pÄ en dataset som representerar ett strömmande chank:

import time
import numpy as np
import pandas as pd
import pyarrow as pa

def generate_data(total_size, ncols):
    nrows = int(total_size / ncols / np.dtype('float64').itemsize)
    return pd.DataFrame({
        'c' + str(i): np.random.randn(nrows)
        for i in range(ncols)
    })	

Nu, lÄt oss anta att vi vill skriva 1 Gb av data som bestÄr av chankar pÄ 1 Mb vardera, totalt 1024 chankar. För att börja, lÄt oss skapa den första dataram som Àr 1 Mb stor med 16 kolumner:

KILOBYTE = 1 << 10
MEGABYTE = KILOBYTE * KILOBYTE
DATA_SIZE = 1024 * MEGABYTE
NCOLS = 16

df = generate_data(MEGABYTE, NCOLS)

Sedan konverterar jag dem till pyarrow.RecordBatch:

batch = pa.RecordBatch.from_pandas(df)

Nu ska jag skapa en utdataström som kommer att skriva till RAM och skapa StreamWriter:

sink = pa.InMemoryOutputStream()
stream_writer = pa.StreamWriter(sink, batch.schema)

DĂ€refter kommer vi att skriva 1024 block, vilket kommer att sammanfatta 1 GB av data:

for i in range(DATA_SIZE // MEGABYTE):
    stream_writer.write_batch(batch)

Eftersom vi skrev till RAM, kommer vi att kunna fÄ hela strömmen i en enda buffert:

In [13]: source = sink.get_result()

In [14]: source
Out[14]: 

In [15]: source.size
Out[15]: 1074750744

Eftersom dessa data finns i minnet, sÄ Àr lÀsningen av Arrow-paket en zero-copy operation. Jag öppnar StreamReader, lÀser data till pyarrow.Table, och konverterar dem sedan till DataFrame pandas:

In [16]: reader = pa.StreamReader(source)

In [17]: table = reader.read_all()

In [18]: table
Out[18]: 

In [19]: df = table.to_pandas()

In [20]: df.memory_usage().sum()
Out[20]: 1073741904

Detta Àr förstÄs bra, men du kanske har frÄgor. Hur snabbt sker detta? Hur pÄverkar blockstorleken prestandan för att fÄ DataFrame pandas?

Prestanda för datastreaming

NÀr blockstorleken minskar sÄ ökar kostnaden för att rekonstruera ett kontinuerligt kolumnformat DataFrame i pandas pÄ grund av ineffektiva cacheminnesÄtkomstscheman. Det finns ocksÄ vissa overheadkostnader för att arbeta med C++ datatyper och deras minnesbufferar.

För 1 MB, som angivet ovan, fÄr jag pÄ min bÀrbara dator (Quad-core Xeon E3-1505M):

In [20]: %timeit pa.StreamReader(source).read_all().to_pandas()
10 loops, best of 3: 129 ms per loop

Det visar sig att den effektiva bandbredden Àr 7,75 GB/s för att ÄterstÀlla en DataFrame pÄ 1 GB frÄn 1024 block om 1 MB. Vad hÀnder om vi anvÀnder större eller mindre block? HÀr Àr resultaten:

Strömmande överföring av kolonndata med Apache Arrow

Prestandan minskar avsevÀrt frÄn 256K till 64K block. Jag blev överraskad över att block om 1 MB hanterades snabbare Àn 16 MB. Det Àr vÀrt att göra en grundligare undersökning för att förstÄ om detta Àr en normal fördelning eller om nÄgot annat spelar in.

I den nuvarande implementeringen av formatet komprimeras data överhuvudtaget inte, sÄ storleken i minnet och "i överföringen" Àr ungefÀr densamma. I framtiden kan komprimering bli ett tillval.

Resultat

Strömmande överföring av kolumndata kan vara ett effektivt sÀtt att överföra stora datamÀngder till kolumnanalytiska verktyg, sÄsom pandas, med hjÀlp av smÄ bitar. DatatjÀnster som anvÀnder lagring inriktad pÄ rader kan överföra och transponera smÄ databit som Àr mer lÀmpliga för L2 och L3-cachen i din processor.

FullstÀndig kod

import time
import numpy as np
import pandas as pd
import pyarrow as pa

def generate_data(total_size, ncols):
    nrows = total_size / ncols / np.dtype('float64').itemsize
    return pd.DataFrame({
        'c' + str(i): np.random.randn(nrows)
        for i in range(ncols)
    })

KILOBYTE = 1 << 10
MEGABYTE = KILOBYTE * KILOBYTE
DATA_SIZE = 1024 * MEGABYTE
NCOLS = 16

def get_timing(f, niter):
    start = time.clock_gettime(time.CLOCK_REALTIME)
    for i in range(niter):
        f()
    return (time.clock_gettime(time.CLOCK_REALTIME) - start) / NITER

def read_as_dataframe(klass, source):
    reader = klass(source)
    table = reader.read_all()
    return table.to_pandas()
NITER = 5
results = []

CHUNKSIZES = [16 * KILOBYTE, 64 * KILOBYTE, 256 * KILOBYTE, MEGABYTE, 16 * MEGABYTE]

for chunksize in CHUNKSIZES:
    nchunks = DATA_SIZE // chunksize
    batch = pa.RecordBatch.from_pandas(generate_data(chunksize, NCOLS))

    sink = pa.InMemoryOutputStream()
    stream_writer = pa.StreamWriter(sink, batch.schema)

    for i in range(nchunks):
        stream_writer.write_batch(batch)

    source = sink.get_result()

    elapsed = get_timing(lambda: read_as_dataframe(pa.StreamReader, source), NITER)

    result = (chunksize, elapsed)
    print(result)
    results.append(result)

KĂ€lla: habr.com

Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar đŸ”„ Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar | ProHoster