Paralēlie vaicājumi PostgreSQL

Paralēlie vaicājumi PostgreSQL
Mūsdienu CPU ir ļoti daudz kodolu. Gadiem ilgi programmas nosūtīja pieprasījumus datu bāzēm paralēli. Ja tas ir ziņojumu pieprasījums uz daudzām tabulas rindām, tas tiek izpildīts ātrāk, kad tiek izmantoti vairāki CPU, un PostgreSQL to atbalsta kopš 9.6 versijas.

Bija nepieciešami trīs gadi, lai īstenotu paralēlo pieprasījumu funkcionalitāti — bija jāpārstrādā kods dažādos pieprasījumu izpildes posmos. PostgreSQL 9.6 ieviesa infrastruktūru koda tālākai uzlabošanai. Nākamajās versijās arī citi pieprasījumu veidi tiek izpildīti paralēli.

ierobežojumi

  • Neieslēdziet paralēlo izpildi, ja visi kodoli jau ir noslogoti, citādi citi pieprasījumi tiks kavēti.
  • Galvenais, ka paralēlā apstrāde ar augstām WORK_MEM vērtībām izmanto daudz atmiņas — katrs hash savienojums vai kārtošana aizņem atmiņu tilpumā, kas noteikts work_mem.
  • OLTP pieprasījumus ar zemu latentumu nav iespējams paātrināt, izmantojot paralēlo izpildi. Ja pieprasījums atgriež vienu rindu, paralēlā apstrāde to tikai palēninās.
  • Izstrādātājiem patīk izmantot TPC-H veiktspējas testu. Varbūt jums ir līdzīgi pieprasījumi ideālai paralēlā izpildei.
  • Tikai SELECT pieprasījumi bez predikāta bloķēšanas tiek izpildīti paralēli.
  • Dažkārt pareiza indeksācija būs labāka par secīgu tabulas skenēšanu paralēlajā režīmā.
  • Pieteikumu apturēšana un kursoru atbalsts netiek nodrošināts.
  • Logu funkcijas un kārtošanas funkcijām nav paralēlās apstrādes.
  • Jūs negūstat nekādu labumu no ievades-izvades darba slodzes.
  • Nav paralēlo kārtošanas algoritmu. Bet pieprasījumi ar kārtošanu var tikt izpildīti paralēli noteiktos aspektos.
  • Aizvietojiet CTE (WITH …) ar iegulto SELECT, lai iekļautu paralēlo apstrādi.
  • Trešo pušu datu apvalki pašlaik neatbalsta paralēlo apstrādi (bet varētu!).
  • FULL OUTER JOIN netiek atbalstīts.
  • max_rows atspējo paralēlo apstrādi.
  • Ja pieprasījumā ir funkcija, kas nav atzīta par PARALLEL SAFE, tas darbosies vienā pavedienā.
  • TRANSACTION SERIALIZABLE izolācijas līmenis atspējo paralēlo apstrādi.

Testēšanas vide

PostgreSQL izstrādātāji centās samazināt TPC-H veiktspējas testu pieprasījumu atbildes laiku. Lejupielādējiet veiktspējas testu un pielāgojiet to PostgreSQL. Tas ir neoficiāls TPC-H veiktspējas testa izmantojums — nav paredzēts datu bāzu vai aprīkojuma salīdzināšanai.

  1. Lejupielādējiet TPC-H_Tools_v2.17.3.zip (vai jaunāku versiju) no TPC oficiālās vietnes.
  2. Pārsaukt makefile.suite par Makefile un mainiet, kā aprakstīts šeit: https://github.com/tvondra/pg_tpch . Sastādiet kodu ar komandu make.
  3. Izveidojiet datus: ./dbgen -s 10 izveido datubāzi ar 23 GB. Šis apjoms ir pietiekams, lai redzētu veiktspējas atšķirību starp paralēlajiem un nepārtraukto vaicājumiem.
  4. Pārveidojiet failus tbl par csv ar for un sed.
  5. Klonējiet repozitoriju pg_tpch un kopējiet failus csv par pg_tpch/dss/data.
  6. Izveidojiet vaicājumus ar komandu qgen.
  7. Ielādējiet datus datubāzē ar komandu ./tpch.sh.

