Zbiorcza optymalizacja zapytań PostgreSQL. Kirill Borovikov (Tensor)

W raporcie przedstawiono kilka podejść, które na to pozwalają monitoruj wydajność zapytań SQL, gdy jest ich miliony dzienniei istnieją setki monitorowanych serwerów PostgreSQL.

Jakie rozwiązania techniczne pozwalają nam sprawnie przetworzyć taką ilość informacji i jak ułatwia to życie zwykłego programisty?


Kto jest zainteresowany? analiza konkretnych problemów i różne techniki optymalizacji Zapytania SQL i rozwiązywanie typowych problemów DBA w PostgreSQL - Ty też możesz przeczytaj serię artykułów na ten temat.

Zbiorcza optymalizacja zapytań PostgreSQL. Kirill Borovikov (Tensor)
Nazywam się Kirill Borovikov i reprezentuję Firma Tensor. W szczególności specjalizuję się w pracy z bazami danych w naszej firmie.

Dzisiaj opowiem Ci jak optymalizujemy zapytania, kiedy nie musisz „rozbierać” wydajności pojedynczego zapytania, ale rozwiązać problem masowo. Kiedy są miliony próśb i musisz je znaleźć podejścia do rozwiązania ten duży problem.

Ogólnie rzecz biorąc, Tensor dla miliona naszych klientów jest VLSI to nasza aplikacja: korporacyjna sieć społecznościowa, rozwiązania do komunikacji wideo, wewnętrznego i zewnętrznego obiegu dokumentów, systemy księgowe dla księgowości i magazynów,... Czyli taki „megakombinat” do zintegrowanego zarządzania przedsiębiorstwem, w którym znajduje się ponad 100 różnych projekty wewnętrzne.

Aby zapewnić im normalną pracę i rozwój, posiadamy 10 centrów rozwoju na terenie całego kraju, a jest ich coraz więcej 1000 programistów.

Z PostgreSQL współpracujemy od 2008 roku i zgromadziliśmy dużą ilość tego, co przetwarzamy - dane klientów, statystyczne, analityczne, dane z zewnętrznych systemów informatycznych - ponad 400TB. W samej produkcji znajduje się około 250 serwerów, a łącznie monitorujemy około 1000 serwerów baz danych.

Zbiorcza optymalizacja zapytań PostgreSQL. Kirill Borovikov (Tensor)

SQL jest językiem deklaratywnym. Opisujesz nie „jak” coś powinno działać, ale „co” chcesz osiągnąć. DBMS wie lepiej, jak wykonać JOIN - jak połączyć tabele, jakie warunki narzucić, co przejdzie przez indeks, a co nie...

Niektóre systemy DBMS akceptują podpowiedzi: „Nie, połącz te dwie tabele w taką a taką kolejkę”, ale PostgreSQL nie może tego zrobić. Takie jest świadome stanowisko wiodących programistów: „Wolelibyśmy dokończyć optymalizację zapytań, niż pozwolić programistom na skorzystanie z jakichś wskazówek”.

Ale pomimo tego, że PostgreSQL nie pozwala „zewnętrznemu” kontrolować się, doskonale na to pozwala zobaczyć, co się w nim dziejekiedy uruchamiasz zapytanie i gdzie występują problemy.

Zbiorcza optymalizacja zapytań PostgreSQL. Kirill Borovikov (Tensor)

Ogólnie rzecz biorąc, z jakimi klasycznymi problemami zwykle spotyka się programista [do administratora bazy danych]? „Tutaj spełniliśmy prośbę i u nas wszystko jest powolne, wszystko wisi, coś się dzieje... Jakieś kłopoty!”

Powody są prawie zawsze takie same:

  • nieefektywny algorytm zapytań
    Deweloper: „Teraz daję mu 10 tabel w SQL przez JOIN...” - i oczekuje, że jego warunki zostaną cudownie „rozwiązane” i wszystko szybko dostanie. Ale cuda się nie zdarzają i każdy system o takiej zmienności (10 tabel w jednej FROM) zawsze daje jakiś błąd. [artykuł]
  • nieaktualne statystyki
    Ten punkt jest bardzo istotny szczególnie w przypadku PostgreSQL, kiedy „przelewasz” duży zbiór danych na serwer, wysyłasz żądanie, a to „sekskanizuje” Twój tablet. Bo wczoraj było w nim 10 rekordów, a dziś jest 10 milionów, ale PostgreSQL jeszcze o tym nie wie i trzeba mu o tym powiedzieć. [artykuł]
  • „podłącz” zasoby
    Zainstalowałeś dużą i mocno obciążoną bazę danych na słabym serwerze, który nie ma wystarczającej ilości dysku, pamięci lub procesora. I tyle... Gdzieś jest pułap wydajności, powyżej którego nie da się już przeskoczyć.
  • bloking
    Jest to trudny punkt, ale są one najbardziej istotne w przypadku różnych zapytań modyfikujących (INSERT, UPDATE, DELETE) - jest to osobny duży temat.

