Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

Im Vortrag werden einige AnsĂ€tze vorgestellt, die es ermöglichen, die Leistung von SQL-Abfragen zu ĂŒberwachen, wenn tĂ€glich Millionen ausgefĂŒhrt werden,, wĂ€hrend die kontrollierten PostgreSQL-Server in Hunderten zĂ€hlen.

Welche technischen Lösungen ermöglichen es uns, solch ein Informationsvolumen effizient zu verarbeiten, und wie erleichtert dies das Leben der normalen Entwickler?

Video abspielen

Interessiert an der Analyse spezifischer Probleme und verschiedenen Techniken der Optimierung von SQL-Abfragen sowie der Lösung typischer DBA-Aufgaben in PostgreSQL — Sie können sich auch ĂŒber eine Reihe von Artikeln zu diesem Thema informieren.

Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)
Mein Name ist Kirill Borovikov, ich vertrete das Unternehmen „Tensor“. Ich spezialisiere mich konkret auf die Arbeit mit Datenbanken in unserem Unternehmen.

Heute werde ich Ihnen erzĂ€hlen, wie wir die Abfragen optimieren, wenn es nicht darum geht, die Leistung einer einzigen Abfrage zu steigern, sondern ein Problem massenhaft zu lösen. Wenn es Millionen von Abfragen gibt, und Sie AnsĂ€tze zur Lösung finden mĂŒssen fĂŒr dieses große Problem. Im Grunde genommen ist „Tensor“ fĂŒr unsere Millionen von Kunden — das

SBIS — unsere Anwendung СБИС — unsere Anwendung: ein soziales Unternehmensnetzwerk, Lösungen fĂŒr Videokommunikation, fĂŒr den internen und externen Dokumentenaustausch, Buchhaltungssysteme und Lagerverwaltung,... Das heißt, eine Art „Mega-Tool“ fĂŒr das umfassende Management von Unternehmen, das ĂŒber 100 verschiedene interne Projekte umfasst.

Damit all diese Projekte reibungslos funktionieren und sich weiterentwickeln können, haben wir 10 Entwicklungszentren im ganzen Land mit mehr als 1000 Entwicklern..

Wir arbeiten seit 2008 mit PostgreSQL und haben eine umfangreiche Datenbasis aufgebaut – das umfasst Kundendaten, statistische Informationen, Analysen und Daten aus externen Informationssystemen – ĂŒber 400 TB.Allein in der Produktionsumgebung betreiben wir etwa 250 Server, und insgesamt ĂŒberwachen wir rund 1000 Datenbankserver.

Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

SQL ist eine deklarative Sprache. Sie beschreiben nicht, „wie“ etwas funktionieren soll, sondern „was“ Sie erreichen möchten. Die Datenbank weiß am besten, wie man einen JOIN macht – wie man Ihre Tabellen verbindet, welche Bedingungen anzuwenden sind, was ĂŒber den Index geht und was nicht...

Einige Datenbanken akzeptieren Hinweise: „Nein, verbinde diese beiden Tabellen in dieser Reihenfolge“, aber PostgreSQL kann das nicht. Dies ist eine bewusste Entscheidung der Hauptentwickler: „Lieber verbessern wir den Abfrageoptimierer, als Entwicklern zu erlauben, irgendwelche Hinweise zu verwenden.“

Aber trotz der Tatsache, dass PostgreSQL nicht „von außen“ gesteuert werden kann, erlaubt es hervorragend, zu sehen, was „innen“ passiert,wenn Sie Ihre Abfrage ausfĂŒhren und wo es Probleme gibt.

Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

Generell, mit welchen klassischen Problemen kommt ein Entwickler [zum DBA] ĂŒblicherweise? „Wir haben diese Abfrage ausgefĂŒhrt und es ist alles langsam,alles hĂ€ngt, irgendetwas passiert
 Irgendwas stimmt nicht!“

Die GrĂŒnde sind fast immer die gleichen:

  • ineffizienter Abfragealgorithmus.
    Entwickler: „Jetzt verbinde ich in SQL 10 Tabellen ĂŒber JOIN
“ – und erwartet, dass seine Bedingungen auf wundersame Weise effizient „gelöst“ werden und er alles schnell erhĂ€lt. Aber Wunder gibt es nicht, und jedes System gibt bei solch einer VariabilitĂ€t (10 Tabellen in einem FROM) immer irgendeine Ungenauigkeit. [Artikel]
  • veraltete Statistiken.
    Der Moment ist besonders relevant fĂŒr PostgreSQL, wenn Sie ein großes Dataset auf den Server geladen haben, eine Abfrage durchfĂŒhren – und es Ihnen "sequential scan" ĂŒber die Tabelle macht. Denn gestern waren dort 10 DatensĂ€tze, heute sind es 10 Millionen, aber PostgreSQL hat das noch nicht mitbekommen und benötigt einen Hinweis darauf.Artikel]
  • "Engpass" bei den Ressourcen
    Sie haben eine große und stark belastete Datenbank auf einen schwachen Server gestellt, der nicht ĂŒber genĂŒgend Speicherplatz, RAM oder die notwendige Prozessorleistung verfĂŒgt. Und das war's
 Irgendwo gibt es eine Leistungsobergrenze, die Sie nicht ĂŒberschreiten können.
  • Sperrung
    Ein komplexer Punkt, aber er ist besonders relevant fĂŒr verschiedene modifizierende Abfragen (INSERT, UPDATE, DELETE) – das ist ein ganz eigenes großes Thema.

