Share via


Leistungsoptimierung in Azure Cosmos DB for PostgreSQL

GILT FÜR: Azure Cosmos DB for PostgreSQL (unterstützt von der Citus-Datenbankerweiterung auf PostgreSQL)

Das Ausführen einer verteilten Datenbank unter Ausschöpfung des vollen Potenzials sorgt für Hochleistungen. Durch das Erreichen dieser Leistung können jedoch verschiedene Anpassungen in Anwendungscode und Datenmodellierung vorgenommen werden. In diesem Artikel werden einige der am häufigsten verwendeten und effektivsten Techniken zur Verbesserung der Leistung behandelt.

Clientseitiges Verbindungspooling

Ein Verbindungspool enthält offene Datenbankverbindungen für die Wiederverwendung. Eine Anwendung fordert bei Bedarf eine Verbindung vom Pool an, und der Pool gibt, sofern möglich, eine Verbindung zurück, die bereits eingerichtet wurde, oder er erstellt eine neue. Anschließend gibt die Anwendung die Verbindung wieder an den Pool frei anstatt sie zu schließen.

Das Hinzufügen eines clientseitigen Verbindungspools ist eine einfache Möglichkeit, die Anwendungsleistung mit minimalen Codeänderungen zu erhöhen. Wir haben festgestellt, dass das Ausführen einzeiliger INSERT-Anweisungen in einem Cluster etwa 24 Mal schneller abläuft, wenn Pooling aktiviert ist.

Sprachspezifische Beispiele zum Hinzufügen von Pooling zum Anwendungscode finden Sie im Leitfaden für App-Stapel.

Hinweis

Azure Cosmos DB for PostgreSQL bietet außerdem serverseitiges Verbindungspooling mit pgbouncer, dient jedoch hauptsächlich dazu, die Clientverbindungsgrenze zu erhöhen. Die Leistung einer einzelnen Anwendung profitiert mehr von clientseitigem als von serverseitigem Pooling. (Obwohl problemlos beide Arten von Pooling gleichzeitig verwendet werden können.)

Eingrenzung von verteilten Abfragen

Updates

Versuchen Sie beim Aktualisieren einer verteilten Tabelle, Abfragen in der Verteilungsspalte zu filtern – zumindest, wenn es sinnvoll ist und die neuen Filter die Bedeutung der Abfrage nicht ändern.

In einigen Workloads ist es einfach. Transaktions-/Betriebsworkloads wie SaaS-Apps mit mehreren Mandanten oder das Internet der Dinge verteilen Tabellen nach Mandanten oder Geräten. Abfragen werden nach Mandanten- oder Geräte-ID zugeordnet.

In unserem Tutorial zur Verwendung mehrerer Mandanten ist z. B. eine ads-Tabelle enthalten die nach company_id verteilt wird. Die Naive-Methode zum Aktualisieren einer Anzeige besteht darin, sie wie folgt zu vereinzeln:

-- slow

UPDATE ads
   SET impressions_count = impressions_count+1
 WHERE id = 42; -- missing filter on distribution column

Obwohl mit der Abfrage eine Zeile eindeutig identifiziert und aktualisiert wird, weiß Azure Cosmos DB for PostgreSQL beim Planen nicht, welcher Shard durch die Abfrage aktualisiert wird. Die Citus-Erweiterung wendet zur Sicherheit einen ShareUpdateExclusiveLock auf alle Shards an, mit dem andere Abfragen zur Aktualisierung der Tabelle blockiert werden.

Obwohl mit diesem id eine Zeile identifiziert werden konnte, können wir einen zusätzlichen Filter anwenden, um die Abfrage zu beschleunigen:

-- fast

UPDATE ads
   SET impressions_count = impressions_count+1
 WHERE id = 42
   AND company_id = 1; -- the distribution column

