Caricamenti bulk più veloci e carichi di lavoro OLTP più sicuri con l'architettura LTAP
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.
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.

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.

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.

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 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:
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:
COPYI 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.
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.

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:
COPY degli altri.Esistono diverse ottimizzazioni per la creazione dei file Postgres e la costruzione dell'indice della chiave primaria.
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. |
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.
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.
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
Iscriviti al nostro blog e ricevi gli ultimi articoli direttamente nella tua casella di posta.