Uzyskanie planu

...I za wszystko inne my potrzebuję planu! Musimy zobaczyć, co dzieje się wewnątrz serwera.

Zbiorcza optymalizacja zapytań PostgreSQL. Kirill Borovikov (Tensor)

Plan wykonania zapytania dla PostgreSQL to drzewo algorytmu wykonania zapytania w reprezentacji tekstowej. To właśnie algorytm, który w wyniku analizy przeprowadzonej przez planistę, okazał się najskuteczniejszy.

Każdy węzeł drzewa to operacja: pobieranie danych z tabeli lub indeksu, tworzenie mapy bitowej, łączenie dwóch tabel, łączenie, przecinanie lub wykluczanie zaznaczeń. Wykonanie zapytania polega na przejściu przez węzły tego drzewa.

Aby uzyskać plan zapytania, najłatwiej jest wykonać instrukcję EXPLAIN. Aby uzyskać wszystkie rzeczywiste atrybuty, czyli faktycznie wykonać zapytanie w bazie - EXPLAIN (ANALYZE, BUFFERS) SELECT ....

Zła część: kiedy go uruchamiasz, dzieje się to „tu i teraz”, więc nadaje się tylko do lokalnego debugowania. Jeśli weźmiesz pod uwagę bardzo obciążony serwer, na który narażony jest duży przepływ zmian danych, i zobaczysz: „Och! Tutaj mamy powolną realizacjęsya wniosek." Pół godziny, godzinę temu - kiedy uruchamiałeś i pobierałeś to żądanie z dzienników, a następnie przenosiłeś je z powrotem na serwer, cały twój zbiór danych i statystyki uległy zmianie. Uruchamiasz go w celu debugowania - i działa szybko! I nie możesz zrozumieć dlaczego, dlaczego było powoli.

Zbiorcza optymalizacja zapytań PostgreSQL. Kirill Borovikov (Tensor)

Aby zrozumieć, co dokładnie stało się w momencie wykonania żądania na serwerze, napisali mądrzy ludzie moduł auto_explain. Jest obecny w prawie wszystkich najpopularniejszych dystrybucjach PostgreSQL i można go po prostu aktywować w pliku konfiguracyjnym.

Jeśli zorientuje się, że jakieś żądanie trwa dłużej niż ustawiony limit, robi to „migawkę” planu tego żądania i zapisuje je razem w logu.

Zbiorcza optymalizacja zapytań PostgreSQL. Kirill Borovikov (Tensor)

Wszystko wydaje się być w porządku, idziemy do kłody i tam widzimy... [tekst podnóżek]. Ale nie możemy nic na ten temat powiedzieć poza tym, że jest to doskonały plan, ponieważ jego wykonanie zajęło 11 ms.

Wszystko wydaje się być w porządku – jednak nie jest jasne, co się właściwie stało. Poza ogólnym czasem nie widzimy właściwie nic. Ponieważ patrzenie na takiego „baranka” zwykłego tekstu na ogół nie jest wizualne.

Ale nawet jeśli nie jest to oczywiste, nawet jeśli jest to niewygodne, istnieją bardziej podstawowe problemy:

  • Węzeł wskazuje suma zasobów całego poddrzewa pod nim. Oznacza to, że nie można po prostu dowiedzieć się, ile czasu minęło na tym konkretnym skanowaniu indeksu, jeśli znajduje się pod nim jakiś zagnieżdżony warunek. Musimy dynamicznie sprawdzić, czy w środku znajdują się „dzieci” i zmienne warunkowe, CTE – i odjąć to wszystko „w naszych głowach”.
  • Punkt drugi: czas wskazany w węźle to czas wykonania pojedynczego węzła. Jeśli węzeł ten został wykonany w wyniku np. kilkukrotnego przechodzenia przez rekordy tabeli w pętli, to liczba pętli – cykli tego węzła – w planie wzrasta. Ale sam czas wykonania atomowego pozostaje taki sam pod względem planu. Oznacza to, że aby zrozumieć, jak długo w sumie wykonywano ten węzeł, należy pomnożyć jedną rzecz przez drugą - znowu „w głowie”.

W takich sytuacjach należy zrozumieć „Kto jest najsłabszym ogniwem?” Prawie niemożliwe. Dlatego nawet sami programiści piszą to w „podręczniku”. „Zrozumienie planu to sztuka, której trzeba się nauczyć, doświadczyć…”.

Ale mamy 1000 programistów i nie da się przekazać tego doświadczenia każdemu z nich. Ja, ty, on wiemy, ale ktoś tam już nie wie. Może się nauczy, a może nie, ale teraz musi popracować – a gdzie miałby zdobyć to doświadczenie?

