Partitionierung in MySQL für Zabbix mit einer großen Anzahl von zu überwachenden Objekten

Für das Monitoring von Servern und Diensten nutzen wir seit langem erfolgreich eine Kombination aus Nagios und Munin. Dieses Setup hat jedoch einige Nachteile, daher setzen wir, wie viele andere, aktiv auf Zabbix. In diesem Artikel zeigen wir Ihnen, wie Sie mit minimalem Aufwand das Leistungsproblem bei einer steigenden Anzahl abgerufener Metriken und dem Wachstum der MySQL-Datenbank lösen können.

Probleme bei der Verwendung von MySQL in Verbindung mit Zabbix

Solange die Datenbank klein war und die Anzahl der gespeicherten Metriken gering war, lief alles wunderbar. Der eingebaute Housekeeper-Prozess, der vom Zabbix Server selbst gestartet wird, entfernte erfolgreich veraltete Einträge aus der Datenbank, sodass diese nicht wuchs. Sobald jedoch die Anzahl der abgerufenen Metriken zunahm und das Volumen der Datenbank eine bestimmte Größe erreichte, verschlechterte sich die Situation. Der Housekeeper kam nicht mehr mit dem Löschen der Daten im vorgegebenen Zeitrahmen nach, sodass alte Daten in der Datenbank verblieben. Während der Arbeitszeit des Housekeepers entstand eine erhöhte Belastung für den Zabbix Server, die lange andauern konnte. Es wurde klar, dass die Situation irgendwie gelöst werden musste.

Dies ist ein bekanntes Problem; fast jeder, der mit großen Monitoring-Umfängen in Zabbix gearbeitet hat, ist auf dasselbe gestoßen. Es gab auch mehrere Lösungen: beispielsweise den Wechsel von MySQL zu PostgreSQL oder sogar Elasticsearch, aber die einfachste und bewährte Lösung war der Übergang zur Partitionierung der Tabellen, die die Metrikdaten in der MySQL-Datenbank speichern. Wir haben uns entschieden, diesen Weg zu gehen.

Übergang von regulären MySQL-Tabellen zu partitionierten Tabellen

Zabbix ist gut dokumentiert und die Tabellen, in denen es Metriken speichert, sind bekannt. Es handelt sich um folgende Tabellen: history, wo float-Werte gespeichert werden, history_str, wo kurze Zeichenfolgen gespeichert werden, history_text, wo lange Textwerte gespeichert werden und history_uint, wo ganzzahlige Werte gespeichert werden. Zudem gibt es eine Tabelle trends, die die Dynamik der Änderungen speichert, die wir jedoch nicht anfassen wollten, da ihre Größe gering ist, und wir uns später noch einmal damit beschäftigen werden.

Insgesamt war klar, welche Tabellen bearbeitet werden mussten. Wir haben beschlossen, Partitionen für jede Woche zu erstellen, mit Ausnahme der letzten, basierend auf den Zahlen des Monats, also vier Partitionen pro Monat: vom 1. bis 7., vom 8. bis 14., vom 15. bis 21. und vom 22. bis zum 1. (des folgenden Monats). Die Herausforderung bestand darin, die benötigten Tabellen „on-the-fly“ in partitionierte Tabellen umzuwandeln, ohne den Betrieb des Zabbix Servers und die Datensammlung zu unterbrechen.

Seltsamerweise kam uns bei dieser Aufgabe die Struktur der Tabellendaten selbst zu Hilfe. Beispielsweise hat die Tabelle history die folgende Struktur:

`itemid` bigint(20) unsigned NOT NULL,
`clock` int(11) NOT NULL DEFAULT '0',
`value` double(16,4) NOT NULL DEFAULT '0.0000',
`ns` int(11) NOT NULL DEFAULT '0',

wobei

KEY `history_1` (`itemid`,`clock`)

Wie wir sehen, wird jede Metrik schließlich in eine Tabelle mit zwei sehr wichtigen und uns nützlichen Feldern eingetragen: itemid und clock. So können wir ganz einfach eine temporäre Tabelle erstellen, zum Beispiel mit dem Namen history_tmp, für die wir dann die Partitionierung einrichten und alle Daten aus der Tabelle dorthin übertragen history, um anschließend die Tabelle umzubenennen history in history_old, während die Tabelle history_tmp in history, danach die noch fehlenden Daten aus history_old in history hinzufügen und history_old. Dies kann völlig sicher durchgeführt werden, wir verlieren nichts, denn die oben genannten Felder itemid und clock stellen die Bindung einer bestimmten Metrik an einen bestimmten Zeitpunkt sicher, anstatt an eine einfache fortlaufende Nummer.

Der Übergangsprozess selbst

Achtung! Es wird dringend empfohlen, vor Beginn irgendeiner Aktion eine vollständige Sicherung der Datenbank zu erstellen. Wir sind schließlich Menschen und können Fehler bei der Eingabe von Befehlen machen, was zu Datenverlust führen kann. Ja, eine Sicherung garantiert nicht die maximale Aktualität, aber es ist besser, eine zu haben, als keine.

