
Në procesorët modernë ka shumë bërthama. Për vite me radhë, aplikacionet dërgojnë kërkesa në bazat e të dhënave në mënyrë paralele. Nëse ky është një kërkesë raporti për shumë rreshta në tabelë, ajo ekzekutohet më shpejt kur angazhon disa procesorë, dhe në PostgreSQL kjo është e mundur që nga versioni 9.6.
Kërkoi 3 vite për të realizuar funksionin e kërkesave paralele — duhej të ri-shkruhej kodi në faza të ndryshme të ekzekutimit të kërkesave. Në PostgreSQL 9.6 u krijua infrastruktura për përmirësimin e mëtejshëm të kodit. Në versionet pasuese, dhe lloje të tjera kërkesash ekzekutohen në mënyrë paralele.
Kufizimet
- Mos aktivizoni ekzekutimin paralel nëse të gjitha bërthamën janë të zëna, përndryshe kërkesat e tjera do të ngadalësohen.
- E rëndësishme, përpunimi paralel me vlera të larta të WORK_MEM angazhon shumë memorie — çdo lidhje hëzho ose renditje konsumon memorie në volum të work_mem.
- Kërkesat OLTP me vonesë të ulët nuk mund të përshpejtohen me ekzekutimin paralel. Nëse një kërkesë kthen një rresht, përpunimi paralel vetëm e ngadalëson atë.
- Zhvilluesve u pëlqen të përdorin benchmark TPC-H. Ndoshta keni kërkesa të ngjashme për ekzekutimin e përsosur paralel.
- Vetëm kërkesat SELECT pa bllokim predikativ ekzekutohen në mënyrë paralele.
- Nganjëherë, indeksimi i saktë është më mirë se skanimi sekondar në mënyrë paralele.
- Pejsazhet e kërkesave dhe kursori nuk mbështeten.
- Funksionet e dritareve dhe funksionet agregate të grupeve të renditura nuk janë paralele.
- Nuk fitoni asgjë në ngarkesën e I/O.
- Nuk ka algorithma të paralelizuara të renditjes. Por kërkesat me renditje mund të ekzekutohen paralelisht në disa aspekte.
- Zëvendëso CTE (ME …) me një SELECT të ngulitur për të përfshirë përpunimin paralel.
- Përmbajtjet e të dhënave të jashtme ende nuk mbështesin përpunimin paralel (edhe pse mund të mbështeteshin!)
- FULL OUTER JOIN nuk mbështetet.
- max_rows çaktivizon përpunimin paralel.
- Nëse kërkesa ka një funksion, i cili nuk është e shënuar si PARALLEL SAFE, ajo do të jetë një-kohore.
- Niveli i izolimit të transaksionit SERIALIZABLE çaktivizon përpunimin paralel.
Mjedisi i testimit
Zhvilluesit e PostgreSQL u përpoqën të pakësonin kohën e përgjigjes për kërkesat e benchmark-ut TPC-H. Shkarko benchmark-un dhe . Ky është një përdorim jozyrtar i benchmark-ut TPC-H — jo për të krahasuar bazat e të dhënave ose pajisjet.
- Shkarko TPC-H_Tools_v2.17.3.zip (apo versionin më të ri) .
- Rivendos makefile.suite në Makefile dhe ndrysho si është përshkruar këtu: . Kompliko kodin me komandën make.
- Gjenere të dhënat:
./dbgen -s 10krijon një bazë të dhënash prej 23 GB. Kjo do të mjaftojë për të parë diferencën në performancën e kërkesave paralele dhe jo-paralele. - Konverto skedarët
tblnëcsv me fordhesed. - Klononi depot
pg_tpchdhe kopjo skedarëtcsvnëpg_tpch/dss/data. - Krijo kërkesat me komandën
qgen. - Ngarko të dhënat në bazë me komandën
./tpch.sh.
Skanimi paralel sekondar
Ai mund të jetë më i shpejtë jo nga leximi paralel, por sepse të dhënat janë shpërndara në shumë bërthama të procesorit. Në sistemet operuese moderne, skedarët e të dhënave PostgreSQL janë mirë të ruajtur në cache. Me leximin parashikues, mund të merrni nga ruajtja një bllok më shumë se sa kërkon demon PG. Prandaj, performanca e kërkesës nuk kufizohet nga I/O e diskut. Ai konsumon cikle të procesorit për të:
- lexuar rreshta një nga një nga faqet e tabelës;
- krahason vlerat e rreshtave dhe kushtet
WHERE.
Ekzekuto një kërkesë të thjeshtë select:
tpch=# explain analyze select l_quantity as sum_qty from lineitem where l_shipdate <= date '1998-12-01' - interval '105' day;
QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------
Skanimi Sekondar mbi lineitem (cost=0.00..1964772.00 rows=58856235 width=5) (actual time=0.014..16951.669 rows=58839715 loops=1)
Filtri: (l_shipdate <= '1998-08-18 00:00:00'::timestamp without time zone)
Rreshtat e hequra nga Filtri: 1146337
Koha e Planifikimit: 0.203 ms
Koha e Ekzekutimit: 19035.100 msSkanimi sekondar jep shumë rreshta pa agregim, kështu që kërkesa ekzekutohet me një bërthamë procesori.
Nëse shtoni SUM(), është e qartë se dy procese ndihmës do të ndihmojnë për të përshpejtuar kërkesën:
explain analyze select sum(l_quantity) as sum_qty from lineitem where l_shipdate <= date '1998-12-01' - interval '105' day;
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------------------
Përfundimi i Agregimit (cost=1589702.14..1589702.15 rows=1 width=32) (actual time=8553.365..8553.365 rows=1 loops=1)
-> Grumbull (cost=1589701.91..1589702.12 rows=2 width=32) (actual time=8553.241..8555.067 rows=3 loops=1)
Punëtorë të Planifikuar: 2
Punëtorë të Lançuar: 2
-> Agregimi i Pjesës (cost=1588701.91..1588701.92 rows=1 width=32) (actual time=8547.546..8547.546 rows=1 loops=3)
-> Skanimi Sekondar Paralel mbi lineitem (cost=0.00..1527393.33 rows=24523431 width=5) (actual time=0.038..5998.417 rows=19613238 loops=3)
Filtri: (l_shipdate <= '1998-08-18 00:00:00'::timestamp without time zone)
Rreshtat e hequra nga Filtri: 382112
Koha e Planifikimit: 0.241 ms
Koha e Ekzekutimit: 8555.131 msAgregimi Paralel
Noda 'Skanimi Sekondar Paralel' prodhon rreshta për agregimin e pjesëve. Noda 'Agregimi i Pjesës' i zvogëlon këto rreshta me SUM(). Në fund, numri i SUM nga çdo proces ndihmës grumbullohet nga noda 'Grumbull'.
Rezultati përfundimtar llogaritet nga nodi 'Finalize Aggregate'. Nëse keni funksione të tua agregimi, sigurohuni që t'i shënoni si 'parallel safe'.
Numri i proceseve punuese
Numri i proceseve punuese mund të rritet pa e rinisur serverin:
explain analyze select sum(l_quantity) as sum_qty from lineitem where l_shipdate <= date '1998-12-01' - interval '105' day;
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------------------
Përfundimi i Agregimit (cost=1589702.14..1589702.15 rows=1 width=32) (actual time=8553.365..8553.365 rows=1 loops=1)
-> Grumbull (cost=1589701.91..1589702.12 rows=2 width=32) (actual time=8553.241..8555.067 rows=3 loops=1)
Punëtorë të Planifikuar: 2
Punëtorë të Lançuar: 2
-> Agregimi i Pjesës (cost=1588701.91..1588701.92 rows=1 width=32) (actual time=8547.546..8547.546 rows=1 loops=3)
-> Skanimi Sekondar Paralel mbi lineitem (cost=0.00..1527393.33 rows=24523431 width=5) (actual time=0.038..5998.417 rows=19613238 loops=3)
Filtri: (l_shipdate <= '1998-08-18 00:00:00'::timestamp without time zone)
Rreshtat e hequra nga Filtri: 382112
Koha e Planifikimit: 0.241 ms
Koha e Ekzekutimit: 8555.131 msÇfarë po ndodh këtu? Numri i proceseve punuese është dyfishuar, ndërsa kërkesa u bë vetëm 1,6599 herë më e shpejtë. Llogaritjet janë interesante. Kishim 2 procese punuese dhe 1 lider. Pas ndryshimit, u bë 4+1.
Shpejtësia maksimale që arrijmë nga përpunimi paralel: 5/3 = 1,66(6) herë.
Si funksionon?
Proceset
Ekzekutimi i kërkesës fillon gjithmonë me procesin drejtues. Lideri bën të gjitha proceset jo-paralel dhe një pjesë të përpunimit paralel. Proceset e tjera që ekzekutojnë të njëjtat kërkesa quhen procese punuese. Përpunimi paralel përdor infrastrukturën (nga versioni 9.4). Pasi pjesët e tjera të PostgreSQL përdorin procese dhe jo fijet, një kërkesë me 3 procese punuese mund të jetë 4 herë më e shpejtë se përpunimi tradicional.
Ndërveprimi
Proceset punuese komunikojnë me liderin përmes një rrethi mesazhe (bazuar në memorie të përbashkët). Çdo proces ka 2 rreth mesazhesh: për gabime dhe për tuple.
Sa procese punuese nevojiten?
Kufizimi minimal caktohet nga parametri . Pastaj ekzekutori i kërkesave merr proceset punuese nga puli, i kufizuar nga parametri . Kufizimi i fundit është , pra numri total i proceseve të sfondit.
Nëse nuk arrihet ndarja e një procesi punues, përpunimi do të jetë me një proces.
Planifikuesi i kërkesave mund të zvogëlojë proceset punuese në varësi të madhësisë së tabelës ose indekseve. Për këtë ka parametra dhe .
set min_parallel_table_scan_size='8MB'
8MB tabelë => 1 punonjës
24MB tabelë => 2 punonjës
72MB tabelë => 3 punonjës
x => log(x / min_parallel_table_scan_size) / log(3) + 1 punonjësÇdo herë që tabela është 3 herë më e madhe se min_parallel_(index|table)_scan_size, Postgres shton një proces punues. Numri i proceseve punuese nuk bazohet në kosto. Varësia e rrethit e vështirëson realizimet e komplikuara. Në vend të kësaj, planifikuesi përdor rregulla të thjeshta.
Në praktikë, këto rregulla nuk janë gjithmonë të përshtatshme për prodhim, kështu që është e mundur të ndryshohet numri i proceseve punuese për një tabelë specifike: ALTER TABLE … SET (parallel_workers = N).
Pse nuk përdoret përpunimi paralel?
Përveç listës së gjatë të kufizimeve ka edhe kontrollime kostoje:
— për të shmangur përpunimin paralel të kërkesave të shkurtra. Ky parametr koston kohën për të përgatitur memorien, për të nisur procesin dhe për shkëmbimin fillestar të të dhënave.
: komunikimi midis liderit dhe punonjësve mund të zgjasë në proporcioni me numrin e tupleve nga proceset punuese. Ky parametr llogarit kostot për shkëmbimin e të dhënave.
Bashkimet e cikleve të thella — Nested Loop Join
PostgreSQL 9.6+ может выполнять вложенные циклы параллельно — это простая операция.
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 '%special%deposits%'
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 !~~ '%special%deposits%'::text)Grupi formohet në fazën e fundit, kështu që Nested Loop Left Join është një operacion paralel. Parallel Index Only Scan është paraqitur vetëm në versionin 10. Ai funksionon në mënyrë të ngjashme me skanimin paralel sekondar. Kushti c_custkey = o_custkey lexon një rend nga çdo rresht të klientit. Pra, ai nuk është paralel.
Bashkim me Hash — Hash Join
Çdo proces punues krijon tabelën e tij të hash deri në PostgreSQL 11. Dhe nëse këto procese janë më shumë se katër, performanca nuk përmirësohet. Në versionin e ri, tabela e hash-it është e përbashkët. Çdo proces punues mund të përdorë WORK_MEM për të krijuar një 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 msKërkesa 12 nga TPC-H ilustron qartë bashkimin e realizuar me hash në mënyrë të paralel. Çdo proces punëtor merr pjesë në krijimin e një tabele hash të përbashkët.
Bashkimi me shkrirje — Merge Join
Bashkimi me shkrirje ka karakteristika jo-paralele. Mos u shqetësoni nëse ky është hapi i fundit i kërkesës, — ai mund të ekzekutohet gjithësesi në mënyrë paralele.
-- Kërkesa 2 nga TPC-H
shpjego (kostot jashtë) përzgjedh s_acctbal, s_name, n_name, p_partkey, p_mfgr, s_address, s_phone, s_comment
nga part, supplier, partsupp, nation, region
ku
p_partkey = ps_partkey
dhe s_suppkey = ps_suppkey
dhe p_size = 36
dhe p_type si '%BRASS'
dhe s_nationkey = n_nationkey
dhe n_regionkey = r_regionkey
dhe r_name = 'AMERIKA'
dhe ps_supplycost = (
përzgjedh
min(ps_supplycost)
nga partsupp, supplier, nation, region
ku
p_partkey = ps_partkey
dhe s_suppkey = ps_suppkey
dhe s_nationkey = n_nationkey
dhe n_regionkey = r_regionkey
dhe r_name = 'AMERIKA'
)
rendit sipas s_acctbal zbritës, n_name, s_name, p_partkey
LIMIT 100;
PLANNA E KËRKESËS
----------------------------------------------------------------------------------------------------------
Limit
-> Rendit
Çelësi i Rendit: supplier.s_acctbal ZBRITËS, nation.n_name, supplier.s_name, part.p_partkey
-> Bashkimi i Bashkëve
Kushti i Bashkimit: (part.p_partkey = partsupp.ps_partkey)
Filtri i Bashkimit: (partsupp.ps_supplycost = (SubPlani 1))
-> Mbledhja e Bashkimeve
Punëtorë të Planifikuar: 4
-> Skemi Paralele e Indeksit duke përdorur <strong>part_pkey</strong> në pjesë
Filtri: (((p_type)::text ~~ '%BRASS'::text) DHE (p_size = 36))
-> Materializo
-> Rendit
Çelësi i Rendit: partsupp.ps_partkey
-> Cikli i Foleve
-> Cikli i Foleve
Filtri i Bashkimit: (nation.n_regionkey = region.r_regionkey)
-> Skemi Sekuencial në region
Filtri: (r_name = 'AMERIKA'::bpchar)
-> Bashkimi i Hash
Kushti i Hash: (supplier.s_nationkey = nation.n_nationkey)
-> Skemi Sekuencial në supplier
-> Hash
-> Skemi Sekuencial në nation
-> Skemi Indeksi duke përdorur idx_partsupp_suppkey në partsupp
Kushti i Indeksit: (ps_suppkey = supplier.s_suppkey)
SubPlani 1
-> Agregati
-> Cikli i Foleve
Filtri i Bashkimit: (nation_1.n_regionkey = region_1.r_regionkey)
-> Skemi Sekuencial në region region_1
Filtri: (r_name = 'AMERIKA'::bpchar)
-> Cikli i Foleve
-> Cikli i Foleve
-> Skemi Indeksi duke përdorur idx_partsupp_partkey në partsupp partsupp_1
Kushti i Indeksit: (part.p_partkey = ps_partkey)
-> Skemi Indeksi duke përdorur supplier_pkey në supplier supplier_1
Kushti i Indeksit: (s_suppkey = partsupp_1.ps_suppkey)
-> Skemi Indeksi duke përdorur nation_pkey në nation nation_1
Kushti i Indeksit: (n_nationkey = supplier_1.s_nationkey)Noda «Merge Join» ndodhet mbi «Gather Merge». Pra, bashkimi nuk përdor përpunim paralel. Por nodi «Parallel Index Scan» ende ndihmon me segmentin. part_pkey.
Bashkimi sipas seksioneve
Në PostgreSQL 11 çaktivizuar në mënyrë të parazgjedhur: ka një planifikim shumë të shtrenjtë. Tabelat me seksionim të ngjashëm mund të bashkohen seksion pas seksioni. Kështu, Postgres do të përdorë tabela hesh të vogla. Çdo bashkim seksionesh mund të jetë 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;
PLANI I KËRQESJES
---------------------------------------------------
Shtesë
-> Bashkimi Hash
Kusht Hash: (t2.b = t1.a)
-> Skanim Sekuencial në prt2_p1 t2
Filtri: ((b >= 0) DHE (b <= 10000))
-> Hesh
-> Skanim Sekuencial në prt1_p1 t1
Filtri: (b = 0)
-> Bashkimi Hash
Kusht Hash: (t2_1.b = t1_1.a)
-> Skanim Sekuencial në prt2_p2 t2_1
Filtri: ((b >= 0) DHE (b <= 10000))
-> Hesh
-> Skanim Sekuencial në prt1_p2 t1_1
Filtri: (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;
PLANI I KËRQESJES
-----------------------------------------------------------
Mblidhen
Punonjësit e Planifikuar: 4
-> Shtesë Paralel
-> Bashkimi Hash Paralel
Kusht Hash: (t2_1.b = t1_1.a)
-> Skanim Sekuencial Paralel në prt2_p2 t2_1
Filtri: ((b >= 0) DHE (b <= 10000))
-> Hesh Paralel
-> Skanim Sekuencial Paralel në prt1_p2 t1_1
Filtri: (b = 0)
-> Bashkimi Hash Paralel
Kusht Hash: (t2.b = t1.a)
-> Skanim Sekuencial Paralel në prt2_p1 t2
Filtri: ((b >= 0) DHE (b <= 10000))
-> Hesh Paralel
-> Skanim Sekuencial Paralel në prt1_p1 t1
Filtri: (b = 0)E rëndësishme, bashkimi sipas seksioneve është paralel vetëm nëse këto seksione janë mjaft të mëdha.
Shtesë Paralel — Parallel Append
mund të përdoret në vend të blloqeve të ndryshme në procese të ndryshme. Zakonisht ndodh kjo me kërkesat UNION ALL. Disavantazhi — më pak paralelizëm, pasi çdo proces përpunon vetëm 1 kërkesë.
Këtu janë aktivizuar 2 procese, ndonëse janë planifikuar 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)Variablat më të rëndësishme
- WORK_MEM kufizon sasinë e memories për çdo proces, jo vetëm për kërkesat: work_mem proceset konneksionet = shumë memoria.
- — sa shumë procese punuese do të përdorë programi për procesimin paralel nga plani.
- — përshtat numrin total të proceseve punuese me numrin e bërthamave të CPU në server.
- — e njëjta, por për proceset punuese paralel.
Përfundimet
Që nga versioni 9.6, procesimi paralel mund të përmirësojë ndjeshëm performancën e kërkesave të komplikuara që skanojnë shumë rreshta ose indekse. Në PostgreSQL 10 procesimi paralel është aktivizuar si parazgjedhje. Mos harroni ta çaktivizoni në serverët me ngarkesë të lartë OLTP. Skanat sekondar ose skanat e indekseve konsumojnë shumë burime. Nëse nuk po kryeni një raport mbi të gjithë grumbullin e të dhënave, kërkesat mund të bëhen më efikase thjesht duke shtuar indekset që mungojnë ose duke përdorur ndarjen e duhur.
Linke
Burimi: habr.com
