
Intro
Ich möchte Sie an einige einleitende Anmerkungen erinnern:
- Wir bauen einen Dienst auf, der Informationen aus den Protokollen von PostgreSQL-Servern empfängt
- Wenn wir Protokolle sammeln, möchten wir online etwas damit anfangen (analysieren, analysieren, zusätzliche Informationen anfordern)
- Alles, was gesammelt und "analysiert" wird, muss irgendwo gespeichert werden
Lassen Sie uns über den letzten Punkt sprechen - wie all dies an den PostgreSQL-Speicher geliefert werden kann . In unserem Fall handelt es sich bei diesen Daten um ein Vielfaches der ursprünglichen Laststatistik im Kontext einer bestimmten Anwendungs- und Planvorlage, des Ressourcenverbrauchs und der Berechnung abgeleiteter Probleme , die auf einen einzelnen Plan-Knoten genau sind, der Überwachung von Sperren und vielem mehr.
Weitere Informationen zu den Prinzipien des Dienstes finden Sie im Videobericht und im Artikel "Massenoptimierung von PostgreSQL-Abfragen" .
Push vs Pull
Es gibt zwei Hauptmodelle zum Abrufen von Protokollen oder anderen ständig eintreffenden Metriken:
- Push - Auf dem Dienst befinden sich viele Peer-to-Peer-Empfänger auf den überwachten Servern. Einige lokale Agenten geben die gesammelten Informationen regelmäßig in den Dienst ein
- Pull- On-Service: Jeder Prozess / Thread / Coroutine / ... verarbeitet Informationen aus nur einer "eigenen" Quelle , deren Datenempfang von selbst initiiert wird
Jedes dieser Modelle hat positive und negative Seiten.
drücken
Die Interaktion wird vom beobachteten Knoten initiiert:

... ist von Vorteil, wenn:
- Sie haben viele Quellen (Hunderttausende)
- Die Belastung unterscheidet sich kaum voneinander und überschreitet ~ 1 U / min nicht
- Eine komplizierte Verarbeitung ist nicht erforderlich
Beispiel: Der Empfänger des OFD-Betreibers erhält Schecks von jeder Kundenkasse.
... verursacht Probleme:
- Sperren / Deadlocks beim Versuch, Wörterbücher / Analysen / Aggregate im Kontext des Überwachungsobjekts aus verschiedenen Streams zu schreiben
- Die schlechteste Auslastung des Caches jedes BL-Prozesses / jeder Verbindung zur Datenbank - zum Beispiel muss dieselbe Verbindung zur Datenbank zuerst in eine Tabelle oder ein Indexsegment und sofort in eine andere geschrieben werden
- Für jede Quelle muss ein spezieller Agent eingesetzt werden , der die Belastung erhöht
- Hoher Overhead bei der Netzwerkinteraktion - Header müssen das Senden jedes Pakets und nicht die gesamte Verbindung zur Quelle als Ganzes "binden"
ziehen
Der Initiator ist ein bestimmter Host / Prozess / Thread des Kollektors, der den Knoten an sich selbst "bindet" und unabhängig Daten aus dem "Ziel" extrahiert:

... ist von Vorteil, wenn:
- Sie haben nur wenige Quellen (Hunderttausende)
- Es gibt fast immer eine Last von ihnen, manchmal erreicht es 1Krps
- erfordert eine komplexe Verarbeitung mit Segmentierung nach Quelle
Beispiel: Lader / Analysator von Trades im Kontext jeder Handelsplattform.
... verursacht Probleme:
- Beschränkung der Ressourcen für die Verarbeitung einer Quelle durch einen Prozess (CPU-Kern), da diese nicht auf zwei Empfänger "verschmiert" werden kann
- Es wird ein Koordinator benötigt , der die Last von Quellen dynamisch auf vorhandene Prozesse / Threads / Ressourcen verteilt
Da unser Lastmodell für die PostgreSQL-Überwachung eindeutig auf den Pull-Algorithmus ausgerichtet ist und die Ressourcen eines Prozesses und des Kerns einer modernen CPU für eine Quelle ausreichen, haben wir uns darauf festgelegt.
Pull-Pull-Protokolle
Unsere Kommunikation mit dem Server ermöglichte sehr viele Netzwerkoperationen und die Arbeit mit slaboformatirovannymi-Textzeichenfolgen , sodass JavaScript als Sammlerkern in seiner Inkarnation als Server Node.js perfekt war .
Die einfachste Lösung zum Abrufen von Daten aus dem Serverprotokoll bestand darin, die gesamte Protokolldatei mit einem einfachen Linux-Befehl in die Konsole zu "spiegeln"
tail -F <current.log>. Nur unsere Konsole ist nicht einfach, sondern virtuell - innerhalb einer sicheren Verbindung zum Server, die sich über das SSH-Protokoll erstreckt .

Daher erhält der Kollektor auf der zweiten Seite der SSH-Verbindung eine vollständige Kopie des gesamten Protokollverkehrs als Eingabe. Bei Bedarf werden die Server um erweiterte Systeminformationen zum aktuellen Stand der Dinge gebeten.
Warum nicht Syslog?
Es gibt zwei Hauptgründe:
syslogarbeitet mit einem Push-Modell, so dass es unmöglich ist, die Verarbeitungslast des von ihm am Empfangspunkt erzeugten Streams schnell zu verwalten . Das heißt, wenn ein Hostpaar plötzlich anfing, Tausende von Plänen langsamer Anforderungen zu "gießen", ist es äußerst schwierig, ihre Verarbeitung auf verschiedene Knoten zu verteilen.
Die Verarbeitung bedeutet hier weniger "dummes" Empfangen / Parsen des Protokolls als vielmehr das Parsen von Plänen und das Berechnen der tatsächlichen Ressourcenintensität jedes der Knoten .- PostgreSQL, , «» (relation/page/tuple/...).
«DBA: ».
-
Grundsätzlich könnten andere Lösungen als DBMS zum Speichern von aus dem Protokoll analysierten Daten verwendet werden, aber das Volumen eingehender Informationen von 150 bis 200 GB / Tag lässt nicht zu viel Handlungsspielraum. Daher haben wir auch PostgreSQL als Speicher gewählt.
- PostgreSQL zum Speichern von Protokollen? Ernsthaft?
- Erstens gibt es bei weitem nicht nur weniger Protokolle als verschiedene analytische Darstellungen . Zweitens: "Sie wissen einfach nicht, wie man sie kocht!" :) :)

