Passa al contenuto principale
Lakebase

Carica terabyte di dati in pochi minuti su Lakebase Postgres

Caricamenti bulk più veloci e carichi di lavoro OLTP più sicuri con l'architettura LTAP

di Yecheng Yang, Nolan Biscaro, Szu-Po Wang e Pranav Aurora

  • L'architettura LTAP (Lake Transactional/Analytical Processing) scarica le pesanti operazioni in blocco dal calcolo primario di Lakebase Postgres a motori distribuiti come Spark.
  • Consentendo a Spark di creare pagine e indici Postgres validi in parallelo e di scrivere direttamente nello storage, Lakebase Postgres ottiene caricamenti dati fino a 147 volte più veloci senza consumare risorse dell'applicazione in esecuzione.
  • Il nodo primario di Lakebase Postgres pubblica il manifest finale tramite un record WAL compatto, garantendo che le query OLTP attive non subiscano impatti e mantenendo prestazioni elevate.

I database operativi come Postgres sono progettati per eseguire in modo affidabile query ad alta concorrenza e a bassa latenza su sottoinsiemi di dati. Ciò che spesso compromette questa affidabilità sono le operazioni in blocco, come il caricamento di terabyte di dati o l'esecuzione di query analitiche che scansionano un'intera tabella. Queste operazioni competono per le stesse risorse dei carichi di lavoro delle applicazioni, rischiando di degradare le prestazioni o causare tempi di inattività.

Il nostro obiettivo è rendere Lakebase Postgres il luogo più sicuro e affidabile per i tuoi carichi di lavoro operativi. Raggiungiamo questo obiettivo scaricando le operazioni batch pesanti dal compute primario a motori distribuiti come Spark, progettati appositamente per questi compiti. Ciò è reso possibile dall'architettura LTAP (Lake Transactional/Analytical Processing), che consente sia ai motori transazionali che a quelli analitici di lavorare esattamente sugli stessi dati nel lake.

Senza questo isolamento, i team sono spesso costretti a prestare estrema attenzione alle operazioni in blocco. Possono sacrificare la freschezza dei dati, eseguire carichi di rado durante le ore non di punta e gestire manualmente backfill e checkpoint complessi. Oggi, sfruttando l'architettura LTAP, possono scaricare completamente le pipeline di inserimento su Spark, garantendo che le app ricevano dati freschi senza compromettere le prestazioni del sistema in produzione.

Guardiamo i numeri

Durante la nostra fase beta, un cliente utilizzava Synced Tables per caricare circa 1 miliardo di righe in Lakebase ogni giorno. Sfruttando l'architettura LTAP, abbiamo accelerato drasticamente tali caricamenti, mantenendo del tutto inalterati i loro carichi di lavoro operativi.

image1.png

In precedenza, il loro caricamento in blocco richiedeva oltre 8 ore e saturava completamente CPU e memoria. Anche con la scalabilità automatica di Lakebase, erano costretti a sovradimensionare notevolmente le risorse OLTP solo per sopravvivere alla sincronizzazione. Questo accade perché le architetture Postgres tradizionali rendono il nodo primario l'unico gestore dello stato durevole. I caricamenti in blocco sono forzati attraverso questo singolo collo di bottiglia: ogni riga importata genera pagine di heap, aggiorna indici e scrive record WAL esattamente sulla stessa istanza che gestisce il traffico live dell'applicazione.

L'architettura LTAP ha sollevato completamente la pressione sull'istanza primaria, proteggendo il traffico live dell'applicazione. Ottieni la massima potenza di un motore distribuito, aumentando il throughput di caricamento in modo quasi lineare man mano che i dati crescono. I nostri benchmark interni mostrano che il caricamento di 1 TB ora richiede meno di 5 minuti.

image6.png

Nota: questo benchmark misura il tempo di caricamento dei dati (creazione delle pagine di heap). Stiamo lavorando attivamente anche alla parallelizzazione della creazione degli indici per questi dati caricati.

image7.png

Nel resto di questo post, approfondiremo le sfide del caricamento in blocco dei dati in un database OLTP come Postgres ed esploreremo come sfruttiamo l'architettura LTAP per risolverle.

Il problema dei caricamenti in blocco su larga scala in Postgres

Il comando nativo di Postgres COPY è efficiente per le acquisizioni standard di dimensioni minori.

