Massenoptimierung von PostgreSQL-Anfragen. Kirill Borovikov (Tensor)

Der Bericht prĂ€sentiert einige AnsĂ€tze, die es ermöglichen, die Leistung von SQL-Abfragen zu ĂŒberwachen, wenn es tĂ€glich Millionen sind,, und die kontrollierten PostgreSQL-Server – Hunderte.

Welche technischen Lösungen ermöglichen es uns, solche Mengen an Informationen effizient zu verarbeiten, und wie erleichtert dies das Leben eines gewöhnlichen Entwicklers?

Video abspielen

Wer interessiert sich fĂŒr die Analyse spezifischer Probleme und verschiedene Optimierungstechniken von SQL-Abfragen sowie die Lösung typischer DBA-Aufgaben in PostgreSQL – kann sich auch mit einer Artikelreihe vertraut machen. zu diesem Thema.

Massenoptimierung von PostgreSQL-Anfragen. Kirill Borovikov (Tensor)
Mein Name ist Kirill Borovikov, ich vertrete das Unternehmen «Tensor».Konkret spezialisiere ich mich auf die Arbeit mit Datenbanken in unserem Unternehmen.

Heute werde ich Ihnen erzĂ€hlen, wie wir die Abfragen optimieren, wenn Sie nicht die Leistung einer einzelnen Abfrage verbessern mĂŒssen, sondern ein Problem in großem Maßstab lösen möchten. Wenn es Millionen von Abfragen gibt und Sie nach AnsĂ€tzen zur Lösung dieses großen Problems suchen.

Im Allgemeinen ist «Tensor» fĂŒr eine Million unserer Kunden SBIS – unsere Anwendung:ein soziales Unternehmensnetzwerk, Lösungen fĂŒr Videoanrufe, interne und externe Dokumentenarbeit, Buchhaltungssysteme und Lagerverwaltung,
 Das heißt, ein «Mega-Toolkit» fĂŒr das umfassende Management von Unternehmen, in dem es mehr als 100 verschiedene interne Projekte gibt.

Damit all diese Projekte reibungslos funktionieren und sich entwickeln – haben wir 10 Entwicklungszentren im ganzen Land, in denen mehr als 1000 Entwickler.

arbeiten. Wir arbeiten seit 2008 mit PostgreSQL und haben eine große Menge an Daten gesammelt, die wir verarbeiten – das sind Kundendaten, statistische, analytische und Daten aus externen Informationssystemen – ĂŒber 400 TB.Allein in «Production» gibt es etwa 250 Server, und insgesamt ĂŒberwachen wir etwa 1000 DB-Server.

Massenoptimierung von PostgreSQL-Anfragen. Kirill Borovikov (Tensor)

SQL ist eine deklarative Sprache. Sie beschreiben nicht «wie» etwas funktionieren soll, sondern «was» Sie erhalten möchten. Die DBMS kennt sich besser aus, wie man JOIN macht – wie man Ihre Tabellen verbindet, welche Bedingungen anzuwenden sind, was ĂŒber den Index geht und was nicht,


Einige DBMS akzeptieren Hinweise: «Nein, diese beiden Tabellen sollen in dieser Reihenfolge verbunden werden», aber PostgreSQL kann das nicht. Das ist eine bewusste Position der Hauptentwickler: «Es ist besser, den Abfrage-Optimierer zu verbessern, als den Entwicklern zu erlauben, irgendwelche Hinweise zu verwenden.»

Aber trotz der Tatsache, dass PostgreSQL sich nicht „von außen“ steuern lĂ€sst, ermöglicht es hervorragend, zu sehen, was „innen“ bei ihm passiert,, wenn Sie Ihre Abfrage ausfĂŒhren, und wo es Probleme gibt.

Massenoptimierung von PostgreSQL-Anfragen. Kirill Borovikov (Tensor)

Welche klassischen Probleme bringt ein Entwickler normalerweise [zum DBA]? „Hier, wir haben die Abfrage ausgefĂŒhrt, und es ist alles langsam,, alles hĂ€ngt, irgendetwas passiert
 Es ist ein großes Problem!“

Die GrĂŒnde sind fast immer die gleichen:

  • ein ineffizienter Abfragealgorithmus.
    Entwickler: „Ich habe ihm gerade 10 Tabellen ĂŒber JOIN in SQL gegeben
“ – und erwartet, dass sich seine Bedingungen auf wunderbare Weise effizient „entwirren“, und er alles schnell erhĂ€lt. Aber Wunder gibt es nicht, und jedes System gibt bei solcher Variantenvielfalt (10 Tabellen in einem FROM) immer eine gewisse Ungenauigkeit. [Artikel]
  • nicht aktuelle Statistiken.
    Dieser Punkt ist besonders relevant fĂŒr PostgreSQL, wenn Sie einen großen Datensatz auf den Server „geladen“ haben, eine Abfrage machen – und er „Scan“ durch die Tabelle macht. Denn gestern waren dort 10 DatensĂ€tze, und heute 10 Millionen, aber PostgreSQL weiß noch nichts davon, und man muss ihm das mitteilen. [Artikel]
  • ein Ressourcenengpass.
    Sie haben eine große, schwer belastete Datenbank auf einen schwachen Server gestellt, der nicht ĂŒber genĂŒgend Speicherplatz, RAM oder CPU-Leistung verfĂŒgt. Und das war's
 Irgendwo gibt es eine Leistungsgrenze, ĂŒber die Sie nicht hinauskommen können.
  • von Sperren
    Ein komplizierter Punkt, aber er ist am relevantesten fĂŒr verschiedene modifizierende Abfragen (INSERT, UPDATE, DELETE) – das ist ein eigenes großes Thema.