Abrufen des Plans


 Und fĂŒr alles andere benötigen wir einen Plan! Wir mĂŒssen sehen, was im Inneren des Servers vor sich geht.

Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

Der AusfĂŒhrungsplan einer Abfrage fĂŒr PostgreSQL ist ein Baum, der den Algorithmus zur AusfĂŒhrung der Abfrage in textlicher Form darstellt. Dieser Algorithmus wurde im Ergebnis der Analyse vom Planner als der effizienteste erkannt.

Jeder Knoten des Baumes stellt eine Operation dar: das Abrufen von Daten aus einer Tabelle oder einem Index, den Aufbau einer Bitmaske, das VerknĂŒpfen zweier Tabellen, das ZusammenfĂŒhren, den Schnitt oder das Ausschließen von Auswahlmöglichkeiten. Die AusfĂŒhrung einer Anfrage entspricht dem Durchlaufen der Knoten dieses Baumes.

Um einen Abfrageplan zu erhalten, ist der einfachste Weg, den Befehl EXPLAINauszufĂŒhren. Um alle tatsĂ€chlichen Attribute zu erhalten, also tatsĂ€chlich die Anfrage auf der Datenbank auszufĂŒhren, verwenden Sie: EXPLAIN (ANALYZE, BUFFERS) SELECT ....

Ein unangenehmer Punkt: Wenn Sie ihn ausfĂŒhren, geschieht dies ‚hier und jetzt‘, was nur fĂŒr lokale Debugging-Zwecke geeignet ist. Wenn Sie jedoch einen stark belasteten Server haben, der unter einem hohen DatenĂ€nderungsstrom steht, und Sie sehen: ‚Oh! Hier lief die Anfrage langsam,‘wĂ€hrend Sie die Anfrage aus den Logs gezogen und wieder auf den Server gebracht haben, haben sich Ihre gesamten DatensĂ€tze und Statistiken geĂ€ndert. Sie fĂŒhren es zur Debugging-Zwecken aus – und es lĂ€uft schnell! Und Sie können nicht verstehen, „warum“, warum es langsam war.

Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

Um herauszufinden, was genau zu dem Zeitpunkt war, als die Anfrage auf dem Server ausgefĂŒhrt wurde, haben kluge Menschen das auto_explain-Modul geschrieben.. Es ist in fast allen gĂ€ngigen PostgreSQL-Distributionen vorhanden und kann einfach in der Konfigurationsdatei aktiviert werden.

Wenn es erkennt, dass eine Anfrage lÀnger dauert als die von Ihnen festgelegte Grenze, macht es eine "Momentaufnahme" des Plans dieser Anfrage und dokumentiert diese zusammen im Protokoll..

Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

Soweit sieht alles gut aus, wir gehen ins Protokoll und sehen dort
 [ĐżĐŸŃ€Ń‚ŃĐœĐșа тДĐșста]. Aber wir können nichts darĂŒber sagen, außer der Tatsache, dass es ein hervorragender Plan war, denn er wurde in 11 ms ausgefĂŒhrt.

Es sieht also alles gut aus – aber wir verstehen nicht, was wirklich passiert ist. Abgesehen von der Gesamtzeit sehen wir nicht viel. Denn auf solche "Rohtexte" in klarem Text zu schauen, ist ĂŒberhaupt nicht anschaulich.

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

  • Im Knoten wird angegeben die Summe der Ressourcen des gesamten Unterbaums darunter. Das heißt, wir können nicht einfach herausfinden, wie viel Zeit konkret bei diesem Index-Scan aufgewendet wurde, wenn es darunter eine eingefĂŒgte Bedingung gibt. Wir mĂŒssen dynamisch prĂŒfen, ob es innerhalb "Kinder" und bedingte Variablen, CTE gibt – und das alles "im Kopf" subtrahieren.
  • Ein zweiter Punkt: die Zeit, die im Knoten angegeben wird, ist AusfĂŒhrungszeit eines Knotens. Wenn dieser Knoten zum Beispiel als Teil einer Schleife ĂŒber Datensatztabellen mehrfach ausgefĂŒhrt wurde, erhöht sich im Plan die Anzahl der Schleifen — also die Zyklen dieses Knotens. Die atomare AusfĂŒhrungszeit bleibt jedoch im Plan unverĂ€ndert. Um herauszufinden, wie oft dieser Knoten insgesamt ausgefĂŒhrt wurde, sollte man das eine mit dem anderen multiplizieren — wieder einmal „im Kopf“.

Unter diesen UmstĂ€nden ist es praktisch unmöglich zu verstehen, „Wer ist das schwĂ€chste Glied?“. Daher schreiben sogar die Entwickler in ihrem „Handbuch“, dass „Das VerstĂ€ndnis des Plans ist eine Kunst, die man erlernen muss, Erfahrung
“.