Ma man mano che i clienti avvicinano i propri patrimoni di dati operativi e analitici, la scala cambia. Un carico di lavoro sempre più comune prevede il servizio di enormi tabelle Lakehouse di livello Gold ad applicazioni con pattern di query operativi. Inviare dati con un volume così estremo in un database di produzione evidenzia due limiti fondamentali:

  1. Il caricamento è intrinsecamente lento perché ogni riga importata deve passare attraverso un unico writer primario.
  2. La stessa elaborazione utilizzata per il caricamento gestisce anche le query dell'applicazione. I caricamenti in blocco richiedono un elevato consumo di CPU, I/O, connessioni e larghezza di banda WAL, entrando in competizione con il traffico OLTP.

Ciò rimane vero anche se si avvia il caricamento come processo Spark distribuito. Spark può leggere le partizioni di origine in parallelo, ma ogni riga deve comunque passare attraverso un unico writer Postgres:

  1. I worker inviano le righe a Postgres tramite COPY
  2. Il primario trasforma tali righe in pagine di heap e di indice
  3. Il primario registra le modifiche nel Write-Ahead Log (WAL)
  4. Il WAL deve essere salvato su una memoria di archiviazione durevole prima di confermare il caricamento

I caricamenti in blocco tradizionali sono limitati da un singolo writer

Il lato di origine può scalare orizzontalmente, ma il lato di destinazione no. L'aggiunta di executor Spark velocizza la scansione, ma non rimuove il collo di bottiglia del singolo writer. Inoltre, questo stesso primario Postgres gestisce le transazioni dell'applicazione online. Un'importazione di grandi dimensioni compete con esse per CPU, memoria, I/O, connessioni e larghezza di banda WAL. La latenza aumenta, i team pianificano i caricamenti nelle finestre orarie meno cariche e spesso dimensionano il primario per l'importazione più grande anziché per il traffico quotidiano.

Cosa cambia con LTAP

L'architettura LTAP apre una strada alternativa, perché il primario non è più l'unico modo per creare uno stato Postgres durevole. L'elaborazione transazionale è stateless nel lakebase: lo stato durevole risiede in un livello di archiviazione distribuito, non sul disco locale del primario. In un certo senso, Postgres è un client dell'archiviazione: gestisce query e transazioni, ma non deve essere necessariamente il processo che materializza ogni nuova pagina.

image2.png

Per i caricamenti in blocco, ciò significa che Spark può creare lo stato di Postgres e scriverlo nell'archiviazione, mentre il primario si limita a pubblicare il risultato.

Le conseguenze operative sono:

  • I caricamenti in blocco non competono con l'OLTP sul primario. L'operazione viene eseguita all'esterno dell'endpoint di elaborazione attivo. I carichi di lavoro dell'applicazione mantengono la CPU, l'I/O, le connessioni e la larghezza di banda WAL di cui hanno bisogno.
  • Caricamenti di grandi dimensioni verso la stessa destinazione possono essere eseguiti in modo concorrente. Ogni importazione termina scrivendo un singolo record WAL sul primario, quindi i caricamenti non si mettono più in coda dietro i flussi COPY degli altri.
  • Il primario non deve scalare in base alle dimensioni del carico. Un primario da 1 CU può continuare a gestire il traffico mentre Spark carica miliardi di righe su un'elaborazione separata, e l'elaborazione di Spark termina non appena il caricamento è completato.

Creazione di pagine Postgres valide, in modo sicuro e in parallelo

Esistono diverse ottimizzazioni per la creazione dei file Postgres e la costruzione dell'indice della chiave primaria.

Creazione dell'heap in parallelo

Le pagine prodotte da Spark devono essere valide per il database di destinazione, proprio come se le avesse create il suo primario.

Ogni executor Spark avvia un'istanza Postgres isolata (sandboxed) in modalità binary-upgrade, lo stesso meccanismo utilizzato da pg_upgrade per preservare gli OID del catalogo durante gli aggiornamenti delle versioni principali. Lo usiamo per trapiantare gli OID del catalogo di destinazione in ciascuna sandbox, garantendo che gli OID incorporati nelle pagine generate corrispondano a quelli della destinazione. Il driver assegna inoltre intervalli OID non sovrapposti alle sandbox, in modo che gli oggetti creati in modo concorrente non entrino in collisione.

All'interno di ciascuna sandbox, un file binario COPY con FREEZE crea la porzione di heap di quell'executor. Il congelamento (freezing) contrassegna le tuple importate come già confermate, in modo che Postgres possa considerarle visibili senza consultare la cronologia delle transazioni dalla sandbox.

Ogni worker calcola quindi i checksum per ogni pagina generata. I pageserver di Lakebase convalidano tali checksum quando acquisiscono i file, rilevando eventuali danneggiamenti prima che le pagine importate diventino autorevoli.