Den Plan abrufen.


 Und fĂŒr alles andere brauchen wir einen Plan!Wir mĂŒssen sehen, was im Server passiert.

Massenoptimierung von PostgreSQL-Anfragen. Kirill Borovikov (Tensor)

Der AusfĂŒhrungsplan einer Abfrage fĂŒr PostgreSQL ist ein Baum des AbfrageausfĂŒhrungsalgorithmus in textlicher Darstellung. Es ist genau der Algorithmus, der nach Analyse durch den Planner als der effizienteste erkannt wurde.

Jeder Knoten des Baumes ist eine Operation: Daten aus einer Tabelle oder einem Index abrufen, eine Bitmaske erstellen, zwei Tabellen verknĂŒpfen, vereinigen, schneiden oder Auswahl ausschließen. Die AusfĂŒhrung der Abfrage erfolgt ĂŒber die Knoten dieses Baumes.

Um den Abfrageplan zu erhalten, ist der einfachste Weg, den Operator auszufĂŒhren EXPLAIN.Um ihn mit allen tatsĂ€chlichen Attributen zu erhalten, das heißt, um die Abfrage tatsĂ€chlich auf der Datenbank auszufĂŒhren – EXPLAIN (ANALYZE, BUFFERS) SELECT ....

Schlechter Moment: Wenn Sie ihn ausfĂŒhren, geschieht dies „hier und jetzt“, deshalb ist es nur fĂŒr lokale Debugging geeignet. Wenn Sie jedoch einen stark belasteten Server haben, der starkem DatenĂ€nderungsfluss unterliegt, und sehen: „Autsch! Hier kam unsere Anfrage langsam vorbei.sich eine Anfrage.“ Vor eineinhalb Stunden — wĂ€hrend Sie herumgerannt sind, diese Anfrage aus den Logs geholt haben, sie erneut zum Server gebracht haben, hat sich Ihr gesamter Datensatz und die Statistik geĂ€ndert. Sie fĂŒhren sie aus, um zu debuggen — und sie wird schnell ausgefĂŒhrt! Und Sie können nicht verstehen, „warum“, warum damit langsam.

Massenoptimierung von PostgreSQL-Anfragen. Kirill Borovikov (Tensor)

Um zu verstehen, was genau in dem Moment passiert ist, als die Anfrage auf dem Server ausgefĂŒhrt wird, haben kluge Leute das Modul auto_explain. Es ist in praktisch allen gĂ€ngigen Distributionen von PostgreSQL vorhanden und kann einfach in der Konfigurationsdatei aktiviert werden.

Wenn es versteht, dass eine Anfrage lĂ€nger als die von Ihnen angegebene Grenze benötigt, macht es einen „Schnappschuss“ des Plans dieser Anfrage und schreibt ihn zusammen in das Log..

Massenoptimierung von PostgreSQL-Anfragen. Kirill Borovikov (Tensor)

Es scheint alles gut zu sein, wir gehen ins Log und sehen dort
 [portŃĐœĐșа тДĐșста]. Aber wir können nichts darĂŒber sagen, außer der Tatsache, dass es ein hervorragender Plan ist, weil er in 11 ms ausgefĂŒhrt wurde.

Es scheint alles gut zu sein — aber nichts ist klar, was wirklich passiert ist. Abgesehen von der Gesamtzeit sehen wir nicht viel. Denn auf so eine „Latex“-Plain-Text-Darstellung zu schauen, ist ja ĂŒberhaupt nicht anschaulich.

Aber selbst wenn es nicht anschaulich ist, und unbequem, gibt es bei weitem gravierendere Probleme:

  • Im Knoten wird die Summe der Ressourcen des gesamten Unterbaums unter ihm angegeben. Das heißt, es ist einfach unmöglich zu erfahren, wie viel Zeit konkret bei diesem Index-Scan verbraucht wurde — wenn darunter eine Art von verschachtelten Bedingungen steht. Wir mĂŒssen dynamisch ĂŒberprĂŒfen, ob es innen „Kinder“ und bedingte Variablen, CTE — gibt und das alles „im Kopf“ abziehen.
  • Zweiter Punkt: Die Zeit, die im Knoten angegeben ist, ist die Zeit der einmaligen AusfĂŒhrung des Knotens.Wenn dieser Knoten zum Beispiel das Ergebnis eines Schleifen ĂŒber die DatensĂ€tze der Tabelle ist und mehrfach ausgefĂŒhrt wurde, erhöht sich die Anzahl der Schleifen — Zyklen dieses Knotens im Plan. Aber die eigentliche atomare AusfĂŒhrungszeit bleibt im Plan unverĂ€ndert. Das heißt, um zu verstehen, wie lange dieser Knoten insgesamt ausgefĂŒhrt wurde, muss man das eine mit dem anderen multiplizieren — wiederum „im Kopf“.

Bei solchen Gegebenheiten ist es praktisch unmöglich zu verstehen, „Wer ist die schwĂ€chste Glied?“ Deshalb schreiben selbst die Entwickler im „Handbuch“, dass „Das VerstĂ€ndnis des Plans ist eine Kunst, die man erlernen muss, Erfahrung
“.