Wizualizacja planu

Dlatego zdaliśmy sobie sprawę, że aby uporać się z tymi problemami, potrzebujemy dobra wizualizacja planu. [artykuł]

Zbiorcza optymalizacja zapytań PostgreSQL. Kirill Borovikov (Tensor)

Najpierw przeszliśmy „przez rynek” - zajrzyjmy do Internetu, aby zobaczyć, co w ogóle istnieje.

Okazało się jednak, że stosunkowo „żywych” rozwiązań, które są mniej lub bardziej rozwijające się, jest bardzo niewiele – dosłownie tylko jedno: wyjaśnij.depesz.com Huberta Lubaczewskiego. Kiedy wpiszesz tekstową reprezentację planu w polu „feed”, wyświetli się tabela z przeanalizowanymi danymi:

  • własny czas przetwarzania węzła
  • całkowity czas dla całego poddrzewa
  • liczba pobranych rekordów, która była statystycznie oczekiwana
  • samą treść węzła

Usługa ta posiada również możliwość udostępniania archiwum linków. Wrzuciłeś tam swój plan i powiedziałeś: „Hej, Wasya, tu jest link, coś tu jest nie tak”.

Zbiorcza optymalizacja zapytań PostgreSQL. Kirill Borovikov (Tensor)

Ale są też małe problemy.

Po pierwsze, ogromna ilość „kopiuj-wklej”. Bierzesz kawałek kłody, wbijasz go tam i jeszcze raz, i jeszcze raz.

Po drugie, brak analizy ilości odczytanych danych — te same bufory, które wysyłają EXPLAIN (ANALYZE, BUFFERS), tutaj tego nie widzimy. Po prostu nie wie, jak je rozebrać, zrozumieć i pracować z nimi. Kiedy czytasz dużo danych i zdajesz sobie sprawę, że być może błędnie alokujesz dysk i pamięć podręczną, ta informacja jest bardzo ważna.

Trzecim negatywnym punktem jest bardzo słaby rozwój tego projektu. Zatwierdzenia są bardzo małe, dobrze jeśli są raz na pół roku, a kod jest w Perlu.

Zbiorcza optymalizacja zapytań PostgreSQL. Kirill Borovikov (Tensor)

Ale to wszystko „teksty”, moglibyśmy jakoś z tym żyć, ale jest jedna rzecz, która bardzo nas odwiodła od tej usługi. Są to błędy w analizie wspólnego wyrażenia tabelowego (CTE) i różnych węzłów dynamicznych, takich jak InitPlan/SubPlan.

Jeśli wierzyć temu obrazowi, całkowity czas wykonania każdego pojedynczego węzła jest większy niż całkowity czas wykonania całego żądania. To proste - czas generowania tego CTE nie został odjęty od węzła skanowania CTE. Dlatego nie znamy już prawidłowej odpowiedzi na pytanie, ile czasu trwało samo skanowanie CTE.

Zbiorcza optymalizacja zapytań PostgreSQL. Kirill Borovikov (Tensor)

Wtedy zdaliśmy sobie sprawę, że nadszedł czas, aby napisać własny – hurra! Każdy programista mówi: „Teraz napiszemy własne, to będzie super proste!”

Wzięliśmy stos typowy dla usług sieciowych: rdzeń oparty na Node.js + Express, wykorzystaliśmy Bootstrap i D3.js do pięknych diagramów. I nasze oczekiwania były w pełni uzasadnione – pierwszy prototyp otrzymaliśmy w 2 tygodnie:

  • niestandardowy analizator planu
    Oznacza to, że teraz możemy przeanalizować dowolny plan z tych wygenerowanych przez PostgreSQL.
  • poprawna analiza węzłów dynamicznych - Skanowanie CTE, InitPlan, SubPlan
  • analiza rozkładu buforów - skąd odczytywane są strony danych z pamięci, skąd z lokalnej pamięci podręcznej, skąd z dysku
  • uzyskałem jasność
    Żeby nie „przekopywać” tego wszystkiego w logu, a żeby od razu zobaczyć na obrazku „najsłabsze ogniwo”.

Zbiorcza optymalizacja zapytań PostgreSQL. Kirill Borovikov (Tensor)

Mamy coś takiego, z uwzględnieniem podświetlania składni. Ale zazwyczaj nasi programiści nie pracują już z pełną reprezentacją planu, ale z krótszą. Przecież przeanalizowaliśmy już wszystkie liczby i rzuciliśmy je w lewo i prawo, a w środku zostawiliśmy tylko pierwszą linijkę, jaki to rodzaj węzła: CTE Scan, generacja CTE lub Seq Scan według jakiegoś znaku.

