Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

Der Bericht stellt einige Ansätze vor, die dies ermöglichen Überwachen Sie die Leistung von SQL-Abfragen, wenn es Millionen davon pro Tag gibt, und es gibt Hunderte von überwachten PostgreSQL-Servern.

Welche technischen Lösungen ermöglichen es uns, eine solche Informationsmenge effizient zu verarbeiten, und wie erleichtert dies das Leben eines normalen Entwicklers?


Wer hat Interesse? Analyse spezifischer Probleme und verschiedene Optimierungstechniken SQL-Abfragen und das Lösen typischer DBA-Probleme in PostgreSQL – das können Sie auch eine Artikelserie lesen zu diesem Thema.

Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)
Mein Name ist Kirill Borovikov, ich vertrete Tensor-Unternehmen. Konkret habe ich mich auf die Arbeit mit Datenbanken in unserem Unternehmen spezialisiert.

Heute erzähle ich Ihnen, wie wir Abfragen optimieren, wenn Sie nicht die Leistung einer einzelnen Abfrage „zerlegen“, sondern das Problem in großen Mengen lösen müssen. Wenn es Millionen von Anfragen gibt und Sie welche finden müssen Lösungsansätze dieses große Problem.

Im Allgemeinen ist Tensor für eine Million unserer Kunden geeignet VLSI ist unsere Anwendung: soziales Unternehmensnetzwerk, Lösungen für die Videokommunikation, für den internen und externen Dokumentenfluss, Buchhaltungssysteme für Buchhaltung und Lager, ... Das heißt, so ein „Mega-Kombinat“ für die integrierte Unternehmensführung, in dem es mehr als 100 verschiedene gibt interne Projekte.

Um sicherzustellen, dass sie alle normal funktionieren und sich normal entwickeln, verfügen wir über zehn Entwicklungszentren im ganzen Land, darunter noch mehr 1000 Entwickler.

Wir arbeiten seit 2008 mit PostgreSQL und haben eine große Menge von dem, was wir verarbeiten – Kundendaten, statistische, analytische, Daten aus externen Informationssystemen – gesammelt. mehr als 400 TB. Allein in der Produktion sind etwa 250 Server im Einsatz, insgesamt gibt es etwa 1000 Datenbankserver, die wir überwachen.

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. Das DBMS weiß besser, wie man einen JOIN durchführt – wie man seine Tabellen verbindet, welche Bedingungen auferlegt werden müssen, was durch den Index geht und was nicht ...

Einige DBMS akzeptieren Hinweise: „Nein, verbinden Sie diese beiden Tabellen in dieser und jener Warteschlange“, aber PostgreSQL kann dies nicht. Dies ist die bewusste Position führender Entwickler: „Wir würden den Abfrageoptimierer lieber fertigstellen, als Entwicklern die Verwendung irgendeiner Art von Hinweisen zu ermöglichen.“

Aber trotz der Tatsache, dass PostgreSQL es dem „Außenstehenden“ nicht erlaubt, sich selbst zu kontrollieren, erlaubt es dies durchaus Sehen Sie, was in ihm vorgehtwann Sie Ihre Abfrage ausführen und wo Probleme auftreten.

Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

Mit welchen klassischen Problemen hat ein Entwickler [zu einem DBA] im Allgemeinen normalerweise zu kämpfen? „Hier sind wir der Bitte nachgekommen, und Bei uns ist alles langsam, alles hängt, etwas passiert ... Irgendein Ärger!“

Die Gründe sind fast immer die gleichen:

  • ineffizienter Abfragealgorithmus
    Entwickler: „Jetzt gebe ich ihm 10 Tabellen in SQL per JOIN …“ – und erwartet, dass seine Bedingungen auf wundersame Weise effektiv „aufgelöst“ werden und er alles schnell bekommt. Aber Wunder geschehen nicht, und jedes System mit einer solchen Variabilität (10 Tabellen in einem FROM) gibt immer einen Fehler aus. [Beitrag]
  • irrelevante Statistiken
    Dieser Punkt ist insbesondere für PostgreSQL sehr relevant, wenn Sie einen großen Datensatz auf den Server „gegossen“ haben, eine Anfrage stellen und Ihr Tablet „sexcanits“ wird. Denn gestern waren es 10 Datensätze und heute sind es 10 Millionen, aber PostgreSQL ist sich dessen noch nicht bewusst und wir müssen es darüber informieren. [Beitrag]
  • Ressourcen „einstecken“.
    Sie haben eine große und stark ausgelastete Datenbank auf einem schwachen Server installiert, der nicht über genügend Festplatten-, Speicher- oder Prozessorleistung verfügt. Und das ist alles... Irgendwo gibt es eine Leistungsgrenze, über die man nicht mehr springen kann.
  • blockieren
    Dies ist ein schwieriger Punkt, aber sie sind am relevantesten für verschiedene modifizierende Abfragen (INSERT, UPDATE, DELETE) – dies ist ein separates großes Thema.

