Piszemy w PostgreSQL na podświetlonym: 1 host, 1 dzień, 1TB

Niedawno opowiedziałem, jak przy pomocy standardowych przepisów zwiększyć wydajność zapytań SQL „do odczytu” z bazy PostgreSQL. Dziś natomiast omówimy, jak można uczynić bardziej efektywnym zapis do bazy danych bez użycia jakichkolwiek „kręciołów” w konfiguracji — po prostu odpowiednio organizując przepływ danych.

Piszemy w PostgreSQL na podświetlonym: 1 host, 1 dzień, 1TB

#1. Секционирование

Artykuł o tym, jak i dlaczego warto organizować aplikacyjne sekcjonowanie „w teorii” już był, tutaj natomiast omówimy praktyczne zastosowanie niektórych podejść w ramach naszego serwisu monitorowania setek serwerów PostgreSQL.

„Czasy dawno minione...”

Początkowo, jak każdy MVP, nasz projekt wystartował z dość niewielkim obciążeniem — monitorowanie dotyczyło tylko dziesięciu najważniejszych serwerów, wszystkie tabele były relatywnie kompaktowe… Jednak czas mijał, liczba monitorowanych hostów rosła, a próbując kolejny raz coś zrobić z jedną z tabel o rozmiarze 1.5TB, zrozumieliśmy, że można żyć tak dalej, ale jest to bardzo niewygodne.

Czasy były niemalże legendarnie, aktualne były różne wersje PostgreSQL 9.x, więc całe sekcjonowanie musiało odbywać się „ręcznie” — przez dziedziczenie tabel i wyzwalacze routingu z dynamicznym EXECUTE.

Piszemy w PostgreSQL na podświetlonym: 1 host, 1 dzień, 1TB
Ostatecznie rozwiązanie okazało się na tyle uniwersalne, aby można było zastosować je do wszystkich tabel:

  • Ogłoszono pustą „nagłówkową” tabelę nadrzędną, na której opisano wszystkie potrzebne indeksy i wyzwalacze.
  • Zapis z punktu widzenia klienta był realizowany w „tabeli głównej”, a wewnętrznie przy pomocy wyzwalacza routingu BEFORE INSERT zapis „fizycznie” był wstawiany do odpowiedniej sekcji. Jeśli takiej jeszcze nie było — łapaliśmy wyjątek i …
  • … przy pomocy CREATE TABLE ... (LIKE ... INCLUDING ...) według wzoru tabeli nadrzędnej tworzono sekcję z ograniczeniem na potrzebną datę, aby przy pobieraniu danych odczyt odbywał się tylko w niej.

PG10: pierwsza próba

Ale sekcjonowanie przez dziedziczenie w historii nie było zbyt dobrze przystosowane do pracy z aktywnym przepływem zapisów lub dużą ilością sekcji-potomków. Na przykład można przypomnieć sobie, że algorytm wyboru odpowiedniej sekcji miał kwadratową złożoność, co przy 100+ sekcjach działa, sami rozumiecie jak…

W PG10 tę sytuację znacznie zoptymalizowano, wprowadzając wsparcie dla natywnego sekcjonowaniaDlatego od razu spróbowaliśmy go zastosować zaraz po migracji magazynu, ale…

Jak się okazało po przeszukaniu instrukcji, natywnie sekcjonowana tabela w tej wersji:

  • nie obsługuje opisu indeksów
  • nie obsługuje na niej triggerów
  • nie może być nikim «potomkiem»
  • nie obsługuje INSERT ... ON CONFLICT
  • nie umie automatycznie tworzyć sekcji

Po bolesnym uderzeniu w czoło zrozumieliśmy, że nie obędzie się bez modyfikacji aplikacji i odłożyliśmy dalsze badania na pół roku.

PG10: druga szansa

Zaczęliśmy więc rozwiązywać pojawiające się problemy po kolei:

  1. Ponieważ triggery i ON CONFLICT okazały się w pewnych miejscach naprawdę potrzebne, do ich obsługi stworzyliśmy pośrednią tabelę proxy.
  2. Pozbyliśmy się «routing» w triggerach — to znaczy od EXECUTE.
  3. Wydzieliliśmy osobno szablon tabeli ze wszystkimi indeksami, aby nawet nie mogły być obecne w tabeli proxy.

Piszemy w PostgreSQL na podświetlonym: 1 host, 1 dzień, 1TB
W końcu, po wszystkim tym, natywnie odsekwencjonowaliśmy główną tabelę. Tworzenie nowej sekcji wciąż pozostało w gestii aplikacji.

«Tworzymy» słowniki

Jak w każdym systemie analitycznym, mieliśmy również «fakty» i «cięcia» (słowniki). W naszym przypadku, w tej roli występowały na przykład, treść «szablonu» jednorodnych wolnych zapytań lub tekst samego zapytania.

«Fakty» były już od dłuższego czasu sekcjonowane według dni, więc spokojnie usuwaliśmy przestarzałe sekcje, nie przeszkadzały nam (w końcu to logi!). Ale z słownikami pojawił się problem…

Nie można powiedzieć, że było ich bardzo dużo, ale na około 100TB «faktów» uzyskaliśmy słownik na 2.5TB.Z takiej tabeli trudno cokolwiek usunąć, nie można skompresować w rozsądnym czasie, a zapis do niej stawał się stopniowo coraz wolniejszy.

