sql >> Databasteknik >  >> RDS >> PostgreSQL

PostgreSQL-baserad applikationsprestanda:latens och dolda förseningar

Goldfields Pipeline, av SeanMac (Wikimedia Commons)

Om du försöker optimera prestandan för din PostgreSQL-baserade applikation fokuserar du förmodligen på de vanliga verktygen:FÖRKLARA (BUFFARAR, ANALYSE) , pg_stat_statements , auto_explain , log_statement_min_duration , etc.

Kanske tittar du på låsstrid med log_lock_waits , övervaka din kontrollpunkts prestanda, etc.

Men tänkte du på nätverkslatens ? Spelare känner till nätverkslatens, men trodde du att det spelade någon roll för din applikationsserver?

Latensen spelar roll

Typiska nätverkslatenser för klient/server tur och retur kan sträcka sig från 0,01 ms (lokal värd) till ~0,5 ms för ett växlat nätverk, 5 ms wifi, 20 ms ADSL, 300 ms interkontinental routing och ännu mer för saker som satellit- och WWAN-länkar .

En trivial SELECT kan ta i storleksordningen 0,1 ms för att köra serversidan. En trivial INSERT kan ta 0,5 ms.

Varje gång din applikation kör en fråga måste den vänta på att servern svarar med framgång/misslyckande och eventuellt en resultatuppsättning, frågemetadata, etc. Detta medför minst en nätverksfördröjning tur och retur.

När du arbetar med små, enkla frågor kan nätverkslatens vara betydande i förhållande till exekveringstiden för dina frågor om din databas inte finns på samma värd som din applikation.

Många applikationer, särskilt ORM, är mycket benägna att köra massor av ganska enkla frågor. Till exempel, om din Hibernate-app hämtar en enhet med en lätt hämtad @OneToMany förhållande till 1000 underordnade föremål kommer det förmodligen att göra 1001 frågor tack vare n+1-valproblemet, om inte fler. Det betyder att det förmodligen spenderar 1 000 gånger ditt nätverk tur och retur latens bara väntar . Du kan vänster gå med i hämta för att undvika det... men sedan överför du den överordnade enheten 1000 gånger i joinen och måste deduplicera den.

På liknande sätt, om du fyller i databasen från en ORM, gör du förmodligen hundratusentals triviala INSERT s... och väntar efter var och en på att servern ska bekräfta att det är OK.

Det är lätt att försöka fokusera på frågekörningstiden och försöka optimera det, men det finns bara så mycket du kan göra med en trivial INSERT INTO ...VÄRDEN ... . Släpp några index och begränsningar, se till att det är batchat i en transaktion, och du är i stort sett klar.

Vad sägs om att bli av med alla nätverksväntningar? Även på ett LAN börjar de lägga ihop över tusentals frågor.

KOPIERA

Ett sätt att undvika latens är att använda COPY . För att använda PostgreSQL:s COPY-stöd måste din applikation eller drivrutin skapa en CSV-liknande uppsättning rader och strömma dem till servern i en kontinuerlig sekvens. Eller så kan servern bli ombedd att skicka en CSV-liknande stream till din applikation.

Hur som helst, appen kan inte interfoliera en COPY med andra frågor, och kopia-inlägg måste laddas direkt i en destinationstabell. Ett vanligt tillvägagångssätt är att KOPIERA in i en temporär tabell, gör sedan en INSERT INTO ... SELECT ... , UPPDATERA ... FRÅN .... , RADERA FRÅN ... ANVÄNDER... , etc för att använda den kopierade datan för att modifiera huvudtabellerna i en enda operation.

Det är praktiskt om du skriver din egen SQL direkt, men många applikationsramverk och ORM:er stöder det inte, plus att det bara direkt kan ersätta enkla INSERT . Din applikation, ditt ramverk eller din klientdrivrutin måste hantera konvertering för den speciella representation som krävs av COPY , slå upp alla nödvändiga typer av metadata själv, etc.

(Anmärkningsvärda drivrutiner som gör stödja COPY inkluderar libpq, PgJDBC, psycopg2 och Pg-ädelstenen... men inte nödvändigtvis ramverken och ORM:erna som byggts ovanpå dem.)

PgJDBC – batchläge

PostgreSQL:s JDBC-drivrutin har en lösning för detta problem. Det är beroende av stöd som finns i PostgreSQL-servrar sedan 8.4 och på JDBC API:s batchfunktioner för att skicka en batch av förfrågningar till servern väntar sedan bara en gång på bekräftelse på att hela batchen kördes OK.

Tja, i teorin. I verkligheten begränsar vissa implementeringsutmaningar detta så att partier i bästa fall endast kan göras i bitar av några hundra frågor. Drivrutinen kan också bara köra frågor som returnerar resultatrader i batchbitar om den kan räkna ut hur stora resultaten kommer att bli i förväg. Trots dessa begränsningar, använd Statement.executeBatch() kan erbjuda en enorm prestandaökning för applikationer som utför uppgifter som bulkdataladdar fjärrdatabasinstanser.