Der Azure Cosmos DB for PostgreSQL-Abfrageplaner erkennt einen direkten Filter in der Verteilungsspalte und weiß genau, welcher einzelne Shard gesperrt werden soll. In unseren Tests wird die parallele Aktualisierungsleistung durch Hinzufügung von Filtern für die Verteilungsspalte um das 100-Fache erhöht.

Verknüpfungen und CTEs

Sie haben bereits gelernt, wie UPDATE-Anweisungen durch die Verteilungsspalte eingegrenzt werden, damit unnötige Shardsperren vermieden werden. Andere Abfragen profitieren ebenfalls von der Eingrenzung, da in der Regel der Netzwerkaufwand vermieden wird, der durch das unnötige Shuffling von Daten zwischen Workerknoten entsteht.

-- logically correct, but slow

WITH single_ad AS (
  SELECT *
    FROM ads
   WHERE id=1
)
SELECT *
  FROM single_ad s
  JOIN campaigns c ON (s.campaign_id=c.id);

Die Abfrage lässt sich durch Filtern der Verteilungsspalte company_id in der CTE und der Haupt-SELECT-Anweisung beschleunigen.

-- faster, joining on distribution column

WITH single_ad AS (
  SELECT *
    FROM ads
   WHERE id=1 and company_id=1
)
SELECT *
  FROM single_ad s
  JOIN campaigns c ON (s.campaign_id=c.id)
 WHERE s.company_id=1 AND c.company_id = 1;

Im Allgemeinen sollten Sie beim Verknüpfen der verteilten Spalten versuchen, die Verteilungsspalte in die Verknüpfungsbedingungen einzubeziehen. Beim Verknüpfen einer verteilten und einer Verweistabelle ist das jedoch nicht erforderlich, da der Verweistabelleninhalt über alle Workerknoten repliziert wird.

Wenn es unpraktisch zu sein scheint, all Ihren Abfragen zusätzliche Filter hinzuzufügen, denken Sie daran, dass es Hilfsbibliotheken für mehrere beliebte Anwendungsframeworks gibt, mit denen dies leichter geht. Hier sind Anweisungen:

Effiziente Datenbankprotokollierung

Durch das jederzeitige Protokollieren aller SQL-Anweisungen erhöht sich der Aufwand. Wir haben festgestellt, dass ein sinnvolleres Protokollierungsmaß die Transaktionen pro Sekunde gegenüber der vollständigen Protokollierung um das 10-Fache verbessert hat.

Im Sinne eines effizienten täglichen Betriebs können Sie die Protokollierung mit Ausnahme von Fehlern und ungewöhnlich langen Abfragen deaktivieren:

Festlegen value reason
log_statement_stats OFF Profilerstellungsaufwand vermeiden
log_duration OFF Die Dauer normaler Abfragen spielt keine Rolle.
log_statement Keine Keine Abfragen ohne genaueren Grund protokollieren
log_min_duration_statement Ein Wert, der Ihrer Meinung nach länger ist als normale Abfragen dauern sollten Zeigt die ungewöhnlich langen Abfragen an

Hinweis

Bei den protokollbezogenen Einstellungen in unserem verwalteten Dienst werden die oben genannten Empfehlungen berücksichtigt. Ändern Sie sie nicht. Allerdings haben wir manchmal gesehen, wie Kunden die Einstellungen geändert haben, um die Protokollierung offensiver einzustellen, was zu Leistungsproblemen geführt hat.

Konflikt bei Sperre

Die Datenbank verwendet Sperrungen, um Daten bei gleichzeitigem Zugriff konsistent zu halten. Einige Abfragemuster erfordern jedoch eine übermäßig hohe Anzahl von Sperrungen, und es gibt schnellere Alternativen.

Systemintegrität und Sperren

Bevor wir uns genauer mit den häufig auftretenden Sperrineffizienzen beschäftigen, schauen wir uns an, wie sich Sperren und Aktivitäten im gesamten Datenbankcluster anzeigen lassen. Die Ansicht citus_stat_activity liefert eine detaillierte Ansicht.