Einen Plan bekommen

...Und für alles andere wir brauche einen Plan! Wir müssen sehen, was im Server passiert.

Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

Ein Abfrageausführungsplan für PostgreSQL ist ein Baum des Abfrageausführungsalgorithmus in Textdarstellung. Gerade der Algorithmus erwies sich in der Analyse des Planers als der effektivste.

Jeder Baumknoten ist eine Operation: Abrufen von Daten aus einer Tabelle oder einem Index, Erstellen einer Bitmap, Verbinden zweier Tabellen, Verbinden, Überschneiden oder Ausschließen von Auswahlen. Das Ausführen einer Abfrage erfordert das Durchlaufen der Knoten dieses Baums.

Um den Abfrageplan zu erhalten, ist es am einfachsten, die Anweisung auszuführen EXPLAIN. Um alle echten Attribute zu erhalten, d. h. um tatsächlich eine Abfrage auf der Basis auszuführen – EXPLAIN (ANALYZE, BUFFERS) SELECT ....

Das Schlimme daran: Wenn Sie es ausführen, geschieht es „hier und jetzt“, sodass es nur für das lokale Debuggen geeignet ist. Wenn Sie einen hoch ausgelasteten Server nehmen, der einem starken Datenfluss unterliegt, sehen Sie: „Oh! Hier haben wir eine langsame AusführungXia Anfrage." Vor einer halben Stunde, einer Stunde – während Sie diese Anfrage aus den Protokollen ausgeführt und zum Server zurückgebracht haben, haben sich Ihr gesamter Datensatz und Ihre Statistiken geändert. Sie führen es zum Debuggen aus – und es läuft schnell! Und Sie können nicht verstehen, warum, warum war langsam.

Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

Um zu verstehen, was genau in dem Moment passierte, als die Anfrage auf dem Server ausgeführt wurde, haben kluge Leute geschrieben auto_explain-Modul. 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 ausgeführt wird als das von Ihnen festgelegte Limit, wird dies der Fall sein „Schnappschuss“ des Plans dieser Anfrage und schreibt diese zusammen in das Protokoll.

Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

Jetzt scheint alles in Ordnung zu sein, wir gehen zum Protokoll und sehen dort ... [Text Fußtuch]. Aber wir können nichts dazu sagen, außer dass es ein ausgezeichneter Plan ist, da die Ausführung 11 ms gedauert hat.

Alles scheint in Ordnung zu sein – aber nichts ist klar, was tatsächlich passiert ist. Außer der allgemeinen Zeit sehen wir eigentlich nichts. Denn der Blick auf ein solches „Lamm“ aus Klartext ist in der Regel nicht visuell.

Aber auch wenn es nicht offensichtlich ist, auch wenn es unbequem ist, gibt es grundlegendere Probleme:

  • Der Knoten zeigt an Summe der Ressourcen des gesamten Teilbaums unter ihm. Das heißt, Sie können nicht einfach herausfinden, wie viel Zeit für diesen bestimmten Index-Scan aufgewendet wurde, wenn darunter eine verschachtelte Bedingung vorliegt. Wir müssen dynamisch nachsehen, ob darin „Kinder“ und bedingte Variablen, CTEs, vorhanden sind – und all dies „in unseren Gedanken“ subtrahieren.
  • Zweiter Punkt: Die auf dem Knoten angegebene Zeit ist Ausführungszeit eines einzelnen Knotens. Wenn dieser Knoten beispielsweise aufgrund einer mehrmaligen Schleife durch Tabellendatensätze ausgeführt wurde, erhöht sich die Anzahl der Schleifen – Zyklen dieses Knotens – im Plan. Aber die atomare Ausführungszeit selbst bleibt planmäßig gleich. Das heißt, um zu verstehen, wie lange dieser Knoten insgesamt ausgeführt wurde, müssen Sie eine Sache mit einer anderen multiplizieren – wiederum „in Ihrem Kopf“.

Verstehen Sie in solchen Situationen: „Wer ist das schwächste Glied?“ nahezu unmöglich. Deshalb schreiben das sogar die Entwickler selbst im „Handbuch“. „Einen Plan zu verstehen ist eine Kunst, die man lernen und erleben muss ...“.

Aber wir haben 1000 Entwickler und man kann diese Erfahrung nicht jedem von ihnen vermitteln. Ich, du, er weiß es, aber jemand da drüben weiß es nicht mehr. Vielleicht wird er es lernen, vielleicht auch nicht, aber er muss jetzt arbeiten – und woher soll er diese Erfahrung nehmen?

Planvisualisierung