To jest skrócona reprezentacja, którą nazywamy szablon planu.

Zbiorcza optymalizacja zapytań PostgreSQL. Kirill Borovikov (Tensor)

Co jeszcze byłoby wygodne? Wygodnie byłoby sprawdzić, jaka część naszego całkowitego czasu jest przydzielona któremu węzłowi - i po prostu „przykleić to” na bok wykres kołowy.

Wskazujemy na węzeł i widzimy – okazuje się, że Seq Scan zajęło mniej niż jedną czwartą całkowitego czasu, a pozostałe 3/4 zajął CTE Scan. Przerażenie! To mała uwaga na temat „szybkości” CTE Scan, jeśli aktywnie używasz ich w swoich zapytaniach. Nie są zbyt szybkie - ustępują nawet zwykłemu skanowaniu tabeli. [artykuł] [artykuł]

Ale zazwyczaj takie diagramy są ciekawsze, bardziej złożone, gdy od razu wskazujemy na segment i widzimy, że np. w ponad połowie przypadków „zjadł” jakiś Seq Scan. Co więcej, w środku znajdował się jakiś Filtr, zgodnie z nim odrzucono wiele rekordów... Możesz bezpośrednio rzucić to zdjęcie deweloperowi i powiedzieć: „Wasya, wszystko jest tu dla ciebie złe! Pomyśl, spójrz – coś jest nie tak!”

Zbiorcza optymalizacja zapytań PostgreSQL. Kirill Borovikov (Tensor)

Oczywiście w grę wchodziły pewne „grabie”.

Pierwszą rzeczą, na którą natrafiliśmy, był problem z zaokrągleniami. Czas każdego pojedynczego węzła w planie jest wskazywany z dokładnością do 1 μs. A gdy liczba cykli węzła przekroczy np. 1000 - po wykonaniu PostgreSQL podzielonego „w granicach dokładności”, to po przeliczeniu otrzymamy całkowity czas „gdzieś pomiędzy 0.95ms a 1.05ms”. Gdy licznik liczy się w mikrosekundach, wszystko jest w porządku, ale gdy już są [mili]sekundy, należy wziąć tę informację pod uwagę przy „rozłączaniu” zasobów z węzłami planu „kto zużył ile”.

Zbiorcza optymalizacja zapytań PostgreSQL. Kirill Borovikov (Tensor)

Drugi punkt, bardziej złożony, to dystrybucja zasobów (tych buforów) pomiędzy węzłami dynamicznymi. Kosztowało nas to pierwsze 2 tygodnie prototypu plus kolejne 4 tygodnie.

Dość łatwo o taki problem - robimy CTE i rzekomo coś w nim czytamy. W rzeczywistości PostgreSQL jest „inteligentny” i nie będzie tam niczego czytać bezpośrednio. Następnie pobieramy z niego pierwszy zapis i do niego sto pierwszy z tego samego CTE.

Zbiorcza optymalizacja zapytań PostgreSQL. Kirill Borovikov (Tensor)

Patrzymy na plan i rozumiemy - to dziwne, mamy 3 bufory (strony danych) „zużyte” w Seq Scan, 1 więcej w CTE Scan i 2 kolejne w drugim CTE Scan. Oznacza to, że jeśli po prostu wszystko podsumujemy, otrzymamy 6, ale z tabliczki odczytamy tylko 3! CTE Scan nie czyta niczego znikąd, ale współpracuje bezpośrednio z pamięcią procesową. To znaczy, że coś tu jest wyraźnie nie tak!

W rzeczywistości okazuje się, że oto wszystkie 3 strony danych, o które poproszono od Seq Scan, najpierw 1 poprosiła o 1. skan CTE, a następnie o 2. i 2 kolejne. To znaczy w sumie Dane zostały odczytane z 3 stron, a nie z 6.

Zbiorcza optymalizacja zapytań PostgreSQL. Kirill Borovikov (Tensor)

I ten obraz doprowadził nas do zrozumienia, że ​​wykonanie planu nie jest już drzewem, ale po prostu jakimś acyklicznym wykresem. Otrzymaliśmy taki diagram, żebyśmy w pierwszej kolejności zrozumieli, „co skąd się wzięło”. Oznacza to, że tutaj stworzyliśmy CTE z pg_class i poprosiliśmy o to dwukrotnie, a prawie cały nasz czas spędziliśmy na gałęzi, gdy poprosiliśmy o to drugi raz. Wiadomo, że odczytanie 2. wpisu jest znacznie droższe niż samo odczytanie 101. wpisu z tabletu.

Zbiorcza optymalizacja zapytań PostgreSQL. Kirill Borovikov (Tensor)

Oddychaliśmy przez chwilę. Powiedzieli: „Teraz, Neo, znasz kung fu! Teraz nasze doświadczenie jest bezpośrednio na Twoim ekranie. Teraz możesz z niego skorzystać.” [artykuł]

