Cereri paralele în PostgreSQL

Cereri paralele în PostgreSQL
În procesoarele moderne există foarte multe nuclee. De ani de zile, aplicațiile trimiteau cereri în baze de date în paralel. Dacă este o cerere de raportare pentru un număr mare de rânduri dintr-o tabelă, aceasta se execută mai repede atunci când utilizează mai multe procesoare, iar în PostgreSQL acest lucru este posibil începând cu versiunea 9.6.

Au fost necesari 3 ani pentru a implementa funcția cererilor paralele — a fost necesar să rescriem codul în diferite etape ale execuției cererilor. În PostgreSQL 9.6 a fost introdusă infrastructura pentru îmbunătățirea ulterioară a codului. În versiunile ulterioare, și alte tipuri de cereri sunt executate în paralel.

Limitări

  • Nu activați execuția paralelă dacă toate nucleele sunt deja ocupate, altfel alte cereri vor fi încetinite.
  • Cel mai important, procesarea paralelă cu valori mari WORK_MEM folosește multă memorie — fiecare conexiune hash sau sortare ocupă memorie în volum de work_mem.
  • Cererile OLTP cu latență mică nu pot fi accelerate prin execuția paralelă. Iar dacă cererea returnează un singur rând, procesarea paralelă o va încetini.
  • Dezvoltatorii iubesc să folosească benchmark-ul TPC-H. Poate că aveți cereri asemănătoare pentru o execuție paralelă ideală.
  • Numai cererile SELECT fără blocare prin predicat sunt executate în paralel.
  • Uneori, indexarea corectă este mai bună decât scanarea secvențială a tabelului în modul paralel.
  • Suspendarea cererilor și cursurile nu sunt suportate.
  • Funcțiile fereastră și funcțiile agregate ale seturilor ordonate nu sunt paralele.
  • Nu obțineți nimic în sarcina de lucru de intrare-ieșire.
  • Nu există algoritmi de sortare paralele. Dar cererile cu sortări pot fi executate în paralel în anumite aspecte.
  • Înlocuiți CTE (WITH …) cu un SELECT încorporat pentru a activa procesarea paralelă.
  • Înfășurăturile de date externe nu suportă încă procesarea paralelă (dar ar putea!)
  • FULL OUTER JOIN nu este suportat.
  • max_rows dezactivează procesarea paralelă.
  • Dacă în cerere există o funcție care nu este marcată ca PARALLEL SAFE, aceasta va fi unică.
  • Nivelul de izolare al tranzacției SERIALIZABLE dezactivează procesarea paralelă.

Mediu de testare

Dezvoltatorii PostgreSQL au încercat să reducă timpul de răspuns al cererilor benchmark-ului TPC-H. Descărcați benchmark-ul și adaptați-l la PostgreSQL. Aceasta este o utilizare neoficială a benchmark-ului TPC-H — nu pentru compararea bazelor de date sau a echipamentelor.

  1. Descărcați TPC-H_Tools_v2.17.3.zip (sau o versiune mai recentă) de pe site-ul TPC.
  2. Renumiți makefile.suite în Makefile și modificați-l conform instrucțiunilor de aici: https://github.com/tvondra/pg_tpch . Compilați codul cu comanda make.
  3. Generați datele: .\/dbgen -s 10 creează o bază de date de 23 GB. Acest lucru este suficient pentru a observa diferența de performanță între interogările paralele și cele neparalele.
  4. Conversați fișierele tbl în csv cu for și sed.
  5. Clonare repository pg_tpch și copiați fișierele csv în pg_tpch\/dss\/data.
  6. Creați interogările cu comanda qgen.
  7. Încărcați datele în bază cu comanda .\/tpch.sh.

Scanare secvențială paralelă

Aceasta poate fi mai rapidă nu din cauza citirii paralele, ci pentru că datele sunt dispersate pe multe nuclee ale CPU-ului. În sistemele de operare moderne, fișierele de date PostgreSQL sunt bine cache-uie. Cu citirea anticipată, se pot obține blocuri mai mari din magazin decât cererea demonului PG. Prin urmare, performanța interogării nu este limitată de intrările și ieșirile discului. Consuma cicluri CPU pentru a:

  • citi rânduri câte unul de pe paginile tabelului;
  • compara valorile rândurilor și condițiile WHERE.

