Nella prima parte di questa serie di blog, ho presentato un paio di risultati di benchmark che mostrano come sono cambiate le prestazioni di PostgreSQL OLTP dalla versione 8.3, rilasciata nel 2008. In questa parte ho intenzione di fare la stessa cosa ma per query analitiche/BI, elaborando grandi quantità di dati.
Esistono numerosi benchmark del settore per testare questo carico di lavoro, ma probabilmente il più comunemente usato è TPC-H, quindi è quello che userò per questo post sul blog. C'è anche TPC-DS, un altro benchmark TPC per testare i sistemi di supporto alle decisioni, che può essere visto come un'evoluzione o una sostituzione di TPC-H. Ho deciso di attenermi a TPC-H per un paio di motivi.
In primo luogo, TPC-DS è molto più complesso, sia in termini di schema (più tabelle) che di numero di query (22 contro 99). Regolarlo correttamente, in particolare quando si ha a che fare con più versioni di PostgreSQL, sarebbe molto più difficile. In secondo luogo, alcune delle query TPC-DS utilizzano funzionalità che non sono supportate dalle versioni precedenti di PostgreSQL (ad es. set di raggruppamento), rendendo tali query irrilevanti per alcune versioni. E infine, direi che le persone hanno molta più familiarità con TPC-H rispetto a TPC-DS.
L'obiettivo di questo non è quello di consentire il confronto con altri prodotti di database, ma solo di fornire una ragionevole caratterizzazione a lungo termine su come si sono evolute le prestazioni di PostgreSQL da PostgreSQL 8.3.
Nota :Per un'analisi molto interessante del benchmark TPC-H, consiglio vivamente il documento "TPC-H Analyzed:Hidden Messages and Lessons Learned from an Influential Benchmark" di Boncz, Neumann ed Erling.
L'hardware
La maggior parte dei risultati in questo post del blog proviene dalla "scatola più grande" che ho nel nostro ufficio, che ha questi parametri:
- 2x E5-2620 v4 (16 core, 32 thread)
- 64 GB di RAM
- SSD Intel Optane 900P 280 GB NVMe (dati)
- 3 x 7.2k SATA RAID0 (tablespace temporaneo)
- kernel 5.6.15, filesystem ext4
Sono sicuro che puoi acquistare macchine significativamente più robuste, ma credo che questo sia abbastanza buono da fornirci dati rilevanti. C'erano due varianti di configurazione:una con parallelismo disabilitato, una con parallelismo abilitato. La maggior parte dei valori dei parametri sono gli stessi in entrambi i casi, sintonizzati sulle risorse hardware disponibili (CPU, RAM, storage). Puoi trovare informazioni più dettagliate sulla configurazione alla fine di questo post.
Il punto di riferimento
Voglio chiarire che non è mio obiettivo implementare un benchmark TPC-H valido che possa superare tutti i criteri richiesti dal TPC. Il mio obiettivo è valutare come le prestazioni di diverse query analitiche sono cambiate nel tempo, non inseguire una misura astratta delle prestazioni per dollaro o qualcosa del genere.
Quindi ho deciso di utilizzare solo un sottoinsieme di TPC-H, essenzialmente basta caricare i dati ed eseguire le 22 query (stessi parametri su tutte le versioni). Non ci sono aggiornamenti dei dati, il set di dati è statico dopo il caricamento iniziale. Ho selezionato un certo numero di fattori di scala, 1, 10 e 75, in modo da avere risultati per i buffer fit-in-shared (1), fit-in-memory (10) e more-than-memory (75) . Sceglierei 100 per renderlo una "bella sequenza", che in alcuni casi non si adatterebbe allo spazio di archiviazione da 280 GB (grazie a indici, file temporanei, ecc.). Si noti che il fattore di scala 75 non è nemmeno riconosciuto da TPC-H come fattore di scala valido.
Ma ha senso confrontare set di dati da 1 GB o 10 GB? Le persone tendono a concentrarsi su database molto più grandi, quindi potrebbe sembrare un po' sciocco preoccuparsi di testarli. Ma non credo che sarebbe utile:la stragrande maggioranza dei database in circolazione è piuttosto piccola, secondo la mia esperienza. E anche quando l'intero database è grande, le persone di solito lavorano solo con un piccolo sottoinsieme di esso:dati recenti, ordini irrisolti, ecc. Quindi credo che abbia senso testare anche con quei piccoli set di dati.
Caricamenti dati
Per prima cosa, vediamo quanto tempo ci vuole per caricare i dati nel database, senza e con parallelismo. Mostrerò solo i risultati del set di dati da 75 GB, perché il comportamento generale è quasi lo stesso per i casi più piccoli.
Durata del caricamento dei dati TPC-H:scalabilità di 75 GB, nessun parallelismo
Puoi vedere chiaramente che c'è una tendenza costante di miglioramenti, riducendo circa il 30% della durata semplicemente migliorando l'efficienza in tutti e quattro i passaggi:COPIA, creazione di chiavi primarie e indici e (soprattutto) configurazione di chiavi esterne. Il miglioramento "alter" in 9.2 è particolarmente evidente.
COPIA | PKEYS | INDICI | ALTER | |
8.3 | 2531 | 1156 | 1922 | 1615 |
8.4 | 2374 | 1171 | 1891 | 1370 |
9.0 | 2374 | 1137 | 1797 | 1282 |
9.1 | 2376 | 1118 | 1807 | 1268 |
9.2 | 2104 | 1120 | 1833 | 1157 |
9.3 | 2008 | 1089 | 1836 | 1229 |
9.4 | 1990 | 1168 | 1818 | 1197 |
9.5 | 1982 | 1000 | 1903 | 1203 |
9.6 | 1779 | 872 | 1797 | 1174 |
10 | 1773 | 777 | 1469 | 1012 |
11 | 1807 | 762 | 1492 | 758 |
12 | 1760 | 768 | 1513 | 741 |
13 | 1782 | 836 | 1587 | 675 |
Ora, vediamo come l'abilitazione del parallelismo cambia il comportamento. Il grafico seguente confronta i risultati con il parallelismo abilitato, contrassegnati da "(p)" - con i risultati con il parallelismo disabilitato.
Durata del caricamento dei dati TPC-H:scalabilità di 75 GB, parallelismo abilitato.
Sfortunatamente, sembra che l'effetto del parallelismo sia molto limitato in questo test:aiuta un po', ma le differenze sono piuttosto piccole. Quindi il miglioramento complessivo rimane di circa il 30%.
COPIA | PKEYS | INDICI | ALTER | |
9.6 | 344 | 3902 | 1786 | 831 |
9.6 (p) | 346 | 3781 | 1780 | 832 |
10 | 318 | 3259 | 1766 | 671 |
10 (p) | 315 | 3400 | 1769 | 693 |
11 | 319 | 3357 | 1817 | 690 |
11 (p) | 320 | 3144 | 1791 | 618 |
12 | 314 | 3643 | 1803 | 754 |
12 (p) | 313 | 3296 | 1752 | 657 |
13 | 276 | 3437 | 1790 | 744 |
13 (P) | 274 | 3011 | 1770 | 641 |
Query
Ora possiamo dare un'occhiata alle query. TPC-H ha 22 modelli di query:ho generato un set di query effettive e le ho eseguite due volte su tutte le versioni, prima dopo aver eliminato tutte le cache e riavviato l'istanza, quindi con la cache riscaldata. Tutti i numeri presentati nelle classifiche sono i migliori di queste due serie (nella maggior parte dei casi è la seconda, ovviamente).
Nessun parallelismo
Senza parallelismo, i risultati sul set di dati più piccolo sono abbastanza chiari:ogni barra è divisa in più parti con colori diversi per ciascuna delle 22 query. È difficile dire quale parte corrisponda a quale query esatta, ma è sufficiente per identificare i casi in cui una query migliora o peggiora notevolmente tra due esecuzioni. Ad esempio nel primo grafico è molto chiaro che il Q21 è diventato molto più veloce tra 8,3 e 8,4.
Query TPC-H su set di dati di piccole dimensioni (1 GB) – parallelismo disabilitato
Per la scala da 10 GB, i risultati sono piuttosto difficili da interpretare, perché su 8.3 una delle query (Q21) impiega così tanto tempo per essere eseguita che sminuisce tutto il resto.
Query TPC-H su set di dati medi (10 GB) – parallelismo disabilitato
Quindi vediamo come sarebbe il grafico senza Q21:
Query TPC-H su set di dati medi (10 GB):parallelismo disabilitato, senza problemi nel secondo trimestre
OK, è più facile da leggere. Possiamo vedere chiaramente che la maggior parte delle query (fino a Q17) è diventata più veloce, ma poi due delle query (Q18 e Q20) sono diventate leggermente più lente. Vedremo un problema simile sul set di dati più grande, quindi discuterò quale potrebbe essere la causa principale.
Query TPC-H su set di dati di grandi dimensioni (75 GB) – parallelismo disabilitato
Ancora una volta, vediamo un improvviso aumento per una delle query in 9.3 – questa volta è il secondo trimestre, senza il quale il grafico appare così:
Query TPC-H su set di dati di grandi dimensioni (75 GB):parallelismo disabilitato, senza problemi nel secondo trimestre
Questo è un bel miglioramento in generale, velocizzando l'intera esecuzione da ~2,7 ore a solo ~1,2 ore, semplicemente rendendo il pianificatore e l'ottimizzatore più intelligenti e rendendo l'esecutore più efficiente (ricorda, il parallelismo è stato disabilitato in queste esecuzioni) .
Quindi, quale potrebbe essere il problema con Q2, rendendolo più lento in 9.3? La semplice risposta è che ogni volta che rendi il pianificatore e l'ottimizzatore più intelligenti, sia costruendo nuovi tipi di percorsi/piani, sia rendendolo dipendente da alcune statistiche, significa anche che possono essere commessi nuovi errori quando le statistiche o le stime sono sbagliate. Nel secondo trimestre, la clausola WHERE fa riferimento a una sottoquery aggregata:una versione semplificata della query potrebbe essere simile a questa:
select 1 from partsupp where ps_supplycost = ( select min(ps_supplycost) from partsupp, supplier, nation, region where p_partkey = ps_partkey and s_suppkey = ps_suppkey and s_nationkey = n_nationkey and n_regionkey = r_regionkey and r_name = 'AMERICA' );;
Il problema è che non conosciamo il valore medio al momento della pianificazione, rendendo impossibile calcolare stime sufficientemente buone per la condizione WHERE. L'attuale Q2 contiene join aggiuntivi e la pianificazione di quelli dipende fondamentalmente da buone stime delle relazioni unite. Nelle versioni precedenti l'ottimizzatore sembra aver fatto la cosa giusta, ma poi nella 9.3 lo abbiamo reso in qualche modo più intelligente, ma con la scarsa stima non riesce a prendere la decisione giusta. In altre parole, i buoni piani nelle versioni precedenti erano solo fortuna, grazie alle limitazioni del pianificatore.
Scommetto che anche le regressioni di Q18 e Q20 sul set di dati più piccolo sono causate da qualcosa di simile, anche se non le ho studiate in dettaglio.
Credo che alcuni di questi problemi di ottimizzazione potrebbero essere risolti ottimizzando i parametri di costo (ad es. random_page_cost ecc.) Ma non l'ho provato a causa di vincoli di tempo. Tuttavia, mostra che gli aggiornamenti non migliorano automaticamente tutte le query:a volte un aggiornamento può innescare una regressione, quindi è una buona idea eseguire un test appropriato dell'applicazione.
Parallelismo
Vediamo quindi quanto il parallelismo delle query modifica i risultati. Anche in questo caso, esamineremo solo i risultati delle versioni dalla 9.6 etichettando i risultati con "(p)" in cui è abilitata la query parallela.
Query TPC-H su set di dati di piccole dimensioni (1 GB) – parallelismo abilitato
Chiaramente, il parallelismo aiuta parecchio:si riduce di circa il 30% anche su questo piccolo set di dati. Sul set di dati medio, non c'è molta differenza tra esecuzioni regolari e parallele:
Query TPC-H su set di dati medi (10 GB) – parallelismo abilitato
Questa è un'altra dimostrazione del problema già discusso:abilitare il parallelismo consente di prendere in considerazione piani di query aggiuntivi e chiaramente le stime o i costi non corrispondono alla realtà, con conseguenti scelte di piano scadenti.
E infine l'ampio set di dati, in cui i risultati completi si presentano così:
Query TPC-H su set di dati di grandi dimensioni (75 GB) – parallelismo abilitato
In questo caso, l'abilitazione del parallelismo funziona a nostro vantaggio:l'ottimizzatore riesce a costruire un piano parallelo più economico per il secondo trimestre, annullando la scelta del piano scadente introdotta in 9.3. Ma solo per completezza, ecco i risultati senza Q2.
Query TPC-H su set di dati di grandi dimensioni (75 GB):parallelismo abilitato, senza problemi nel secondo trimestre
Anche qui puoi individuare alcune scelte sbagliate del piano parallelo, ad esempio il piano parallelo per Q9 è peggiore fino a 11 dove diventa più veloce, probabilmente grazie a 11 che supportano nodi esecutori paralleli aggiuntivi. D'altra parte, alcune query parallele (Q18, Q20) diventano più lente su 11, quindi non si tratta solo di arcobaleni e unicorni.
Riepilogo e futuro
Penso che questi risultati dimostrino bene le ottimizzazioni implementate da PostgreSQL 8.3. I test con il parallelismo disabilitato illustrano miglioramenti nell'efficienza (ovvero facendo di più con la stessa quantità di risorse):i carichi di dati sono diventati circa il 30% più veloci e le query sono diventate circa 2 volte più veloci. È vero che ho riscontrato alcuni problemi con piani di query inefficienti, ma questo è un rischio intrinseco quando si rende più intelligente il pianificatore di query. Lavoriamo continuamente per rendere i risultati più affidabili e sono sicuro di poter mitigare la maggior parte di questi problemi ottimizzando un po' la configurazione.
I risultati con il parallelismo abilitato mostrano che possiamo utilizzare in modo efficace risorse extra (core CPU in particolare). I carichi di dati non sembrano trarne molto vantaggio, almeno non in questo benchmark, ma l'impatto sull'esecuzione delle query è significativo, con un conseguente aumento della velocità di circa 2 volte (sebbene query diverse siano interessate in modo diverso, ovviamente).
Ci sono molte opportunità per migliorare questo nelle future versioni di PostgreSQL. Ad esempio, c'è una serie di patch che implementa il parallelismo per COPY, accelerando il caricamento dei dati. Esistono varie patch che migliorano l'esecuzione delle query analitiche, dalle piccole ottimizzazioni localizzate ai grandi progetti come l'archiviazione e l'esecuzione delle colonne, il push-down aggregato, ecc. Si può ottenere molto anche utilizzando il partizionamento dichiarativo, una funzionalità che ho per lo più ignorato mentre lavoravo su questo benchmark, semplicemente perché aumenterebbe troppo il campo di applicazione. E sono sicuro che ci sono molte altre opportunità che non riesco nemmeno a immaginare, ma le persone più intelligenti nella comunità di PostgreSQL ci stanno già lavorando.
Appendice:configurazione di PostgreSQL
Parallelismo disabilitato
shared_buffers = 4GB work_mem = 128MB vacuum_cost_limit = 1000 max_wal_size = 24GB checkpoint_timeout = 30min checkpoint_completion_target = 0.9 # logging log_checkpoints = on log_connections = on log_disconnections = on log_line_prefix = '%t %c:%l %x/%v ' log_lock_waits = on log_temp_files = 1024 # parallel query max_parallel_workers_per_gather = 0 max_parallel_maintenance_workers = 0 # optimizer default_statistics_target = 1000 random_page_cost = 60 effective_cache_size = 32GB
Parallelismo abilitato
shared_buffers = 4GB work_mem = 128MB vacuum_cost_limit = 1000 max_wal_size = 24GB checkpoint_timeout = 30min checkpoint_completion_target = 0.9 # logging log_checkpoints = on log_connections = on log_disconnections = on log_line_prefix = '%t %c:%l %x/%v ' log_lock_waits = on log_temp_files = 1024 # parallel query max_parallel_workers_per_gather = 16 max_parallel_maintenance_workers = 16 max_worker_processes = 32 max_parallel_workers = 32 # optimizer default_statistics_target = 1000 random_page_cost = 60 effective_cache_size = 32GB