Aber wir haben 1000 Entwickler, und diese Erfahrung kann man nicht einfach so vermitteln. Ich, du, er — wissen es, aber jemand dort drĂŒben — bereits nicht. Vielleicht lernt er es, vielleicht nicht, aber arbeiten muss er jetzt schon — woher soll er diese Erfahrung nehmen.

Visualisierung des Plans

Deshalb haben wir verstanden — um mit diesen Problemen umzugehen, brauchen wir eine gute Visualisierung des Plans. [статья]

Massenoptimierung von PostgreSQL-Anfragen. Kirill Borovikov (Tensor)

Wir haben zuerst „den Markt“ erkundet — lass uns im Internet suchen, was ĂŒberhaupt existiert.

Aber es stellte sich heraus, dass es relativ wenige „lebendige“ Lösungen gibt, die mehr oder weniger weiterentwickelt werden — nur eine: explain.depesz.com von Hubert Lubaczewski. Du gibst einen textuellen Plan in das Feld ein, und er zeigt dir eine Tabelle mit den aufgeschlĂŒsselten Daten:

  • die eigene Verarbeitungszeit des Knotens
  • die gesamte Zeit im gesamten Subbaum
  • die Anzahl der extrahierten DatensĂ€tze und die statistisch erwartete
  • der eigentliche Inhalt des Knotens

Außerdem hat dieser Service die Möglichkeit, einen Link-Archiv zu teilen. Du hast deinen Plan dort hochgeladen und sagst: „Hey, Vasya, hier ist der Link, da stimmt etwas nicht.“

Massenoptimierung von PostgreSQL-Anfragen. Kirill Borovikov (Tensor)

Aber es gibt auch einige kleine Probleme.

Erstens eine riesige Menge an „Copy-Paste“. Du nimmst einen Teil des Logs, steckst ihn rein, und wieder und wieder.

Zweitens, es gibt keine Analyse der gelesenen Datenmengen — genau der Buffers, die ausgegeben werden von EXPLAIN (ANALYZE, BUFFERS), hier sehen wir nichts. Er kann sie einfach nicht analysieren, verstehen und damit arbeiten. Wenn du viele Daten liest und verstehst, dass du es falsch „auf die Festplatte und den Cache im RAM verteilen“ könntest, ist diese Information sehr wichtig.

Ein dritter negativer Punkt — die sehr schwache Entwicklung dieses Projekts. Die Commits sind sehr klein, gut, wenn einmal im halben Jahr, und der Code ist in Perl.

Massenoptimierung von PostgreSQL-Anfragen. Kirill Borovikov (Tensor)

Aber das sind alles „Lyrik“, damit könnte man irgendwie leben, aber es gibt eine Sache, die uns von diesem Dienst stark abgewandt hat. Es sind die Analysefehler der Common Table Expressions (CTE) und verschiedener dynamischer Knoten wie InitPlan/SubPlan.

Wenn man diesem Bild glauben kann, dann ist die Gesamtzeit fĂŒr die AusfĂŒhrung jedes einzelnen Knotens höher als die Gesamtzeit fĂŒr die AusfĂŒhrung der gesamten Abfrage. Ganz einfach — aus dem Knoten CTE Scan wurde die Zeit fĂŒr die Generierung dieses CTE nicht abgezogen.. Daher wissen wir mittlerweile nicht mehr, wie lange das CTE-Scannen selbst gedauert hat.

Massenoptimierung von PostgreSQL-Anfragen. Kirill Borovikov (Tensor)

Hier haben wir verstanden, dass es Zeit ist, etwas Eigenes zu schreiben – hurra! Jeder Entwickler sagt: „Jetzt schreiben wir unser eigenes, das wird super einfach!“

Wir haben einen typischen Stack fĂŒr Web-Services genommen: Kern auf Node.js + Express, Bootstrap integriert und fĂŒr die schönen Diagramme – D3.js. Unsere Erwartungen wurden vollends erfĂŒllt – den ersten Prototyp haben wir in 2 Wochen erhalten:

  • einen eigenen Parser fĂŒr den Plan
    Das bedeutet, dass wir jetzt jeden Plan analysieren können, den PostgreSQL generiert.
  • eine korrekte Analyse der dynamischen Knoten – CTE Scan, InitPlan, SubPlan
  • Analyse der Pufferverteilung – wo die Datenseiten aus dem Speicher gelesen werden, wo aus dem lokalen Cache, wo von der Festplatte
  • wir haben Anschaulichkeit erhalten
    Um nicht all dies im Log nachzuvollziehen, sondern sofort das „schwĂ€chste Glied“ direkt im Bild zu sehen.

Massenoptimierung von PostgreSQL-Anfragen. Kirill Borovikov (Tensor)

Wir haben ungefĂ€hr so ein Bild erhalten – direkt mit Syntax-Hervorhebung. Aber normalerweise arbeiten unsere Entwickler nicht mehr mit dem vollstĂ€ndigen Plan, sondern mit einer kĂŒrzeren Version. Denn alle Zahlen haben wir bereits geparst und nach links und rechts geschoben, wĂ€hrend wir in der Mitte nur die erste Zeile belassen haben, die zeigt, um welchen Knoten es sich handelt: CTE Scan, CTE-Generierung oder Seq Scan fĂŒr eine bestimmte Tabelle.

Diese verkĂŒrzte Darstellung nennen wir Plan-Template.