Să executăm o interogare simplă select:

tpch=# explain analyze select l_quantity as sum_qty from lineitem where l_shipdate <= date '1998-12-01' - interval '105' day;
PLANUL INTEROGĂRII
--------------------------------------------------------------------------------------------------------------------------
Scanare Secvențială pe lineitem (cost=0.00..1964772.00 rows=58856235 width=5) (timp efectiv=0.014..16951.669 rows=58839715 loops=1)
Filtru: (l_shipdate <= '1998-08-18 00:00:00'::timestamp fără fus orar)
Rânduri eliminate de Filtru: 1146337
Timp de planificare: 0.203 ms
Timp de execuție: 19035.100 ms

Scanarea secvențială oferă prea multe rânduri fără agregare, astfel încât interogarea este executată pe un singur nucleu CPU.

Dacă adăugăm SUM(), se observă că două procese de lucru ajută la accelerarea interogării:

explain analyze select sum(l_quantity) as sum_qty from lineitem where l_shipdate <= date '1998-12-01' - interval '105' day;
PLANUL INTEROGĂRII
----------------------------------------------------------------------------------------------------------------------------------------------------
Finalizează Agregarea (cost=1589702.14..1589702.15 rows=1 width=32) (timp efectiv=8553.365..8553.365 rows=1 loops=1)
-> Adună (cost=1589701.91..1589702.12 rows=2 width=32) (timp efectiv=8553.241..8555.067 rows=3 loops=1)
Lucrători Planificați: 2
Lucrători Lansati: 2
-> Agregare Parțială (cost=1588701.91..1588701.92 rows=1 width=32) (timp efectiv=8547.546..8547.546 rows=1 loops=3)
-> Scanare Secvențială Paralelă pe lineitem (cost=0.00..1527393.33 rows=24523431 width=5) (timp efectiv=0.038..5998.417 rows=19613238 loops=3)
Filtru: (l_shipdate <= '1998-08-18 00:00:00'::timestamp fără fus orar)
Rânduri eliminate de Filtru: 382112
Timp de planificare: 0.241 ms
Timp de execuție: 8555.131 ms

Agregare paralelă

Nodul „Scanare Secvențială Paralelă” generează rânduri pentru agregarea parțială. Nodul „Agregare Parțială” taie aceste rânduri folosind SUM(). La final, contorul SUM din fiecare proces de lucru este adunat de nodul „Adună”.

Rezultatul final este calculat de nodul „Finalize Aggregate”. Dacă aveți funcții de agregare proprii, nu uitați să le marcați ca „parallel safe”.

Numărul de procese de lucru

Numărul de procese de lucru poate fi crescut fără a reporni serverul:

explain analyze select sum(l_quantity) as sum_qty from lineitem where l_shipdate <= date '1998-12-01' - interval '105' day;
PLANUL INTEROGĂRII
----------------------------------------------------------------------------------------------------------------------------------------------------
Finalizează Agregarea (cost=1589702.14..1589702.15 rows=1 width=32) (timp efectiv=8553.365..8553.365 rows=1 loops=1)
-> Adună (cost=1589701.91..1589702.12 rows=2 width=32) (timp efectiv=8553.241..8555.067 rows=3 loops=1)
Lucrători Planificați: 2
Lucrători Lansati: 2
-> Agregare Parțială (cost=1588701.91..1588701.92 rows=1 width=32) (timp efectiv=8547.546..8547.546 rows=1 loops=3)
-> Scanare Secvențială Paralelă pe lineitem (cost=0.00..1527393.33 rows=24523431 width=5) (timp efectiv=0.038..5998.417 rows=19613238 loops=3)
Filtru: (l_shipdate <= '1998-08-18 00:00:00'::timestamp fără fus orar)
Rânduri eliminate de Filtru: 382112
Timp de planificare: 0.241 ms
Timp de execuție: 8555.131 ms