Aber wir haben 1000 Entwickler, und es ist unmöglich, jedem von ihnen diese Erfahrung im Kopf zu vermitteln. Ich, du, er — wir wissen es, aber der da drĂŒben — vielleicht nicht. Möglicherweise lernt er es, vielleicht auch nicht, aber er muss jetzt schon arbeiten — und wo soll er diese Erfahrung hernehmen?

Visualisierung des Plans

Deshalb haben wir erkannt — um mit diesen Problemen umzugehen, benötigen wir eine gute Visualisierung des Plans. [Artikel]

Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

Wir haben zunĂ€chst „den Markt“ erkundet — lass uns im Internet nachsehen, was es ĂŒberhaupt gibt.

Es stellte sich jedoch heraus, dass es nur sehr wenige „lebendige“ Lösungen gibt, die sich mehr oder weniger weiterentwickeln – tatsĂ€chlich nur eine: explain.depesz.com von Hubert Lubaczewski. In das Eingabefeld geben Sie die Textdarstellung des Plans ein, und es zeigt Ihnen eine Tabelle mit den aufgeschlĂŒsselten Daten an:

  • eigene Bearbeitungszeit des Knotens
  • Gesamtzeit ĂŒber den gesamten Unterbaum
  • die Anzahl der DatensĂ€tze, die extrahiert wurden und die statistisch erwartet wurden
  • der eigentliche Knoteninhalt

Dieser Dienst bietet auch die Möglichkeit, ein Archiv von Links zu teilen. Sie haben Ihren Plan dort eingetragen und sagen: „Hey, Vasja, hier ist der Link, da stimmt etwas nicht.“

Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

Es gibt jedoch auch einige kleinere Probleme.

Erstens handelt es sich um eine enorme Menge an „Copy-Paste“. Sie nehmen einen Teil des Protokolls, fĂŒgen ihn ein, und immer wieder.

Zweitens, gibt es keine Analyse der Menge der gelesenen Daten – denjenigen Buffern, die ausgegeben werden durch EXPLAIN (ANALYZE, BUFFERS), hier sehen wir das nicht. Es kann sie einfach nicht aufschlĂŒsseln, verstehen und damit arbeiten. Wenn Sie viele Daten lesen und verstehen, dass Sie möglicherweise nicht korrekt auf der Festplatte und im Cache im Speicher „aufgeschlĂŒsselt“ werden, ist diese Information sehr wichtig.

Der dritte negative Punkt ist die sehr schwache Entwicklung dieses Projekts. Die Commits sind sehr klein, gut, wenn es einmal im halben Jahr welche gibt, und der Code ist in Perl.

Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

Aber das sind alles „Randbemerkungen“, damit könnten wir irgendwie leben, doch es gibt eine Sache, die uns stark von diesem Dienst abgebracht hat. Das sind die Fehler bei der Analyse von Common Table Expressions (CTE) und verschiedenen dynamischen Knoten wie InitPlan/SubPlan.

Wenn man diesem Bild glauben schenken darf, dann ist die GesamtausfĂŒhrungszeit jedes einzelnen Knotens grĂ¶ĂŸer als die Gesamtzeit fĂŒr die gesamte Abfrage. Es ist ganz einfach — im Knoten CTE Scan wurde die Zeit fĂŒr die Generierung dieser CTE nicht abgezogen.. Daher wissen wir nicht mehr, wie viel Zeit das Scannen der CTE in Anspruch genommen hat.

Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

Hier kamen wir zu dem Schluss, dass es Zeit war, unser eigenes Ding zu schreiben — hurra! Jeder Entwickler sagt: „Jetzt schreiben wir unser eigenes, das wird super einfach!“

Wir nahmen einen typischen Stack fĂŒr Webdienste: Kern auf Node.js + Express, fĂŒgten Bootstrap hinzu und fĂŒr schöne Diagramme — D3.js. Und unsere Erwartungen wurden durchaus erfĂŒllt — den ersten Prototypen erhielten wir in 2 Wochen:

  • einen eigenen Plan-Parser
    Das heißt, wir können jetzt jeden beliebigen Plan analysieren, den PostgreSQL generiert.
  • eine korrekte Analyse dynamischer Knoten — CTE Scan, InitPlan, SubPlan
  • Analyse der Verteilung von Buffern — wo Datenseiten aus dem Speicher gelesen werden, wo aus dem lokalen Cache, wo von der Festplatte
  • Wir haben eine Übersicht erhalten
    Um nicht alles im Log zu "graben", sondern das "schwÀchste Glied" sofort auf einem Bild zu sehen.

Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

Wir haben ungefĂ€hr so eine Ansicht erhalten – sofort mit Syntax-Highlighting. Normalerweise arbeiten unsere Entwickler jedoch nicht mit einer vollstĂ€ndigen Darstellung des Plans, sondern mit einer verkĂŒrzten Version. Denn alle Zahlen haben wir bereits analysiert und zur Seite geschoben, wĂ€hrend wir in der Mitte nur die erste Zeile gelassen haben, um anzuzeigen, was das fĂŒr ein Knoten ist: CTE Scan, Generierung von CTE oder Seq Scan fĂŒr eine bestimmte Tabelle.

