PostgreSQL
 sql >> Datenbank >  >> RDS >> PostgreSQL

PostgreSQL-basierte Anwendungsleistung:Latenz und versteckte Verzögerungen

Goldfields Pipeline, von SeanMac (Wikimedia Commons)

Wenn Sie versuchen, die Leistung Ihrer PostgreSQL-basierten Anwendung zu optimieren, konzentrieren Sie sich wahrscheinlich auf die üblichen Tools:EXPLAIN (BUFFERS, ANALYZE) , pg_stat_statements , auto_explain , log_statement_min_duration usw.

Vielleicht suchen Sie nach Sperrkonflikten mit log_lock_waits , Überwachung Ihrer Checkpoint-Leistung usw.

Aber haben Sie an Netzwerklatenz gedacht? ? Gamer kennen sich mit Netzwerklatenz aus, aber dachten Sie, dass dies für Ihren Anwendungsserver wichtig ist?

Latenz ist wichtig

Typische Client/Server-Roundtrip-Netzwerklatenzen können von 0,01 ms (lokaler Host) bis zu ~0,5 ms eines Switched-Netzwerks, 5 ms von WiFi, 20 ms von ADSL, 300 ms von interkontinentalem Routing und noch mehr für Dinge wie Satelliten- und WWAN-Verbindungen reichen .

Ein triviales SELECT kann in der Größenordnung von 0,1 ms dauern, um serverseitig ausgeführt zu werden. Ein triviales INSERT kann 0,5 ms dauern.

Jedes Mal, wenn Ihre Anwendung eine Abfrage ausführt, muss sie darauf warten, dass der Server mit Erfolg/Fehler und möglicherweise einem Ergebnissatz, Abfragemetadaten usw. antwortet. Dies führt zu mindestens einer Netzwerk-Roundtrip-Verzögerung.

Wenn Sie mit kleinen, einfachen Abfragen arbeiten, kann die Netzwerklatenz relativ zur Ausführungszeit Ihrer Abfragen erheblich sein, wenn sich Ihre Datenbank nicht auf demselben Host wie Ihre Anwendung befindet.

Viele Anwendungen, insbesondere ORMs, neigen sehr dazu, Lots auszuführen von ganz einfachen Abfragen. Wenn Ihre Hibernate-App beispielsweise eine Entität mit einem träge abgerufenen @OneToMany abruft Beziehung zu 1000 untergeordneten Elementen werden dank des n+1-Auswahlproblems wahrscheinlich 1001 Abfragen ausgeführt, wenn nicht sogar mehr. Das bedeutet, dass es wahrscheinlich das 1000-fache Ihrer Netzwerk-Roundtrip-Latenz nur wartet verbringt . Sie können beitreten links abrufen um das zu vermeiden… aber dann überträgt man die übergeordnete Entität 1000 Mal im Join und muss sie deduplizieren.

Wenn Sie die Datenbank aus einem ORM füllen, führen Sie in ähnlicher Weise wahrscheinlich Hunderttausende von trivialen INSERT aus s… und nach jedem einzelnen darauf warten, dass der Server bestätigt, dass es in Ordnung ist.

Es ist einfach zu versuchen, sich auf die Abfrageausführungszeit zu konzentrieren und zu versuchen, diese zu optimieren, aber mit einem trivialen INSERT INTO ...VALUES ... können Sie nur begrenzt etwas erreichen . Lassen Sie einige Indizes und Einschränkungen fallen, stellen Sie sicher, dass es in eine Transaktion gestapelt ist, und Sie sind ziemlich fertig.

Wie wäre es, alle Netzwerkwartezeiten loszuwerden? Selbst in einem LAN summieren sie sich über Tausende von Abfragen.

KOPIEREN

Eine Möglichkeit, Latenz zu vermeiden, ist die Verwendung von COPY . Um die COPY-Unterstützung von PostgreSQL zu verwenden, muss Ihre Anwendung oder Ihr Treiber einen CSV-ähnlichen Satz von Zeilen erzeugen und diese in einer kontinuierlichen Sequenz an den Server streamen. Oder der Server kann aufgefordert werden, Ihrer Anwendung einen CSV-ähnlichen Stream zu senden.

In jedem Fall kann die App eine COPY nicht mit anderen Abfragen verschachteln, und Copy-Inserts müssen direkt in eine Zieltabelle geladen werden. Ein gängiger Ansatz ist das KOPIEREN in eine temporäre Tabelle, dann von dort ein INSERT INTO ... SELECT ... , AKTUALISIEREN ... VON .... , LÖSCHEN AUS ... MIT ... , usw., um die kopierten Daten zu verwenden, um die Haupttabellen in einem einzigen Vorgang zu ändern.

Das ist praktisch, wenn Sie Ihr eigenes SQL direkt schreiben, aber viele Anwendungsframeworks und ORMs unterstützen es nicht, außerdem kann es nur das einfache INSERT direkt ersetzen . Ihre Anwendung, Ihr Framework oder Ihr Client-Treiber muss sich um die Konvertierung für die spezielle Darstellung kümmern, die von COPY benötigt wird , alle erforderlichen Typ-Metadaten selbst nachschlagen usw.

(Bemerkenswerte Treiber, die tun COPY unterstützen Dazu gehören libpq, PgJDBC, psycopg2 und das Pg-Gem … aber nicht unbedingt die darauf aufbauenden Frameworks und ORMs.)

PgJDBC – Stapelmodus

Der JDBC-Treiber von PostgreSQL hat eine Lösung für dieses Problem. Es stützt sich auf die seit 8.4 in PostgreSQL-Servern vorhandene Unterstützung und auf die Batching-Funktionen der JDBC-API, um einen Batch zu senden der Abfragen an den Server warten dann nur noch einmal auf die Bestätigung, dass der gesamte Batch OK gelaufen ist.

Nun, in der Theorie. In Wirklichkeit schränken einige Implementierungsherausforderungen dies ein, sodass Batches bestenfalls nur in Blöcken von einigen hundert Abfragen ausgeführt werden können. Der Treiber kann auch nur Abfragen ausführen, die Ergebniszeilen in gestapelten Blöcken zurückgeben, wenn er im Voraus ermitteln kann, wie groß die Ergebnisse sein werden. Trotz dieser Einschränkungen kann die Verwendung von Statement.executeBatch() kann eine enorme Leistungssteigerung für Anwendungen bieten, die Aufgaben wie das Laden von Remote-Datenbankinstanzen durch Massendaten ausführen.

Da es sich um eine Standard-API handelt, kann sie von Anwendungen verwendet werden, die über mehrere Datenbank-Engines hinweg arbeiten. Hibernate kann beispielsweise JDBC-Batching verwenden, tut dies jedoch standardmäßig nicht.

libpq und Stapelverarbeitung

Die meisten (alle?) anderen PostgreSQL-Treiber unterstützen Batching nicht. PgJDBC implementiert das PostgreSQL-Protokoll völlig eigenständig, während die meisten anderen Treiber intern die C-Bibliothek libpq verwenden das als Teil von PostgreSQL bereitgestellt wird.

libpq unterstützt keine Stapelverarbeitung. Es verfügt zwar über eine asynchrone, nicht blockierende API, aber der Client kann immer noch nur eine Abfrage gleichzeitig „in Flight“ haben. Es muss warten, bis die Ergebnisse dieser Abfrage empfangen werden, bevor es eine weitere senden kann.

Der PostgreSQL Server unterstützt Batching ganz gut, und PgJDBC verwendet es bereits. Also habe ich Batch-Unterstützung für libpq geschrieben und als Kandidat für die nächste PostgreSQL-Version eingereicht. Da es nur den Client ändert, wird es, wenn es akzeptiert wird, die Verbindung zu älteren Servern immer noch beschleunigen.

Ich wäre sehr an Feedback von Autoren und fortgeschrittenen Benutzern von libpq interessiert -basierte Client-Treiber und Entwickler von libpq -basierte Anwendungen. Der Patch lässt sich gut auf PostgreSQL 9.6beta1 anwenden, wenn Sie ihn ausprobieren möchten. Die Dokumentation ist detailliert und es gibt ein umfangreiches Beispielprogramm.

Leistung

Ich dachte, ein gehosteter Datenbankdienst wie RDS oder Heroku Postgres wäre ein gutes Beispiel dafür, wo diese Art von Funktionalität nützlich wäre. Insbesondere der Zugriff auf sie von unserer Seite ihrer eigenen Netzwerke zeigt wirklich, wie viel Latenz schaden kann.

Bei ~320 ms Netzwerklatenz:

  • 500 Beilagen ohne Batching:167,0 s
  • 500 Beilagen mit Batching:1,2s

… was über 120x schneller ist.

Sie werden Ihre App normalerweise nicht über eine interkontinentale Verbindung zwischen dem App-Server und der Datenbank ausführen, aber dies dient dazu, die Auswirkungen der Latenz zu verdeutlichen. Sogar über einen Unix-Socket zu localhost sah ich eine Leistungssteigerung von über 50 % für 10000 Einfügungen.

Batching in bestehenden Apps

Leider ist es nicht möglich, das Batching für bestehende Anwendungen automatisch zu aktivieren. Apps müssen eine etwas andere Schnittstelle verwenden, wo sie eine Reihe von Abfragen senden und erst dann nach den Ergebnissen fragen.

Es sollte ziemlich einfach sein, Apps anzupassen, die bereits die asynchrone libpq-Schnittstelle verwenden, insbesondere wenn sie den nicht blockierenden Modus und ein select() verwenden /abfragen() /epoll() /WaitForMultipleObjectsEx Schleife. Apps, die die synchrone libpq verwenden Schnittstellen erfordern weitere Änderungen.

Batching in anderen Client-Treibern

In ähnlicher Weise benötigen Client-Treiber, Frameworks und ORMs im Allgemeinen Schnittstellen- und interne Änderungen, um die Verwendung von Batching zu ermöglichen. Wenn sie bereits eine Ereignisschleife und nicht blockierende E/A verwenden, sollten sie ziemlich einfach zu ändern sein.

Ich würde gerne sehen, dass Benutzer von Python, Ruby usw. auf diese Funktionalität zugreifen können, also bin ich gespannt, wer interessiert ist. Stellen Sie sich vor, Sie könnten Folgendes tun:

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

Die asynchrone Batch-Ausführung muss auf Client-Ebene nicht kompliziert sein.

COPY ist am schnellsten

Wo praktische Kunden immer noch COPY bevorzugen sollten . Hier sind einige Ergebnisse von meinem Laptop:

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

Das Stapeln der Arbeit bietet einen überraschend großen Leistungsschub, selbst bei einer lokalen Unix-Socket-Verbindung…. sondern KOPIEREN lässt die beiden einzelnen Einsatzansätze weit hinter sich.

Verwenden Sie KOPIEREN .

Das Bild

Das Bild für diesen Beitrag zeigt die Pipeline des Goldfields Water Supply Scheme vom Mundaring Weir in der Nähe von Perth in Westaustralien zu den Goldfeldern im Landesinneren (Wüste). Es ist relevant, weil es so lange dauerte, bis es fertig war, und so heftig kritisiert wurde, dass sein Designer und Hauptbefürworter, C. Y. O’Connor, 12 Monate vor seiner Inbetriebnahme Selbstmord beging. Einheimische sagen oft (fälschlicherweise), dass er nach starb die Pipeline wurde gebaut, als kein Wasser floss – weil es einfach so lange gedauert hat, gingen alle davon aus, dass das Pipeline-Projekt gescheitert sei. Dann, Wochen später, strömte das Wasser aus.