Massenoptimierung von PostgreSQL-Anfragen. Kirill Borovikov (Tensor)

Was wĂ€re noch nĂŒtzlich? Es wĂ€re hilfreich zu sehen, welcher Anteil welcher Knoten von der Gesamtzeit abbekommt – und einfach „angeklebt“ an der Seite. Tortendiagramm.

Wir fahren mit dem Mauszeiger ĂŒber den Knoten und sehen – tatsĂ€chlich hat der Seq Scan von der gesamten Zeit weniger als ein Viertel eingenommen, wĂ€hrend die restlichen 3/4 durch den CTE Scan beansprucht wurden. Schrecklich! Das ist eine kleine Anmerkung zur „Schnelligkeit“ des CTE Scan, wenn Sie diese in Ihren Abfragen aktiv verwenden. Sie sind nicht sehr schnell – sie verlieren sogar gegen die gewöhnliche Tabellen-Scannierung. [статья] [статья]

Aber normalerweise sind solche Diagramme interessanter und komplexer, wenn wir sofort auf ein Segment zeigen und zum Beispiel sehen, dass mehr als die HĂ€lfte der gesamten Zeit durch einen Seq Scan „gegessen“ wurde. Und dann gab es darin noch einen Filter, zahlreiche DatensĂ€tze wurden dadurch verworfen
 Man kann dieses Bild direkt an den Entwickler schicken und sagen: „Wanja, hier lĂ€uft ganz schön etwas schief! KĂŒmmere dich darum, schau nach – irgendetwas stimmt nicht!“

Massenoptimierung von PostgreSQL-Anfragen. Kirill Borovikov (Tensor)

NatĂŒrlich gab es auch einige „Stolpersteine“.

Das erste, worauf wir „gestoßen“ sind, ist das Problem der Rundung. Die Zeit fĂŒr jeden einzelnen Knoten im Plan wird bis auf 1”s genau angegeben. Und wenn die Anzahl der Knotenzyklen beispielsweise 1000 ĂŒberschreitet – nach der AusfĂŒhrung hat PostgreSQL auf „genau bis“ gerechnet, sodass wir beim RĂŒckrechnen eine Gesamtzeit „irgendwo zwischen 0,95ms und 1,05ms“ erhalten. Wenn es um Mikrosekunden geht, ist das noch nichts, aber wenn es bereits um [milli]sekunden geht, mĂŒssen wir bei der „Entwirrung“ der Ressourcen nach Knoten im Plan „wer wie viel verbraucht hat“ diese Informationen berĂŒcksichtigen.

Massenoptimierung von PostgreSQL-Anfragen. Kirill Borovikov (Tensor)

Der zweite Punkt, der komplexer ist, ist die Verteilung der Ressourcen (dieselben Buffers) auf die dynamischen Knoten. Das hat uns zu den ersten 2 Wochen im Prototyp zusÀtzlich etwa 4 Wochen gekostet.

Ein solches Problem ist relativ einfach zu erzeugen – wir erstellen ein CTE und lesen darin angeblich etwas. TatsĂ€chlich ist PostgreSQL „intelligent“ und wird dort nichts direkt lesen. Anschließend nehmen wir den ersten Datensatz heraus und zu ihm den einhunderteinundzwanzigsten aus demselben CTE.

Massenoptimierung von PostgreSQL-Anfragen. Kirill Borovikov (Tensor)

Wir schauen uns den Plan an und verstehen – seltsam, wir hatten 3 Buffers (Datenseiten), die im Seq Scan „verbraucht“ wurden, noch 1 im CTE Scan und noch 2 im zweiten CTE Scan. Wenn wir alles einfach addieren, erhalten wir insgesamt 6, aber aus der Tabelle haben wir nur 3 gelesen! Der CTE Scan liest schließlich nichts von irgendwoher, sondern arbeitet direkt im Prozessspeicher. Das heißt, hier stimmt eindeutig etwas nicht!

TatsĂ€chlich stellt sich heraus, dass all die 3 Datenseiten, die im Seq Scan angefordert wurden, zuerst vom 1. CTE Scan angefordert wurden, und dann vom 2. CTE, und noch 2 wurden ihm mitgegeben. Das heißt, es wurden insgesamt 3 Datenseiten gelesen, nicht 6.

Massenoptimierung von PostgreSQL-Anfragen. Kirill Borovikov (Tensor)

Und dieses Bild fĂŒhrte uns zu dem VerstĂ€ndnis, dass die AusfĂŒhrung des Plans kein Baum mehr ist, sondern einfach ein acyklischer Graph. Und wir erhielten ungefĂ€hr ein solches Diagramm, damit wir verstehen, „was woher ĂŒberhaupt kam“. Hier haben wir ein CTE aus pg_class erstellt, und haben es zweimal angefordert, und die ganze Zeit ging fast auf den Pfad, als wir es ein zweites Mal angefordert haben. Es ist klar, dass das Lesen des 101. Datensatzes viel teurer ist, als einfach den 1. aus der Tabelle zu lesen.

Massenoptimierung von PostgreSQL-Anfragen. Kirill Borovikov (Tensor)

Wir atmeten fĂŒr eine Weile auf. Wir sagten: „Jetzt, Neo, weißt du Kung-Fu! Jetzt ist unsere Erfahrung direkt auf deinem Bildschirm. Jetzt kannst du sie nutzen.“ [статья]

