PostgreSQL
 sql >> Baza danych >  >> RDS >> PostgreSQL

Wydajność aplikacji opartych na PostgreSQL:opóźnienia i ukryte opóźnienia

Goldfields Pipeline, SeanMac (Wikimedia Commons)

Jeśli próbujesz zoptymalizować wydajność swojej aplikacji opartej na PostgreSQL, prawdopodobnie skupiasz się na zwykłych narzędziach:WYJAŚNIJ (BUFORY, ANALIZA) , pg_stat_statements , automatyczne wyjaśnienie , log_statement_min_duration itp.

Może szukasz rywalizacji o blokadę z log_lock_waits , monitorowanie wydajności punktów kontrolnych itp.

Ale czy myślałeś o opóźnieniu sieciowym? ? Gracze wiedzą o opóźnieniach sieciowych, ale czy uważasz, że ma to znaczenie dla Twojego serwera aplikacji?

Opóźnienie ma znaczenie

Typowe opóźnienia sieci klient/serwer w obie strony mogą wynosić od 0,01 ms (localhost) do ~0,5 ms sieci przełączanej, 5 ms Wi-Fi, 20 ms ADSL, 300 ms routingu międzykontynentalnego, a nawet więcej w przypadku łączy satelitarnych i WWAN .

Trywialny SELECT wykonanie po stronie serwera może zająć 0,1 ms. Trywialna WSTAW może zająć 0,5 ms.

Za każdym razem, gdy aplikacja uruchamia zapytanie, musi czekać na odpowiedź serwera z wynikiem powodzenia/porażki i prawdopodobnie zestawem wyników, metadanymi zapytania itp. Powoduje to co najmniej jedno opóźnienie w obie strony sieci.

Gdy pracujesz z małymi, prostymi zapytaniami, opóźnienia w sieci mogą być znaczące w stosunku do czasu wykonania zapytań, jeśli baza danych nie znajduje się na tym samym hoście, co aplikacja.

Wiele aplikacji, zwłaszcza ORM-ów, jest bardzo podatnych na uruchamianie wielu dość prostych zapytań. Na przykład, jeśli Twoja aplikacja Hibernate pobiera encję z leniwym pobraniem @OneToMany związek z 1000 elementów potomnych prawdopodobnie wykona 1001 zapytań dzięki problemowi wyboru n+1, jeśli nie więcej. Oznacza to, że prawdopodobnie wydaje 1000-krotność opóźnienia w podróży w obie strony sieci, po prostu czekając . Możesz pobrać z lewej strony aby tego uniknąć… ale potem przenosisz jednostkę nadrzędną 1000 razy w łączeniu i musisz ją deduplikować.

Podobnie, jeśli wypełniasz bazę danych z ORM, prawdopodobnie wykonujesz setki tysięcy trywialnych WSTAW s… i czekanie za każdym razem, aż serwer potwierdzi, że wszystko jest w porządku.

Łatwo jest skoncentrować się na czasie wykonania zapytania i spróbować go zoptymalizować, ale za pomocą trywialnego INSERT INTO ...VALUES... można zrobić tylko tyle. . Odrzuć niektóre indeksy i ograniczenia, upewnij się, że są one powiązane z transakcją i gotowe.

A co z pozbyciem się wszystkich czekających na sieć? Nawet w sieci LAN zaczynają dodawać tysiące zapytań.

KOPIUJ

Jednym ze sposobów na uniknięcie opóźnień jest użycie KOPIUJ . Aby korzystać z obsługi COPY PostgreSQL, Twoja aplikacja lub sterownik musi utworzyć zestaw wierszy podobny do CSV i przesyłać je strumieniowo na serwer w ciągłej sekwencji. Lub serwer może zostać poproszony o przesłanie Twojej aplikacji strumienia podobnego do CSV.