Konsolidacja logów

1000 naszych programistów odetchnęło z ulgą. Ale zrozumieliśmy, że mamy tylko setki serwerów „bojowych”, a całe to „kopiuj-wklej” ze strony programistów wcale nie jest wygodne. Zdaliśmy sobie sprawę, że musimy to zebrać sami.

Zbiorcza optymalizacja zapytań PostgreSQL. Kirill Borovikov (Tensor)

Ogólnie rzecz biorąc, istnieje standardowy moduł, który może zbierać statystyki, jednak należy go również aktywować w konfiguracji - to moduł pg_stat_statements. Ale nam nie pasował.

Po pierwsze, przypisuje do tych samych zapytań przy użyciu różnych schematów w ramach tej samej bazy danych różne identyfikatory zapytań. To znaczy, jeśli zrobisz to po raz pierwszy SET search_path = '01'; SELECT * FROM user LIMIT 1;a potem SET search_path = '02'; i to samo zapytanie, to statystyki tego modułu będą miały różne zapisy i nie będę mógł zbierać statystyk ogólnych konkretnie w kontekście tego profilu zgłoszeń, bez uwzględnienia schematów.

Drugi punkt, który uniemożliwił nam korzystanie z niego, to brak planów. Oznacza to, że nie ma planu, jest tylko sama prośba. Widzimy, co zwalniało, ale nie rozumiemy dlaczego. I tu wracamy do problemu szybko zmieniającego się zbioru danych.

I ostatnia chwila - brak „faktów”. Oznacza to, że nie można zająć się konkretną instancją wykonania zapytania - taka nie istnieje, istnieją jedynie zagregowane statystyki. Chociaż można z tym pracować, jest to po prostu bardzo trudne.

Zbiorcza optymalizacja zapytań PostgreSQL. Kirill Borovikov (Tensor)

Dlatego postanowiliśmy walczyć z kopiowaniem i wklejaniem i zaczęliśmy pisać kolekcjoner.

Kolektor łączy się poprzez SSH, nawiązuje bezpieczne połączenie z serwerem z bazą danych za pomocą certyfikatu oraz tail -F „przylega” do niego w pliku dziennika. I tak w tej sesji otrzymujemy pełne „lustrzane odbicie” całego pliku dziennika, który generuje serwer. Obciążenie samego serwera jest minimalne, ponieważ niczego tam nie analizujemy, tylko odzwierciedlamy ruch.

Ponieważ zaczęliśmy już pisać interfejs w Node.js, kontynuowaliśmy pisanie w nim kolektora. I ta technologia się usprawiedliwiła, bo bardzo wygodnie jest używać JavaScriptu do pracy ze słabo sformatowanymi danymi tekstowymi, czyli logiem. A sama infrastruktura Node.js jako platforma backendowa pozwala łatwo i wygodnie pracować z połączeniami sieciowymi, a nawet z dowolnymi strumieniami danych.

W związku z tym „rozciągamy” dwa połączenia: pierwsze, aby „słuchać” samego dziennika i zabierać go do siebie, a drugie, aby okresowo pytać bazę. „Ale log pokazuje, że znak z oid 123 jest zablokowany”, ale dla programisty nie ma to żadnego znaczenia, a fajnie byłoby zapytać bazę danych: „Co to w ogóle jest OID = 123?” I tak okresowo pytamy bazę, czego jeszcze o sobie nie wiemy.

Zbiorcza optymalizacja zapytań PostgreSQL. Kirill Borovikov (Tensor)

„Jest tylko jedna rzecz, której nie wziąłeś pod uwagę: istnieje gatunek pszczół przypominających słonie!…” Zaczęliśmy rozwijać ten system, gdy chcieliśmy monitorować 10 serwerów. Najbardziej krytyczny w naszym rozumieniu, gdzie pojawiły się problemy, z którymi trudno było sobie poradzić. Ale w pierwszym kwartale dostaliśmy stówkę za monitoring – bo system działał, wszyscy tego chcieli, każdemu było wygodnie.

To wszystko trzeba zsumować, przepływ danych jest duży i aktywny. Tak naprawdę to, co monitorujemy, z czym możemy sobie poradzić, jest tym, czego używamy. Używamy również PostgreSQL jako magazynu danych. A nic tak szybko nie „wleje” do niego danych, jak operator COPY Jeszcze nie.

Jednak samo „przelewanie” danych tak naprawdę nie jest naszą technologią. Ponieważ jeśli masz około 50 100 żądań na sekundę na stu serwerach, wygeneruje to 150–XNUMX GB logów dziennie. Dlatego musieliśmy ostrożnie „wyciąć” podstawę.