Insieme, la modalità binary-upgrade, le tuple congelate, gli OID coordinati e la convalida dei checksum garantiscono che Spark produca pagine che la destinazione può leggere come normali pagine Postgres. Implementiamo questo approccio tramite estensioni Postgres e un metodo di accesso alle tabelle, senza modificare il core di Postgres.

Le sandbox monouso consentono inoltre ottimizzazioni delle prestazioni, come le tabelle ausiliarie UNLOGGED. Si tratta di ottimizzazioni, non di meccanismi di sicurezza: evitano la contesa non necessaria di WAL e blocchi, poiché nessun traffico applicativo condivide la sandbox.

Si tratta comunque di "semplice Postgres"?
Sì. I file scritti da Spark sono pagine Postgres, non un formato di importazione che il primario deve poi tradurre. Abbiamo aggiunto estensioni e un metodo di accesso alle tabelle attorno a quel percorso, che è uno dei punti di forza di Postgres che ci consente di aggiungerne le funzionalità senza modificare il codice sorgente.

Creazione dell'indice senza scansionare l'heap

Un B-tree è una struttura ordinata sull'intero spazio delle chiavi e le sue voci foglia contengono coppie di chiavi e ID di tupla che puntano di nuovo all'heap.

In genere, Postgres ottiene tali coppie eseguendo la scansione dell'heap. Qui, tuttavia, l'heap è distribuito su slice caricate e scaricarlo interamente su un worker di creazione degli indici vanificherebbe gran parte del vantaggio derivante dalla creazione in parallelo.

Il creatore dell'indice non ha effettivamente bisogno del contenuto dell'heap. Ha bisogno del flusso di coppie (key, ctid) che verrebbe prodotto da una scansione dell'heap. Ciascun worker dell'heap del passaggio precedente esporta quindi le proprie colonne chiave e gli ID tupla in un file separato dell'object storage durante la creazione della sua slice.

Inseriamo tali record in una tabella ausiliaria contenente solo le colonne chiave esportate e l'ID tupla anziché le righe originali. Durante CREATE INDEX, un metodo di accesso alle tabelle personalizzato esegue la scansione della tabella ausiliaria in modo identico a heapam, ma scrive il ctid in ciascuna voce dell'indice anziché l'ID tupla fisico della riga ausiliaria. Man mano che i record vengono caricati, ogni ctid locale della slice viene traslato della dimensione cumulativa delle slice dell'heap precedenti, facendolo puntare alla posizione finale della tupla nell'heap concatenato. In breve, il builder B-tree standard di Postgres può eseguire la scansione di questa tabella ausiliaria senza modifiche e produrre un indice su un heap che non ha mai scaricato. Questo sostituisce lo spostamento dell'intero heap con lo spostamento della rappresentazione chiave e ID tupla, molto più piccola.

Passaggio del risultato a Postgres

Una volta che le slice dell'heap e dell'indice sono nell'object storage, il driver Spark scrive un manifesto e invoca una funzione SQL sulla destinazione. Il primario registra l'importazione come un record WAL compatto. Un COPY convenzionale invierebbe l'intero volume di dati attraverso il quorum safekeeper. Noi inviamo solo la descrizione dell'importazione.

I pageserver richiedono i file caricati come pagine autorevoli e ne convalidano i checksum. Eseguono anche il pre-warming delle pagine sull'SSD locale in modo che le letture iniziali non comportino recuperi a freddo dall'object storage. Infine, il primario scambia in modo atomico i dati in staging nella tabella sincronizzata visibile all'utente.

Fino a quando non viene eseguito il commit della transazione, la tabella importata rimane isolata. Successivamente, il primario vede le normali pagine heap e B-tree prodotte tramite formati e interfacce Postgres standard.

Inizia subito.

Utilizziamo l'architettura LTAP per alimentare Synced Tables, consentendo sincronizzazioni molto più rapide durante il serving dei dataset gold dal Lakehouse. Se stai eseguendo job di ReverseETL manuali dal Lakehouse, con Lakebase, dovresti prendere in considerazione l'utilizzo di Lakebase.

Siamo entusiasti di generalizzare questo protocollo per gestire grandi operazioni e manutenzioni, come la creazione di indici o persino le migrazioni. Lakebase con l'architettura LTAP è il posto migliore per eseguire carichi di lavoro OLTP.

Se non hai ancora provato Lakebase e Synced Tables, inizia subito.

(Questo post sul blog è stato tradotto utilizzando strumenti basati sull'intelligenza artificiale) Post originale

Ricevi gli ultimi articoli nella tua casella di posta

Iscriviti al nostro blog e ricevi gli ultimi articoli direttamente nella tua casella di posta.