
W nowoczesnych procesorach jest wiele rdzeni. Przez lata aplikacje wysyłały zapytania do baz danych równolegle. Jeśli jest to raportowe zapytanie do wielu wierszy w tabeli, wykonuje się szybciej, gdy angażuje wiele rdzeni, a w PostgreSQL jest to możliwe od wersji 9.6.
Zajęło to 3 lata, aby zrealizować funkcję zapytań równoległych — trzeba było przepisać kod na różnych etapach wykonywania zapytań. W PostgreSQL 9.6 pojawiła się infrastruktura do dalszego ulepszania kodu. W kolejnych wersjach również inne typy zapytań są wykonywane równolegle.
Ograniczenia
- Nie włączaj równoległego wykonywania, jeśli wszystkie rdzenie są już zajęte, w przeciwnym razie inne zapytania będą spowalniane.
- Najważniejsze, że równoległe przetwarzanie z wysokimi wartościami WORK_MEM zajmuje dużo pamięci — każde połączenie haszowe lub sortowanie zajmuje pamięć w objętości work_mem.
- Zapytania OLTP o niskim opóźnieniu nie mogą być przyspieszone przez równoległe wykonywanie. A jeśli zapytanie zwraca jeden wiersz, równoległe przetwarzanie tylko go spowolni.
- Programiści lubią korzystać z benchmarka TPC-H. Może masz podobne zapytania do idealnego równoległego wykonywania.
- Tylko zapytania SELECT bez blokowania predykatami są wykonywane równolegle.
- Czasami odpowiednia indeksacja jest lepsza od sekwencyjnego skanowania tabeli w trybie równoległym.
- Wstrzymywanie zapytań i kursory nie są obsługiwane.
- Funkcje okienne i funkcje agregujące uporządkowanych zbiorów nie są równoległe.
- Nie zyskujesz nic w obciążeniu wejścia-wyjścia.
- Nie ma równoległych algorytmów sortowania. Ale zapytania z sortowaniem mogą być wykonywane równolegle w niektórych aspektach.
- Zastąp CTE (WITH …) zagnieżdżonym SELECT, aby włączyć równoległe przetwarzanie.
- Wrappery danych zewnętrznych na razie nie obsługują równoległego przetwarzania (a mogłyby!).
- FULL OUTER JOIN nie jest obsługiwany.
- max_rows wyłącza równoległe przetwarzanie.
- Jeśli w zapytaniu jest funkcja, która nie jest oznaczona jako PARALLEL SAFE, zostanie wykonane w jednym wątku.
- Poziom izolacji transakcji SERIALIZABLE wyłącza równoległe przetwarzanie.
Środowisko testowe
Programiści PostgreSQL próbowali skrócić czas reakcji zapytań z benchmarka TPC-H. Pobierz benchmark i . To nieoficjalne użycie benchmarku TPC-H — nie do porównywania baz danych ani sprzętu.
- Pobierz TPC-H_Tools_v2.17.3.zip (lub nowszą wersję) .
- Zmień nazwę makefile.suite na Makefile i zmodyfikuj, jak opisano tutaj: . Skompiluj kod poleceniem make.
- Wygeneruj dane:
.\/dbgen -s 10tworzy bazę danych o wielkości 23 GB. To wystarczy, żeby zobaczyć różnicę w wydajności zapytań równoległych i nierównoległych. - Przekonwertuj pliki
tbldocsv za pomocąised. - Sklonuj repozytorium
pg_tpchi skopiuj plikicsvdopg_tpch\/dss\/data. - Utwórz zapytania poleceniem
qgen. - Załaduj dane do bazy poleceniem
.\/tpch.sh.
Równoległe sekwencyjne skanowanie
Może być szybsze nie przez równoległe odczyty, lecz dlatego, że dane są rozproszone po wielu rdzeniach CPU. W nowoczesnych systemach operacyjnych pliki danych PostgreSQL są dobrze buforowane. Dzięki wstępnym odczytom można uzyskać z pamięci więcej bloków niż żąda demon PG. Dlatego wydajność zapytania nie jest ograniczona przez dyskowe operacje wejścia-wyjścia. Wykorzystuje cykle CPU, aby:
- czytać linie jedna po drugiej z stron tabeli;
- porównywać wartości wierszy i warunki
WHERE.
Wykonaj proste zapytanie 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 msSekwencyjne skanowanie zwraca zbyt wiele wierszy bez agregacji, tak że zapytanie jest realizowane przez jeden rdzeń CPU.
Jeśli dodasz SUM(), widać, że dwa procesy pomogą przyspieszyć zapytanie:
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)
-> Równoległe skanowanie sekwencyjne na 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 msRównoległa agregacja
Węzeł „Parallel Seq Scan” generuje wiersze do częściowej agregacji. Węzeł „Partial Aggregate” ogranicza te wiersze za pomocą SUM(). Na końcu zliczacz SUM z każdego procesu roboczego jest zbierany przez węzeł „Gather”.
Ostateczny wynik jest obliczany przez węzeł „Finalize Aggregate”. Jeśli masz własne funkcje agregacji, nie zapomnij oznaczyć ich jako „parallel safe”.
Liczba procesów roboczych
Liczbę procesów roboczych można zwiększyć bez ponownego uruchamiania serwera:
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)
-> Równoległe skanowanie sekwencyjne na 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 msCo tu się dzieje? Liczba procesów roboczych wzrosła dwukrotnie, a zapytanie stało się jedynie 1,6599 razy szybsze. Obliczenia są interesujące. Mieliśmy 2 procesy robocze i 1 lidera. Po zmianie mieliśmy 4+1.
Nasze maksymalne przyspieszenie dzięki przetwarzaniu równoległemu: 5/3 = 1,66(6) razy.
Jak to działa?
Procesy
Wykonanie zapytania zawsze zaczyna się od procesu lidera. Lider wykonuje wszystkie operacje nieparalne oraz część przetwarzania równoległego. Inne procesy, które wykonują te same zapytania, nazywają się procesami roboczymi. Przetwarzanie równoległe używa infrastruktury (od wersji 9.4). Ponieważ inne części PostgreSQL korzystają z procesów, a nie wątków, zapytanie z 3 procesami roboczymi mogło być 4 razy szybsze niż tradycyjne przetwarzanie.
Interakcja
Procesy robocze komunikują się z liderem za pośrednictwem kolejki wiadomości (opartej na pamięci współdzielonej). Każdy proces ma 2 kolejki: jedną do błędów i jedną do krotek.
Ile potrzebujesz procesów roboczych?
Minimalne ograniczenie ustawia parametr . Potem wykonawca zapytań pobiera procesy robocze z puli, ograniczonej przez parametr . Ostatnie ograniczenie to , czyli całkowita liczba procesów w tle.
Jeśli nie uda się przydzielić procesu roboczego, przetwarzanie będzie jednoprosesowe.
Planista zapytań może zmniejszyć liczbę procesów roboczych w zależności od rozmiaru tabeli lub indeksu. Do tego są parametry i .
set min_parallel_table_scan_size='8MB'
8MB tabela => 1 proces roboczy
24MB tabela => 2 procesy robocze
72MB tabela => 3 procesy robocze
x => log(x / min_parallel_table_scan_size) / log(3) + 1 proces roboczyZa każdym razem, gdy tabela jest 3 razy większa niż min_parallel_(index|table)_scan_size, Postgres dodaje proces roboczy. Liczba procesów roboczych nie opiera się na kosztach. Cykliczne zależności utrudniają skomplikowane realizacje. Zamiast tego planista stosuje proste zasady.
W praktyce te zasady nie zawsze sprawdzają się w produkcji, dlatego można zmienić liczbę procesów roboczych dla konkretnej tabeli: ALTER TABLE … SET (parallel_workers = N).
Dlaczego nie używa się przetwarzania równoległego?
Oprócz długiej listy ograniczeń istnieją również kontrole kosztów:
— aby uniknąć przetwarzania równoległego w przypadku krótkich zapytań. Ten parametr oszacowuje czas potrzebny na przygotowanie pamięci, uruchomienie procesu i początkową wymianę danych.
: komunikacja lidera z pracownikami może być opóźniona proporcjonalnie do liczby krotek od procesów roboczych. Ten parametr oblicza koszty wymiany danych.
Połączenia zagnieżdżonych pętli — Nested Loop Join
PostgreSQL 9.6+ może wykonywać zagnieżdżone pętle równolegle — to prosta operacja.
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;
PLAN ZAPYTANIA
--------------------------------------------------------------------------------------
Finalize GroupAggregate
Klucz grupy: customer.c_custkey
-> Gather Merge
Zaplanowani pracownicy: 4
-> Partial GroupAggregate
Klucz grupy: customer.c_custkey
-> Zagnieżdżone połączenie lewostronne
-> Równoległe skanowanie indeksu tylko na customer
-> Skanowanie indeksu przy użyciu idx_orders_custkey na orders
Warunek indeksu: (customer.c_custkey = o_custkey)
Filtr: ((o_comment)::text !~~ '%specialposits%'::text)Agregacja odbywa się na ostatnim etapie, więc Nested Loop Left Join — to operacja równoległa. Parallel Index Only Scan pojawił się dopiero w wersji 10. Działa w podobny sposób jak równoległe skanowanie sekwencyjne. Warunek c_custkey = o_custkey odczytuje jedno zamówienie dla każdego klienta. Tak więc nie jest to równoległe.
Hash join — Hash Join
Każdy proces roboczy tworzy swoją tablicę haszującą do PostgreSQL 11. A jeśli tych procesów jest więcej niż cztery, wydajność nie wzrośnie. W nowej wersji tablica haszująca jest wspólna. Każdy proces roboczy może używać WORK_MEM, aby utworzyć tablicę haszującą.
wybierz
l_shipmode,
suma(przypadek
gdy o_orderpriority = '1-URGENT'
lub o_orderpriority = '2-HIGH'
wtedy 1
w przeciwnym razie 0
kończ) jako high_line_count,
suma(przypadek
gdy o_orderpriority '1-URGENT'
i o_orderpriority '2-HIGH'
wtedy 1
w przeciwnym razie 0
kończ) jako low_line_count
z
zamówienia,
pozycja zamówienia
gdzie
o_orderkey = l_orderkey
i l_shipmode w ('MAIL', 'AIR')
i l_commitdate < l_receiptdate
i l_shipdate = data '1996-01-01'
i l_receiptdate < data '1996-01-01' + interwał '1' rok
grupuj według
l_shipmode
zamów według
l_shipmode
LIMIT 1;
PLAN ZAPYTANIA
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Limit (koszt=1964755.66..1964961.44 wiersze=1 szerokość=27) (rzeczywisty czas=7579.592..7922.997 wiersze=1 pętle=1)
-> Finalizuj GrupęAgregat (koszt=1964755.66..1966196.11 wiersze=7 szerokość=27) (rzeczywisty czas=7579.590..7579.591 wiersze=1 pętle=1)
Klucz grupy: lineitem.l_shipmode
-> Zgromadź Merguj (koszt=1964755.66..1966195.83 wiersze=28 szerokość=27) (rzeczywisty czas=7559.593..7922.319 wiersze=6 pętle=1)
Planowane pracownicy: 4
Uruchomieni pracownicy: 4
-> Częściowy GrupujAgregat (koszt=1963755.61..1965192.44 wiersze=7 szerokość=27) (rzeczywisty czas=7548.103..7564.592 wiersze=2 pętle=5)
Klucz grupy: lineitem.l_shipmode
-> Sortuj (koszt=1963755.61..1963935.20 wiersze=71838 szerokość=27) (rzeczywisty czas=7530.280..7539.688 wiersze=62519 pętle=5)
Klucz sortowania: lineitem.l_shipmode
Metoda sortowania: zewnętrzna scalanie Dysk: 2304kB
Pracownik 0: Metoda sortowania: zewnętrzna scalanie Dysk: 2064kB
Pracownik 1: Metoda sortowania: zewnętrzna scalanie Dysk: 2384kB
Pracownik 2: Metoda sortowania: zewnętrzna scalanie Dysk: 2264kB
Pracownik 3: Metoda sortowania: zewnętrzna scalanie Dysk: 2336kB
-> Równoległe Haszowanie Dołączenie (koszt=382571.01..1957960.99 wiersze=71838 szerokość=27) (rzeczywisty czas=7036.917..7499.692 wiersze=62519 pętle=5)
Warunek haszowania: (lineitem.l_orderkey = orders.o_orderkey)
-> Równoległe Przeszukiwanie sekwecyjnie na pozycjach (koszt=0.00..1552386.40 wiersze=71838 szerokość=19) (rzeczywisty czas=0.583..4901.063 wiersze=62519 pętle=5)
Filtr: ((l_shipmode = ANY ('{MAIL,AIR}'::bpchar[])) AND (l_commitdate < l_receiptdate) AND (l_shipdate = '1996-01-01'::data) AND (l_receiptdate < '1997-01-01 00:00:00'::znacznik bez strefy czasowej))
Wiersze usunięte przez filtr: 11934691
-> Równoległe Hasz (koszt=313722.45..313722.45 wiersze=3750045 szerokość=20) (rzeczywisty czas=2011.518..2011.518 wiersze=3000000 pętle=5)
Kosze: 65536 Partii: 256 Użycie pamięci: 3840kB
-> Równoległe Przeszukiwanie sekwecyjnie na zamówienia (koszt=0.00..313722.45 wiersze=3750045 szerokość=20) (rzeczywisty czas=0.029..995.948 wiersze=3000000 pętle=5)
Czas planowania: 0.977 ms
Czas wykonania: 7923.770 msZapytanie 12 z TPC-H ilustruje równoległe połączenie haszowe. Każdy proces uczestniczy w tworzeniu wspólnej tabeli haszowej.
Połączenie złączeniem — Merge Join
Połączenie złączeniem jest z natury nieprzyspieszone. Nie martw się, jeśli to ostatni etap zapytania — nadal może być realizowane równolegle.
-- Zapytanie 2 z TPC-H
wyjaśnij (koszty wyłączone) wybierz s_acctbal, s_name, n_name, p_partkey, p_mfgr, s_address, s_phone, s_comment
z part, supplier, partsupp, nation, region
gdzie
p_partkey = ps_partkey
i s_suppkey = ps_suppkey
i p_size = 36
i p_type podobne '%BRASS'
i s_nationkey = n_nationkey
i n_regionkey = r_regionkey
i r_name = 'AMERICA'
i ps_supplycost = (
wybierz
min(ps_supplycost)
z partsupp, supplier, nation, region
gdzie
p_partkey = ps_partkey
i s_suppkey = ps_suppkey
i s_nationkey = n_nationkey
i n_regionkey = r_regionkey
i r_name = 'AMERICA'
)
kolejność według s_acctbal malejąco, n_name, s_name, p_partkey
LIMIT 100;
PLAN ZAPYTANIA
----------------------------------------------------------------------------------------------------------
Limit
-> Sort
Klucz Sortowania: supplier.s_acctbal MALEJĄCO, nation.n_name, supplier.s_name, part.p_partkey
-> Połączenie Scalające
Warunek Scalania: (part.p_partkey = partsupp.ps_partkey)
Filtr Połączenia: (partsupp.ps_supplycost = (SubPlan 1))
-> Scalanie Gromadzące
Zaplanowani pracownicy: 4
-> Równoległe Skany Indeksu przy użyciu <strong>part_pkey</strong> na części
Filtr: (((p_type)::text ~~ '%BRASS'::text) I (p_size = 36))
-> Materializacja
-> Sort
Klucz Sortowania: partsupp.ps_partkey
-> Pętla Zagnieżdżona
-> Pętla Zagnieżdżona
Filtr Połączenia: (nation.n_regionkey = region.r_regionkey)
-> Se kw scan na region
Filtr: (r_name = 'AMERICA'::bpchar)
-> Połączenie Haszowe
Warunek Haszowy: (supplier.s_nationkey = nation.n_nationkey)
-> Seq Scan na supplier
-> Hasz
-> Seq Scan na nation
-> Scan Indeksu przy użyciu idx_partsupp_suppkey na partsupp
Warunek Indeksu: (ps_suppkey = supplier.s_suppkey)
SubPlan 1
-> Agregacja
-> Pętla Zagnieżdżona
Filtr Połączenia: (nation_1.n_regionkey = region_1.r_regionkey)
-> Seq Scan na region region_1
Filtr: (r_name = 'AMERICA'::bpchar)
-> Pętla Zagnieżdżona
-> Pętla Zagnieżdżona
-> Scan Indeksu przy użyciu idx_partsupp_partkey na partsupp partsupp_1
Warunek Indeksu: (part.p_partkey = ps_partkey)
-> Scan Indeksu przy użyciu supplier_pkey na supplier supplier_1
Warunek Indeksu: (s_suppkey = partsupp_1.ps_suppkey)
-> Scan Indeksu przy użyciu nation_pkey na nation nation_1
Warunek Indeksu: (n_nationkey = supplier_1.s_nationkey)Węzeł «Merge Join» znajduje się nad «Gather Merge». Tak więc, złączenie nie korzysta z przetwarzania równoległego. Jednak węzeł «Parallel Index Scan» nadal pomaga z segmentem. part_pkey.
Połączenie według sekcji
W PostgreSQL 11 domyślnie wyłączone: ma bardzo kosztowne planowanie. Tabele o podobnym podziałach można łączyć sekcja po sekcji. W ten sposób Postgres wykorzysta mniejsze tabele haszowe. Każde połączenie sekcji może być równoległe.
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 ZAPYTANIA
---------------------------------------------------
Dodaj
-> Złączenie haszowe
Warunek hasza: (t2.b = t1.a)
-> Skanowanie sekwencyjne na prt2_p1 t2
Filtr: ((b >= 0) AND (b <= 10000))
-> Hasz
-> Skanowanie sekwencyjne na prt1_p1 t1
Filtr: (b = 0)
-> Złączenie haszowe
Warunek hasza: (t2_1.b = t1_1.a)
-> Skanowanie sekwencyjne na prt2_p2 t2_1
Filtr: ((b >= 0) AND (b <= 10000))
-> Hasz
-> Skanowanie sekwencyjne na prt1_p2 t1_1
Filtr: (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 ZAPYTANIA
-----------------------------------------------------------
Zbierz
Zaplanowani pracownicy: 4
-> Równoległy dodatek
-> Równoległe złączenie haszowe
Warunek hasza: (t2_1.b = t1_1.a)
-> Równoległe skanowanie sekwencyjne na prt2_p2 t2_1
Filtr: ((b >= 0) AND (b <= 10000))
-> Równoległy hasz
-> Równoległe skanowanie sekwencyjne na prt1_p2 t1_1
Filtr: (b = 0)
-> Równoległe złączenie haszowe
Warunek hasza: (t2.b = t1.a)
-> Równoległe skanowanie sekwencyjne na prt2_p1 t2
Filtr: ((b >= 0) AND (b <= 10000))
-> Równoległy hasz
-> Równoległe skanowanie sekwencyjne na prt1_p1 t1
Filtr: (b = 0)Główna kwestia, połączenie według sekcji może być równoległe, tylko jeśli te sekcje są wystarczająco duże.
Równoległy dodatek — Parallel Append
może być używany zamiast różnych bloków w różnych procesach roboczych. Zwykle jest to związane z zapytaniami UNION ALL. Wadą jest mniejszy poziom równoległości, ponieważ każdy proces roboczy przetwarza tylko 1 zapytanie.
Tutaj uruchomiono 2 procesy robocze, chociaż włączono 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)Najważniejsze zmienne
- WORK_MEM ogranicza ilość pamięci dla każdego procesu, nie tylko dla zapytań: work_mem procesy połączenia = bardzo dużo pamięci.
- — ile procesów roboczych program będzie używać do równoległego przetwarzania z planu.
- — dostosowuje ogólną liczbę procesów roboczych do liczby rdzeni CPU na serwerze.
- — to samo, ale dla równoległych procesów roboczych.
Podsumowanie
Od wersji 9.6 równoległe przetwarzanie może znacznie poprawić wydajność skomplikowanych zapytań, które skanują wiele wierszy lub indeksów. W PostgreSQL 10 równoległe przetwarzanie jest domyślnie włączone. Nie zapomnij wyłączyć go na serwerach z dużym obciążeniem OLTP. Sekwencyjne skany lub skany indeksów zużywają bardzo dużo zasobów. Jeśli nie wykonujesz raportu na całym zestawie danych, zapytania można uczynić bardziej efektywnymi, po prostu dodając brakujące indeksy lub korzystając z odpowiedniego partycjonowania.
Linki
Źródło: habr.com
