Parallelle aanvragen in PostgreSQL

Parallelle aanvragen in PostgreSQL
Moderne CPU's hebben veel kernen. Jarenlang hebben applicaties aanvragen parallel naar databases gestuurd. Als het gaat om een rapportaanvraag van meerdere rijen in een tabel, wordt deze sneller uitgevoerd wanneer meerdere CPU's zijn ingezet. In PostgreSQL is dit mogelijk sinds versie 9.6.

Het duurde 3 jaar om de functie voor parallelle aanvragen te implementeren — de code moest op verschillende momenten van de uitvoering van aanvragen herzien worden. PostgreSQL 9.6 introduceerde de infrastructuur voor verdere verbetering van de code. In latere versies kunnen ook andere typen aanvragen parallel worden uitgevoerd.

Beperkingen

  • Schakel parallelle uitvoering niet in als alle kernen al in gebruik zijn, anders zullen andere aanvragen traag worden.
  • Het belangrijkste is dat parallelle verwerking met hoge waarden van WORK_MEM veel geheugen gebruikt — elke hash-verbinding of sortering neemt geheugen in beslag ter grootte van work_mem.
  • OLTP-aanvragen met lage latentie kunnen niet versneld worden door parallelle uitvoering. En als een aanvraag één rij retourneert, vertraagt parallelle verwerking deze alleen maar.
  • Ontwikkelaars houden ervan om de TPC-H benchmark te gebruiken. Misschien hebt u vergelijkbare aanvragen voor perfecte parallelle uitvoering.
  • Alleen SELECT-aanvragen zonder predicatenblokkering worden parallel uitgevoerd.
  • Soms is goede indexing beter dan sequentiële scanning van de tabel in parallelle modus.
  • Het pauzeren van aanvragen en cursors wordt niet ondersteund.
  • Raamfuncties en aggregatiefuncties van geordende sets zijn niet parallel.
  • U wint niets in de invoer-uitvoer werkbelasting.
  • Er zijn geen parallelle sorteeralgoritmen. Maar aanvragen met sorteringen kunnen in bepaalde aspecten parallel worden uitgevoerd.
  • Vervang CTE (WITH …) door een genestelde SELECT om parallelle verwerking mogelijk te maken.
  • Wraps voor externe gegevens ondersteunen momenteel geen parallelle verwerking (wat ze zouden kunnen!).
  • FULL OUTER JOIN wordt niet ondersteund.
  • max_rows schakelt parallelle verwerking uit.
  • Als er in een aanvraag een functie is die niet gemarkeerd is als PARALLEL SAFE, wordt deze enkelvoudig uitgevoerd.
  • Het isolatieniveau van de transactie SERIALIZABLE schakelt parallelle verwerking uit.

Testomgeving

Ontwikkelaars van PostgreSQL hebben geprobeerd de responstijd van TPC-H benchmark aanvragen te verlagen. Download de benchmark en pas deze aan voor PostgreSQL. Dit is een niet-officiële gebruik van de TPC-H benchmark - niet voor het vergelijken van databases of hardware.

  1. Download TPC-H_Tools_v2.17.3.zip (of een nieuwere versie) van de TPC-website.
  2. Hernoem makefile.suite naar Makefile en wijzig het zoals hier beschreven: https://github.com/tvondra/pg_tpch . Compileer de code met het commando make.
  3. Genereer gegevens: .\/dbgen -s 10 maakt een database van 23 GB. Dit is voldoende om het verschil in prestaties van parallelle en niet-parallelle queries te zien.
  4. Converteer bestanden tbl in csv met for en sed.
  5. Clone de repository pg_tpch en kopieer de bestanden csv in pg_tpch\/dss\/data.
  6. Maak de queries met het commando qgen.
  7. Laad de gegevens in de database met het commando .\/tpch.sh.

Parallel sequente scans

Het kan sneller zijn, niet vanwege parallelle lezing, maar omdat de gegevens verspreid zijn over veel CPU-kernen. In moderne besturingssystemen worden PostgreSQL-gegevensbestanden goed gecached. Met vooruitlezing kan meer dan het door de PG-daemon aangevraagde blok uit de opslag worden gehaald. Daarom wordt de query-prestatie niet beperkt door schijfinvoer/-uitvoer. Het verbruikt CPU-cycli om:

  • rijen één voor één te lezen van de tabelpagina's;
  • waarden van rijen en voorwaarden te vergelijken WAAR.

Laten we een eenvoudige query uitvoeren select:

tpch=# explain analyze select l_quantity as sum_qty from lineitem where l_shipdate <= date '1998-12-01' - interval '105' day;
QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------
Seq Scan on lineitem (cost=0.00..1964772.00 rows=58856235 width=5) (actual time=0.014..16951.669 rows=58839715 loops=1)
Filter: (l_shipdate <= '1998-08-18 00:00:00'::timestamp zonder tijdzone)
Rows Removed by Filter: 1146337
Planning Time: 0.203 ms
Execution Time: 19035.100 ms

Een sequentiële scan geeft te veel rijen zonder aggregatie, zodat de query met één CPU-kern wordt uitgevoerd.

Als we toevoegen SUM(), is het duidelijk dat twee werkprocessen de query zullen versnellen:

explain analyze select sum(l_quantity) as sum_qty from lineitem where l_shipdate  Gather (cost=1589701.91..1589702.12 rows=2 width=32) (actual time=8553.241..8555.067 rows=3 loops=1)
Geplande Werkers: 2
Geluide Werkers: 2
-> Partial Aggregate (cost=1588701.91..1588701.92 rows=1 width=32) (actual time=8547.546..8547.546 rows=1 loops=3)
-> Parallel Seq Scan op lineitem (cost=0.00..1527393.33 rows=24523431 width=5) (actual time=0.038..5998.417 rows=19613238 loops=3)
Filter: (l_shipdate <= '1998-08-18 00:00:00'::timestamp zonder tijdzone)
Rows Removed by Filter: 382112
Planning Time: 0.241 ms
Execution Time: 8555.131 ms

Parallel aggregatie

De node «Parallel Seq Scan» genereert rijen voor gedeeltelijke aggregatie. De node «Partial Aggregate» snijdt deze rijen af met behulp van SUM(). Aan het einde worden de SUM-tellers van elk werkproces verzameld door de node «Gather».

Het uiteindelijke resultaat wordt berekend door de node «Finalize Aggregate». Als je eigen aggregatiefuncties hebt, vergeet dan niet ze als «parallel safe» te markeren.

Aantal werkprocessen

Het aantal werkprocessen kan zonder de server opnieuw op te starten worden vergroot:

explain analyze select sum(l_quantity) as sum_qty from lineitem where l_shipdate  Gather (cost=1589701.91..1589702.12 rows=2 width=32) (actual time=8553.241..8555.067 rows=3 loops=1)
Geplande Werkers: 2
Geluide Werkers: 2
-> Partial Aggregate (cost=1588701.91..1588701.92 rows=1 width=32) (actual time=8547.546..8547.546 rows=1 loops=3)
-> Parallel Seq Scan op lineitem (cost=0.00..1527393.33 rows=24523431 width=5) (actual time=0.038..5998.417 rows=19613238 loops=3)
Filter: (l_shipdate <= '1998-08-18 00:00:00'::timestamp zonder tijdzone)
Rows Removed by Filter: 382112
Planning Time: 0.241 ms
Execution Time: 8555.131 ms

Wat gebeurt hier? Het aantal werkprocessen is verdubbeld, en de query is slechts 1,6599 keer sneller geworden. De berekeningen zijn interessant. We hadden 2 werkprocessen en 1 leider. Na de wijziging zijn het er 4+1 geworden.

Onze maximale versnelling door parallelle verwerking: 5/3 = 1,66(6) keer.

How does it work?

Processen

De uitvoering van een query begint altijd met het leidende proces. De leider voert alles niet-parallel en een deel van de parallelle verwerking uit. Andere processen die dezelfde queries uitvoeren, worden werkprocessen genoemd. Parallelle verwerking maakt gebruik van de infrastructuur dynamische achtergrondwerkprocessen (sinds versie 9.4). Aangezien andere onderdelen van PostgreSQL processen in plaats van threads gebruiken, kon een query met 3 werkprocessen 4 keer sneller zijn dan conventionele verwerking.

Interacties

Werkprocessen communiceren met de leider via een berichtenwachtrij (gebaseerd op gedeeld geheugen). Elk proces heeft 2 wachtrijen: voor fouten en voor tuples.

Hoeveel werkprocessen zijn nodig?

De minimale beperking wordt ingesteld door de parameter max_parallel_workers_per_gather. Vervolgens haalt de query-uitvoerder werkprocessen uit de pool, die is beperkt door de parameter max_parallel_workers size. De laatste beperking is max_worker_processes, dat wil zeggen het totale aantal achtergrondprocessen.

Als er geen werkproces kan worden toegewezen, is de verwerking enkelvoudig.

De queryplanner kan het aantal werkprocessen verminderen op basis van de grootte van de tabel of index. Hiervoor zijn er de parameters min_parallel_table_scan_size en min_parallel_index_scan_size.

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

Elke keer dat een tabel 3 keer groter is dan min_parallel_(index|table)_scan_size, voegt Postgres een werkproces toe. Het aantal werkprocessen is niet gebaseerd op kosten. Circulaire afhankelijkheid bemoeilijkt complexe implementaties. In plaats daarvan gebruikt de planner eenvoudige regels.