Konsolidierung von Logs

Unsere 1000 Entwickler atmeten erleichtert auf. Aber wir wussten, dass wir nur hunderte von "Produktiv"-Servern haben und all dieses "Copy-Paste" von Seiten der Entwickler keineswegs bequem ist. Wir erkannten, dass wir das selbst zusammenstellen mussten.

Massenoptimierung von PostgreSQL-Anfragen. Kirill Borovikov (Tensor)

Es gibt tatsĂ€chlich ein Standardmodul, das Statistiken sammeln kann, allerdings muss es auch in der Konfiguration aktiviert werden — das ist das Modul pg_stat_statements. Aber es hat uns nicht ĂŒberzeugt.

Erstens weist es denselben Abfragen in unterschiedlichen Schemata innerhalb einer einzigen Datenbank verschiedene QueryIdzu. Das bedeutet, wenn ich zuerst SET search_path = '01'; SELECT * FROM user LIMIT 1;, und dann SET search_path = '02'; und die gleiche Abfrage mache, zeigen die Statistiken dieses Moduls unterschiedliche EintrĂ€ge, und ich kann keine umfassende Statistik genau in Bezug auf dieses Abfragemuster ohne BerĂŒcksichtigung der Schemata erstellen.

Ein weiterer Punkt, der uns daran hinderte, es zu verwenden — fehlende PlĂ€ne. Das heißt, es gibt keinen Plan — nur die Abfrage selbst. Wir sehen, was die Verlangsamung verursacht hat, aber wir verstehen nicht, warum. Und hier kommen wir zurĂŒck zum Problem eines sich schnell Ă€ndernden Datensatzes.

Und der letzte Punkt — Fehlen von "Fakten". Das heißt, man kann sich nicht auf eine bestimmte AusfĂŒhrung der Abfrage beziehen — diese gibt es nicht, es gibt nur aggregierte Statistiken. Damit kann man zwar arbeiten, es ist jedoch sehr schwierig.

Massenoptimierung von PostgreSQL-Anfragen. Kirill Borovikov (Tensor)

Deshalb beschlossen wir, gegen das "Copy-Paste" vorzugehen und begannen zu schreiben einen Collector.

Der Collector verbindet sich ĂŒber SSH, stellt ĂŒber ein Zertifikat eine gesicherte Verbindung zum Server mit der Datenbank her und tail -F bindet sich an die Log-Datei. So erhalten wir in dieser Sitzung ein vollstĂ€ndiges „Spiegelbild“ der gesamten Log-Datei, die der Server generiert. Die Belastung des Servers selbst ist dabei minimal, da wir nichts parsen, sondern nur den Verkehr spiegeln.

Da wir bereits begonnen hatten, die Schnittstelle in Node.js zu schreiben, setzten wir die Entwicklung des Collectors ebenfalls dort fort. Diese Technologie hat sich bewĂ€hrt, da es sehr praktisch ist, JavaScript fĂŒr die Arbeit mit schwach strukturierten Textdaten zu verwenden, wie sie im Log vorkommen. Und die Infrastruktur von Node.js als Backend-Plattform ermöglicht es, bequem mit Netzwerkverbindungen und allgemein mit Datenströmen zu arbeiten.

Dementsprechend "ziehen" wir zwei Verbindungen: die erste, um das Protokoll selbst "zuhören" zu können und es zu uns zu holen, und die zweite – um regelmĂ€ĂŸig bei der Datenbank nachzufragen. "Und im Protokoll steht, dass die Tabelle mit oid 123 blockiert ist", aber das sagt dem Entwickler nichts, und es wĂ€re nicht schlecht, die Datenbank zu fragen: „Was ist eigentlich OID = 123?“ So fragen wir regelmĂ€ĂŸig bei der Datenbank nach, was wir selbst noch nicht wissen.

Massenoptimierung von PostgreSQL-Anfragen. Kirill Borovikov (Tensor)

"Nur eine Sache hast du nicht bedacht, es gibt eine Art von elefantenĂ€hnlichen Bienen!..." Wir haben mit der Entwicklung dieses Systems begonnen, als wir 10 Server ĂŒberwachen wollten. Die aus unserer Sicht kritischsten, auf denen Probleme auftraten, die schwierig zu handhaben waren. Aber im ersten Quartal erhielten wir bereits eine Hundertschaft zur Überwachung – weil das System "ankam", wollten es alle, es war fĂŒr alle bequem.

All dies muss zusammengefĂŒhrt werden, die Datenströme sind groß und aktiv. Im Grunde, was wir ĂŒberwachen, mit dem wir gut umgehen können – das verwenden wir. Wir nutzen auch PostgreSQL als Datenspeicher. Es gibt nichts Schnelleres, um Daten hinein zu "gießen", als einen Operator. COPY bis jetzt gibt es nichts.

Aber einfach Daten "hinein zu gießen" – das ist nicht ganz unsere Technologie. Denn wenn auf hundert Servern etwa 50k Anfragen pro Sekunde stattfinden, generiert das Ihnen 100-150GB Protokolle pro Tag. Daher mussten wir die Datenbank sorgfĂ€ltig "bearbeiten".

Erstens haben wir Tagesspartitionierung, denn niemand interessiert sich im Grunde fĂŒr die Korrelation zwischen den Tagen. Was spielt es fĂŒr eine Rolle, was du gestern hattest, wenn du heute Nacht eine neue Version der Anwendung ausgegeben hast – und schon gibt es eine neue Statistik.