Po pierwsze, zrobiliśmy to podział w ciągu dnia, bo w zasadzie nikogo nie interesuje korelacja pomiędzy dniami. Co za różnica, co miałeś wczoraj, jeśli dziś wieczorem wdrożyłeś nową wersję aplikacji - i już trochę nowych statystyk.

Po drugie, nauczyliśmy się (byliśmy zmuszeni) bardzo, bardzo szybki do pisania COPY. To znaczy nie tylko COPYponieważ jest szybszy niż INSERT, a nawet szybciej.

Zbiorcza optymalizacja zapytań PostgreSQL. Kirill Borovikov (Tensor)

Trzeci punkt – musiałem porzucić odpowiednio wyzwalacze i klucze obce. Oznacza to, że w ogóle nie mamy integralności referencyjnej. Ponieważ jeśli masz tabelę zawierającą parę FK i w strukturze bazy danych powiesz, że „oto rekord dziennika, do którego FK odwołuje się na przykład do grupy rekordów”, to po wstawieniu go PostgreSQL Nie pozostaje nic innego, jak tylko to wziąć i zrobić uczciwie SELECT 1 FROM master_fk1_table WHERE ... z identyfikatorem, który próbujesz wstawić - tylko po to, aby sprawdzić, czy ten rekord tam występuje, czy wstawiając nie „urywasz” tego Klucza Obcego.

Zamiast jednego rekordu do tabeli docelowej i jej indeksów, uzyskujemy dodatkową korzyść polegającą na czytaniu ze wszystkich tabel, do których się odnosi. Ale nam to wcale nie jest potrzebne – naszym zadaniem jest nagrać jak najwięcej i jak najszybciej przy jak najmniejszym obciążeniu. Więc FK - dół!

Następnym punktem jest agregacja i hashowanie. Początkowo implementowaliśmy je w bazie danych - wszak wygodnie jest od razu, gdy pojawi się zapis, zrobić to na jakimś tablecie „plus jeden” bezpośrednio w spuście. Cóż, jest to wygodne, ale ma tę samą wadę - wstawiasz jeden rekord, ale jesteś zmuszony czytać i zapisywać coś innego z innej tabeli. Co więcej, nie tylko czytasz i piszesz, ale robisz to za każdym razem.

Teraz wyobraź sobie, że masz tabelę, w której po prostu liczysz liczbę żądań, które przeszły przez określonego hosta: +1, +1, +1, ..., +1. A ty w zasadzie tego nie potrzebujesz - wszystko jest możliwe suma pamięci w kolektorze i wysłać do bazy danych za jednym razem +10.

Tak, w przypadku jakichś problemów, Twoja integralność logiczna może się „rozpaść”, ale jest to przypadek wręcz nierealny - bo masz normalny serwer, ma on baterię w kontrolerze, masz logi transakcji, logi na serwerze system plików... Generalnie nie warto. Utrata produktywności wynikająca z uruchamiania wyzwalaczy/FK nie jest warta poniesionych wydatków.

Podobnie jest z hashowaniem. Przylatuje do Ciebie pewna prośba, wyliczasz z niej określony identyfikator w bazie, zapisujesz ją do bazy i następnie przekazujesz wszystkim. Wszystko jest w porządku, dopóki w momencie nagrywania nie przyjdzie do Ciebie druga osoba, która chce nagrać to samo – i zostajesz zablokowany, a to już jest złe. Dlatego jeśli możesz przenieść generację niektórych identyfikatorów na klienta (w stosunku do bazy danych), lepiej to zrobić.

U nas idealnie sprawdziło się wykorzystanie MD5 z tekstu - zapytanie, plan, szablon,... Obliczamy to po stronie kolekcjonera i „wlewamy” gotowy identyfikator do bazy danych. Długość MD5 i codzienne partycjonowanie pozwalają nam nie martwić się o ewentualne kolizje.

Zbiorcza optymalizacja zapytań PostgreSQL. Kirill Borovikov (Tensor)

Aby jednak to wszystko szybko nagrać, musieliśmy zmodyfikować samą procedurę nagrywania.

Jak zazwyczaj zapisujecie dane? Mamy jakiś zbiór danych, dzielimy go na kilka tabel, a następnie KOPIUJEMY - najpierw do pierwszej, potem do drugiej, do trzeciej... Jest to niewygodne, ponieważ wydaje nam się, że jeden strumień danych piszemy w trzech krokach sekwencyjnie. Nieprzyjemny. Czy można to zrobić szybciej? Móc!

Aby to zrobić, wystarczy rozłożyć te przepływy równolegle względem siebie. Okazuje się, że mamy błędy, żądania, szablony, blokady,… latające w osobnych wątkach – i piszemy to wszystko równolegle. Wystarczy na to utrzymuj kanał COPY stale otwarty dla każdej indywidualnej tabeli docelowej.