Diese verkĂŒrzte Darstellung nennen wir Planvorlage.

Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

Was wĂ€re noch hilfreich? Es wĂ€re nĂŒtzlich zu sehen, welcher Anteil an welcher Knoten von der Gesamtzeit verteilt ist – und einfach "nebenan" angeheftet. Tortendiagramm.

Wir bewegen den Cursor ĂŒber den Knoten und sehen – anscheinend hat Seq Scan weniger als ein Viertel der gesamten Zeit in Anspruch genommen, wĂ€hrend die restlichen 3/4 durch CTE Scan in Anspruch genommen wurden. Schrecklich! Das ist eine kleine Anmerkung zur "Geschwindigkeit" von CTE Scan, wenn Sie diese aktiv in Ihren Abfragen verwenden. Sie sind nicht sehr schnell – sie sind sogar langsamer als ein normales Tabellenscanning. [Artikel] [Artikel]

Normalerweise sind solche Diagramme interessanter und komplexer, wenn wir gezielt auf ein Segment zeigen und beispielsweise sehen, dass mehr als die HĂ€lfte der gesamten Zeit von einem bestimmten Seq Scan «verbraucht» wurde. Und innerhalb davon gab es einen Filter, durch den eine Menge von DatensĂ€tzen verworfen wurde
 Dieses Bild kann direkt an den Entwickler gesendet werden mit der Aufforderung: «Vasya, hier stimmt etwas nicht! Schau dir das an, da ist was faul!»

Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

NatĂŒrlich gab es einige Stolpersteine.

Das erste Hindernis war das Problem der Rundung. Die Zeit jedes einzelnen Knotens im Plan wird mit einer Genauigkeit von 1”s angegeben. Wenn die Anzahl der Knotenzyklen 1000 ĂŒberschreitet, teilt PostgreSQL nach der AusfĂŒhrung «bis zur Genauigkeit», sodass wir bei der RĂŒckrechnung eine Gesamtzeit von «etwa zwischen 0,95 ms und 1,05 ms» erhalten. Wenn es um Mikrosekunden geht, ist das noch in Ordnung, aber wenn es bereits um [Millisekunden] geht, mĂŒssen wir bei der «AufschlĂŒsselung» der Ressourcen pro Knoten im Plan berĂŒcksichtigen, «wer wie viel verbraucht hat».

Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

Der zweite, kompliziertere Punkt ist die Verteilung der Ressourcen (diese Puffer) auf die dynamischen Knoten. Das hat uns in den ersten zwei Wochen des Prototyps zusÀtzlich etwa vier Wochen gekostet.

Ein solches Problem tritt recht hĂ€ufig auf – wir erstellen ein CTE und lesen angeblich etwas daraus. In Wirklichkeit ist PostgreSQL „intelligent“ und wird dort nichts direkt lesen. Dann nehmen wir den ersten Datensatz, und zu ihm die einhundertste aus demselben CTE.

Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

Wir betrachten den Plan und merken – seltsam, wir hatten 3 Buffer (DatenblĂ€tter), die beim Seq Scan „genutzt“ wurden, noch 1 beim CTE Scan und nochmals 2 beim zweiten CTE Scan. Wenn man alles einfach zusammenzĂ€hlt, hĂ€tte man 6, aber aus der Tabelle haben wir insgesamt nur 3 gelesen! Der CTE Scan liest schließlich nichts von extern, sondern arbeitet direkt mit dem Prozessspeicher. Hier scheint also etwas nicht zu stimmen!

TatsÀchlich ergibt sich hier, dass alle 3 DatenblÀtter, die beim Seq Scan angefordert wurden, zuerst vom 1. CTE Scan angefordert wurden und dann vom 2. CTE Scan, der zusÀtzlich 2 weitere gelesen hat. Das bedeutet, insgesamt wurden nur 3 DatenblÀtter gelesen und nicht 6.

Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

Dieses Bild hat uns die Erkenntnis gebracht, dass die AusfĂŒhrung des Plans kein Baum mehr ist, sondern einfach ein azyklischer Graph. Wir haben eine Übersicht in Form eines Diagramms erstellt, damit wir verstehen, woher alles kommt. Hier haben wir eine CTE aus pg_class erstellt, die wir zweimal angefordert haben. Fast die gesamte Zeit haben wir im ersten Durchlauf mit der zweiten Anfrage verbracht. Es ist klar, dass das Lesen des 101. Eintrags wesentlich teurer ist als nur den ersten aus der Tabelle abzurufen.

Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

Wir atmeten erleichtert auf und sagten: „Jetzt, Neo, weißt du Kung-Fu! Jetzt ist unsere Erfahrung direkt auf deinem Bildschirm. Du kannst sie jetzt nutzen.“ [Artikel]

Konsolidierung der Logs