Also schalten wir nichts aus und stoppen nichts. Wichtig ist, dass auf dem MySQL-Server ausreichend Speicherplatz vorhanden ist, d.h. dass für jede der oben genannten Tabellen history, history_text, history_str, history_uint, dass mindestens ausreichend Platz zur Verfügung steht, um eine Tabelle mit dem Suffix „_tmp“ zu erstellen, wobei diese die gleiche Größe wie die ursprüngliche Tabelle haben wird.

Wir werden nicht alles mehrfach für jede der oben genannten Tabellen beschreiben und betrachten dies am Beispiel nur einer von ihnen – der Tabelle history.

Jetzt erstellen wir eine leere Tabelle history_tmp basierend auf der Struktur der Tabelle history.

CREATE TABLE `history_tmp` LIKE `history`;

Wir erstellen die benötigten Partitionen. Zum Beispiel machen wir dies nach Monaten. Jede Partition wird auf der Grundlage einer Partitionierungsregel erstellt, die auf dem Wert des Feldes basiert clock, den wir mit einem Zeitstempel vergleichen:

ALTER TABLE `history_tmp` PARTITION BY RANGE( clock ) (
PARTITION p20190201 VALUES LESS THAN (UNIX_TIMESTAMP("2019-02-01 00:00:00")),
PARTITION p20190207 VALUES LESS THAN (UNIX_TIMESTAMP("2019-02-07 00:00:00")),
PARTITION p20190214 VALUES LESS THAN (UNIX_TIMESTAMP("2019-02-14 00:00:00")),
PARTITION p20190221 VALUES LESS THAN (UNIX_TIMESTAMP("2019-02-21 00:00:00")),
PARTITION p20190301 VALUES LESS THAN (UNIX_TIMESTAMP("2019-03-01 00:00:00"))
);

Dieser Befehl fügt der von uns erstellten Tabelle die Partitionierung hinzu history_tmp. Dabei stellen wir klar, dass Daten, bei denen der Wert des Feldes clock weniger als „2019-02-01 00:00:00“ ist, in die Partition p20190201gehen, und die Daten, deren Wert des Feldes clock größer als „2019-02-01 00:00:00“ aber kleiner als „2019-02-07 00:00:00“ sind, in die Partition p20190207 usw.

Wichtiger Hinweis: Was passiert, wenn in unserer partitionierten Tabelle Daten mit einem Zeitstempel größer oder gleich «2019-03-01 00:00:00» erscheinen? Da es für diese Daten keine passende Partition gibt, werden sie nicht in die Tabelle aufgenommen und gehen verloren. Daher ist es wichtig, rechtzeitig weitere Partitionen zu erstellen, um solche Datenverluste zu vermeiden (siehe unten).

So, die temporäre Tabelle ist vorbereitet. Wir laden die Daten hoch. Der Prozess kann ziemlich lange dauern, aber glücklicherweise blockiert er keine anderen Anfragen, sodass Sie einfach Geduld haben sollten:

INSERT IGNORE INTO `history_tmp` SELECT * FROM history;

Das Schlüsselwort IGNORE ist beim ersten Laden nicht zwingend erforderlich, da die Tabelle ohnehin leer ist, jedoch wird es beim Nachladen der Daten von Nutzen sein. Zudem kann es hilfreich sein, falls Sie den Ladevorgang unterbrechen mussten und von vorne beginnen möchten.

So, nach einer gewissen Zeit (möglicherweise sogar mehreren Stunden) ist der erste Datenimport abgeschlossen. Wie Sie sehen können, enthält die Tabelle history_tmp nicht alle Daten aus der Tabelle history, sondern nur die, die zu Beginn des Anforderungsvorgangs in ihr vorhanden waren. Hier haben Sie die Wahl: Entweder wir machen einen weiteren Durchgang (falls der Upload-Prozess lange gedauert hat), oder wir wechseln sofort zur Umbenennung der Tabellen, die weiter oben erwähnt wurde. Lassen Sie uns zuerst über den zweiten Durchgang sprechen. Zunächst müssen wir die Zeit des letzten eingefügten Eintrags verstehen in history_tmp:

SELECT max(clock) FROM history_tmp;

Angenommen, Sie haben erhalten: 1551045645. Jetzt verwenden wir den erhaltenen Wert im zweiten Durchgang des Daten-Uploads:

INSERT IGNORE INTO `history_tmp` SELECT * FROM history WHERE clock>=1551045645;

Dieser Durchgang sollte deutlich schneller abgeschlossen sein. Wenn der erste Durchgang jedoch Stunden gedauert hat und der zweite ebenfalls eine lange Zeit in Anspruch nimmt, könnte es sinnvoll sein, einen dritten Durchgang durchzuführen, der genau wie der zweite funktioniert.

Am Ende führen wir erneut die Abfrage durch, um die Zeit des letzten eingefügten Eintrags in history_tmp, indem wir ausführen:

SELECT max(clock) FROM history_tmp;

Angenommen, Sie haben erhalten 1551085645. Speichern Sie diesen Wert — wir benötigen ihn für den Nachfüllprozess.

Und nun, da der primäre Daten-Upload in history_tmp abgeschlossen ist, beginnen wir mit der Umbenennung der Tabellen:

BEGIN;
RENAME TABLE history TO history_old;
RENAME TABLE history_tmp TO history;
COMMIT;

Wir haben diesen Block als eine Transaktion behandelt, um den Fall zu vermeiden, dass Daten in eine nicht vorhandene Tabelle eingefügt werden, da die Tabelle nach dem ersten RENAME bis zur Ausführung des zweiten RENAME nicht existieren wird. history Aber selbst wenn zwischen den RENAME-Operationen Daten in die Tabelle gelangen, die Tabelle jedoch noch nicht vorhanden ist (wegen der Umbenennung), erhalten wir einige Einfügungsfehler, die vernachlässigbar sind (wir haben Monitoring, nicht Banking). history Jetzt haben wir eine neue Tabelle

mit Partitionierung, aber es fehlen Daten, die während des letzten Datenladevorgangs in die Tabelle erhalten wurden. history Diese Daten haben wir jedoch in der Tabelle history_tmpund wir werden sie jetzt dort ergänzen. Dazu benötigen wir den zuvor gespeicherten Wert 1551085645. Warum haben wir diesen Wert gespeichert und nicht die maximale Ladezeit aus der aktuellen Tabelle verwendet? history_old INSERT IGNORE INTO `history` SELECT * FROM history_old WHERE clock >= 1551045645; history? Потому что новые данные уже в неё поступают и мы получим неверное время. Итак, дозаливаем данные:

Nach Abschluss dieser Operation haben wir in unserer neuen, partitionierten Tabelle

alle Daten, die in der alten enthalten waren, plus die, die bereits nach der Umbenennung der Tabelle angekommen sind. Die Tabelle history enthält alle Daten, die in der alten Tabelle vorhanden waren, sowie die, die nach der Umbenennung der Tabelle bereits eingegangen sind. Tabelle history_old Wir benötigen es nicht mehr. Sie können es sofort löschen oder, falls Sie paranoid sind, vorher eine Sicherungskopie erstellen.

Der gesamte oben beschriebene Prozess muss für die Tabellen wiederholt werden. history_str, history_text und history_uint.

Was muss in den Einstellungen des Zabbix Servers angepasst werden?

Nun liegt die Wartung der Datenbank in Bezug auf die Verlaufsdaten in unseren Händen. Das bedeutet, dass Zabbix keine alten Daten mehr löschen muss – wir kümmern uns selbst darum. Damit Zabbix Server nicht versucht, die Daten selbst zu bereinigen, gehen Sie bitte in die Weboberfläche von Zabbix, wählen Sie im Menü "Administration" das Untermenü "Allgemein" und wählen Sie im Dropdown-Menü auf der rechten Seite "Historienbereinigung". Auf der nun erscheinenden Seite müssen Sie alle Häkchen in der Gruppe "Historie" entfernen und auf die Schaltfläche "Aktualisieren" klicken. Dies verhindert die unerwünschte Bereinigung der Tabellen. history* über den Housekeeper.

Beachten Sie auf dieser Seite auch die Gruppe "Änderungsdynamik". Dies ist genau die Tabelle, trends, zu der wir versprochen haben zurückzukehren. Wenn sie auch bei Ihnen zu groß geworden ist und partitioniert werden muss, entfernen Sie die Häkchen auch in dieser Gruppe und bearbeiten Sie diese Tabelle genau so wie bei den anderen Tabellen. history*.

Weitere Datenbankpflege

Wie bereits erwähnt, ist es für die ordnungsgemäße Funktion von partitionierten Tabellen erforderlich, rechtzeitig Partitionen zu erstellen. Dies kann folgendermaßen erfolgen:

ALTER TABLE `history` ADD PARTITION (PARTITION p20190307 VALUES LESS THAN (UNIX_TIMESTAMP("2019-03-07 00:00:00")));

Da wir partitionierte Tabellen erstellt und Zabbix Server verboten haben, sie zu bereinigen, liegt die Verantwortung für das Löschen alter Daten nun bei uns. Zum Glück gibt es dabei überhaupt keine Probleme. Dies geschieht einfach durch das Löschen der Partition, deren Daten wir nicht mehr benötigen.

Zum Beispiel:

ALTER TABLE history DROP PARTITION p20190201;

Im Gegensatz zu DELETE FROM-Befehlen mit Datumsbereichen wird DROP PARTITION innerhalb von Sekunden ausgeführt und belastet das System nicht. der Server Es funktioniert ebenso problemlos bei der Verwendung von MySQL-Replikationen.

Fazit

Die beschriebene Lösung hat sich bewährt. Der Datenumfang wächst, jedoch ist keine spürbare Leistungseinbuße festzustellen.

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