Deshalb haben wir erkannt, dass wir zur Bewältigung dieser Probleme Folgendes benötigen gute Visualisierung des Plans. [Artikel]

Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

Wir sind zunächst „durch den Markt“ gegangen – schauen wir mal im Internet nach, was es überhaupt gibt.

Es stellte sich jedoch heraus, dass es nur sehr wenige relativ „lebende“ Lösungen gibt, die sich mehr oder weniger in der Entwicklung befinden – im wahrsten Sinne des Wortes nur eine: erklären.depesz.com von Hubert Lubaczewski. Wenn Sie im Feld „Feed“ eine Textdarstellung des Plans eingeben, wird Ihnen eine Tabelle mit den analysierten Daten angezeigt:

  • Die eigene Verarbeitungszeit des Knotens
  • Gesamtzeit für den gesamten Teilbaum
  • Anzahl der abgerufenen Datensätze, die statistisch erwartet wurden
  • der Knotenkörper selbst

Dieser Dienst bietet auch die Möglichkeit, ein Linkarchiv zu teilen. Da hast du deinen Plan hineingeworfen und gesagt: „Hey, Vasya, hier ist ein Link, da stimmt etwas nicht.“

Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

Es gibt aber auch kleine Probleme.

Erstens eine Menge „Kopieren und Einfügen“. Man nimmt ein Stück des Baumstamms, steckt es hinein und immer wieder.

Zweitens, die keine Analyse der gelesenen Datenmenge – die gleichen Puffer, die ausgegeben werden EXPLAIN (ANALYZE, BUFFERS), wir sehen es hier nicht. Er weiß einfach nicht, wie er sie auseinandernehmen, verstehen und mit ihnen arbeiten soll. Wenn Sie viele Daten lesen und feststellen, dass Sie möglicherweise den Festplatten- und Speichercache falsch zuordnen, ist diese Information sehr wichtig.

Der dritte negative Punkt ist die sehr schwache Entwicklung dieses Projekts. Die Commits sind sehr klein, am besten alle sechs Monate, und der Code ist in Perl.

Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

Aber das sind alles „Texte“, damit könnten wir irgendwie leben, aber es gibt eine Sache, die uns stark von diesem Dienst abgehalten hat. Hierbei handelt es sich um Fehler bei der Analyse von Common Table Expression (CTE) und verschiedenen dynamischen Knoten wie InitPlan/SubPlan.

Glaubt man diesem Bild, dann ist die Gesamtausführungszeit jedes einzelnen Knotens größer als die Gesamtausführungszeit der gesamten Anfrage. Es ist einfach - Die Generierungszeit dieses CTE wurde nicht vom CTE-Scan-Knoten abgezogen. Daher kennen wir die richtige Antwort auf die Frage, wie lange der CTE-Scan selbst gedauert hat, nicht mehr.

Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

Dann wurde uns klar, dass es an der Zeit war, unser eigenes zu schreiben – Hurra! Jeder Entwickler sagt: „Jetzt schreiben wir unsere eigenen, das wird ganz einfach!“

Wir haben einen für Webdienste typischen Stack verwendet: einen auf Node.js + Express basierenden Kern, Bootstrap und D3.js für schöne Diagramme. Und unsere Erwartungen haben sich voll und ganz erfüllt – wir haben den ersten Prototypen innerhalb von 2 Wochen erhalten:

  • Benutzerdefinierter Plan-Parser
    Das heißt, wir können jetzt jeden Plan aus den von PostgreSQL generierten Plänen analysieren.
  • korrekte Analyse dynamischer Knoten - CTE-Scan, InitPlan, SubPlan
  • Analyse der Pufferverteilung - wo Datenseiten aus dem Speicher gelesen werden, wo aus dem lokalen Cache, wo von der Festplatte
  • Klarheit bekommen
    Um das alles nicht im Protokoll zu „vergraben“, sondern um das „schwächste Glied“ gleich im Bild zu sehen.

Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

Wir haben so etwas erhalten, inklusive Syntaxhervorhebung. Doch in der Regel arbeiten unsere Entwickler nicht mehr mit einer vollständigen Darstellung des Plans, sondern mit einer kürzeren. Schließlich haben wir bereits alle Zahlen analysiert und nach links und rechts geworfen, und in der Mitte haben wir nur die erste Zeile gelassen, um welche Art von Knoten es sich handelt: CTE-Scan, CTE-Generierung oder Seq-Scan nach einem Vorzeichen.

Dies ist die abgekürzte Darstellung, die wir nennen Planvorlage.

Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

Was wäre sonst noch praktisch? Es wäre praktisch, zu sehen, welcher Anteil unserer Gesamtzeit welchem ​​Knoten zugewiesen ist – und ihn einfach beiseite zu legen Kuchendiagramm.