Die Ansicht zeigt unter anderem, wie Abfragen durch „Wartenereignisse“, wie z. B. Sperren, blockiert werden. Durch die Gruppierung nach wait_event_type wird ein Bild der Systemintegrität gezeichnet:

-- general system health

SELECT wait_event_type, count(*)
  FROM citus_stat_activity
 WHERE state != 'idle'
 GROUP BY 1
 ORDER BY 2 DESC;

Ein wait_event_type von NULL bedeutet, dass die Abfrage auf nichts wartet.

Wenn in der „stat-activity“-Ausgabe Sperren angezeigt werden, können Sie die spezifischen blockierten Abfragen mithilfe von citus_lock_waits anzeigen:

SELECT * FROM citus_lock_waits;

Wenn beispielsweise eine Abfrage blockiert wird, wenn eine andere Abfrage versucht, dieselbe Zeile zu aktualisieren, wird die blockierte und die blockierende Anweisung angezeigt:

-[ RECORD 1 ]-------------------------+--------------------------------------
waiting_gpid                          | 10000011981
blocking_gpid                         | 10000011979
blocked_statement                     | UPDATE numbers SET j = 3 WHERE i = 1;
current_statement_in_blocking_process | UPDATE numbers SET j = 2 WHERE i = 1;
waiting_nodeid                        | 1
blocking_nodeid                       | 1

Um nicht nur die aktuellen Sperren, sondern historische Muster anzuzeigen, können Sie die Sperren in den PostgreSQL-Protokollen erfassen. Weitere Informationen finden Sie in der log_lock_waits-Servereinstellung in der PostgreSQL-Dokumentation. Eine weitere großartige Ressource ist Sieben Tipps zum Umgang mit Sperren im Citus Data-Blog.

Häufige Probleme und Lösungen

DDL-Befehle

DDL-Befehle wie truncate, dropund create index haben alle Schreibsperren und blockieren den Schreibzugriff auf die gesamte Tabelle. Durch die Minimierung dieser Vorgänge kommt es seltener zu Problemen beim Sperren.

Tipps:

  • Versuchen Sie, DDL mit Wartungsfenstern zu konsolidieren, oder verwenden Sie sie weniger häufig.

  • PostgreSQL unterstützt gleichzeitig das Erstellen von Indizes, um eine Schreibsperre in der Tabelle zu vermeiden.

  • Ziehen Sie in Erwägung, lock_timeout in einer SQL-Sitzung festzulegen, bevor Sie einen schweren DDL-Befehl ausführen. Mit lock_timeout bricht PostgreSQL den DDL-Befehl ab, wenn der Befehl zu lange auf eine Schreibsperre wartet. Ein DDL-Befehl, der auf eine Sperre wartet, kann veranlassen, dass spätere Abfragen sich in einer Warteschlange hinter ihm einreihen.

Leerlauf in Transaktionsverbindungen

(Ausgecheckte) Leerlauftransaktionen blockieren manchmal unnötig andere Abfragen. Beispiel:

BEGIN;

UPDATE ... ;

-- Suppose the client waits now and doesn't COMMIT right away.
--
-- Other queries that want to update the same rows will be blocked.

COMMIT; -- finally!

Um Abfragen, die sich lange im Koordinatorknoten im Leerlauf befinden, manuell zu bereinigen, können Sie einen Befehl wie diesen ausführen:

SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE datname = 'citus'
 AND pid <> pg_backend_pid()
 AND state in ('idle in transaction')
 AND state_change < current_timestamp - INTERVAL '15' MINUTE;

PostgreSQL bietet außerdem die Einstellung idle_in_transaction_session_timeout, um das Ende der Leerlaufsitzung zu automatisieren.

Deadlocks

Azure Cosmos DB for PostgreSQL erkennt verteilte Deadlocks und bricht ihre Abfragen ab, aber dieser Vorgang ist weniger leistungsstark, als Deadlocks von vornherein zu vermeiden. Eine allgemeine Deadlock-Quelle hat ihren Ursprung darin, dass dieselbe Gruppe von Zeilen in einer anderen Reihenfolge in mehreren Transaktionen gleichzeitig aktualisiert wird.