Tak czy inaczej, aplikacja nie może przeplatać KOPII z innymi zapytaniami, a kopie wstawiane muszą być ładowane bezpośrednio do tabeli docelowej. Powszechnym podejściem jest KOPIUJ do tabeli tymczasowej, a następnie wykonaj INSERT INTO ... SELECT ... , AKTUALIZACJA... Z.... , USUŃ Z ... UŻYWAJĄC... itp., aby użyć skopiowanych danych do modyfikacji głównych tabel w jednej operacji.

Jest to przydatne, jeśli bezpośrednio piszesz własny SQL, ale wiele frameworków aplikacji i ORM-ów go nie obsługuje, a ponadto może bezpośrednio zastąpić proste INSERT . Twoja aplikacja, platforma lub sterownik klienta musi radzić sobie z konwersją w celu uzyskania specjalnej reprezentacji wymaganej przez KOPIUJ , wyszukaj wymagane metadane typu itp.

(Wybitne sterowniki, które robią wsparcie KOPIUJ obejmują libpq, PgJDBC, psycopg2 i klejnot Pg… ale niekoniecznie frameworki i ORM zbudowane na nich.)

PgJDBC – tryb wsadowy

Sterownik JDBC PostgreSQL ma rozwiązanie tego problemu. Opiera się na wsparciu obecnym na serwerach PostgreSQL od wersji 8.4 oraz na funkcjach wsadowych API JDBC do wysyłania partii zapytań do serwera, a następnie czekaj tylko raz na potwierdzenie, że cała partia działała poprawnie.

Cóż, teoretycznie. W rzeczywistości niektóre wyzwania związane z implementacją ograniczają to, tak że partie mogą być wykonywane tylko w kawałkach po kilkaset zapytań. Sterownik może również uruchamiać zapytania, które zwracają wiersze wyników w porcjach wsadowych, jeśli może określić z wyprzedzeniem, jak duże będą wyniki. Pomimo tych ograniczeń, użyj Statement.executeBatch() może zaoferować ogromny wzrost wydajności aplikacjom wykonującym zadania, takie jak masowe ładowanie danych zdalnych instancji baz danych.

Ponieważ jest to standardowy interfejs API, może być używany przez aplikacje działające w wielu silnikach baz danych. Hibernate, na przykład, może używać przetwarzania wsadowego JDBC, chociaż domyślnie nie robi tego.

libpq i grupowanie

Większość (wszystkie?) innych sterowników PostgreSQL nie obsługuje przetwarzania wsadowego. PgJDBC implementuje protokół PostgreSQL całkowicie niezależnie, podczas gdy większość innych sterowników wewnętrznie korzysta z biblioteki C libpq który jest dostarczany jako część PostgreSQL.

libpq nie obsługuje grupowania. Ma asynchroniczny nieblokujący interfejs API, ale klient nadal może mieć tylko jedno zapytanie „w locie” na raz. Musi poczekać, aż wyniki tego zapytania zostaną odebrane, zanim będzie mogło wysłać kolejne.

serwer PostgreSQL obsługuje grupowanie w porządku, a PgJDBC już go używa. Więc napisałem obsługę wsadową dla libpq i przesłał go jako kandydat do następnej wersji PostgreSQL. Ponieważ zmienia tylko klienta, jeśli zostanie zaakceptowany, nadal przyspieszy działanie podczas łączenia się ze starszymi serwerami.

Byłbym naprawdę zainteresowany opiniami autorów i zaawansowanych użytkowników libpq oparte na sterownikach klienta i programiści libpq aplikacje oparte. Łatka działa dobrze na PostgreSQL 9.6beta1, jeśli chcesz ją wypróbować. Dokumentacja jest szczegółowa i dostępny jest obszerny przykładowy program.

Wydajność

Pomyślałem, że hostowana usługa bazy danych, taka jak RDS lub Heroku Postgres, będzie dobrym przykładem tego, gdzie taka funkcjonalność byłaby przydatna. W szczególności dostęp do nich z naszej własnej sieci naprawdę pokazuje, jak duże opóźnienie może zaszkodzić.

Przy opóźnieniu sieci ~320 ms:

  • 500 wstawek bez grupowania:167,0 s
  • 500 wstawek z grupowaniem:1,2 s

… co jest ponad 120 razy szybsze.

Zwykle nie będziesz uruchamiać swojej aplikacji przez łącze międzykontynentalne między serwerem aplikacji a bazą danych, ale służy to podkreśleniu wpływu opóźnień. Nawet przy gnieździe unix do hosta lokalnego zauważyłem ponad 50% poprawę wydajności dla 10000 wstawek.

Pakietowanie w istniejących aplikacjach

Niestety nie jest możliwe automatyczne włączenie grupowania dla istniejących aplikacji. Aplikacje muszą używać nieco innego interfejsu, w którym wysyłają serię zapytań, a dopiero potem proszą o wyniki.

Dostosowanie aplikacji, które już używają asynchronicznego interfejsu libpq, powinno być dość proste, zwłaszcza jeśli używają trybu nieblokującego i select() /poll() /epoll() /WaitForMultipleObjectsEx pętla. Aplikacje korzystające z synchronicznego libpq interfejsy będą wymagały dalszych zmian.

Dołączanie innych sterowników klienta

Podobnie sterowniki klienta, frameworki i ORM będą generalnie wymagały zmian w interfejsie i wewnętrznych, aby umożliwić użycie przetwarzania wsadowego. Jeśli używają już pętli zdarzeń i nieblokujących operacji we/wy, ich modyfikacja powinna być dość prosta.

Chciałbym, aby użytkownicy Pythona, Ruby itp. mogli uzyskać dostęp do tej funkcjonalności, więc jestem ciekaw, kto jest zainteresowany. Wyobraź sobie, że możesz to zrobić:

import psycopg2
conn = psycopg2.connect(...)
cur = conn.cursor()

# this is just an idea, this code does not work with psycopg2:
futures = [ cur.async_execute(sql) for sql in my_queries ]
for future in futures:
    result = future.result  # waits if result not ready yet
    ... process the result ...
conn.commit()

Asynchroniczne wykonywanie wsadowe nie musi być skomplikowane na poziomie klienta.

KOPIOWANIE jest najszybsze

Tam, gdzie praktyczni klienci powinni nadal preferować KOPIUJ . Oto kilka wyników z mojego laptopa:

inserting 1000000 rows batched, unbatched and with COPY
batch insert elapsed:      23.715315s
sequential insert elapsed: 36.150162s
COPY elapsed:              1.743593s
Done.

Grupowanie pracy zapewnia zaskakująco duży wzrost wydajności nawet na lokalnym połączeniu z gniazdem unix…. ale KOPIUJ pozostawia obie pojedyncze wkładki daleko za sobą w kurzu.

Użyj KOPIUJ .

Obraz

Obraz w tym poście przedstawia rurociąg Goldfields Water Supply Scheme z Mundaring Weir w pobliżu Perth w Australii Zachodniej do śródlądowych (pustynnych) pól złota. Jest to istotne, ponieważ ukończenie trwało tak długo i było tak intensywnie krytykowane, że jego projektant i główny propagator, C.Y. O’Connor, popełnił samobójstwo 12 miesięcy przed oddaniem go do użytku. Miejscowi ludzie często (niesłusznie) mówią, że zmarł po rurociąg został zbudowany, gdy woda nie płynęła – ponieważ trwało to tak długo, że wszyscy zakładali, że projekt rurociągu się nie powiódł. Potem kilka tygodni później wylała się woda.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Przyznaj wszystko w określonym schemacie w bazie danych do roli grupowej w PostgreSQL

  2. Konfigurowanie klucza obcego z innym typem danych

  3. Ściągawka konfiguracji PostgreSQL

  4. Hibernacja, zapisywanie modelu użytkownika w Postgres

  5. Zachowanie NOT LIKE z wartościami NULL