Server Einstellungen
Dieser Punkt ist subjektiv und hängt stark von Ihrer Hardware ab. Wir haben jedoch die folgenden Grundsätze für die Konfiguration des PostgreSQL-Hosts für die aktive Aufzeichnung festgelegt.
Dateisystemeinstellungen
Der wichtigste Faktor für die Schreibleistung ist das [nicht] korrekte Mounten der Datenpartition. Wir haben folgende Regeln gewählt:
- Das PGDATA- Verzeichnis wird (im Fall von ext4) mit Parametern gemountet
noatime,nodiratime,barrier=0,errors=remount-ro,data=writeback,nobh - Das Verzeichnis PGDATA / pg_stat_tmp wird verschoben
tmpfs - Das Verzeichnis PGDATA / pg_wal wird auf ein anderes Medium verschoben , wenn dies sinnvoll ist
Siehe PostgreSQL-Dateisystemoptimierung
Auswählen des optimalen E / A-Schedulers
Standardmäßig haben viele Distributionen den
cfqfür die "Desktop" -Verwendung geschärften E / A-Scheduler in RedHat und CentOS ausgewählt noop. Aber es hat sich für uns als nützlicher herausgestellt deadline.
siehe PostgreSQL vs. E / A-Scheduler (cfq, noop und Deadline)
Reduzieren der Größe des "verschmutzten" Caches
Dieser Parameter
vm.dirty_background_byteslegt die Größe des Caches in Byte fest, sobald das System den Hintergrundprozess zum Leeren auf die Festplatte startet. Es gibt einen ähnlichen, sich jedoch gegenseitig ausschließenden Parameter vm.dirty_background_ratio- er legt den gleichen Wert wie einen Prozentsatz der gesamten Speichergröße fest - standardmäßig ist er festgelegt und nicht "... Bytes".
Bei den meisten Distributionen sind es 10%, bei CentOS 5%. Dies bedeutet, dass das System bei einem Gesamtserverspeicher von 16 GB möglicherweise versucht , mehr als 850 MB einmal auf die Festplatte zu schreiben, was zu einer maximalen IOps-Last führt.
Wir verringern es experimentell, bis sich die Aufzeichnungsspitzen zu glätten beginnen. Um Spitzen zu vermeiden, sollte die Größe erfahrungsgemäß geringer sein als der maximale Mediendurchsatz (in IOps) mal der Speicherseitengröße. Dies ist beispielsweise für 7K-IOps (~ 7000 x 4096) - etwa 28 MB.
Siehe Konfigurieren der Linux- Kerneloptionen für die PostgreSQL-Optimierungseinstellungen
in postgresql.conf
Welche Parameter gesehen werden sollten, verdreht, um die Aufnahme zu beschleunigen. Alles hier ist rein individuell, daher werde ich nur einige Gedanken zum Thema machen:
shared_buffers- Es sollte verkleinert werden, da bei gezielter Erfassung besonders überlappender "gemeinsamer" Daten keine Prozesse entstehensynchronous_commit = off- Sie können das Warten auf das Festschreiben jederzeit deaktivieren, wenn Sie dem Akku Ihres RAID-Controllers vertrauenfsync- Wenn die Daten überhaupt nicht kritisch sind, können Sie versuchen, sie auszuschalten - "im Limit" können Sie sogar In-Memory-DB erhalten
Datenbanktabellenstruktur
Ich habe bereits einige Artikel zur Optimierung der physischen Datenspeicherung veröffentlicht:
- über Partitionierungstabellen - "Wir schreiben in PostgreSQL auf einem Sublight: 1 Host, 1 Tag, 1 TB"
- über die TOAST-Einstellung - "Sparen Sie einen schönen Cent bei großen Volumes in PostgreSQL"
Aber über verschiedene Schlüssel in den Daten - gab es noch nicht. Ich werde dir davon erzählen.
Fremdschlüssel sind für schreiblastige Systeme böse . Tatsächlich handelt es sich hierbei um "Krücken" , die es einem unachtsamen Programmierer nicht ermöglichen, in die Datenbank zu schreiben, was angeblich nicht vorhanden sein sollte.
Viele Entwickler sind daran gewöhnt, dass logisch verwandte Geschäftseinheiten auf der Ebene der Beschreibung von Datenbanktabellen über FK verknüpft werden müssen. Aber das ist nicht so!
Dieser Punkt hängt natürlich stark von den Zielen ab, die Sie beim Schreiben von Daten in die Datenbank festgelegt haben. Wenn Sie keine Bank sind (und wenn Sie auch eine Bank sind, dann nicht verarbeiten!), Ist die Notwendigkeit von FK in einer Datenbank mit hohem Schreibaufwand fraglich.
"Technisch" macht jeder FK beim Einfügen eines Datensatzes ein separates SELECTaus der referenzierten Tabelle. Schauen Sie sich nun die Tabelle an, in der Sie aktiv schreiben, in der 2-3 FKs hängen, und bewerten Sie, ob es sich für Ihre spezifische Aufgabe lohnt , die Leistung um das 3-4-fache zu verringern ... Oder reicht eine logische Verbindung nach Wert aus? Wir haben alle FKs hier entfernt.
UUID-Schlüssel sind gut . Da die Wahrscheinlichkeit einer Kollision von UUIDs, die an verschiedenen nicht verwandten Punkten generiert werden, äußerst gering ist, kann diese Last (durch Generieren einiger Ersatz-IDs) sicher aus der Datenbank für den "Verbraucher" entfernt werden. Die Verwendung von UUIDs ist eine gute Praxis in verbundenen, nicht synchronisierten verteilten Systemen.
Weitere Varianten eindeutiger Bezeichner in PostgreSQL finden Sie im Artikel "PostgreSQL-Antipatterns: Eindeutige Bezeichner ".
Natürliche Schlüssel sind auch gut , selbst wenn sie aus mehreren Feldern bestehen. Man sollte sich nicht vor zusammengesetzten Schlüsseln fürchten, sondern vor einem zusätzlichen Ersatz-PK-Feld und einem Index in einer geladenen Tabelle, auf die man leicht verzichten kann.
Gleichzeitig verbietet niemand das Kombinieren von Ansätzen. Zum Beispiel haben wir eine Ersatz-UUID, die einem "Stapel" von sequentiellen Protokolldatensätzen zugeordnet ist, die sich auf eine ursprüngliche Transaktion beziehen (da es einfach keinen natürlichen Schlüssel gibt), aber ein Paar wird als PK verwendet
(pack::uuid, recno::int2), wobei recnodie "natürliche" Sequenznummer des Datensatzes innerhalb des Stapels ist.
"Endlose" COPY-Streams
PostgreSQL mag OC nicht, wenn Daten in großen Stapeln (z.
INSERTB. 1000 Zeilen ) darauf geschrieben werden. Es COPYist jedoch viel toleranter gegenüber ausgeglichenen Schreibströmen (durch ). Aber sie müssen sehr sorgfältig kochen können.