Wenn Sie beispielsweise diese Transaktionen parallel ausführen:

Sitzung A:

BEGIN;
UPDATE ads SET updated_at = now() WHERE id = 1 AND company_id = 1;
UPDATE ads SET updated_at = now() WHERE id = 2 AND company_id = 1;

Sitzung B:

BEGIN;
UPDATE ads SET updated_at = now() WHERE id = 2 AND company_id = 1;
UPDATE ads SET updated_at = now() WHERE id = 1 AND company_id = 1;

-- ERROR:  canceling the transaction since it was involved in a distributed deadlock

In Sitzung A wird ID 1 und dann 2 aktualisiert, während in Sitzung B erst 2 und dann 1 aktualisiert wird. Schreiben Sie den SQL-Code für Transaktionen sorgfältig, damit Zeilen in derselben Reihenfolge aktualisiert werden. (Die Aktualisierungsreihenfolge wird manchmal als "Sperrhierarchie" bezeichnet.)

Wir haben festgestellt, dass die Massenaktualisierung einer Gruppe von Zeilen mit vielen Transaktionen 3-mal schneller abläuft, wenn Deadlocks vermieden werden.

E/A während der Datenerfassung

E/A-Engpässe sind aufgrund des Shardings bei Azure Cosmos DB for PostgreSQL in der Regel weniger problematisch als bei PostgreSQL mit einem Knoten. Die Shards sind einzelne kleinere Tabellen, mit besseren Index- und Cache-Trefferraten, die eine bessere Leistung erzielen.

Selbst bei Azure Cosmos DB for PostgreSQL kann die Datenträger-E/A mit zunehmender Größe der Tabellen und Indizes jedoch zu einem Problem bei der Datenerfassung werden. Am besten halten Sie Ausschau nach einer zunehmenden Anzahl von „E/A“-wait_event_type-Einträgen, die in citus_stat_activity angezeigt werden:

SELECT wait_event_type, wait_event count(*)
  FROM citus_stat_activity
 WHERE state='active'
 GROUP BY 1,2;

Führen Sie die Abfrage oben wiederholt aus, um ereignisbezogene Informationen zu erfassen. Beachten Sie, wie sich die Anzahl der Ereignistypen mit unterschiedlicher Wartezeit ändern.

Schauen Sie sich auch die Metriken im Azure-Portal an, insbesondere die sehr hohe IOPS-Metrik.

Tipps:

  • Wenn Ihre Daten die natürliche Reihenfolge aufweisen, wie z. B. in einer Zeitreihe, verwenden Sie die PostgreSQL-Tabellenpartitionierung. In diesem Leitfaden erfahren Sie, wie Sie verteilte Tabellen partitionieren.

  • Entfernen Sie nicht verwendete Indizes. Die Indexwartung führt zur E/A-Erweiterung während der Datenerfassung. Um zu ermitteln, welche Indizes nicht verwendet werden, verwenden Sie diese Abfrage.

  • Wenn möglich, vermeiden Sie die Indizierung von Daten in Zufallsreihenfolge. Beispielsweise haben einige UUID-Generierungsalgorithmen keine Reihenfolge. Das Indizieren eines solchen Werts ist sehr aufwendig. Versuchen Sie es stattdessen mit einer bigint-Sequenz oder dem monotonischen Erhöhen der UUIDs.

Zusammenfassung der Ergebnisse

In Bezug auf die einfache Erfassung mit INSERTs, UPDATEs und Transaktionsblöcken haben wir hinsichtlich der in diesem Artikel genannten Verfahren die folgende Abfragebeschleunigung beobachtet.

Verfahren Abfragebeschleunigung
Bereichsabfragen 100x
Verbindungspooling 24x
Effiziente Protokollierung 10x
Vermeiden von Deadlocks 3x

Nächste Schritte