Eftersom det är ett standard-API kan det användas av applikationer som fungerar över flera databasmotorer. Hibernate, till exempel, kan använda JDBC-batchning även om det inte gör det som standard.

libpq och batchning

De flesta (alla?) andra PostgreSQL-drivrutiner har inget stöd för batchning. PgJDBC implementerar PostgreSQL-protokollet helt oberoende, medan de flesta andra drivrutiner internt använder C-biblioteket libpq som tillhandahålls som en del av PostgreSQL.

libpq stöder inte batchning. Den har ett asynkront icke-blockerande API, men klienten kan fortfarande bara ha en fråga "i flygning" åt gången. Den måste vänta tills resultaten av den frågan tas emot innan den kan skicka en annan.

PostgreSQL servern stöder batchning bra, och PgJDBC använder det redan. Så jag har skrivit batchstöd för libpq och skickade in den som en kandidat för nästa PostgreSQL-version. Eftersom det bara ändrar klienten, om det accepteras kommer det fortfarande att påskynda saker och ting när du ansluter till äldre servrar.

Jag skulle verkligen vara intresserad av feedback från författare och avancerade användare av libpq -baserade klientdrivrutiner och utvecklare av libpq -baserade applikationer. Plåstret appliceras fint ovanpå PostgreSQL 9.6beta1 om du vill prova det. Dokumentationen är detaljerad och det finns ett omfattande exempelprogram.

Prestanda

Jag trodde att en värddatabastjänst som RDS eller Heroku Postgres skulle vara ett bra exempel på var den här typen av funktionalitet skulle vara användbar. I synnerhet, att komma åt dem från våra egna nätverk visar verkligen hur mycket latens kan skada.

Vid ~320 ms nätverkslatens:

  • 500 skär utan batchning:167.0s
  • 500 skär med batchning:1,2s

… vilket är över 120 gånger snabbare.

Du kommer vanligtvis inte att köra din app över en interkontinental länk mellan appservern och databasen, men detta tjänar till att belysa effekten av latens. Även över en unix-socket till localhost såg jag en prestandaförbättring på över 50 % för 10 000 insatser.

Batchning i befintliga appar

Det är tyvärr inte möjligt att automatiskt aktivera batchning för befintliga applikationer. Appar måste använda ett lite annorlunda gränssnitt där de skickar en rad frågor och först då frågar efter resultaten.

Det borde vara ganska enkelt att anpassa appar som redan använder det asynkrona libpq-gränssnittet, speciellt om de använder icke-blockerande läge och en select() /poll() /epoll() /WaitForMultipleObjectsEx slinga. Appar som använder den synkrona libpq gränssnitt kommer att kräva fler ändringar.

Batchning i andra klientdrivrutiner

På liknande sätt kommer klientdrivrutiner, ramverk och ORM i allmänhet att behöva gränssnitt och interna ändringar för att tillåta användning av batchning. Om de redan använder en händelseslinga och icke-blockerande I/O bör de vara ganska enkla att modifiera.

Jag skulle älska att se Python, Ruby, etc-användare som kan komma åt den här funktionen, så jag är nyfiken på att se vem som är intresserad. Tänk dig att kunna göra detta:

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()

Asynkron batchkörning behöver inte vara komplicerad på klientnivå.

COPY är snabbast

Där praktiska kunder fortfarande borde gynna COPY . Här är några resultat från min bärbara dator:

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

Att kombinera arbetet ger en förvånansvärt stor prestandaökning även på en lokal unix-uttag... men KOPIERA lämnar båda individuella insatser långt bakom sig i dammet.

Använd COPY .

Bilden

Bilden för det här inlägget är av Goldfields Water Supply Scheme-rörledningen från Mundaring Weir nära Perth i västra Australien till guldfälten i inlandet (öknen). Det är relevant eftersom det tog så lång tid att slutföra och var under så intensiv kritik att dess designer och främsta förespråkare, C. Y. O'Connor, begick självmord 12 månader innan det togs i bruk. Lokalt säger folk ofta (felaktigt) att han dog efter rörledningen byggdes när inget vatten rann – eftersom det bara tog så lång tid att alla antog att rörledningsprojektet hade misslyckats. Sedan veckor senare, hällde vattnet ut.


  1. Postgres misslyckas med 'kunde inte öppna relationsmappningsfilen global/pg_filenode.map'

  2. Typer av SQL JOIN

  3. Importera data från Excel-kalkylblad eller CVS till MySQL

  4. Hur får man en lista kolumnnamn och datatyper för en tabell i PostgreSQL?