Wir zeigen auf den Knoten und sehen – es stellt sich heraus, dass der Seq-Scan weniger als ein Viertel der Gesamtzeit in Anspruch nahm und die restlichen drei Viertel vom CTE-Scan übernommen wurden. Grusel! Dies ist eine kleine Anmerkung zur „Feuerrate“ von CTE Scan, wenn Sie diese aktiv in Ihren Abfragen verwenden. Sie sind nicht sehr schnell – sie sind sogar dem normalen Tabellenscannen unterlegen. [Artikel] [Artikel]

Normalerweise sind solche Diagramme jedoch interessanter und komplexer, wenn wir sofort auf ein Segment zeigen und beispielsweise sehen, dass einige Seq Scans in mehr als der Hälfte der Fälle „gefressen“ haben. Darüber hinaus befand sich darin eine Art Filter, dem zufolge viele Datensätze verworfen wurden... Sie können dieses Bild direkt dem Entwickler zuwerfen und sagen: „Vasya, hier ist alles schlecht für dich!“ Finden Sie es heraus, schauen Sie – etwas stimmt nicht!“

Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

Natürlich waren einige „Rechen“ im Spiel.

Das erste, was uns auffiel, war das Rundungsproblem. Die Zeit jedes einzelnen Knotens im Plan wird mit einer Genauigkeit von 1 μs angegeben. Und wenn die Anzahl der Knotenzyklen beispielsweise 1000 überschreitet – nach der Ausführung von PostgreSQL geteilt „innerhalb der Genauigkeit“, dann erhalten wir bei der Rückrechnung die Gesamtzeit „irgendwo zwischen 0.95 ms und 1.05 ms“. Wenn die Zählung auf Mikrosekunden geht, ist das in Ordnung, aber wenn es bereits [Milli]sekunden sind, müssen Sie diese Informationen berücksichtigen, wenn Sie Ressourcen an die Knoten des Plans „Wer hat wie viel verbraucht“ „aufheben“.

Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

Der zweite, komplexere Punkt ist die Verteilung der Ressourcen (dieser Puffer) auf dynamische Knoten. Das hat uns die ersten 2 Wochen des Prototyps plus weitere 4 Wochen gekostet.

Es ist ziemlich einfach, ein solches Problem zu bekommen – wir machen einen CTE und lesen angeblich etwas darin. Tatsächlich ist PostgreSQL „intelligent“ und liest dort nichts direkt. Dann nehmen wir den ersten Datensatz daraus und dazu den einhundertsten aus demselben CTE.

Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

Wir schauen uns den Plan an und verstehen – es ist seltsam, wir haben 3 Puffer (Datenseiten) im Seq Scan „verbraucht“, 1 weiteren im CTE Scan und 2 weitere im zweiten CTE Scan. Das heißt, wenn wir einfach alles zusammenfassen, erhalten wir 6, aber auf der Tafel lesen wir nur 3! CTE Scan liest nichts von irgendwoher, sondern arbeitet direkt mit dem Prozessspeicher. Das heißt, hier stimmt eindeutig etwas nicht!

Tatsächlich stellt sich heraus, dass hier alle 3 Seiten mit Daten sind, die von Seq Scan angefordert wurden, zuerst 1 für den 1. CTE-Scan und dann für den 2., und ihm wurden noch 2 weitere vorgelesen. Das heißt, insgesamt 3 Seiten waren gelesene Daten, nicht 6.

Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

Und dieses Bild führte uns zu der Erkenntnis, dass die Ausführung eines Plans kein Baum mehr ist, sondern einfach eine Art azyklischer Graph. Und wir haben ein Diagramm wie dieses erhalten, damit wir verstehen, „was überhaupt woher kam“. Das heißt, hier haben wir einen CTE aus pg_class erstellt und zweimal danach gefragt, und als wir das zweite Mal danach gefragt haben, haben wir fast unsere gesamte Zeit in der Verzweigung verbracht. Es ist klar, dass das Lesen des 2. Eintrags viel teurer ist, als nur das Lesen des 101. Eintrags vom Tablet.

Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

Wir atmeten eine Weile aus. Sie sagten: „Nun, Neo, du kennst Kung Fu! Jetzt ist unser Erlebnis direkt auf Ihrem Bildschirm. Jetzt können Sie es verwenden. [Artikel]

Protokollkonsolidierung

Unsere 1000 Entwickler atmeten erleichtert auf. Aber wir haben verstanden, dass wir nur Hunderte von „Kampf“-Servern haben und dieses ganze „Kopieren und Einfügen“ seitens der Entwickler überhaupt nicht praktisch ist. Uns wurde klar, dass wir es selbst abholen mussten.

Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