- Da wir in der vorherigen Phase alle FKs entfernt haben , können wir jetzt asynchron Informationen über sich selbst
packund eine Reihe verwandter FKsreordin beliebiger Reihenfolge schreiben . In diesem Fall ist es am effektivsten, für jede Zieltabelle einen ständig aktiven Kanal beizubehalten .COPY - , , «», ( —
COPY-) . , — 100, . - , , . . .
, , «» , . , . - , node-pg, PostgreSQL Node.js, API —
stream.write(data)COPY-true, ,false, .

, , « »,COPY. -
COPY- LRU «». .

Hier ist der Hauptvorteil zu beachten, den wir mit diesem Schema des Lesens und Schreibens von Protokollen erhalten haben - in unserer Datenbank werden "Fakten" nach wenigen Sekunden fast online für die Analyse verfügbar .
Verfeinerung mit einer Datei
Alles scheint gut zu sein. Wo ist der "Rechen" im vorherigen Schema? Fangen wir einfach an ...
Übersynchronisation
Eines der großen Probleme geladener Systeme ist die Übersynchronisation einiger Vorgänge, für die dies nicht erforderlich ist. Manchmal "weil sie es nicht bemerkt haben", manchmal "es war einfacher so", aber früher oder später muss man es loswerden.
Dies ist leicht zu erreichen. Wir haben bereits fast 1000 Server für die Überwachung eingerichtet, jeder wird von einem separaten logischen Thread verarbeitet, und jeder Thread speichert die gesammelten Informationen zum Senden an die Datenbank mit einer bestimmten Häufigkeit wie folgt:
setInterval(writeDB, interval)
Das Problem liegt hier genau in der Tatsache, dass alle Streams ungefähr zur gleichen Zeit beginnen, so dass die Momente des Sendens fast immer "auf den Punkt" zusammenfallen.