Zbiorcza optymalizacja zapytań PostgreSQL. Kirill Borovikov (Tensor)

To znaczy u kolektora zawsze jest strumień, w którym mogę zapisać potrzebne mi dane. Ale żeby baza danych widziała te dane i ktoś nie utknął w oczekiwaniu na zapis tych danych, KOPIOWANIE należy przerywać w określonych odstępach czasu. Dla nas najskuteczniejszy okres wynosił około 100ms - zamykamy go i od razu otwieramy ponownie przy tym samym stole. A jeśli w niektórych szczytach nie wystarczy nam jednego przepływu, wówczas łączymy je do pewnego limitu.

Dodatkowo odkryliśmy, że przy takim profilu obciążenia jakakolwiek agregacja, gdy rekordy zbierane są partiami, jest zła. Klasyczne zło jest INSERT ... VALUES i kolejnych 1000 rekordów. Ponieważ w tym momencie masz szczyt zapisu na nośniku i wszyscy inni, którzy będą próbowali zapisać coś na dysku, będą czekać.

Aby pozbyć się takich anomalii, po prostu niczego nie agreguj, w ogóle nie buforuj. A jeśli rzeczywiście nastąpi buforowanie na dysk (na szczęście Stream API w Node.js pozwala się o tym przekonać) - odłóż to połączenie. Gdy otrzymasz zdarzenie, że jest znów wolne, napisz do niego z skumulowanej kolejki. A gdy będzie zajęty, weź następny wolny z puli i napisz do niego.

Przed wprowadzeniem takiego podejścia do rejestracji danych mieliśmy około 4K operacji zapisu i w ten sposób zmniejszyliśmy obciążenie 4-krotnie. Teraz wzrosły one kolejne 6-krotnie dzięki nowym monitorowanym bazom danych - do 100MB/s. A teraz przechowujemy logi z ostatnich 3 miesięcy w objętości około 10-15 TB, mając nadzieję, że w ciągu zaledwie trzech miesięcy każdy programista będzie w stanie rozwiązać każdy problem.

Rozumiemy problemy

Ale samo zebranie wszystkich tych danych jest dobre, przydatne, istotne, ale nie wystarczające - trzeba to zrozumieć. Bo to są miliony różnych planów dziennie.

Zbiorcza optymalizacja zapytań PostgreSQL. Kirill Borovikov (Tensor)

Ale milionami nie da się zarządzać, najpierw musimy zrobić „mniej”. Przede wszystkim musisz zdecydować, jak zorganizujesz tę „mniejszą” rzecz.

Zidentyfikowaliśmy trzy kluczowe punkty:

  • kto wysłał tę prośbę
    To znaczy z jakiej aplikacji „przybyło”: interfejs sieciowy, backend, system płatności czy coś innego.
  • gdzie stało się
    Na jakim konkretnym serwerze? Ponieważ jeśli masz kilka serwerów w ramach jednej aplikacji i nagle jeden „zgłupia” (ponieważ „dysk jest zepsuty”, „wyciek pamięci”, jakiś inny problem), musisz konkretnie zaadresować serwer.
  • jak problem objawił się w taki czy inny sposób

Aby zrozumieć „kto” wysłał do nas zapytanie, korzystamy ze standardowego narzędzia – ustawiając zmienną sesyjną: SET application_name = '{bl-host}:{bl-method}'; — wysyłamy nazwę hosta logiki biznesowej, z której przychodzi żądanie, oraz nazwę metody lub aplikacji, która je zainicjowała.

Po przekazaniu „właściciela” żądania należy je wypisać do logu – w tym celu konfigurujemy zmienną log_line_prefix = ' %m [%p:%v] [%d] %r %a'. Dla zainteresowanych może poszukaj w instrukcjico to wszystko znaczy. Okazuje się, że w logu widzimy:

  • czas
  • identyfikatory procesów i transakcji
  • nazwa bazy danych
  • Adres IP osoby, która wysłała to żądanie
  • i nazwa metody

Zbiorcza optymalizacja zapytań PostgreSQL. Kirill Borovikov (Tensor)

Potem zdaliśmy sobie sprawę, że przyglądanie się korelacji jednego żądania między różnymi serwerami nie jest zbyt interesujące. Nieczęsto zdarza się, że jedna aplikacja zawodzi tu i tam. Ale nawet jeśli jest tak samo, spójrz na którykolwiek z tych serwerów.

Oto cięcie „jeden serwer - jeden dzień” okazało się, że wystarczyło nam to do jakichkolwiek analiz.

Pierwsza część analityczna jest taka sama "próbka" - skrócona forma prezentacji planu, oczyszczona ze wszystkich wskaźników liczbowych. Drugie cięcie to aplikacja lub metoda, a trzecie cięcie to konkretny węzeł planu, który spowodował nam problemy.