Im Allgemeinen gibt es ein Standardmodul, das Statistiken sammeln kann, es muss jedoch auch in der Konfiguration aktiviert werden – dieses Modul pg_stat_statements. Aber er passte nicht zu uns.

Erstens werden dieselben Abfragen mithilfe unterschiedlicher Schemata innerhalb derselben Datenbank zugewiesen verschiedene QueryIds. Das heißt, wenn Sie es zuerst tun SET search_path = '01'; SELECT * FROM user LIMIT 1;, und dann SET search_path = '02'; und die gleiche Anfrage, dann werden die Statistiken dieses Moduls unterschiedliche Datensätze haben und ich kann keine allgemeinen Statistiken speziell im Kontext dieses Anfrageprofils sammeln, ohne die Schemata zu berücksichtigen.

Der zweite Punkt, der uns daran gehindert hat, es zu verwenden, ist Mangel an Plänen. Das heißt, es gibt keinen Plan, es gibt nur die Anfrage selbst. Wir sehen, was sich verlangsamte, verstehen aber nicht, warum. Und hier kehren wir zum Problem eines sich schnell ändernden Datensatzes zurück.

Und der letzte Moment - Mangel an „Fakten“. Das heißt, Sie können nicht auf eine bestimmte Instanz der Abfrageausführung eingehen – es gibt keine, es gibt nur aggregierte Statistiken. Obwohl es möglich ist, damit zu arbeiten, ist es nur sehr schwierig.

Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

Deshalb entschieden wir uns gegen Copy-Paste und begannen mit dem Schreiben Sammler.

Der Collector verbindet sich über SSH, baut über ein Zertifikat eine sichere Verbindung zum Server mit der Datenbank auf und tail -F „hängt“ daran in der Protokolldatei fest. Also in dieser Sitzung Wir erhalten einen vollständigen „Spiegel“ der gesamten Protokolldatei, die der Server generiert. Die Belastung des Servers selbst ist minimal, da wir dort nichts analysieren, sondern lediglich den Datenverkehr spiegeln.

Da wir bereits mit dem Schreiben der Schnittstelle in Node.js begonnen hatten, fuhren wir damit fort, den Kollektor darin zu schreiben. Und diese Technologie hat sich bewährt, denn es ist sehr praktisch, JavaScript zu verwenden, um mit schwach formatierten Textdaten, also dem Protokoll, zu arbeiten. Und die Node.js-Infrastruktur selbst als Backend-Plattform ermöglicht Ihnen ein einfaches und komfortables Arbeiten mit Netzwerkverbindungen, und zwar mit beliebigen Datenströmen.

Dementsprechend „dehnen“ wir zwei Verbindungen: die erste, um das Protokoll selbst zu „hören“ und zu uns zu nehmen, und die zweite, um die Basis regelmäßig zu befragen. „Aber das Protokoll zeigt, dass das Schild mit OID 123 blockiert ist“, aber das bedeutet für den Entwickler nichts, und es wäre schön, die Datenbank zu fragen: „Was ist überhaupt OID = 123?“ Und so fragen wir die Basis regelmäßig, was wir noch nicht über uns wissen.

Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

„Nur eines haben Sie nicht berücksichtigt, es gibt eine Art elefantenähnlicher Bienen! …“ Wir haben mit der Entwicklung dieses Systems begonnen, als wir 10 Server überwachen wollten. Die nach unserem Verständnis kritischste Situation, bei der einige Probleme auftraten, die schwer zu bewältigen waren. Aber im ersten Quartal haben wir hundert für die Überwachung bekommen – weil das System funktionierte, jeder es wollte, jeder sich wohl fühlte.

All dies muss addiert werden, der Datenfluss ist groß und aktiv. Tatsächlich nutzen wir das, was wir überwachen und womit wir umgehen können. Als Datenspeicher nutzen wir auch PostgreSQL. Und nichts lässt sich schneller mit Daten „einfüllen“ als der Bediener COPY Noch nicht.

Aber das bloße „Ausschütten“ von Daten ist nicht wirklich unsere Technologie. Denn wenn Sie etwa 50 Anfragen pro Sekunde auf hundert Servern haben, werden 100–150 GB Protokolle pro Tag generiert. Daher mussten wir die Basis sorgfältig „abschneiden“.

Erstens haben wir es getan Aufteilung nach Tag, denn im Großen und Ganzen interessiert sich niemand für die Korrelation zwischen Tagen. Welchen Unterschied macht es zu dem, was Sie gestern hatten, wenn Sie heute Abend eine neue Version der Anwendung herausbringen – und bereits einige neue Statistiken.

Zweitens haben wir gelernt (wurden gezwungen) sehr, sehr schnell zu schreiben COPY. Das heißt, nicht nur COPYweil er schneller ist als INSERT, und noch schneller.

Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