Unsere 1000 Entwickler atmeten erleichtert auf. Aber wir wussten, dass wir nur Hunderte von »Produktions«-Servern haben und dass dieses Kopieren seitens der Entwickler alles andere als praktisch war. Wir erkannten, dass wir das selbst zusammenbauen mussten.

Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

Es gibt tatsĂ€chlich ein Standardmodul, das Statistiken sammeln kann, das allerdings ebenfalls im Konfigurationsfile aktiviert werden muss — es ist das pg_stat_statements-Modul. Aber das hat uns nicht ĂŒberzeugt.

Erstens weist es den gleichen Anfragen in unterschiedlichen Schemata innerhalb derselben Datenbank verschiedene QueryIds zu.Das bedeutet, wenn man zuerst SET search_path = '01'; SELECT * FROM user LIMIT 1;, und dann SET search_path = '02'; und bei einer Ă€hnlichen Anfrage werden in der Statistik dieses Moduls unterschiedliche EintrĂ€ge angezeigt, und ich kann keine allgemeine Statistik genau fĂŒr 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 Anfrage selbst. Wir sehen, was langsamer war, aber verstehen nicht, warum. Und hier kommen wir zurĂŒck zum Problem des sich schnell Ă€ndernden Datensatzes.

Und der letzte Punkt - mangelnde „Fakten“. Das heißt, man kann sich nicht auf einen bestimmten Instanz der AusfĂŒhrung der Anfrage beziehen - sie existiert nicht, es gibt nur aggregierte Statistiken. Damit kann man zwar arbeiten, aber es ist sehr schwierig.

Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

Deshalb haben wir beschlossen, gegen „Copy-Paste“ vorzugehen und begannen, einen Collector.

zu schreiben, der sich ĂŒber SSH verbindet, ĂŒber ein Zertifikat eine sichere Verbindung zum Server mit der Datenbank herstellt und tail -F sich an das Logfile anhĂ€ngt. So erhalten wir in dieser Sitzung ein vollstĂ€ndiges „Spiegelbild“ der gesamten Logdatei , die der Server generiert. Die Belastung des Servers bleibt dabei minimal, denn wir parsen dort nichts, sondern spiegeln nur den Verkehr., der einen Server generiert. Die Belastung des Servers bleibt dabei minimal, da wir dort nichts parsen, sondern lediglich den Verkehr spiegeln.

Da wir bereits mit der Entwicklung des Interfaces in Node.js begonnen haben, setzen wir die Entwicklung des Collectors ebenfalls in dieser Technologie fort. Diese Wahl hat sich bewĂ€hrt, da JavaScript sehr gut fĂŒr die Arbeit mit schwach strukturierten Textdaten, wie sie in Logs vorkommen, geeignet ist. Die Node.js-Infrastruktur als Backend-Plattform ermöglicht zudem eine einfache und komfortable Handhabung von Netzwerkverbindungen und Datenströmen insgesamt.

In diesem Zusammenhang „ziehen“ wir zwei Verbindungen auf: die erste, um das Log zu „abhören“ und es abzuholen, und die zweite, um regelmĂ€ĂŸig bei der Datenbank nachzufragen. „Im Log wurde gemeldet, dass die Tabelle mit der OID 123 gesperrt ist“, was dem Entwickler an dieser Stelle jedoch nicht viel sagt. Es wĂ€re sinnvoll, die Datenbank zu fragen: „Was genau bedeutet OID = 123?“ So stellen wir regelmĂ€ĂŸig Nachfragen bei der Datenbank, um Informationen zu erhalten, die uns noch nicht bekannt sind.

Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

„Einen Aspekt hast du jedoch ĂŒbersehen: Es gibt eine Art von elefantenartigen Bienen!“ Wir haben mit der Entwicklung dieses Systems begonnen, als wir 10 Server ĂŒberwachen wollten. Die kritischsten aus unserer Sicht, bei denen einige Probleme auftraten, die schwer zu lösen waren. Doch bereits im ersten Quartal erhielten wir die Überwachung fĂŒr hundert Server – denn das System wurde angenommen; alle wollten es nutzen, es war fĂŒr alle bequem.

All dies muss zusammengefĂŒhrt werden; die Datenströme sind groß und aktiv. Im Grunde genommen verwenden wir das, was wir ĂŒberwachen und mit dem wir umgehen können. Wir nutzen PostgreSQL auch als Datenspeicher. Und es gibt nichts Schnelleres, um Daten in es hineinzuleiten als den Operator. COPY Im Moment noch nicht.

Aber einfach Daten „einzugießen“ ist nicht ganz unser Ansatz. Denn wenn auf hundert Servern ungefĂ€hr 50.000 Anfragen pro Sekunde stattfinden, dann erzeugt das tĂ€glich 100-150 GB Logs. Daher mussten wir die Datenbank sorgfĂ€ltig „zusammenstellen“.

ZunĂ€chst haben wir die Partitionierung nach Tagenvorgenommen, denn im Grunde genommen interessiert niemanden 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 ausgerollt hast – und bereits neue Statistiken vorliegen.