Zweitens haben wir gelernt (wir mussten) sehr, sehr schnell mit Hilfe von COPYzu schreiben. Also nicht einfach COPY, denn er ist schneller als INSERT, sondern noch schneller.

Massenoptimierung von PostgreSQL-Anfragen. Kirill Borovikov (Tensor)

Drittens mussten wir von Triggern und damit auch von Foreign Keysabsehen. Das bedeutet, dass wir keine referenzielle IntegritĂ€t mehr haben. Denn wenn Sie eine Tabelle haben, auf der es ein paar FKs gibt, und Sie in der Datenbankstruktur sagen, dass "dieser Logeintrag sich auf eine Gruppe von EintrĂ€gen ĂŒber FK bezieht", dann bleibt PostgreSQL nichts anderes ĂŒbrig, als es ehrlich auszufĂŒhren. SELECT 1 FROM master_fk1_table WHERE ... mit der Identifikationsnummer, die Sie einzufĂŒgen versuchen – einfach um zu ĂŒberprĂŒfen, dass dieser Eintrag dort vorhanden ist, dass Sie diesen Foreign Key mit Ihrem Eintrag nicht "brechen".

Wir erhalten anstelle eines Eintrags in die Zieltabelle und dessen Indizes zusĂ€tzlich noch das Lesen aus allen Tabellen, auf die verwiesen wird. Und das brauchen wir ĂŒberhaupt nicht – unser Ziel ist es, so viel wie möglich und so schnell wie möglich mit der geringsten Belastung zu speichern. Also, FK – adieu!

Der nĂ€chste Punkt – Aggregation und Hashing. UrsprĂŒnglich hatten wir dies in der DB implementiert – es ist schließlich praktisch, wenn ein Eintrag ankommt, gleich in einer bestimmten Tabelle ein "plus eins" direkt im Trigger. Gut, praktisch, aber schlecht, denn man fĂŒgt einen Eintrag hinzu und muss gezwungenermaßen noch etwas aus einer anderen Tabelle lesen und schreiben. DarĂŒber hinaus ist es nicht nur notwendig zu lesen und zu schreiben – man muss es auch jedes Mal tun.

Stellen Sie sich nun vor, Sie haben eine Tabelle, in der Sie einfach die Anzahl der Anfragen zĂ€hlen, die ĂŒber einen bestimmten Host gelaufen sind: +1, +1, +1, ..., +1. Und das brauchen Sie prinzipiell nicht – das kann man alles im Speicher auf dem Collector zusammenfassen und alles auf einmal in die Datenbank senden +10.

Ja, im Falle von Problemen kann Ihre logische IntegritĂ€t "zerfallen", aber das ist praktisch ein unrealistischer Fall – denn Sie haben einen normalen Server, eine Batterie im Controller, ein Transaktionsjournal, ein Journal im Dateisystem
 Kurz gesagt, das ist es nicht wert. Der Leistungsverlust, den Sie durch die Verwendung von Triggern/FK erhalten, und die Kosten, die Sie dabei tragen, sind nicht gerechtfertigt.

Das Gleiche gilt fĂŒr das Hashing. Ein gewisser Request kommt zu Ihnen, Sie berechnen in der DB einen bestimmten Identifikator, schreiben ihn in die Datenbank und geben ihn allen weiter. Alles gut, solange Ihnen nicht wĂ€hrend des Schreibens ein weiterer Wunschkandidat begegnet – dann haben Sie eine Blockade, und das ist schon schlecht. Deshalb, wenn Sie die Generierung bestimmter IDs auf den Client (relativ zur Datenbank) auslagern können, ist es besser, dies zu tun.

Es hat sich einfach perfekt ergeben, MD5 vom Text – der Anfrage, dem Plan, der Vorlage,
 zu verwenden. Wir berechnen es auf der Seite des Collectors und "gießen" dann die fertige ID in die Datenbank. Die MD5-LĂ€nge und die tĂ€gliche Partitionierung ermöglichen es uns, uns keine Gedanken ĂŒber mögliche Kollisionen zu machen.

Massenoptimierung von PostgreSQL-Anfragen. Kirill Borovikov (Tensor)

Um dies alles schnell zu speichern, mussten wir jedoch das eigentliche Schreibverfahren modifizieren.

Wie werden Daten normalerweise geschrieben? Wir haben einen bestimmten Datensatz, den wir auf mehrere Tabellen aufteilen, und dann COPY – zuerst in die erste, dann in die zweite, die dritte... Das ist unpraktisch, weil wir scheinbar einen Datenstrom in drei aufeinanderfolgende Schritte schreiben. Unangenehm. Kann man es schneller machen? Ja!

Dazu reicht es aus, diese Ströme parallel zueinander zu ordnen. Das bedeutet, dass wir Fehler, Anfragen, Vorlagen, Sperren... in separaten Strömen laufen lassen und alles parallel schreiben. DafĂŒr reicht es aus den COPY-Kanal fĂŒr jede einzelne Ziel-Tabelle stĂ€ndig offen zu halten.

Massenoptimierung von PostgreSQL-Anfragen. Kirill Borovikov (Tensor)