Der dritte Punkt – ich musste Abandon-Trigger bzw. Fremdschlüssel. Das heißt, wir haben überhaupt keine referenzielle Integrität. Denn wenn Sie eine Tabelle mit einem FK-Paar haben und in der Datenbankstruktur sagen: „Hier ist ein Protokolldatensatz, der von FK beispielsweise auf eine Gruppe von Datensätzen verwiesen wird“, dann wird beim Einfügen PostgreSQL verwendet Bleibt mir nichts anderes übrig, als es anzunehmen und es ehrlich zu tun SELECT 1 FROM master_fk1_table WHERE ... mit der Kennung, die Sie einfügen möchten – nur um zu überprüfen, ob dieser Datensatz dort vorhanden ist und Sie diesen Fremdschlüssel beim Einfügen nicht „abbrechen“.

Anstelle eines Datensatzes für die Zieltabelle und ihre Indizes erhalten wir den zusätzlichen Vorteil, dass wir aus allen Tabellen lesen können, auf die sie verweist. Das brauchen wir aber überhaupt nicht – unsere Aufgabe ist es, möglichst viel und so schnell wie möglich mit der geringsten Belastung aufzunehmen. Also FK – runter!

Der nächste Punkt ist Aggregation und Hashing. Zunächst haben wir sie in der Datenbank implementiert – schließlich ist es praktisch, dies sofort, wenn ein Datensatz eintrifft, auf einem Tablet zu erledigen „plus eins“ direkt im Abzug. Nun, es ist praktisch, aber das gleiche Schlechte: Sie fügen einen Datensatz ein, sind aber gezwungen, etwas anderes aus einer anderen Tabelle zu lesen und zu schreiben. Darüber hinaus lesen und schreiben Sie nicht nur, Sie tun es auch jedes Mal.

Stellen Sie sich nun vor, Sie hätten eine Tabelle, in der Sie einfach die Anzahl der Anfragen zählen, die einen bestimmten Host durchlaufen haben: +1, +1, +1, ..., +1. Und das brauchen Sie im Prinzip nicht – es ist alles möglich Summe im Speicher des Kollektors und in einem Rutsch an die Datenbank senden +10.

Ja, bei einigen Problemen kann Ihre logische Integrität „auseinanderfallen“, aber das ist ein fast unrealistischer Fall – denn Sie haben einen normalen Server, dieser hat eine Batterie im Controller, Sie haben ein Transaktionsprotokoll, ein Protokoll auf dem Dateisystem... Im Allgemeinen lohnt es sich nicht. Der Produktivitätsverlust, den Sie durch die Ausführung von Triggern/FK erleiden, ist die Kosten, die Ihnen entstehen, nicht wert.

Beim Hashing ist es genauso. Eine bestimmte Anfrage fliegt zu Ihnen, Sie berechnen daraus eine bestimmte Kennung in der Datenbank, schreiben sie in die Datenbank und teilen sie dann allen mit. Alles ist in Ordnung, bis zum Zeitpunkt der Aufnahme eine zweite Person zu Ihnen kommt, die dasselbe aufnehmen möchte – und Sie werden blockiert, und das ist schon schlimm. Wenn Sie daher die Generierung einiger IDs auf den Client (relativ zur Datenbank) übertragen können, ist es besser, dies zu tun.

Für uns war es einfach perfekt, MD5 aus dem Text zu verwenden – Anfrage, Plan, Vorlage, … Wir berechnen es auf der Kollektorseite und „gießen“ die vorgefertigte ID in die 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 dies jedoch schnell aufzeichnen zu können, mussten wir den Aufnahmevorgang selbst ändern.

Wie schreibt man normalerweise Daten? Wir haben eine Art Datensatz, teilen ihn in mehrere Tabellen auf und kopieren ihn dann – zuerst in die erste, dann in die zweite, in die dritte ... Das ist unpraktisch, weil wir scheinbar einen Datenstrom in drei Schritten schreiben der Reihe nach. Unangenehm. Geht es schneller? Dürfen!

Dazu reicht es aus, diese Flüsse parallel zueinander zu zerlegen. Es stellt sich heraus, dass bei uns Fehler, Anfragen, Vorlagen, Blockierungen usw. in separaten Threads herumschwirren – und wir schreiben alles parallel. Genug dafür Halten Sie für jede einzelne Zieltabelle ständig einen COPY-Kanal offen.

Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

Das heißt, beim Sammler Es gibt immer einen Stream, in die ich die benötigten Daten schreiben kann. Aber damit die Datenbank diese Daten sieht und nicht jemand darauf warten muss, dass diese Daten geschrieben werden, COPY muss in bestimmten Abständen unterbrochen werden. Für uns war der effektivste Zeitraum etwa 100 ms – wir schließen ihn und öffnen ihn sofort wieder für dieselbe Tabelle. Und wenn wir bei einigen Spitzen nicht genug von einem Fluss haben, dann bündeln wir ihn bis zu einer bestimmten Grenze.