Zweitens haben wir gelernt (wir mussten), sehr, sehr schnell zu schreiben mit Hilfe von COPY. Das bedeutet nicht nur, COPY, weil es schneller ist als INSERT, sondern sogar noch schneller.

Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

Der dritte Punkt ist, dass wir auf Trigger und entsprechend auch auf Foreign Keys verzichten mussten.. Das heißt, wir haben ĂŒberhaupt keine referenzielle IntegritĂ€t mehr. Denn wenn Sie eine Tabelle haben, auf der es ein paar FKs gibt, und Sie in der Datenbankstruktur sagen, dass „der Datensatz aus dem Protokoll auf eine Gruppe von DatensĂ€tzen ĂŒber den FK verweist“, dann bleibt PostgreSQL nichts anderes ĂŒbrig, als einfach zu nehmen und ehrlich auszufĂŒhren, SELECT 1 FROM master_fk1_table WHERE ... mit der ID, die Sie einzufĂŒgen versuchen - einfach um zu ĂŒberprĂŒfen, dass dieser Datensatz dort vorhanden ist, damit Sie nicht mit Ihrem Insert diesen Foreign Key „brechen“.

Wir erhalten anstelle eines Datensatzes in der Zieltabelle und deren Indizes zusĂ€tzlich noch das Lesen aus allen Tabellen, auf die er verweist. Und das brauchen wir ĂŒberhaupt nicht – unsere Aufgabe ist es, so viel wie möglich und so schnell wie möglich mit minimaler Belastung zu speichern. Also, FK – ade!

Der nĂ€chste Punkt ist die Aggregation und Hashbildung. UrsprĂŒnglich hatten wir das in der Datenbank implementiert – es ist einfach praktisch, wenn ein Datensatz ankommt, dies direkt in einer Tabelle zu tun. Ein „Plus eins“ direkt im Trigger.Das ist gut, praktisch, aber auch problematisch – Sie fĂŒgen einen Datensatz ein, mĂŒssen aber gleichzeitig noch etwas aus einer anderen Tabelle lesen und schreiben. Zudem mĂŒssen Sie nicht nur lesen und schreiben, sondern das auch jedes Mal tun.

Stellen Sie sich vor, Sie haben eine Tabelle, in der Sie einfach die Anzahl der Anfragen zĂ€hlen, die ĂŒber einen bestimmten Host laufen: +1, +1, +1, ..., +1Das ist fĂŒr Sie im Grunde nicht notwendig – das Ganze kann man im Collector im Speicher summieren und die Daten einmalig in die Datenbank senden. +10.

Ja, im Falle von Problemen kann die logische IntegritĂ€t verloren gehen, aber das ist praktisch ein unrealistisches Szenario – denn Sie haben einen stabilen Server, eine Batterie im Controller, ein Transaktionsprotokoll, ein Protokoll im Dateisystem
 Insgesamt ist es das nicht wert. Die Leistungseinbußen, die Sie durch die Nutzung von Triggern / FK erleiden, sind den Aufwand nicht wert.

Das gleiche gilt fĂŒr das Hashing. Ein Anfrage kommt zu Ihnen, Sie berechnen in der Datenbank eine bestimmte ID, speichern sie und teilen sie dann allen mit. Das funktioniert gut, bis ein zweiter Wunsch kommt, um die gleiche ID zu speichern – dann gibt es eine Blockierung, und das ist problematisch. Deshalb sollten Sie, wenn möglich, die Generierung von IDs auf den Client auslagern (im VerhĂ€ltnis zur Datenbank).

FĂŒr uns war es ideal, MD5 vom Text – Anfrage, Plan, Template,
 zu verwenden. Wir berechnen es auf der Seite des Collectors und speichern dann bereits die fertige ID in der Datenbank. Die LĂ€nge von MD5 und die tĂ€gliche Partitionierung ermöglichen es uns, uns keine Sorgen ĂŒber mögliche Kollisionen zu machen.

Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

Um all das schnell zu speichern, mussten wir die Speicherprozedur selbst modifizieren.

Wie werden normalerweise Daten geschrieben? Wir haben einen Datensatz, den wir auf mehrere Tabellen aufteilen und dann COPY – zuerst in die erste, dann in die zweite, in die dritte
 Es ist unpraktisch, weil wir anscheinend einen Datenstrom in drei Schritten sequenziell schreiben. Unangenehm. Kann es schneller gemacht werden? Ja!

DafĂŒr genĂŒgt es, diese Ströme einfach parallel zueinander anzuordnen. So haben wir in separaten Strömen Fehler, Anfragen, Vorlagen, Sperren,
 und wir schreiben das alles parallel. DafĂŒr ist es ausreichend, einen COPY-Kanal fĂŒr jede einzelne Ziel-Tabelle stĂ€ndig offen zu halten..

Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