Kiedy przeszliśmy od konkretnych instancji do szablonów, uzyskaliśmy dwie korzyści jednocześnie:

  • wielokrotne zmniejszenie liczby obiektów do analizy
    Musimy już analizować problem poprzez tysiące zapytań czy planów, ale poprzez dziesiątki szablonów.
  • oś czasu
    Oznacza to, że podsumowując „fakty” w określonej sekcji, możesz wyświetlić ich wygląd w ciągu dnia. I tu można zrozumieć, że jeśli masz jakiś schemat, który zdarza się np. raz na godzinę, ale powinien zdarzać się raz dziennie, to warto pomyśleć o tym, co poszło nie tak – kto to spowodował i dlaczego, może powinno być tutaj nie powinien. Jest to kolejna nienumeryczna, czysto wizualna metoda analizy.

Zbiorcza optymalizacja zapytań PostgreSQL. Kirill Borovikov (Tensor)

Pozostałe metody opierają się na wskaźnikach, które wyciągamy z planu: ile razy taki schemat wystąpił, całkowity i średni czas, ile danych zostało odczytanych z dysku, a ile z pamięci...

Bo np. wchodzisz na stronę analityki dla hosta, spójrz – coś zaczyna za dużo czytać z dysku. Dysk na serwerze tego nie wytrzyma - kto z niego czyta?

I możesz sortować według dowolnej kolumny i decydować, czym się teraz zajmiesz - obciążeniem procesora lub dysku, czy całkowitą liczbą żądań... Posortowaliśmy to, przejrzeliśmy „najwyższe”, naprawiliśmy i udostępnił nową wersję aplikacji.
[wykład wideo]

I od razu możesz zobaczyć różne aplikacje, które pochodzą z tego samego szablonu na podstawie żądania SELECT * FROM users WHERE login = 'Vasya'. Frontend, backend, przetwarzanie... I zastanawiasz się, po co przetwarzanie miałoby czytać użytkownika, jeśli nie wchodzi z nim w interakcję.

Odwrotnym sposobem jest natychmiastowe sprawdzenie aplikacji, co ona robi. Na przykład frontend to to, to, to i to raz na godzinę (pomaga oś czasu). I od razu pojawia się pytanie: wygląda na to, że zadaniem frontendu nie jest robienie czegoś raz na godzinę…

Zbiorcza optymalizacja zapytań PostgreSQL. Kirill Borovikov (Tensor)

Po pewnym czasie zdaliśmy sobie sprawę, że brakuje nam zagregowanego statystyki według węzłów planu. Z planów wyizolowaliśmy tylko te węzły, które robią coś z danymi samych tabel (odczytują/zapisują je według indeksu lub nie). W rzeczywistości dodano tylko jeden aspekt w stosunku do poprzedniego obrazu - ile rekordów przyniósł nam ten węzeł?i ile zostało odrzuconych (wiersze usunięte przez filtr).

Nie masz odpowiedniego indeksu na płycie, składasz do niego prośbę, przelatuje obok indeksu, wpada do Seq Scan... odfiltrowałeś wszystkie rekordy oprócz jednego. Po co Ci 100M filtrowanych rekordów dziennie?Czy nie lepiej zwinąć indeks?

Zbiorcza optymalizacja zapytań PostgreSQL. Kirill Borovikov (Tensor)

Po przeanalizowaniu wszystkich planów węzeł po węźle zdaliśmy sobie sprawę, że w planach znajdują się pewne typowe struktury, które z dużym prawdopodobieństwem będą wyglądać podejrzanie. I miło byłoby powiedzieć programiście: „Przyjacielu, tutaj najpierw czytasz według indeksu, potem sortujesz, a potem odcinasz” - z reguły jest jeden rekord.

Każdy, kto pisał zapytania, spotkał się zapewne ze schematem: „Podaj ostatnie zamówienie dla Wasi, jego datę”. A jeśli nie masz indeksu według dat, albo nie ma daty w indeksie, z którego korzystałeś, to nadepnij na dokładnie te same „grabie”.

Ale wiemy, że to „grabie” - więc dlaczego nie od razu powiedzieć programiście, co powinien zrobić. W związku z tym, otwierając teraz plan, nasz programista natychmiast widzi piękny obraz ze wskazówkami, na którym natychmiast mu mówią: „Tu i tam masz problemy, ale rozwiązuje się je w ten i inny sposób”.

W rezultacie ilość doświadczenia potrzebnego do rozwiązywania problemów na początku i obecnie znacznie spadła. Takie właśnie mamy narzędzie.

Zbiorcza optymalizacja zapytań PostgreSQL. Kirill Borovikov (Tensor)

Źródło: www.habr.com

Dodaj komentarz