In de praktijk zijn deze regels niet altijd geschikt voor productie, dus je kunt het aantal werkprocessen voor een specifieke tabel wijzigen: ALTER TABLE … SET (parallel_workers = N).

Waarom wordt parallele verwerking niet gebruikt?

Naast de lange lijst met beperkingen zijn er ook kostencontroles:

parallel_setup_cost — om zonder parallele verwerking korte aanvragen af te handelen. Deze parameter schat de tijd voor geheugenv voorbereiding, processtart en initiële gegevensuitwisseling.

parallel_tuple_cost: de communicatie van de leider met de werkprocessen kan evenredig toenemen met het aantal tuples van de werkprocessen. Deze parameter berekent de kosten voor gegevensuitwisseling.

Geneste lusverbindingen — Nested Loop Join

PostgreSQL 9.6+ kan geneste lussen parallel uitvoeren — dit is een eenvoudige operatie.

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)

De verzameling vindt plaats in de laatste fase, zodat de Nested Loop Left Join een parallelle bewerking is. Parallel Index Only Scan verscheen pas in versie 10. Het werkt op dezelfde manier als parallel sequentieel scannen. Voorwaarde c_custkey = o_custkey leest één bestelling voor elke klantregel. Het is dus niet parallel.

Hash-verbinding — Hash Join

Elke werkproces creëert zijn eigen hash-tabel tot PostgreSQL 11. En als er meer dan vier van deze processen zijn, zal de prestaties niet toenemen. In de nieuwe versie is de hashtabel gemeenschappelijk. Elke werkproces kan WORK_MEM gebruiken om een hash-tabel te maken.

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

Query 12 van TPC-H toont duidelijk een parallel hash-verbinding. Elke werker draagt bij aan het creëren van een gezamenlijke hashtabel.

Samenvoegen - Merge Join

Samenvoegen is van nature niet parallel. Maak je geen zorgen als dit de laatste stap van de query is; het kan nog steeds parallel worden uitgevoerd.

-- Query 2 van TPC-H
explain (kosten uit) 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 = 'AMERIKA'
        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 = 'AMERIKA'
        )
order by s_acctbal desc, n_name, s_name, p_partkey
LIMIT 100;
                                                QUERY PLAN
----------------------------------------------------------------------------------------------------------
 Beperking
   -&gt;  Sorteer
         Sorteer Sleutel: supplier.s_acctbal DESC, nation.n_name, supplier.s_name, part.p_partkey
         -&gt;  Samenvoegen
               Samenvoeg Voorwaarde: (part.p_partkey = partsupp.ps_partkey)
               Join Filter: (partsupp.ps_supplycost = (SubPlan 1))
               -&gt;  Verzamelen
                     Werknemers Gepland: 4
                     -&gt;  Parallel Index Scan using <strong>part_pkey</strong> op part
                           Filter: (((p_type)::text ~~ '%BRASS'::text) EN (p_size = 36))
               -&gt;  Materialiseren
                     -&gt;  Sorteer
                           Sorteer Sleutel: partsupp.ps_partkey
                           -&gt;  Geneste Lus
                                 -&gt;  Geneste Lus
                                       Join Filter: (nation.n_regionkey = region.r_regionkey)
                                       -&gt;  Seq Scan op region
                                             Filter: (r_name = 'AMERIKA'::bpchar)
                                       -&gt;  Hash Join
                                             Hash Voorwaarde: (supplier.s_nationkey = nation.n_nationkey)
                                             -&gt;  Seq Scan op supplier
                                             -&gt;  Hash
                                                   -&gt;  Seq Scan op nation
                                 -&gt;  Index Scan using idx_partsupp_suppkey op partsupp
                                       Index Voorwaarde: (ps_suppkey = supplier.s_suppkey)
               SubPlan 1
                 -&gt;  Aggregate
                       -&gt;  Geneste Lus
                             Join Filter: (nation_1.n_regionkey = region_1.r_regionkey)
                             -&gt;  Seq Scan op region region_1
                                   Filter: (r_name = 'AMERIKA'::bpchar)
                             -&gt;  Geneste Lus
                                   -&gt;  Geneste Lus
                                         -&gt;  Index Scan using idx_partsupp_partkey op partsupp partsupp_1
                                               Index Voorwaarde: (part.p_partkey = ps_partkey)
                                         -&gt;  Index Scan using supplier_pkey op supplier supplier_1
                                               Index Voorwaarde: (s_suppkey = partsupp_1.ps_suppkey)
                                   -&gt;  Index Scan using nation_pkey op nation nation_1
                                         Index Voorwaarde: (n_nationkey = supplier_1.s_nationkey)