Ce se întâmplă aici? Numărul de procese de lucru s-a dublat, iar cererea a devenit de doar 1,6599 ori mai rapidă. Calculul este interesant. Aveam 2 procese de lucru și 1 lider. După modificare, avem 4+1.

Accelerația maximă obținută prin procesare paralelă este: 5/3 = 1,66(6) ori.

Cum funcționează?

Procese

Executarea cererii începe întotdeauna cu procesul de conducere. Liderul se ocupă de tot ce este nepătruns și de o parte din procesarea paralelă. Altele procese care execută aceleași cereri sunt numite procese de lucru. Procesarea paralelă folosește infrastructura proceselor de lucru de fundal dinamice (începând cu versiunea 9.4). Deoarece alte părți ale PostgreSQL folosesc procese, nu fire, o cerere cu 3 procese de lucru ar putea fi cu 4 ori mai rapidă decât procesarea tradițională.

Interacțiune

Procesele de lucru comunică cu liderul printr-o coadă de mesaje (bazată pe memorie comună). Fiecare proces are 2 cozi: pentru erori și pentru tupluri.

Câte procese de lucru sunt necesare?

Limitarea minimă este stabilită de parametrul max_parallel_workers_per_gather. Apoi, executorul cererii ia procese de lucru din pool-ul limitat de parametrul max_parallel_workers size. Ultima limitare este max_worker_processes, adică numărul total de procese de fundal.

Dacă nu s-a reușit alocarea unui proces de lucru, procesarea va fi uniprotocarea.

Planificatorul de cereri poate reduce procesele de lucru în funcție de dimensiunea tabelei sau a indexului. Pentru aceasta, există parametrii min_parallel_table_scan_size și min_parallel_index_scan_size.

set min_parallel_table_scan_size='8MB'
8MB tabel => 1 muncitor
24MB tabel => 2 muncitori
72MB tabel => 3 muncitori
x => log(x / min_parallel_table_scan_size) / log(3) + 1 muncitor

De fiecare dată când tabela este de 3 ori mai mare decât min_parallel_(index|table)_scan_size, Postgres adaugă un proces de lucru. Numărul de procese de lucru nu se bazează pe costuri. Dependența circulară îngreunează implementările complexe. În schimb, planificatorul folosește reguli simple.

În practică, aceste reguli nu sunt întotdeauna adecvate pentru producție, așa că se poate modifica numărul de procese de lucru pentru o tabelă specifică: ALTER TABLE … SET (parallel_workers = N).

De ce nu se utilizează procesarea paralelă?

Pe lângă lunga listă de restricții, există și verificări ale costurilor:

costul_setării_paralele — pentru a evita procesarea paralelă a cererilor scurte. Acest parametru estimează timpul pentru pregătirea memoriei, pornirea procesului și schimbul inițial de date.

costul_tuple-urilor_paralele: comunicarea liderului cu lucrătorii poate dura proporțional cu numărul de tuple-uri din procesele de lucru. Acest parametru calculează costurile schimbului de date.

Îmbinările ciclice înfășurate — Nested Loop Join

PostgreSQL 9.6+ poate executa bucle înlănțuite în paralel — aceasta este o operațiune simplă.

explain (costs off) select c_custkey, count(o_orderkey)
 from customer left outer join orders on
 c_custkey = o_custkey and o_comment not like '%specialposits%'
 group by c_custkey;
 QUERY PLAN
--------------------------------------------------------------------------------------
 Finalize GroupAggregate
 Group Key: customer.c_custkey
 -> Gather Merge
 Workers Planned: 4
 -> Partial GroupAggregate
 Group Key: customer.c_custkey
 -> Nested Loop Left Join
 -> Parallel Index Only Scan using customer_pkey on customer
 -> Index Scan using idx_orders_custkey on orders
 Index Cond: (customer.c_custkey = o_custkey)
 Filter: ((o_comment)::text !~~ '%specialposits%'::text)

