sql >> Database teknologi >  >> RDS >> PostgreSQL

PostgreSQL-baseret applikationsydelse:latens og skjulte forsinkelser

Goldfields Pipeline, af SeanMac (Wikimedia Commons)

Hvis du forsøger at optimere ydeevnen af ​​din PostgreSQL-baserede applikation, fokuserer du sandsynligvis på de sædvanlige værktøjer:EXPLAIN (BUFFERE, ANALYSE) , pg_stat_statements , auto_explain , log_statement_min_duration osv.

Måske kigger du på låsestrid med log_lock_waits , overvågning af dit kontrolpunkts ydeevne osv.

Men tænkte du på netværksforsinkelse ? Spillere kender til netværksforsinkelse, men troede du, at det havde betydning for din applikationsserver?

Latens er vigtig

Typiske klient/server tur-retur-netværksforsinkelser kan variere fra 0,01 ms (localhost) til ~0,5 ms af et switchet netværk, 5 ms wifi, 20 ms ADSL, 300 ms interkontinental routing og endnu mere for ting som satellit- og WWAN-links .

Et trivielt SELECT kan tage i størrelsesordenen 0,1 ms at udføre server-side. En triviel INDSÆT kan tage 0,5 ms.

Hver gang din applikation kører en forespørgsel, skal den vente på, at serveren reagerer med succes/fejl og muligvis et resultatsæt, forespørgselsmetadata osv. Dette medfører mindst én netværksforsinkelse.

Når du arbejder med små, enkle forespørgsler, kan netværksforsinkelsen være betydelig i forhold til udførelsestiden for dine forespørgsler, hvis din database ikke er på samme vært som din applikation.

Mange applikationer, især ORM'er, er meget tilbøjelige til at køre partier af ganske simple forespørgsler. For eksempel, hvis din Hibernate-app henter en enhed med en dovent hentet @OneToMany forhold til 1000 underordnede elementer, vil det sandsynligvis gøre 1001 forespørgsler takket være n+1 select-problemet, hvis ikke mere. Det betyder, at den sandsynligvis bruger 1000 gange dit netværks tur-retur-forsinkelse på bare at vente . Du kan tilslutte hentning for at undgå det... men så overfører du den overordnede enhed 1000 gange i joinforbindelsen og skal deduplikere den.

På samme måde, hvis du udfylder databasen fra en ORM, laver du sandsynligvis hundredtusindvis af trivielle INSERT s... og venter efter hver og en på, at serveren bekræfter, at det er OK.

Det er nemt at forsøge at fokusere på forespørgselsudførelsestid og forsøge at optimere det, men der er kun så meget, du kan gøre med en triviel INSERT INTO ...VALUES ... . Drop nogle indekser og begrænsninger, sørg for, at det er samlet i en transaktion, og du er stort set færdig.

Hvad med at slippe af med alle netværksventer? Selv på et LAN begynder de at samle sig over tusindvis af forespørgsler.

KOPI

En måde at undgå latency på er at bruge COPY . For at bruge PostgreSQL's COPY-understøttelse skal din applikation eller driver producere et CSV-lignende sæt rækker og streame dem til serveren i en kontinuerlig sekvens. Eller serveren kan blive bedt om at sende din applikation en CSV-lignende stream.

Uanset hvad, kan appen ikke sammenflette en COPY med andre forespørgsler, og kopi-indsættelser skal indlæses direkte i en destinationstabel. En almindelig tilgang er at KOPIERE ind i en midlertidig tabel, og lav derefter en INSERT INTO ... SELECT ... , OPDATERING ... FRA .... , SLET FRA ... BRUGER... osv. for at bruge de kopierede data til at ændre hovedtabellerne i en enkelt operation.

Det er praktisk, hvis du skriver din egen SQL direkte, men mange applikationsrammer og ORM'er understøtter det ikke, plus det kan kun direkte erstatte simpel INSERT . Din applikation, dit rammeværk eller din klientdriver skal håndtere konvertering til den særlige repræsentation, der kræves af COPY , slå enhver påkrævet type metadata op i sig selv osv.

(Bemærkelsesværdige drivere, der gør understøtte COPY inkludere libpq, PgJDBC, psycopg2 og Pg-perlen... men ikke nødvendigvis de rammer og ORM'er, der er bygget oven på dem.)

PgJDBC – batch-tilstand

PostgreSQL's JDBC-driver har en løsning på dette problem. Den er afhængig af support, der er til stede i PostgreSQL-servere siden 8.4 og på JDBC API's batchfunktioner for at sende en batch af forespørgsler til serveren, så vent kun én gang på bekræftelse af, at hele batchen kørte OK.

Nå, i teorien. I virkeligheden begrænser nogle implementeringsudfordringer dette, så batches i bedste fald kun kan udføres i bidder af et par hundrede forespørgsler. Driveren kan også kun køre forespørgsler, der returnerer resultatrækker i batchede bidder, hvis den kan finde ud af, hvor store resultaterne vil være på forhånd. På trods af disse begrænsninger, brug af Statement.executeBatch() kan tilbyde et enormt ydelsesboost til applikationer, der udfører opgaver som f.eks. bulkdataindlæsning af eksterne databaseinstanser.

Fordi det er en standard API, kan den bruges af applikationer, der fungerer på tværs af flere databasemotorer. Hibernate kan for eksempel bruge JDBC-batching, selvom det ikke gør det som standard.

libpq og batching

De fleste (alle?) andre PostgreSQL-drivere har ingen understøttelse af batching. PgJDBC implementerer PostgreSQL-protokollen fuldstændig uafhængigt, mens de fleste andre drivere internt bruger C-biblioteket libpq der er leveret som en del af PostgreSQL.

libpq understøtter ikke batching. Den har en asynkron ikke-blokerende API, men klienten kan stadig kun have én forespørgsel "i flyvning" ad gangen. Den skal vente, indtil resultaterne af den forespørgsel er modtaget, før den kan sende en anden.

PostgreSQL serveren understøtter batching fint, og PgJDBC bruger det allerede. Så jeg har skrevet batch-understøttelse til libpq og indsendte den som kandidat til den næste PostgreSQL-version. Da det kun ændrer klienten, vil det, hvis det accepteres, stadig gøre tingene hurtigere, når der oprettes forbindelse til ældre servere.

Jeg ville være virkelig interesseret i feedback fra forfattere og avancerede brugere af libpq -baserede klientdrivere og udviklere af libpq -baserede applikationer. Patchen gælder fint oven på PostgreSQL 9.6beta1, hvis du vil prøve det. Dokumentationen er detaljeret, og der er et omfattende eksempelprogram.

Ydeevne

Jeg troede, at en hostet databasetjeneste som RDS eller Heroku Postgres ville være et godt eksempel på, hvor denne form for funktionalitet ville være nyttig. Især at få adgang til dem fra vores side af deres egne netværk viser virkelig, hvor meget latenstid kan skade.

Ved ~320 ms netværksforsinkelse:

  • 500 skær uden batching:167.0s
  • 500 skær med batching:1.2s

… hvilket er over 120 gange hurtigere.

Du vil normalt ikke køre din app over et interkontinentalt link mellem appserveren og databasen, men dette tjener til at fremhæve virkningen af ​​latens. Selv over en unix-socket til localhost så jeg en ydeevneforbedring på over 50 % for 10.000 indsatser.

Batching i eksisterende apps

Det er desværre ikke muligt automatisk at aktivere batching for eksisterende applikationer. Apps skal bruge en lidt anden grænseflade, hvor de sender en række forespørgsler og først derefter spørger efter resultaterne.

Det burde være forholdsvis enkelt at tilpasse apps, der allerede bruger den asynkrone libpq-grænseflade, især hvis de bruger ikke-blokerende tilstand og en select() /afstemning() /epoll() /WaitForMultipleObjectsEx sløjfe. Apps, der bruger den synkrone libpq grænseflader vil kræve flere ændringer.

Batching i andre klientdrivere

Tilsvarende vil klientdrivere, rammer og ORM'er generelt have brug for interface og interne ændringer for at tillade brugen af ​​batching. Hvis de allerede bruger en hændelsesløkke og ikke-blokerende I/O, burde de være ret enkle at ændre.

Jeg ville elske at se Python, Ruby osv. brugere kunne få adgang til denne funktionalitet, så jeg er nysgerrig efter at se, hvem der er interesseret. Forestil dig at kunne gøre dette:

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 batchudførelse behøver ikke at være kompliceret på klientniveau.

COPY er hurtigst

Hvor praktiske kunder stadig bør foretrække COPY . Her er nogle resultater fra min bærbare computer:

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

At samle arbejdet giver et overraskende stort ydelsesboost selv på en lokal unix-stikforbindelse…. men KOPI efterlader begge individuelle indsatser langt bagved i støvet.

Brug COPY .

Billedet

Billedet til dette indlæg er af Goldfields Water Supply Scheme-rørledningen fra Mundaring Weir nær Perth i det vestlige Australien til de indre (ørken) guldmarker. Det er relevant, fordi det tog så lang tid at blive færdigt og var under så intens kritik, at dets designer og hovedfortaler, C. Y. O'Connor, begik selvmord 12 måneder før det blev sat i drift. Lokalt siger folk ofte (ukorrekt), at han døde efter rørledningen blev bygget, da der ikke strømmede vand – for det tog bare så lang tid, at alle antog, at rørledningsprojektet var mislykket. Så uger senere, hældte vandet ud.


  1. Ægte flugtstreng og PDO

  2. Hvordan sletter man en enum type værdi i postgres?

  3. Sådan fungerer Atan2d() i PostgreSQL

  4. SQL INSERT for begyndere