De ‘Merge Join’ node bevindt zich boven de ‘Gather Merge’. Hierdoor maakt de samenvoeging geen gebruik van parallelle verwerking. Maar de ‘Parallel Index Scan’ helpt nog steeds met het segment. part_pkey.

Sectie-gebaseerd samenvoegen

In PostgreSQL 11 sectie-gebaseerd samenvoegen staat standaard uit: het heeft een zeer kostbare planning. Tabellen met vergelijkbare secties kunnen sectie voor sectie worden samengevoegd. Op deze manier zal Postgres kleinere hash-tabellen gebruiken. Elke sectie-samenvoeging kan parallel zijn.

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;
                    QUERY PLAN
---------------------------------------------------
 Append
   ->  Hash Join
         Hash Cond: (t2.b = t1.a)
         ->  Seq Scan on prt2_p1 t2
               Filter: ((b >= 0) AND (b <= 10000))
         ->  Hash
               ->  Seq Scan on prt1_p1 t1
                     Filter: (b = 0)
   ->  Hash Join
         Hash Cond: (t2_1.b = t1_1.a)
         ->  Seq Scan on prt2_p2 t2_1
               Filter: ((b >= 0) AND (b <= 10000))
         ->  Hash
               ->  Seq Scan on prt1_p2 t1_1
                     Filter: (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;
                        QUERY PLAN
-----------------------------------------------------------
 Gather
   Workers Planned: 4
   ->  Parallel Append
         ->  Parallel Hash Join
               Hash Cond: (t2_1.b = t1_1.a)
               ->  Parallel Seq Scan on prt2_p2 t2_1
                     Filter: ((b >= 0) AND (b <= 10000))
               ->  Parallel Hash
                     ->  Parallel Seq Scan on prt1_p2 t1_1
                           Filter: (b = 0)
         ->  Parallel Hash Join
               Hash Cond: (t2.b = t1.a)
               ->  Parallel Seq Scan on prt2_p1 t2
                     Filter: ((b >= 0) AND (b <= 10000))
               ->  Parallel Hash
                     ->  Parallel Seq Scan on prt1_p1 t1
                           Filter: (b = 0)

Belangrijk is dat sectie-gebaseerd samenvoegen alleen parallel kan zijn als deze secties groot genoeg zijn.

Parallel toevoegen - Parallel Append

Parallel Append kan worden gebruikt in plaats van verschillende blokken in verschillende werkprocessen. Dit gebeurt meestal bij UNION ALL queries. Het nadeel is minder parallelisme, omdat elk werkproces slechts 1 query verwerkt.

Hier zijn 2 werkprocessen gestart, hoewel er 4 zijn ingeschakeld.

tpch=# uitleg (kosten uit) select sum(l_quantity) as sum_qty from lineitem where l_shipdate <= date '1998-12-01' - interval '105' dag union all select sum(l_quantity) as sum_qty from lineitem where l_shipdate <= date '2000-12-01' - interval '105' dag;
                                           QUERY PLAN
------------------------------------------------------------------------------------------------
 Verzamel
   Geplande Werknemers: 2
   ->  Parallel Voeg samen
         ->  Aggregate
               ->  Seq Scan op lineitem
                     Filter: (l_shipdate <= '2000-08-18 00:00:00'::timestamp zonder tijdzone)
         ->  Aggregate
               ->  Seq Scan op lineitem lineitem_1
                     Filter: (l_shipdate <= '1998-08-18 00:00:00'::timestamp zonder tijdzone)

De belangrijkste variabelen

  • WORK_MEM beperkt de hoeveelheid geheugen voor elk proces, niet alleen voor aanvragen: work_mem processen verbindingen = veel geheugen.
  • max_parallel_workers_per_gather — hoeveel werkprocessen het programma zal gebruiken voor parallelle verwerking vanuit het plan.
  • max_worker_processes — past het totale aantal werkprocessen aan op het aantal CPU-kernen op de server.
  • max_parallel_workers — hetzelfde, maar voor parallelle werkprocessen.

Conclusies

Vanaf versie 9.6 kan parallelle verwerking de prestaties van complexe aanvragen, die veel rijen of indexen doorzoeken, aanzienlijk verbeteren. In PostgreSQL 10 is parallelle verwerking standaard ingeschakeld. Vergeet niet het uit te schakelen op servers met een hoge OLTP-werklast. Sequentiële scans of indexscans verbruiken veel middelen. Als je geen rapport over de volledige dataset uitvoert, kunnen aanvragen efficiënter worden gemaakt door simpelweg de ontbrekende indexen toe te voegen of het juiste partitioneren te gebruiken.

Links

Bron: habr.com

Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers 🔥 Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers | ProHoster