Niby słownik… w nim każdy wpis powinien być przedstawiony dokładnie jeden raz… i to jest prawidłowe, ale!.. Nikt nie przeszkadza nam mieć po jednym słowniku na każdy dzień! Tak, to przynosi pewną nadmiarowość, ale pozwala:

  • czytać/zapisywać szybciej dzięki mniejszemu rozmiarowi sekcji
  • zużywać mniej pamięci dzięki pracy z bardziej kompaktowymi indeksami
  • przechowywać mniej danych dzięki możliwości szybkiego usuwania przestarzałych

W rezultacie całego kompleksu działań obciążenie CPU zmniejszyło się o ~30%, obciążenie dysku — o ~50%:

Piszemy w PostgreSQL na podświetlonym: 1 host, 1 dzień, 1TB
Jednocześnie nadal pisaliśmy do bazy dokładnie to samo, po prostu z mniejszym obciążeniem.

#2. Эволюция и рефакторинг БД

Więc zatrzymaliśmy się na tym, że mamy na każdy dzień swoją sekcję z danymi. Właściwie, CHECK (dt = '2018-10-12'::date) — to jest klucz do sekcjonowania i warunek umieszczania wpisu w konkretnej sekcji.

Ponieważ wszystkie raporty w naszym serwisie są tworzone w odniesieniu do konkretnej daty, indeksy z "niesekcjonowanych czasów" były dla nich wszystkie typu (Serwer, Data, Szablon planu), (Serwer, Data, Węzeł planu), (Data, Klasa błędu, Serwer),…

Ale teraz w każdej sekcji żyją swoje instancje każdego takiego indeksu… A w ramach każdej sekcji data to stała… Okazuje się, że teraz wpisujemy do każdego takiego indeksu banalnie stałą jako jedno z pól, co zwiększa zarówno jego objętość, jak i czas wyszukiwania, ale nie przynosi żadnych wyników. Sami zostawiliśmy sobie pułapki, ups…

Piszemy w PostgreSQL na podświetlonym: 1 host, 1 dzień, 1TB
Kierunek optymalizacji jest oczywisty — po prostu usuwamy pole z datą ze wszystkich indeksów na sekcjonowanych tabelach. Przy naszych obciążeniach zysk to około 1TB/tydzień!

A teraz zauważmy, że ten terabajt również musiałby być jakoś zapisany. To znaczy my również musimy teraz obciążyć dysk mniej! Na tym obrazku dobrze widać uzyskany efekt przeprowadzonej czyszczenia, któremu poświęciliśmy tydzień:

Piszemy w PostgreSQL na podświetlonym: 1 host, 1 dzień, 1TB

#3. «Размазываем» пиковую нагрузку

Jednym z dużych problemów obciążonych systemów jest nadmiarowa synchronizacja pewnych operacji, które tego nie wymagają. Czasami "bo nie zauważyliśmy", czasami "tak było łatwiej", ale prędzej czy później trzeba się jej pozbyć.

Przybliżamy poprzedni obrazek — i widzimy, że dysk "ciągnie" obciążenie z dwukrotną amplitudą między sąsiednimi pomiarami, czego jasno "statystycznie" nie powinno być przy takiej liczbie operacji:

Piszemy w PostgreSQL na podświetlonym: 1 host, 1 dzień, 1TB

Osiągnięcie tego jest dość proste. Mieliśmy już monitorowanie na prawie 1000 serwerów, każdy przetwarzany był przez oddzielny logiczny wątek, a każdy wątek zrzucał zgromadzone informacje do wysłania do bazy z określoną regularnością, mniej więcej tak:

setInterval(sendToDB, interval)

Problem kryje się dokładnie w tym, że wszystkie wątki startują mniej więcej w tym samym czasie, dlatego momenty wysyłania są prawie zawsze zgodne "do punktu". Ups nr 2…

Na szczęście, to można dość łatwo naprawić, dodając "losową" rozbieżność w czasie:

setInterval(sendToDB, interval * (1 + 0.1 * (Math.random() - 0.5)))

#4. Кэшируем, что нужно можно

Trzeci tradycyjny problem highload — brak cache'u tam, gdzie powinien mógłby być.

Na przykład, wprowadziliśmy możliwość analizy na poziomie węzłów planu (wszystkie te Seq Scan na użytkownikach), ale myślenie, że są one w większości identyczne — to błąd.

Nie, oczywiście, do bazy nic nie jest ponownie zapisywane, to odcina trigger z INSERT ... ON CONFLICT DO NOTHING. Ale te dane i tak docierają do bazy, a nawet generują dodatkowe odczyty do weryfikacji konfliktu które trzeba przeprowadzić. Ups nr 3…

Różnica w liczbie rekordów wysyłanych do bazy przed/po włączeniu cachowania — jest oczywista:

Piszemy w PostgreSQL na podświetlonym: 1 host, 1 dzień, 1TB

A to — towarzyszący spadek obciążenia na składowisku:

Piszemy w PostgreSQL na podświetlonym: 1 host, 1 dzień, 1TB

Podsumowując

„Terabajt-na-dzień” tylko brzmi strasznie. Jeśli robisz wszystko dobrze, to to tylko 2^40 bajtów / 86400 sekund = ~12.5MB/s, co były w stanie wytrzymać nawet biurowe dyski IDE. 🙂

A jeśli mówimy poważnie, to nawet przy dziesięciokrotnym „przekroczeniu” obciążenia w ciągu doby, możesz spokojnie zmieścić się w możliwościach nowoczesnych SSD.

Piszemy w PostgreSQL na podświetlonym: 1 host, 1 dzień, 1TB

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster