
Hallo, habr.
Wenn jemand das System und auf ein Speicherproblem gestoßen ist, (IO, genutzter Speicherplatz), dann ist die Wahrscheinlichkeit, dass ClickHouse als Alternative in Betracht gezogen wurde, nahezu sicher. Dies setzt voraus, dass bereits eine Drittanbieterimplementierung als Metriken-Daemon verwendet wird, zum Beispiel oder .
ClickHouse löst die beschriebenen Probleme gut. Nach der Migration von 2TiB Daten aus whisper waren diese in 300GiB untergebracht. Ich werde nicht näher auf den Vergleich eingehen, es gibt ausreichend Artikel zu diesem Thema. Zudem war unser ClickHouse-Speicher bis vor Kurzem nicht ganz perfekt.
Probleme mit dem benötigten Speicherplatz
Auf den ersten Blick sollte alles einwandfrei funktionieren. Indem wir , erstellen wir eine Konfiguration für das Metriken-Speichermodell (im Folgenden retention), dann erstellen wir eine Tabelle gemäß der Empfehlung des gewählten Backends für graphite-web: + oder , abhängig davon, welcher Stack verwendet wird. Und… aktiviert sich die Zeitbombe.
Um zu verstehen, welches relevant ist, muss man wissen, wie Inserts und der anschließende Lebenszyklus von Daten in den Tabellen der Engine-Familie funktionieren.MergeTree ClickHouse (Diagramme stammen aus von Alexey Zatelepin):
- Es wird
ein Blockvon Daten eingefügt. In unserem Fall handelt es sich um die eingehenden Metriken.

- Jeder solche Block wird vor dem Schreiben auf die Festplatte nach dem Schlüssel sortiert,
ORDER BY, der bei der Erstellung der Tabelle angegeben wurde. - Nach der Sortierung wird
ein Stück(part) von Daten auf die Festplatte geschrieben.

- Der Server überwacht im Hintergrund, dass nicht zu viele solcher Stücke entstehen, und startet im Hintergrund
Zusammenführungen(Zusammenführen, also Merges.)


- Der Server hört auf, die Merges automatisch zu starten, sobald die Daten nicht mehr aktiv in die
Partition(partition), aber der Prozess kann manuell mit dem BefehlOPTIMIZE. - gestart werden. Wenn in der Partition nur noch ein Stück verbleibt, ist es nicht möglich, einen Merge mit dem üblichen Befehl durchzuführen, stattdessen muss man
OPTIMIZE ... FINAL
verwenden. Zunächst kommen die ersten Metriken an. Diese beanspruchen einen gewissen Speicherplatz. Die folgenden Ereignisse können in Abhängigkeit von vielen Faktoren variieren:
- Der Partitionierungsschlüssel kann sowohl sehr klein (einen Tag) als auch sehr groß (mehrere Monate) sein.
- Die Konfiguration der Retention kann mehrere bedeutende Aggregationsschwellen innerhalb der aktiven Partition (wo die Metriken geschrieben werden) enthalten oder auch nicht.
- Wenn es sehr viele Daten gibt, werden die frühesten Teile, die durch Hintergrundfusionen bereits riesig sein können (bei der Wahl eines suboptimalen Partitionierungsschlüssels), nicht selbst mit den neuen, kleinen Teilen zusammengeführt.
Und am Ende läuft es immer gleich ab. Der Platz, den die Metriken in ClickHouse einnehmen, wächst nur, wenn:
- nicht angewendet wird
OPTIMIZE ... FINALmanuell oder - die Daten nicht dauerhaft in alle Partitionen eingefügt werden, um irgendwann eine Hintergrundfusion zu starten.
Der zweite Weg scheint am einfachsten umzusetzen zu sein, und das macht ihn falsch; er wurde zunächst getestet.
Ich habe ein recht einfaches Skript in Python geschrieben, das fiktive Metriken für jeden Tag der letzten 4 Jahre gesendet hat und jede Stunde über Cron ausgeführt wurde.
Da die gesamte Arbeit von ClickHouse DBMS darauf basiert, dass dieses System irgendwann die gesamte Hintergrundarbeit erledigt, jedoch unklar ist, wann das geschehen wird, konnte ich den Moment nicht abwarten, in dem die alten, großen Teile sich entschließen würden, mit den neuen, kleinen zu verschmelzen. Es wurde klar, dass ich einen Weg finden musste, um die zwingenden Optimierungen zu automatisieren.

Informationen in den Systemtabellen von ClickHouse
Schauen wir uns die Struktur der Tabelle an . Diese enthält umfassende Informationen zu jedem Stück aller Tabellen auf dem ClickHouse-Server. Sie enthält unter anderem folgende Spalten:
- Datenbankname (
database); - Tabellenname (
Tabelle); - Name und ID der Partition (
partition&partition_id); - Wann das Stück erstellt wurde (
modification_time); - Frühestes und spätestes Datum im Stück (Partitionierung erfolgt nach Tagen) (
min_date&max_date);
Es gibt auch eine Tabelle , mit den folgenden interessanten Feldern:
- Datenbankname (
Tables.database); - Tabellenname (
Tables.table); - Alter der Metrik, wann die nächste Aggregation angewendet werden soll (
age);
Also:
- Wir haben eine Tabelle mit Stücke und eine Tabelle mit Aggregationsregeln.
- Wir kombinieren deren Schnittmenge und erhalten alle Tabellen *GraphiteMergeTree.
- Wir suchen alle Partitionen, in denen:
- mehr als ein Stück
- oder es an der Zeit ist, die nächste Aggregationsregel anzuwenden, und
modification_timeälter als dieser Zeitpunkt.
Implementierung
Diese Abfrage
WÄHLEN
concat(p.database, '.', p.table) AS table,
p.partition_id AS partition_id,
p.partition AS partition,
-- Die "älteste" Regel, die auf
-- die Partition angewendet werden kann, aber nicht für die Zukunft, siehe (*)
max(g.age) AS age,
-- Anzahl der Teile in der Partition
countDistinct(p.name) AS parts,
-- Die älteste Metrik in der Partition wird als 00:00:00 des nächsten Tages betrachtet
toDateTime(max(p.max_date + 1)) AS max_time,
-- Wann die Partition optimiert werden sollte
max_time + age AS rollup_time,
-- Wann das älteste Stück in der Partition aktualisiert wurde
min(p.modification_time) AS modified_at
FROM system.parts AS p
INNER JOIN
(
-- Alle Regeln für alle Tabellen *GraphiteMergeTree
WÄHLEN
Tables.database AS database,
Tables.table AS table,
age
FROM system.graphite_retentions
ARRAY JOIN Tables
GROUP BY
database,
table,
age
) AS g ON
(p.table = g.table)
AND (p.database = g.database)
WHERE
-- Nur aktive Stücke
p.active
-- (*) Und nur Zeilen, in denen die Aggregationsregeln bereits angewendet werden sollten
AND ((toDateTime(p.max_date + 1) + g.age) < now())
GROUP BY
table,
partition
HAVING
-- Nur Partitionen, die jünger sind als der Optimierungszeitpunkt
(modified_at 1)
ORDER BY
table ASC,
partition ASC,
age ASCgibt jede der Partitionen der Tabellen *GraphiteMergeTree zurück, deren Zusammenführung dazu führen sollte, dass Speicherplatz freigegeben wird. Es bleibt nur noch die Aufgabe, sie alle mit einer Abfrage zu durchlaufen. OPTIMIZE ... FINAL. In der finalen Umsetzung wurde auch berücksichtigt, dass aktive Schreibpartitionen nicht angefasst werden müssen.
Genau das macht das Projekt . Ehemalige Kollegen von Yandex.Market haben es in der Produktion getestet, die Ergebnisse sind unten zu sehen.

Wenn das Programm auf einem Server mit ClickHouse gestartet wird, läuft es einfach im Daemon-Modus. Einmal pro Stunde wird eine Abfrage ausgeführt, um zu überprüfen, ob neue Partitionen älter als drei Tage optimiert werden können.
In naher Zukunft planen wir, mindestens deb-Pakete anzubieten und wenn möglich auch rpm-Pakete.
Abschließend
In den letzten 9 Monaten habe ich innerhalb meines Unternehmens Ich habe viel Zeit damit verbracht, an der Schnittstelle zwischen ClickHouse und graphite-web zu arbeiten. Es war eine gute Erfahrung, die es uns ermöglicht hat, bald von Whisper auf ClickHouse als Metrik-Speicher umzusteigen. Ich hoffe, dass dieser Artikel eine Art Anfang eines Zyklus darstellt, in dem wir die Verbesserungen, die wir in verschiedene Teile dieses Stacks eingebracht haben, sowie zukünftige Vorhaben beleuchten.
Für die Entwicklung der Anfrage wurden mehrere Liter Bier und einige Admin-Tage zusammen mit , wofür ich ihm meinen Dank aussprechen möchte. Auch für das Lektorat dieses Artikels.
Quelle: habr.com