Paralēla secīga skenēšana

Tā var būt ātrāka nevis paralēlās lasīšanas dēļ, bet gan tāpēc, ka dati ir izkliedēti pa daudzām CPU kodolām. Mūsdienu operētājsistēmās PostgreSQL datu faili ir labi kešoti. Ar iepriekšēju lasīšanu var iegūt vairāk blokus no krātuves, nekā pieprasa PG demons. Tādējādi vaicājuma veiktspēja nav ierobežota ar diska ievadi-izvadi. Tas patērē CPU ciklus, lai:

  • lasītu rindas pa vienai no tabulas lapām;
  • salīdzinātu rindas vērtības un nosacījumus WHERE.

Izpildīsim vienkāršu vaicājumu 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 without time zone)
Rows Removed by Filter: 1146337
Planning Time: 0.203 ms
Execution Time: 19035.100 ms

Secīga skenēšana dod pārāk daudz rindu bez agregācijas, tādēļ vaicājumu izpilda viens CPU kodols.

Ja pievienojam SUM(), tad redzam, ka divi darba procesi palīdzēs paātrināt vaicājumu:

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)
Workers Planned: 2
Workers Launched: 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 on 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 without time zone)
Rows Removed by Filter: 382112
Planning Time: 0.241 ms
Execution Time: 8555.131 ms

Paralēlā agregācija

Noda „Parallel Seq Scan” ģenerē rindas daļējai agregācijai. Noda „Partial Aggregate” apgrūtina šīs rindas ar SUM(). Nobeigumā katra darba procesa SUM skaitītājs tiek apkopots ar nodu „Gather”.

Galīgais rezultāts tiek aprēķināts ar mezgla «Finalize Aggregate». Ja jums ir savas agregācijas funkcijas, neaizmirstiet tās atzīmēt kā «parallel safe».

Darba procesu skaits

Darba procesu skaitu var palielināt bez servera restartēšanas:

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)
Workers Planned: 2
Workers Launched: 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 on 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 without time zone)
Rows Removed by Filter: 382112
Planning Time: 0.241 ms
Execution Time: 8555.131 ms

Kas šeit notiek? Darba procesu skaits ir divreiz lielāks, bet pieprasījums ir kļuvis tikai 1,6599 reizes ātrāks. Aprēķini ir interesanti. Mums bija 2 darba procesi un 1 līderis. Pēc izmaiņām ir 4+1.

Mūsu maksimālais paātrinājums no paralēlas apstrādes: 5/3 = 1,66(6) reizes.

Kā tas darbojas?

Procesi

Pieprasījuma izpilde vienmēr sākas ar līderi. Līderis veic visus nepārveidotos un daļu paralēlās apstrādes. Citus procesus, kas veic tos pašus pieprasījumus, sauc par darba procesiem. Paralēlā apstrāde izmanto infrastruktūru dinamiskajiem fona darba procesiem (sākot ar versiju 9.4). Tā kā citas PostgreSQL daļas izmanto procesus, nevis pavedienus, pieprasījums ar 3 darba procesiem varēja būt 4 reizes ātrāks nekā tradicionālā apstrāde.

Sadarbība

Darba procesi sazinās ar līderi caur ziņojumu rindu (balstītu uz kopēju atmiņu). Katram procesam ir 2 rindas: kļūdu un kropļojumu.

Cik daudz darba procesu ir nepieciešams?

Minimālās robežas nosaka parametrs max_parallel_workers_per_gather. Pēc tam pieprasījumu izpildītājs ņem darba procesus no rijuma, ko ierobežo parametrs max_parallel_workers size. Pēdējā robeža ir max_worker_processes, tas ir, kopējais fona procesu skaits.

Ja neizdodas piešķirt darba procesu, apstrāde būs vienprocesa.

Pieprasījumu plānotājs var samazināt darba procesus atkarībā no tabulas vai indeksa lieluma. Tam ir parametri min_parallel_table_scan_size un min_parallel_index_scan_size.