Das bedeutet, dass der Collector immer einen Stream hat,in den ich die benötigten Daten schreiben kann. Damit die Daten von der Datenbank erkannt werden, darf jedoch niemand in einer Sperre hĂ€ngen, wĂ€hrend diese Daten geschrieben werden. Der COPY-Vorgang muss in bestimmten Intervallen unterbrochen werden.FĂŒr uns hat sich ein Intervall von etwa 100 ms als die effizienteste Lösung ergeben – wir schließen und öffnen sofort wieder zur gleichen Tabelle. Und wenn ein Strom bei bestimmten Spitzen nicht ausreicht, dann machen wir ein Polling bis zu einem bestimmten Limit.

ZusĂ€tzlich haben wir festgestellt, dass fĂŒr dieses Lastprofil jede Aggregation, bei der EintrĂ€ge in Paketen gesammelt werden, problematisch ist. Das klassische Problem ist INSERT ... VALUES und dann 1000 EintrĂ€ge. Denn in diesem Moment entsteht ein Spitzenwert beim Schreiben auf das Medium, und alle anderen, die versuchen, etwas auf die Festplatte zu schreiben, mĂŒssen warten.

Um solche Anomalien zu vermeiden, aggregieren Sie einfach nichts, puffernd gar nicht. Und wenn es dennoch zu einer Pufferung auf die Festplatte kommt (zum GlĂŒck erlaubt das Stream API in Node.js, dies zu ermitteln) — verzögern Sie diese Verbindung. Sobald Sie das Signal erhalten, dass sie wieder frei ist — schreiben Sie den Inhalt aus der angesammelten Warteschlange hinein. Solange sie beschĂ€ftigt ist — nehmen Sie die nĂ€chste verfĂŒgbare aus dem Pool und schreiben Sie in sie.

Vor der Implementierung dieses Ansatzes hatten wir etwa 4K SchreibvorgĂ€nge, und auf diese Weise haben wir die Last um das Vierfache reduziert. Jetzt sind wir durch die neuen beobachtbaren Datenbanken noch einmal um das Sechsfache gewachsen — bis zu 100 MB/s. Und nun speichern wir die Logs der letzten 3 Monate mit einem Volumen von etwa 10-15 TB, in der Hoffnung, dass jeder Entwickler innerhalb von drei Monaten jedes Problem lösen kann.

Wir verstehen die Probleme

Aber alle diese Daten einfach zu sammeln — das ist gut, nĂŒtzlich und angemessen, aber zu wenig — man muss sie verstehen. Denn es sind Millionen verschiedener PlĂ€ne pro Tag.

Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

Aber Millionen sind nicht handhabbar, man muss zuerst "weniger" schaffen. Und zunÀchst muss entschieden werden, wie dieses "weniger" organisiert werden soll.

Wir haben drei SchlĂŒsselthemen identifiziert:

  • wer diese Anfrage wurde gesendet
    Das heißt, aus welcher Anwendung es gekommen ist: Web-Interface, Backend, Zahlungssystem oder etwas anderes.
  • wobei es ist passiert
    Auf welchem spezifischen Server. Denn wenn Sie mehrere Server fĂŒr eine Anwendung haben und plötzlich einer davon 'stottert' (weil die 'Festplatte beschĂ€digt' ist, 'Speicherfehler' aufgetreten ist oder ein anderes Problem vorliegt), mĂŒssen Sie genau den Server ansprechen.
  • als wo genau das Problem in der einen oder anderen Hinsicht aufgetreten ist

Um herauszufinden, 'wer' die Anfrage gesendet hat, verwenden wir ein internes Mittel – die Einrichtung einer Sessionsvariable: SET application_name = '{bl-host}:{bl-method}'; - wir speichern den Hostnamen der Business-Logik, von dem die Anfrage kommt, sowie den Namen der Methode oder Anwendung, die sie initiiert hat.

Nachdem wir den 'Absender' der Anfrage ĂŒbermittelt haben, mĂŒssen wir ihn im Log ausgeben – dafĂŒr konfigurieren wir die Variable log_line_prefix = ' %m [%p:%v] [%d] %r %a'. Wer interessiert ist, kann im Handbuch nachlesen, was das alles bedeutet. Das bedeutet, dass wir im Log sehen:

  • die Zeit
  • die Identifikatoren von Prozess und Transaktion
  • den Namen der Datenbank
  • den IP desjenigen, der diese Anfrage gesendet hat
  • und den Namen der Methode

Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

Es war uns schnell klar, dass es nicht besonders spannend ist, die Korrelation einer einzelnen Anfrage zwischen verschiedenen Servern zu betrachten. Es kommt nicht oft vor, dass eine Anwendung sowohl hier als auch dort die gleiche Fehlfunktion aufweist. Selbst wenn sie gleich ist – schauen Sie sich jeden dieser Server an.

Nun, der Schnitt „ein Server – ein Tag“ hatte sich fĂŒr jede Analyse als ausreichend erwiesen.

Der erste Analyseschnitt ist der bekannte „Template“ – eine verkĂŒrzte Darstellung des Plans, die von allen numerischen Werten bereinigt wurde. Der zweite Schnitt – Anwendung oder Methode, und der dritte – ist ein spezifischer Knoten im Plan, der uns Probleme bereitet hat.