Darüber hinaus haben wir herausgefunden, dass bei einem solchen Lastprofil jede Aggregation, bei der Datensätze stapelweise gesammelt werden, schlecht ist. Das klassische Böse ist INSERT ... VALUES und weitere 1000 Datensätze. Denn zu diesem Zeitpunkt haben Sie eine Schreibspitze auf dem Medium und alle anderen, die versuchen, etwas auf die Festplatte zu schreiben, werden warten.

Um solche Anomalien zu beseitigen, aggregieren Sie einfach nichts. Überhaupt nicht puffern. Und wenn es tatsächlich zu einer Pufferung auf der Festplatte kommt (glücklicherweise können Sie dies mit der Stream-API in Node.js herausfinden), verschieben Sie diese Verbindung. Wenn Sie ein Ereignis erhalten, dass es wieder frei ist, schreiben Sie es aus der angesammelten Warteschlange. Und während es beschäftigt ist, nehmen Sie das nächste freie aus dem Pool und schreiben Sie darauf.

Bevor wir diesen Ansatz zur Datenaufzeichnung einführten, hatten wir etwa 4K-Schreibvorgänge und konnten so die Last um das Vierfache reduzieren. Jetzt sind sie aufgrund neuer überwachter Datenbanken um das Sechsfache gewachsen – bis zu 4 MB/s. Und jetzt speichern wir die Protokolle der letzten drei Monate in einem Volumen von etwa 6 bis 100 TB, in der Hoffnung, dass jeder Entwickler in nur drei Monaten jedes Problem lösen kann.

Wir verstehen die Probleme

Aber das bloße Sammeln all dieser Daten ist gut, nützlich und relevant, reicht aber nicht aus – sie müssen verstanden werden. Denn das sind Millionen verschiedener Pläne pro Tag.

Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

Aber Millionen sind unüberschaubar, wir müssen erst einmal „kleiner“ machen. Und zunächst müssen Sie entscheiden, wie Sie diese „kleinere“ Sache organisieren.

Wir haben drei Kernpunkte identifiziert:

  • welche habe diese Anfrage gesendet
    Das heißt, von welcher Anwendung ist es „angekommen“: Webschnittstelle, Backend, Zahlungssystem oder etwas anderes.
  • wo es passierte
    Auf welchem ​​spezifischen Server? Denn wenn Sie mehrere Server unter einer Anwendung haben und plötzlich einer „dumm“ wird (weil die „Festplatte kaputt ist“, „Speicher verloren geht“ oder ein anderes Problem), dann müssen Sie den Server gezielt ansprechen.
  • als Das Problem hat sich auf die eine oder andere Weise manifestiert

Um zu verstehen, „wer“ uns eine Anfrage gesendet hat, verwenden wir ein Standardtool – das Setzen einer Sitzungsvariablen: SET application_name = '{bl-host}:{bl-method}'; – Wir senden den Namen des Geschäftslogik-Hosts, von dem die Anfrage kommt, und den Namen der Methode oder Anwendung, die sie initiiert hat.

Nachdem wir den „Eigentümer“ der Anfrage übergeben haben, muss diese im Protokoll ausgegeben werden – dazu konfigurieren wir die Variable log_line_prefix = ' %m [%p:%v] [%d] %r %a'. Für Interessierte vielleicht schau mal ins Handbuchwas soll das alles heißen. Es stellt sich heraus, dass wir im Protokoll sehen:

  • Zeit
  • Prozess- und Transaktionskennungen
  • Name der Datenbank
  • IP der Person, die diese Anfrage gesendet hat
  • und Methodenname

Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

Dann wurde uns klar, dass es nicht sehr interessant ist, die Korrelation für eine Anfrage zwischen verschiedenen Servern zu betrachten. Es kommt nicht oft vor, dass eine Anwendung hier und da gleichermaßen fehlschlägt. Aber selbst wenn es dasselbe ist, schauen Sie sich einen dieser Server an.

Hier also der Schnitt „Ein Server – ein Tag“ Es stellte sich heraus, dass es für uns für jede Analyse ausreichend war.

Der erste analytische Abschnitt ist derselbe "Probe" - eine verkürzte Form der Darstellung des Plans, bereinigt um alle numerischen Indikatoren. Der zweite Schnitt ist die Anwendung oder Methode, und der dritte Schnitt ist der spezifische Planknoten, der uns Probleme bereitet hat.