set min_parallel_table_scan_size='8MB'
8MB tabula => 1 darbinieks
24MB tabula => 2 darbinieki
72MB tabula => 3 darbinieki
x => log(x / min_parallel_table_scan_size) / log(3) + 1 darbinieks

Ikreiz, kad tabula ir 3 reizes lielāka nekā min_parallel_(index|table)_scan_size, Postgres pievieno darba procesu. Darba procesu skaits nav balstīts uz izmaksām. Cirkulārā atkarība apgrūtina sarežģītu realizāciju. Vietā plānotājs izmanto vienkāršas normas.

Praksē šīs normas ne vienmēr der ražošanai, tāpēc var mainīt darba procesu skaitu konkrētai tabulai: ALTER TABLE … SET (parallel_workers = N).

Kāpēc paralēlā apstrāde netiek izmantota?

Papildus garajam ierobežojumu sarakstam ir arī izmaksu pārbaudes:

parallel_setup_cost — lai izvairītos no paralēlas apstrādes īsiem pieprasījumiem. Šis parametrs aplēš laiku, kas nepieciešams atmiņas sagatavošanai, procesa palaišanai un sākotnējai datu apmaiņai.

parallel_tuple_cost: saziņa starp vadītāju un darbiniekiem var ilgt proporcionāli tuple skaitam darba procesiem. Šis parametrs aprēķina apmaiņas izmaksas.

Iekļauto ciklu savienojumi — Nested Loop Join

PostgreSQL 9.6+ var veikt iekļautos ciklus paralēli — tā ir vienkārša operācija.

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)

Savākšana notiek pēdējā posmā, tāpēc Nested Loop Left Join — tā ir paralēla operācija. Parallel Index Only Scan parādījās tikai versijā 10. Tas darbojas līdzīgi paralēlajai secīgajai skenēšanai. Nosacījums c_custkey = o_custkey nolasa vienu pasūtījumu katrai klienta rindai. Tāpēc tas nav paralēls.

Hash savienojums — Hash Join

Katrs darba procesus izveido savu hash tabulu līdz PostgreSQL 11. Un, ja šo procesu ir vairāk nekā četri, veiktspēja neuzlabosies. Jaunajā versijā hash tabula ir kopīga. Katrs darba process var izmantot WORK_MEM, lai izveidotu hash tabulu.

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 from TPC-H clearly demonstrates a parallel hash join. Each worker process contributes to creating a common hash table.

Apvienošanas operācija — Merge Join

Apvienošanas operācija pēc savas būtības nav paralēla. Neuztraucieties, ja tas ir pēdējais pieprasījuma posms — tas joprojām var tikt izpildīts paralēli.

-- Vaicājums 2 no TPC-H
explain (izmaksas izslēgtas) 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;
                                                VAICĀJUMA PLĀNS
----------------------------------------------------------------------------------------------------------
 Limit
   -&gt;  Sort
         Sort Key: supplier.s_acctbal DESC, nation.n_name, supplier.s_name, part.p_partkey
         -&gt;  Merge Join
               Merge Cond: (part.p_partkey = partsupp.ps_partkey)
               Join Filter: (partsupp.ps_supplycost = (SubPlan 1))
               -&gt;  Gather Merge
                     Workers Planned: 4
                     -&gt;  Parallel Index Scan using <strong>part_pkey</strong> uz part
                           Filtrs: (((p_type)::text ~~ '%BRASS'::text) AND (p_size = 36))
               -&gt;  Materialize
                     -&gt;  Sort
                           Sort Key: partsupp.ps_partkey
                           -&gt;  Nested Loop
                                 -&gt;  Nested Loop
                                       Join Filter: (nation.n_regionkey = region.r_regionkey)
                                       -&gt;  Seq Scan on region
                                             Filtrs: (r_name = 'AMERIKA'::bpchar)
                                       -&gt;  Hash Join
                                             Hash Cond: (supplier.s_nationkey = nation.n_nationkey)
                                             -&gt;  Seq Scan on supplier
                                             -&gt;  Hash
                                                   -&gt;  Seq Scan on nation
                                 -&gt;  Index Scan using idx_partsupp_suppkey on partsupp
                                       Index Cond: (ps_suppkey = supplier.s_suppkey)
               SubPlan 1
                 -&gt;  Aggregate
                       -&gt;  Nested Loop
                             Join Filter: (nation_1.n_regionkey = region_1.r_regionkey)
                             -&gt;  Seq Scan on region region_1
                                   Filtrs: (r_name = 'AMERIKA'::bpchar)
                             -&gt;  Nested Loop
                                   -&gt;  Nested Loop
                                         -&gt;  Index Scan using idx_partsupp_partkey on partsupp partsupp_1
                                               Index Cond: (part.p_partkey = ps_partkey)
                                         -&gt;  Index Scan using supplier_pkey on supplier supplier_1
                                               Index Cond: (s_suppkey = partsupp_1.ps_suppkey)
                                   -&gt;  Index Scan using nation_pkey on nation nation_1
                                         Index Cond: (n_nationkey = supplier_1.s_nationkey)