Colectarea se face în ultima etapă, astfel că Nested Loop Left Join — este o operație paralelă. Parallel Index Only Scan a apărut doar în versiunea 10. Funcționează similar cu scanarea secvențială paralelă. Condiția c_custkey = o_custkey citește o comandă pentru fiecare linie a clientului. Așadar, nu este paralel.

Îmbinarea prin hashtable — Hash Join

Fiecare proces de lucru își creează propria tabelă hash până la PostgreSQL 11. Și dacă procesele sunt mai mult de patru, performanța nu va crește. În noua versiune, tabela hash este comună. Fiecare proces de lucru poate utiliza WORK_MEM pentru a crea o tabelă hash.

select
        l_shipmode,
        sum(case
                when o_orderpriority = '1-URGENT'
                        or o_orderpriority = '2-HIGH'
                        then 1
                else 0
        end) as high_line_count,
        sum(case
                when o_orderpriority  '1-URGENT'
                        and o_orderpriority  '2-HIGH'
                        then 1
                else 0
        end) as low_line_count
from
        orders,
        lineitem
where
        o_orderkey = l_orderkey
        and l_shipmode in ('MAIL', 'AIR')
        and l_commitdate < l_receiptdate
        and l_shipdate = date '1996-01-01'
        and l_receiptdate   Finalize GroupAggregate  (cost=1964755.66..1966196.11 rows=7 width=27) (actual time=7579.590..7579.591 rows=1 loops=1)
         Group Key: lineitem.l_shipmode
         ->  Gather Merge  (cost=1964755.66..1966195.83 rows=28 width=27) (actual time=7559.593..7922.319 rows=6 loops=1)
               Workers Planned: 4
               Workers Launched: 4
               ->  Partial GroupAggregate  (cost=1963755.61..1965192.44 rows=7 width=27) (actual time=7548.103..7564.592 rows=2 loops=5)
                     Group Key: lineitem.l_shipmode
                     ->  Sort  (cost=1963755.61..1963935.20 rows=71838 width=27) (actual time=7530.280..7539.688 rows=62519 loops=5)
                           Sort Key: lineitem.l_shipmode
                           Sort Method: external merge  Disk: 2304kB
                           Worker 0:  Sort Method: external merge  Disk: 2064kB
                           Worker 1:  Sort Method: external merge  Disk: 2384kB
                           Worker 2:  Sort Method: external merge  Disk: 2264kB
                           Worker 3:  Sort Method: external merge  Disk: 2336kB
                           ->  Parallel Hash Join  (cost=382571.01..1957960.99 rows=71838 width=27) (actual time=7036.917..7499.692 rows=62519 loops=5)
                                 Hash Cond: (lineitem.l_orderkey = orders.o_orderkey)
                                 ->  Parallel Seq Scan on lineitem  (cost=0.00..1552386.40 rows=71838 width=19) (actual time=0.583..4901.063 rows=62519 loops=5)
                                       Filter: ((l_shipmode = ANY ('{MAIL,AIR}'::bpchar[])) AND (l_commitdate < l_receiptdate) AND (l_shipdate = '1996-01-01'::date) AND (l_receiptdate   Parallel Hash  (cost=313722.45..313722.45 rows=3750045 width=20) (actual time=2011.518..2011.518 rows=3000000 loops=5)
                                       Buckets: 65536  Batches: 256  Memory Usage: 3840kB
                                       ->  Parallel Seq Scan on orders  (cost=0.00..313722.45 rows=3750045 width=20) (actual time=0.029..995.948 rows=3000000 loops=5)
 Planning Time: 0.977 ms
 Execution Time: 7923.770 ms

Interogarea 12 din TPC-H arată clar un join hash paralel. Fiecare proces lucrează la crearea unei tabele hash comune.

Îmbinarea — Merge Join

Îmbinarea este prin natura sa non-paralelă. Nu vă faceți griji dacă aceasta este ultima etapă a interogării — poate totuși fi executată în paralel.

-- Interogare 2 din TPC-H
explain (costuri dezactivate) select s_acctbal, s_name, n_name, p_partkey, p_mfgr, s_address, s_phone, s_comment
from    part, supplier, partsupp, nation, region
where
        p_partkey = ps_partkey
        and s_suppkey = ps_suppkey
        and p_size = 36
        and p_type like '%BRASS'
        and s_nationkey = n_nationkey
        and n_regionkey = r_regionkey
        and r_name = 'AMERICA'
        and 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'
        )
order by s_acctbal desc, n_name, s_name, p_partkey
LIMIT 100;
                                                PLAN DE INTEROGARE
----------------------------------------------------------------------------------------------------------
 Limit
   -&gt;  Sort
         Cheie Sortare: supplier.s_acctbal DESC, nation.n_name, supplier.s_name, part.p_partkey
         -&gt;  Merge Join
               Condiție de îmbinare: (part.p_partkey = partsupp.ps_partkey)
               Filtru de îmbinare: (partsupp.ps_supplycost = (SubPlan 1))
               -&gt;  Gather Merge
                     Lucrători planificați: 4
                     -&gt;  Scan de Indice Paralel folosind <strong>part_pkey</strong> on part
                           Filtru: (((p_type)::text ~~ '%BRASS'::text) ȘI (p_size = 36))
               -&gt;  Materialize
                     -&gt;  Sort
                           Cheie Sortare: partsupp.ps_partkey
                           -&gt;  Nested Loop
                                 -&gt;  Nested Loop
                                       Filtru de îmbinare: (nation.n_regionkey = region.r_regionkey)
                                       -&gt;  Scan Secvențial pe region
                                             Filtru: (r_name = 'AMERICA'::bpchar)
                                       -&gt;  Hash Join
                                             Condiție Hash: (supplier.s_nationkey = nation.n_nationkey)
                                             -&gt;  Scan Secvențial pe supplier
                                             -&gt;  Hash
                                                   -&gt;  Scan Secvențial pe nation
                                 -&gt;  Scan de Indice folosind idx_partsupp_suppkey pe partsupp
                                       Condiție de Indice: (ps_suppkey = supplier.s_suppkey)
               SubPlan 1
                 -&gt;  Aggregate
                       -&gt;  Nested Loop
                             Filtru de îmbinare: (nation_1.n_regionkey = region_1.r_regionkey)
                             -&gt;  Scan Secvențial pe region region_1
                                   Filtru: (r_name = 'AMERICA'::bpchar)
                             -&gt;  Nested Loop
                                   -&gt;  Nested Loop
                                         -&gt;  Scan de Indice folosind idx_partsupp_partkey pe partsupp partsupp_1
                                               Condiție de Indice: (part.p_partkey = ps_partkey)
                                         -&gt;  Scan de Indice folosind supplier_pkey pe supplier supplier_1
                                               Condiție de Indice: (s_suppkey = partsupp_1.ps_suppkey)
                                   -&gt;  Scan de Indice folosind nation_pkey pe nation nation_1
                                         Condiție de Indice: (n_nationkey = supplier_1.s_nationkey)

Nodul „Merge Join” se află deasupra „Gather Merge”. Astfel, îmbinarea nu folosește procesare paralelă. Totuși, nodul „Parallel Index Scan” ajută în continuare cu segmentul part_pkey.

Îmbinarea pe secțiuni

În PostgreSQL 11 îmbinarea pe secțiuni dezactivat implicit: are un plan foarte costisitor. Tabele cu secționare similare pot fi îmbinate secțiune cu secțiune. Astfel, Postgres va utiliza table hash mai mici. Fiecare îmbinare de secțiuni poate fi paralelă.

tpch=# set enable_partitionwise_join=t;
tpch=# explain (costs off) select * from prt1 t1, prt2 t2
where t1.a = t2.b and t1.b = 0 and t2.b between 0 and 10000;
                    PLAN INTEROGARE
---------------------------------------------------
 Append
   ->  Hash Join
         Condiție Hash: (t2.b = t1.a)
         ->  Scanare Secvențială pe prt2_p1 t2
               Filtru: ((b >= 0) ȘI (b <= 10000))
         ->  Hash
               ->  Scanare Secvențială pe prt1_p1 t1
                     Filtru: (b = 0)
   ->  Hash Join
         Condiție Hash: (t2_1.b = t1_1.a)
         ->  Scanare Secvențială pe prt2_p2 t2_1
               Filtru: ((b >= 0) ȘI (b <= 10000))
         ->  Hash
               ->  Scanare Secvențială pe prt1_p2 t1_1
                     Filtru: (b = 0)
tpch=# set parallel_setup_cost = 1;
tpch=# set parallel_tuple_cost = 0.01;
tpch=# explain (costs off) select * from prt1 t1, prt2 t2
where t1.a = t2.b and t1.b = 0 and t2.b between 0 and 10000;
                        PLAN INTEROGARE
-----------------------------------------------------------
 Gather
   Lucrători Planificați: 4
   ->  Îmbinare paralelă
         ->  Îmbinare Hash Paralelă
               Condiție Hash: (t2_1.b = t1_1.a)
               ->  Scanare Secvențială Paralelă pe prt2_p2 t2_1
                     Filtru: ((b >= 0) ȘI (b <= 10000))
               ->  Hash
                     ->  Scanare Secvențială Paralelă pe prt1_p2 t1_1
                           Filtru: (b = 0)
         ->  Îmbinare Hash Paralelă
               Condiție Hash: (t2.b = t1.a)
               ->  Scanare Secvențială Paralelă pe prt2_p1 t2
                     Filtru: ((b >= 0) ȘI (b <= 10000))
               ->  Hash
                     ->  Scanare Secvențială Paralelă pe prt1_p1 t1
                           Filtru: (b = 0)

Principalul aspect, îmbinarea pe secțiuni este paralelă, doar dacă acele secțiuni sunt suficient de mari.

Adăugare paralelă — Parallel Append

Parallel Append poate fi utilizat în locul diferitelor blocuri în diferite procese de lucru. De obicei, acest lucru se întâmplă cu interogări UNION ALL. Dezavantajul este că există mai puțin paralelism, deoarece fiecare proces de lucru procesează doar o interogare.

Aici sunt active 2 procese de lucru, deși sunt activate 4.

tpch=# explain (costs off) select sum(l_quantity) as sum_qty from lineitem where l_shipdate <= date '1998-12-01' - interval '105' day union all select sum(l_quantity) as sum_qty from lineitem where l_shipdate <= date '2000-12-01' - interval '105' day;
                                           QUERY PLAN
------------------------------------------------------------------------------------------------
 Gather
   Workers Planned: 2
   ->  Parallel Append
         ->  Aggregate
               ->  Seq Scan on lineitem
                     Filter: (l_shipdate <= '2000-08-18 00:00:00'::timestamp without time zone)
         ->  Aggregate
               ->  Seq Scan on lineitem lineitem_1
                     Filter: (l_shipdate <= '1998-08-18 00:00:00'::timestamp without time zone)

Cele mai importante variabile

  • WORK_MEM limitează cantitatea de memorie pentru fiecare proces, nu doar pentru interogări: work_mem procese conexiuni = foarte multă memorie.
  • max_parallel_workers_per_gather — câte procese de lucru va utiliza programul pentru procesarea paralelă din plan.
  • max_worker_processes — ajustează numărul total de procese de lucru la numărul de nuclee CPU de pe server.
  • max_parallel_workers — același lucru, dar pentru procese de lucru paralele.

Concluzii

Începând cu versiunea 9.6, procesarea paralelă poate îmbunătăți semnificativ performanța interogărilor complexe care scanează multe rânduri sau indexuri. În PostgreSQL 10, procesarea paralelă este activată implicit. Nu uitați să o dezactivați pe serverele cu o sarcină de lucru OLTP mare. Scanările secvențiale sau scanările indexurilor consumă foarte multe resurse. Dacă nu efectuați un raport pe întregul set de date, interogările pot fi mai eficiente, adăugând pur și simplu indexurile lipsă sau folosind o partiționare corectă.

Linkuri

Sursa: habr.com

Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS 🔥 Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS | ProHoster