Als wir von bestimmten Instanzen zu Vorlagen übergingen, hatten wir gleich zwei Vorteile:

  • mehrfache Reduzierung der Anzahl der zu analysierenden Objekte
    Wir müssen das Problem nicht mehr anhand Tausender Abfragen oder Pläne analysieren, sondern anhand Dutzender Vorlagen.
  • Zeitleiste
    Das heißt, indem Sie die „Fakten“ innerhalb eines bestimmten Abschnitts zusammenfassen, können Sie deren Erscheinungsbild im Laufe des Tages anzeigen. Und hier können Sie verstehen, dass Sie, wenn Sie ein Muster haben, das zum Beispiel einmal pro Stunde auftritt, aber einmal am Tag passieren sollte, darüber nachdenken sollten, was schief gelaufen ist – wer es verursacht hat und warum, vielleicht sollte es hier sein sollte nicht. Dies ist eine weitere nicht-numerische, rein visuelle Analysemethode.

Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

Die übrigen Methoden basieren auf den Indikatoren, die wir aus dem Plan extrahieren: wie oft ein solches Muster aufgetreten ist, die Gesamt- und Durchschnittszeit, wie viele Daten wurden von der Festplatte gelesen und wie viele aus dem Speicher ...

Wenn Sie beispielsweise auf die Analyseseite für den Host gelangen, sehen Sie, dass etwas zu viel auf der Festplatte liest. Die Festplatte auf dem Server kann damit nicht umgehen – wer liest davon?

Und Sie können nach jeder Spalte sortieren und entscheiden, womit Sie sich gerade befassen möchten – die Auslastung des Prozessors oder der Festplatte oder die Gesamtzahl der Anfragen ... Wir haben es sortiert, uns die „obersten“ angeschaut, es behoben und eine neue Version der Anwendung eingeführt.
[Videovortrag]

Und sofort können Sie bei einer Anfrage verschiedene Anwendungen sehen, die mit derselben Vorlage geliefert werden SELECT * FROM users WHERE login = 'Vasya'. Frontend, Backend, Verarbeitung ... Und Sie fragen sich, warum die Verarbeitung den Benutzer lesen würde, wenn er nicht mit ihm interagiert.

Der umgekehrte Weg besteht darin, in der Anwendung sofort zu sehen, was sie tut. Das Frontend ist zum Beispiel dies, dies, dies und dies einmal pro Stunde (die Zeitleiste hilft). Und sofort stellt sich die Frage: Es scheint, als wäre es nicht die Aufgabe des Frontends, einmal in der Stunde etwas zu tun ...

Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

Nach einiger Zeit stellten wir fest, dass es uns an Aggregation mangelte Statistiken nach Planknoten. Wir haben aus den Plänen nur diejenigen Knoten isoliert, die etwas mit den Daten der Tabellen selbst machen (sie nach Index lesen/schreiben oder nicht). Tatsächlich kommt gegenüber dem vorherigen Bild nur ein Aspekt hinzu – Wie viele Datensätze hat uns dieser Knoten gebracht?und wie viele wurden verworfen (Vom Filter entfernte Zeilen).

Sie haben keinen passenden Index auf der Platte, Sie stellen eine Anfrage an ihn, er fliegt am Index vorbei, fällt in Seq Scan ... Sie haben alle Datensätze bis auf einen herausgefiltert. Warum benötigen Sie 100 Millionen gefilterte Datensätze pro Tag? Ist es nicht besser, den Index hochzurollen?

Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

Nachdem wir alle Pläne Knoten für Knoten analysiert hatten, stellten wir fest, dass es einige typische Strukturen in den Plänen gibt, die sehr wahrscheinlich verdächtig aussehen. Und es wäre schön, dem Entwickler zu sagen: „Freund, hier liest du zuerst nach Index, dann sortierst du und schneidest dann ab“ – in der Regel gibt es einen Datensatz.

Jeder, der Abfragen geschrieben hat, ist wahrscheinlich auf dieses Muster gestoßen: „Gib mir die letzte Bestellung für Vasya, ihr Datum.“ Und wenn Sie keinen Index nach Datum haben oder der von Ihnen verwendete Index kein Datum enthält, dann werden Sie es tun Treten Sie auf genau denselben „Rechen“.

Aber wir wissen, dass es sich hierbei um einen „Rake“ handelt – warum also dem Entwickler nicht sofort sagen, was er tun soll? Dementsprechend sieht unser Entwickler beim Öffnen eines Plans sofort ein schönes Bild mit Tipps, wo er ihm sofort sagt: „Du hast hier und da Probleme, aber die werden so und so gelöst.“

Dadurch ist der Erfahrungsschatz, der zu Beginn und heute zur Lösung von Problemen nötig war, deutlich gesunken. Das ist die Art von Werkzeug, die wir haben.

Massenoptimierung von PostgreSQL-Abfragen. Kirill Borovikov (Tensor)

Source: habr.com

Kommentar hinzufügen