Noda «Merge Join» atrodas virs «Gather Merge». Tādējādi apvienošana neizmanto paralēlu apstrādi. Taču noda «Parallel Index Scan» joprojām palīdz ar segmentu. part_pkey.

Apvienošana pēc sekcijām

PostgreSQL 11 apvienošana pēc sekcijām pēc noklusējuma atslēgta: tai ir ļoti dārga plānošana. Tabulas ar līdzīgām sekcijām var apvienot sekcija pa sekcijai. Tādējādi Postgres izmantos mazākas hash tabulas. Katras sekciju apvienošanas var būt paralēlas.

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)

Galvenais, ka apvienošana pēc sekcijām var būt paralēla tikai tad, ja šīs sekcijas ir pietiekami lielas.

Paralēlais papildinājums — Parallel Append

Parallel Append var tikt izmantots vietā dažādiem blokiem dažādos darba procesos. Parasti tas notiek ar UNION ALL pieprasījumiem. Trūkums — mazāk paralēlisms, jo katrs darba process apstrādā tikai 1 pieprasījumu.

Šeit darbojas 2 darba procesi, lai gan ir iespējoti 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   Parallel Append
         ->  Aggregate
               ->  Seq Scan on lineitem
                     Filter: (l_shipdate   Aggregate
               ->  Seq Scan on lineitem lineitem_1
                     Filter: (l_shipdate <= '1998-08-18 00:00:00'::timestamp without time zone)

Svarīgākās mainīgās vērtības

  • WORK_MEM ierobežo atmiņas apjomu katram procesam, nevis tikai vaicājumiem: work_mem procesi savienojumi = ļoti daudz atmiņas.
  • max_parallel_workers_per_gather — cik darba procesu izpildes programma izmantos paralēlai apstrādei no plāna.
  • max_worker_processes — pielāgo vispārējo darba procesu skaitu atbilstoši servera CPU kodolu skaitam.
  • max_parallel_workers — tas pats, bet paralēlajiem darba procesiem.

Secinājumi

Sākot ar 9.6 versiju, paralēlā apstrāde var ievērojami uzlabot sarežģītu vaicājumu veiktspēju, kas skenē daudz rindu vai indeksu. PostgreSQL 10 paralēlā apstrāde ir iespējota pēc noklusējuma. Neaizmirstiet to atslēgt serveros ar lielu OLTP slodzi. Sekvenciāras skenēšanas vai indeksu skenēšana patērē ļoti daudz resursu. Ja neizpildāt pārskatu uz visu datu kopu, vaicājumi var tikt efektīvāki, vienkārši pievienojot trūkstošos indeksus vai izmantojot pareizu sekciju.

Saites

Avots: habr.com

Iegādājieties uzticamu tīmekļa hostingu ar DDoS aizsardzību, VPS VDS serveriem 🔥 Iegādājieties uzticamu tīmekļa hostingu ar DDoS aizsardzību, VPS VDS serveriem | ProHoster