Das heißt, der Collector hat immer einen Stream, in den ich die benötigten Daten schreiben kann. Aber damit die Datenbank diese Daten sieht und niemand in einer Sperre hĂ€ngt, wĂ€hrend er wartet, dass diese Daten geschrieben werden, muss COPY mit einer bestimmten HĂ€ufigkeit unterbrochen werden. FĂŒr uns hat sich ein Intervall von etwa 100 ms als am effektivsten erwiesen – wir schließen und öffnen sofort wieder die Verbindung zur gleichen Tabelle. Und wenn wir bei bestimmten Spitzen nicht einen Stream haben, dann machen wir ein Pooling bis zu einer bestimmten Obergrenze.

ZusĂ€tzlich haben wir festgestellt, dass jede Aggregation fĂŒr dieses Lastprofil, wenn DatensĂ€tze in Paketen gebĂŒndelt werden, schĂ€dlich ist. Das klassische Übel ist INSERT ... VALUES und dann 1000 DatensĂ€tze. Denn in diesem Moment tritt ein Schreibspitzenreiz auf dem Medium auf, und alle anderen, die versuchen, etwas auf die Festplatte zu schreiben, mĂŒssen warten.

Um solche Anomalien zu vermeiden, aggregieren Sie einfach nichts, puffern Sie ĂŒberhaupt nicht. Und wenn doch eine Pufferung auf die Festplatte auftritt (zum GlĂŒck erlaubt das Stream API in Node.js, dies zu erkennen) – trennen Sie diese Verbindung. Sobald Sie das Ereignis erhalten, dass sie wieder frei ist – schreiben Sie daraus aus der angesammelten Warteschlange. Solange sie beschĂ€ftigt ist – nehmen Sie den nĂ€chsten freien aus dem Pool und schreiben Sie in diesen.

Vor der Implementierung dieses Ansatzes fĂŒr das Schreiben von Daten hatten wir etwa 4K Schreiboperationen, und auf diese Weise haben wir die Last auf das Vierfache reduziert. Jetzt sind wir noch einmal um das Sechsfache gewachsen, dank neuer beobachtbarer Datenbanken – auf 100MB/s. Und jetzt speichern wir Logs fĂŒr die letzten 3 Monate mit einem Volumen von etwa 10-15TB in der Hoffnung, dass in drei Monaten jeder Entwickler jedes Problem lösen kann.

Wir verstehen die Probleme

Aber all diese Daten zu sammeln – das ist gut, nĂŒtzlich, passend, aber wenig – man muss sie verstehen. Denn es handelt sich um Millionen verschiedener PlĂ€ne pro Tag.

Massenoptimierung von PostgreSQL-Anfragen. Kirill Borovikov (Tensor)

Aber Millionen sind unĂŒberschaubar, man muss zuerst "weniger" machen. Und zuerst muss man entscheiden, wie man dieses "weniger" organisieren wird.

Wir haben uns drei SchlĂŒsselpunkte herausgearbeitet:

  • wer diese Anfrage gesendet hat
    Das heißt, aus welcher Anwendung sie „gekommen“ ist: Web-OberflĂ€che, Backend, Zahlungssystem oder etwas anderes.
  • wo wann es passiert ist
    Auf welchem konkreten Server. Denn wenn Sie mehrere Server fĂŒr eine Anwendung haben und plötzlich einer „stecken bleibt“ (weil „die Festplatte kaputt ist“, „der Speicher defekt ist“, oder ein anderes Problem), dann muss man sich konkret an den Server richten.
  • als in welchem Plan genau das Problem aufgetreten ist

Um zu verstehen, „wer“ uns die Anfrage gesendet hat, verwenden wir das Standardmittel – die Festlegung einer Session-Variablen: SET application_name = '{bl-host}:{bl-method}'; – wir erfassen den Namen des Business-Logic-Hosts, von dem die Anfrage kommt, sowie den Namen der Methode oder Anwendung, die sie initiiert hat.

Nachdem wir den „EigentĂŒmer“ der Anfrage ĂŒbergeben haben, muss dieser in das Log geschrieben werden – dafĂŒr konfigurieren wir die Variable log_line_prefix = ' %m [%p:%v] [%d] %r %a'. Wer interessiert ist, kann im Handbuch nachsehen, was das alles bedeutet. Das bedeutet, dass wir im Log sehen:

  • Zeit
  • die Prozess- und Transaktions-IDs
  • den Datenbanknamen
  • die IP-Adresse desjenigen, der diese Anfrage gesendet hat
  • und den Namen der Methode

Massenoptimierung von PostgreSQL-Anfragen. Kirill Borovikov (Tensor)

DarĂŒber hinaus haben wir verstanden, dass es nicht sehr interessant ist, die Korrelation einer einzigen Anfrage zwischen verschiedenen Servern zu betrachten. Es kommt nicht oft vor, dass eine Anwendung an beiden Stellen gleich „scheitert“. Aber selbst wenn sie es tut – schauen Sie sich einen dieser Server an.

Also, der Schnitt „ein Server – ein Tag“ war fĂŒr jede Analyse ausreichend.

Der erste analytische Schnitt – das ist der sogenannte „Schnelltest“ – eine verkĂŒrzte Form der Darstellung des Plans, frei von allen quantitativen Angaben. Der zweite Schnitt – die Anwendung oder Methode, und der dritte – das spezifische Plan-Element, das uns Probleme bereitet hat.