Glücklicherweise ist dies leicht zu beheben - indem ein "zufälliges" Zeitintervall sowohl für den Startmoment als auch für das Intervall hinzugefügt wird:
setInterval(writeDB, interval * (1 + 0.1 * (Math.random() - 0.5)))

Mit dieser Methode können Sie die Last auf der Aufnahme statistisch "verteilen" und sie nahezu gleichmäßig gestalten.
Skalierung nach CPU-Kernen
Ein Prozessorkern reicht eindeutig nicht für unsere gesamte Last aus, aber das Cluster-Modul hilft uns hier , sodass wir die Erstellung untergeordneter Prozesse einfach verwalten und über IPC mit ihnen kommunizieren können.
Jetzt haben wir 16 untergeordnete Prozesse für 16 Prozessorkerne - und das ist gut so, wir können die gesamte CPU nutzen! Aber in jedem Prozess schreiben wir in 16 Zielplatten , und wenn die Spitzenlast kommt, öffnen wir auch zusätzliche COPY-Kanäle. Das heißt, basierend auf ständig 256+ aktiv schreibenden Threads ... oh! Ein solches Chaos hat keine guten Auswirkungen auf die Festplattenleistung, und die Basis begann zu brennen.
Dies war besonders traurig, als versucht wurde, einige gängige Wörterbücher aufzuschreiben - zum Beispiel denselben Anforderungstext, der von verschiedenen Knoten stammte - unnötige Sperren, Wartezeiten ...

Lassen Sie uns die Situation "umkehren" - das heißt, die untergeordneten Prozesse sammeln und verarbeiten weiterhin Informationen aus ihren Quellen, schreiben jedoch nicht in die Datenbank! Lassen Sie sie stattdessen eine Nachricht über IPC an den Master senden, und er schreibt bereits etwas, wo es sein muss:

Wer das Problem im Schema des vorherigen Absatzes sofort sah - gut gemacht. Es liegt genau in dem Moment, in dem Master auch ein Prozess mit begrenzten Ressourcen ist. Daher stellten wir irgendwann fest, dass er bereits anfing zu brennen - es wurde einfach nicht mehr damit fertig, alle Threads in die Datenbank zu verschieben, da dies auch durch die Ressourcen eines CPU-Kerns begrenzt ist . Infolgedessen haben wir die meisten der am wenigsten geladenen "Wörterbuch" -Streams über den Master geschrieben und die am meisten geladenen, aber nicht zusätzlichen Verarbeitungsschritte an die Mitarbeiter zurückgegeben:

Multicollector
Aber selbst ein Knoten reicht nicht aus, um die gesamte verfügbare Last zu bedienen - es ist Zeit, über eine lineare Skalierung nachzudenken. Die Lösung war ein Multikollektor , der sich je nach Belastung selbst ausbalancierte und an dessen Spitze ein Koordinator stand.

Jeder Master gibt die aktuelle Last aller seiner Mitarbeiter an ihn weiter und erhält als Antwort Empfehlungen, welche Knotenüberwachung an einen anderen Mitarbeiter oder sogar an einen anderen Kollektor übertragen werden soll. Es wird einen separaten Artikel über solche Ausgleichsalgorithmen geben.
Pooling und Warteschlangenbegrenzung
Die nächste richtige Frage ist, was mit Schreibströmen bei plötzlicher Spitzenlast zu tun ist .
Schließlich können wir nicht endlos immer mehr neue Verbindungen zur Basis herstellen - es ist ineffektiv und wird nicht helfen. Eine triviale Lösung - beschränken wir sie so, dass wir nicht mehr als 16 gleichzeitig aktive Threads für jede der Zieltabellen haben. Aber was tun mit den Daten, für deren Schreiben wir noch "keine Zeit hatten"? ..
Wenn dieser "Anstieg" der Last nur eine Spitzenleistung ist, dh kurzfristig , können wir die Daten vorübergehend in der Warteschlange im Speicher des Kollektors selbst speichern . Sobald ein Kanal zur Basis freigegeben ist, rufen wir den Datensatz aus der Warteschlange ab und senden ihn an den Stream.
Ja, dies erfordert, dass der Kollektor über einen Puffer zum Speichern von Warteschlangen verfügt, dieser ist jedoch recht klein und wird schnell freigegeben:

Warteschlangenprioritäten
Der aufmerksame Leser, der sich das vorherige Bild ansah, war erneut verwirrt: „Was passiert, wenn der Speicher vollständig erschöpft ist ?“ Es gibt bereits wenige Optionen - jemand muss geopfert werden.
Aber nicht alle Datensätze, die wir an die Datenbank liefern möchten, sind "gleich nützlich". Es liegt in unserem Interesse, so viele wie möglich quantitativ aufzuschreiben. Die primitive "exponentielle Priorisierung" nach der Größe der geschriebenen Zeichenfolge hilft uns dabei:
let priority = Math.trunc(Math.log2(line.length));
queue[priority].push(line);
Dementsprechend beginnen wir beim Schreiben in einen Kanal immer , aus den "unteren" Warteschlangen zu schöpfen - es ist nur so, dass jede einzelne Zeile dort kürzer ist und wir sie quantitativ mehr senden können:
let qkeys = Object.keys(queue);
qkeys.sort((x, y) => x.valueOf() - y.valueOf()); // - - !
Blockaden überwinden
Gehen wir jetzt zwei Schritte zurück. Bis wir uns entschieden haben, maximal 16 Threads an der Adresse einer Tabelle zu belassen. Wenn die Zieltabelle "Streaming" ist, dh die Datensätze nicht miteinander korrelieren, ist alles in Ordnung. Maximum - Wir haben "physische" Sperren auf Festplattenebene.
Wenn dies jedoch eine Tabelle mit Aggregaten oder sogar ein "Wörterbuch" ist, erhalten wir beim Versuch, Zeilen mit derselben PK aus verschiedenen Streams zu schreiben , eine Wartezeit auf die Sperre oder sogar einen Deadlock. Es ist traurig ...
Aber was soll ich schreiben - wir definieren uns selbst! Der entscheidende Punkt ist nicht, zu versuchen, eine PK von verschiedenen Orten aus zu schreiben .
Das heißt, wenn wir die Warteschlange übergeben, prüfen wir sofort, ob bereits ein Thread mit einer solchen PK in dieselbe Tabelle schreibt (wir erinnern uns, dass sich alle im gemeinsamen Adressraum eines Prozesses befinden). Wenn nicht, nehmen wir es für uns und schreiben es "für uns" in das In-Memory-Wörterbuch. Wenn es bereits jemand anderem gehört, stellen wir es in die Warteschlange.
Am Ende der Transaktion "bereinigen" wir einfach den Anhang "an uns selbst" aus dem Wörterbuch.
Ein kleiner Beweis
Erstens werden mit LRU die „ersten“ Verbindungen und die PostgreSQL-Prozesse, die sie bedienen, fast immer ausgeführt. Dies bedeutet, dass das Betriebssystem sie viel seltener zwischen CPU-Kernen wechselt , wodurch Ausfallzeiten minimiert werden.

Zweitens, wenn Sie fast immer mit denselben Prozessen auf der Serverseite arbeiten, verringert sich die Wahrscheinlichkeit, dass zwei Prozesse gleichzeitig aktiv sind, erheblich. Dementsprechend nimmt die Spitzenlast der gesamten CPU ab (grauer Bereich im zweiten Diagramm von links) ) und LA fällt, weil weniger Prozesse auf ihren Einsatz warten.

Das ist alles für heute.
Und ich möchte Sie daran erinnern, dass Sie mit Hilfe von EXPLAIN.tensor.ru verschiedene Optionen zur Visualisierung des Abfrageausführungsplans sehen können, mit denen Sie die Problembereiche klar erkennen können.