Als wir von spezifischen Instanzen zu Templates ĂŒbergingen, erhielten wir sofort zwei Vorteile:

  • eine signifikante Reduktion der zu analysierenden Objekte
    Sie mĂŒssen das Problem nicht mehr anhand von Tausenden von Anfragen oder PlĂ€nen analysieren, sondern nur noch anhand von Dutzenden von Templates.
  • Zeitleiste
    Das heißt, durch die Zusammenfassung von „Fakten“ in einem bestimmten Kontext kann man deren Auftreten im Laufe des Tages darstellen. Hierbei wird deutlich, dass wenn ein bestimmtes Muster beispielsweise einmal pro Stunde auftritt, wĂ€hrend es eigentlich einmal tĂ€glich sein sollte, es an der Zeit ist, darĂŒber nachzudenken, was schiefgelaufen ist — wer es ausgelöst hat und warum, vielleicht sollte es hier gar nicht sein. Dies ist eine weitere nicht-numerische, rein visuelle Analyseweise.

Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

Die anderen Methoden basieren auf den Kennzahlen, die wir aus dem Plan ableiten: wie oft ein solches Muster aufgetreten ist, die Gesamt- und Durchschnittszeit, wie viele Daten vom Disk gelesen wurden und wie viele aus dem Speicher


Weil Sie beispielsweise auf die Analytics-Seite des Hosts kommen und sehen — da wird plötzlich zu viel vom Disk gelesen. Der Disk im Server kommt nicht mehr mit — aber wer liest von ihm?

Und Sie können nach jeder Spalte sortieren und entscheiden, mit was Sie sich jetzt direkt auseinandersetzen möchten — mit der CPU- oder Disk-Auslastung oder der Gesamtanzahl an Anfragen
 Sortiert, die „Top-FĂ€lle“ angeschaut, behoben — eine neue Version der Anwendung bereitgestellt.
[ĐČĐžĐŽĐ”ĐŸĐ»Đ”Đșцоя]

Und sofort sehen Sie verschiedene Anwendungen, die mit demselben Template aus einem Abfragetyp arbeiten. SELECT * FROM users WHERE login = 'Vasya'. Frontend, Backend, Processing
 Und man fragt sich, warum das Processing den Benutzer lesen sollte, wenn er nicht mit ihm interagiert.

Der umgekehrte Ansatz ist, dass man direkt sieht, was die Anwendung tut. Zum Beispiel, das Frontend – das, das, das hier, und noch zusĂ€tzlich das einmal pro Stunde (genau hier hilft die Timeline). Und sofort stellt sich die Frage – eigentlich ist es nicht die Aufgabe des Frontends, das einmal pro Stunde zu tun


Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

Nach einer Weile haben wir gemerkt, dass uns aggregierte Statistiken im Hinblick auf die Knoten des Plans fehlen.. Wir haben aus den PlĂ€nen nur die Knoten herausgefiltert, die etwas mit den Daten der Tabellen machen (lesen/schreiben, ob sie nach Index arbeiten 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 herausgefiltert wurden (Rows Removed by Filter).

Sie haben keinen passenden Index auf der Tabelle, Sie machen eine Abfrage, sie fliegt am Index vorbei und fĂ€llt in Seq Scan
 Sie haben alle DatensĂ€tze, außer einem, herausgefiltert. Warum benötigen Sie erschienen 100M herausgefilterte DatensĂ€tze pro Tag, wĂ€re es da nicht besser, einen Index zu erstellen?

Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

Nachdem wir alle PlĂ€ne fĂŒr die Knoten durchgesehen haben, haben wir festgestellt, dass es einige gĂ€ngige Strukturen in den PlĂ€nen gibt, die mit sehr großer Wahrscheinlichkeit verdĂ€chtig aussehen. Es wĂ€re hilfreich, dem Entwickler zu sagen: „Freund, hier liest du zuerst nach dem Index, dann sortierst du und schneidest danach“ – normalerweise gibt es dort nur einen Eintrag.

Alle, die solche Anfragen gestellt haben, sind sicher auf folgendes Muster gestoßen: „Gib mir die letzte Bestellung von Vase, sein Datum.“ Und wenn du keinen Index nach Datum hast oder im verwendeten Index kein Datum enthalten ist, dann wirst du genau auf solche ‚Hindernisse‘ stoßen.

Aber wir wissen doch, dass es sich um ‚Hindernisse‘ handelt – warum sollte man dem Entwickler nicht gleich sagen, was er tun sollte? Wenn unser Entwickler jetzt den Plan öffnet, sieht er sofort ein ĂŒbersichtliches Bild mit Hinweisen, die ihm sagen: „Hier und hier hast du Probleme, und sie werden so und so gelöst.“

Infolgedessen ist das Volumen der Erfahrung, die erforderlich war, um zu Beginn und jetzt Probleme zu lösen, drastisch gesunken. So ist unser Instrument entstanden.

Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

Quelle: habr.com

ZuverlĂ€ssiges Webhosting mit DDoS-Schutz, VPS- und VDS-Server kaufen đŸ”„ ZuverlĂ€ssiges Webhosting mit DDoS-Schutz, VPS- und VDS-Server kaufen | ProHoster