Als wir von konkreten Instanzen zu Mustern ĂŒbergingen, erhielten wir sofort zwei Vorteile:

  • eine drastische Reduzierung der Anzahl der Objekte zur Analyse
    Man muss die Probleme nicht mehr anhand von Tausenden Anfragen oder PlÀnen, sondern durch Dutzende von Mustern untersuchen.
  • Zeitachse
    Das heißt, wenn man die "Fakten" in einem bestimmten Kontext zusammenfasst, kann man deren Auftreten im Laufe des Tages darstellen. Und hier können Sie verstehen, dass, wenn Sie ein bestimmtes Muster haben, das zum Beispiel einmal pro Stunde auftritt, obwohl es einmal tĂ€glich sein sollte, es sinnvoll ist, darĂŒber nachzudenken, was schiefgelaufen ist – wer und warum hat es ausgelöst, vielleicht sollte es hier gar nicht sein. Dies ist ein weiterer nicht-numerischer, rein visueller Analyseansatz.

Massenoptimierung von PostgreSQL-Anfragen. Kirill Borovikov (Tensor)

Die anderen Methoden basieren auf den Kennzahlen, die wir aus dem Plan extrahieren: wie oft dieses Muster aufgetreten ist, die Gesamt- und Durchschnittszeit, wie viele Daten vom DatentrÀger gelesen wurden und wie viele aus dem Speicher


Denn Sie kommen zum Beispiel auf die Analytics-Seite des Hosts, schauen – es scheint, dass zu viele Daten vom DatentrĂ€ger gelesen wurden. Der DatentrĂ€ger auf dem Server kann nicht mithalten – aber wer liest von ihm?

Und Sie können nach jeder Spalte sortieren und entscheiden, womit Sie sich gerade beschĂ€ftigen möchten – mit der CPU- oder Disklast, oder mit der Gesamtzahl der Anfragen
 Sie haben sortiert, die „Top-Punkte“ angeschaut, repariert – eine neue Version der Anwendung ausgerollt.
[ĐČĐžĐŽĐ”ĐŸĐ»Đ”Đșцоя]

Und sofort können Sie verschiedene Anwendungen sehen, die mit demselben Muster von Anfragen des Typs arbeiten SELECT * FROM users WHERE login = 'Vasya'. Frontend, Backend, Processing
 Und Sie fragen sich, warum das Processing den Benutzer lesen sollte, wenn keine Interaktion mit ihm stattfindet.

Der umgekehrte Weg – sofort zu sehen, was die Anwendung macht. Zum Beispiel: Das Frontend – das, das, das hier, und zusĂ€tzlich das einmal pro Stunde (genau da hilft der Zeitstrahl). Und sofort taucht die Frage auf – es sollte eigentlich nicht die Aufgabe des Frontends sein, etwas einmal pro Stunde zu tun


Massenoptimierung von PostgreSQL-Anfragen. Kirill Borovikov (Tensor)

Nach einiger Zeit haben wir festgestellt, dass uns aggregierte Statistiken im Rahmen der Plan-Knoten fehlen.Wir haben aus den PlĂ€nen nur diejenigen Knoten herausgefiltert, die mit den Daten der Tabellen etwas tun (ob sie sie nach Index lesen/schreiben oder nicht). Im Wesentlichen wird im Vergleich zum vorherigen Bild nur ein Aspekt hinzugefĂŒgt – wie viele DatensĂ€tze dieser Knoten uns gebracht hat, und wie viele verworfen wurden (Rows Removed by Filter).

Sie haben keinen geeigneten Index auf der Tabelle, Sie fĂŒhren eine Anfrage durch, sie ignoriert den Index und fĂ€llt in einen Seq Scan
 Sie haben alle DatensĂ€tze außer einem herausgefiltert. Aber warum benötigen Sie am Tag 100M herausgefilterte DatensĂ€tze, ist es nicht besser, einen Index zu erstellen?

Massenoptimierung von PostgreSQL-Anfragen. Kirill Borovikov (Tensor)

Nachdem wir alle PlĂ€ne fĂŒr die Knoten durchgegangen sind, haben wir festgestellt, dass es bestimmte standardisierte Strukturen in den PlĂ€nen gibt, die mit sehr hoher Wahrscheinlichkeit verdĂ€chtig aussehen. Es wĂ€re nicht schlecht, dem Entwickler zu sagen: „Freund, hier liest du zuerst nach Index, dann sortierst du und schneidest dann ab“ – in der Regel gibt es dort nur einen Eintrag.

Alle, die Anfragen mit diesem Muster gestellt haben, sind sicherlich auf folgendes gestoßen: „Gib mir die letzte Bestellung von Vasja, inklusive seines Datums.“ Und wenn ihr keinen Index nach Datum habt oder in dem verwendeten Index kein Datum vorhanden ist, dann werdet ihr genau auf solche „Schaufeln“ treten.

Aber wir wissen ja, dass das „Schaufeln“ sind – warum also dem Entwickler nicht gleich sagen, was er tun sollte? Dementsprechend sieht unser Entwickler beim Öffnen des Plans sofort ein schönes Bild mit Hinweisen, wo ihm sofort gesagt wird: „Hier und hier hast du Probleme, und sie lösen sich so und so.“

Infolgedessen hat sich das Volumen der Erfahrungen, die erforderlich waren, um Probleme am Anfang und jetzt zu lösen, um ein Vielfaches verringert. So ein Werkzeug haben wir entwickelt.

Massenoptimierung von PostgreSQL-Anfragen. Kirill Borovikov (Tensor)

Quelle: habr.com

60GB SSD 8Gb DDR4