Zeitreihendaten - Teil 1.

Zeitreihendaten - Teil 1

Einleitung

Als Interim CTO befasse ich mich mit Managementthemen. Aber ich beschäftige mich auch intensiv mit IT-Architektur und Hochleistungssystemen. In einem meiner letzten Projekte musste ich mit großen Mengen an IoT-Zeitreihendaten umgehen.

Zeitreihendaten sind etwas Besonderes, weil sie einige spezifische Eigenschaften haben. Erstens sind sie nach - nun ja - Zeit geordnet. Zweitens werden alte Daten in der Regel nicht aktualisiert - es werden nur neue Daten hinzugefügt.

Solche Systeme haben auch ein hohes Volumen. Viele Zeilen werden pro Sekunde hinzugefügt. Und aus Anwendersicht muss man in der Lage sein, die Daten zu analysieren. Das geschieht oft durch die Aggregation von Daten auf Tagesbasis. Wie viele Ereignisse sind heute im Vergleich zu gestern eingegangen. Und dann kann man tiefer eintauchen, bestimmte Ereignisse auswählen und vergleichen.

Die Teams hatten noch keine Technologie für ein neues Produkt gewählt, und so machte ich einige kurze Tests mit ein paar Datenbanksystemen, mit denen ich Erfahrung hatte. Ich habe - natürlich - mit dem guten alten PostgreSQL (Version 15) begonnen.

PostgreSQL und Zeitreihendaten

Hinweis: Ich habe alle Messungen auf meinem MacBook M1 Pro 32GB durchgeführt. Ich habe versucht, die Abfragen so “kalt” wie möglich zu machen, damit keine Daten im Arbeitsspeicher gecacht werden.

Meine Anforderungen waren die Speicherung von ereignisartigen Daten. Daten waren immer an einen bestimmten Benutzer gebunden. In SQL ausgedrückt wäre das etwa so:

CREATE TABLE events (
  user_id UUID NOT NULL,
  timestamp TIMESTAMPTZ NOT NULL,
  payload TEXT NOT NULL,
  source TEXT NOT NULL,
  target TEXT NOT NULL
);

Zuerst habe ich einige Testdaten mit der folgenden Funktion erstellt:

DO $$
DECLARE
  user_id UUID;
BEGIN
  FOR i IN 1..10 LOOP
    user_id = gen_random_uuid();
    INSERT INTO events (
      user_id,
      timestamp,
      payload,
      source,
      target
    )
    SELECT
      user_id,
      NOW() - interval '1 second' * trunc(random() * 60 * 60 * 24 * 365 * 5),
      'dummypayload-.' || md5(random()::text),
      'dummysource-' || md5(random()::text),
      'dummytarget-' || md5(random()::text)
    FROM generate_series(1, 10000000);
  END LOOP;
END $$;

Dies erstellt 10 Millionen Ereignisse über 5 Jahre für 10 Benutzer.

Meine Aufgabe war es, ein Dashboard der Ereignisse bereitzustellen. Zum Beispiel wie viele Ereignisse täglich eingegangen sind. Schalten wir das Timing über \timing ein und messen die Abfrage.

SELECT
    date_trunc('day', timestamp) AS day,
    COUNT(*) AS count
FROM
    hits
WHERE user_id = 'a valid user'
  AND timestamp >= NOW() - INTERVAL '5 years'
GROUP BY
    day
ORDER BY
    day;

Das dauerte etwa 15 Sekunden. Nicht interaktiv. Aber natürlich haben wir im Moment keine Indizes. Also erstellen wir einen für unsere Abfrage:

CREATE INDEX on events(user_id, timestamp);
SELECT
    date_trunc('day', timestamp) AS day,
    COUNT(*) AS count
FROM
    hits
WHERE user_id = 'a valid user'
  AND timestamp >= NOW() - INTERVAL '5 years'
GROUP BY
    day
ORDER BY
    day;

Das dauert jetzt etwa 2,5 Sekunden. Sehr schöne Leistungsverbesserung. Die Abfrage lief jetzt als Index-Only-Abfrage und hat den Heap nicht berührt. Großartig!

Aber war es wirklich schnell und interaktiv? Hmm. Vermutlich ist es immer noch zu langsam.

Optionen von hier aus

Produktdesign hinterfragen

Die erste Frage wäre: “Brauchen wir wirklich 5 Jahre Daten auf einmal?”. Das ist die Geheimwaffe guter CTOs und technischer Führungskräfte. Sie akzeptieren Produktentscheidungen nicht einfach, sondern hinterfragen sie und versuchen, Produktvision und technische Machbarkeit zu vereinen.

Wir könnten zum Beispiel die Daten in Stapeln abrufen und eine hypothetische Tabelle von den neuesten zu den ältesten Ereignissen rendern. Die hypothetische Webseite wäre sofort interaktiv und würde dann schnell Daten hinzufügen.

Mehr CPU, Arbeitsspeicher, SSD - alles!

Mein MacBook ist schnell, aber nicht so schnell wie ein leistungsstarker Server mit viel RAM, CPU und Festplatte. Wir könnten daher einfach vertikal skalieren. Die Maschine, die ich bei Hetzner konfiguriert habe, hatte 1 TB RAM, 7TB Festplatte und eine 32-Kern-AMD-CPU für 600 Euro pro Monat. Das ist ziemlich massiv und wird die Abfragen deutlich interaktiver machen.

Mein Bauchgefühl sagt mir auch, dass diese 600 Euro Sie weit bringen werden. Und es ist viel vorhersehbarer (und günstiger) als jedes Cloud-Angebot (ein Thema für einen anderen Beitrag).

Materialized Views und Voraggregation

Materialized Views sind eine schöne Möglichkeit, diese Aggregationsabfragen auszuführen und die Ergebnisse zu speichern. Das ist viel, viel schneller als die Abfrage selbst auszuführen. Leider bedeutet das auch, die Daten selbst regelmäßig zu aktualisieren (über pg_cron oder ähnliches) und erhöhte Komplexität bei der Kombination von voraggregierten Daten mit neuen Daten.

In meinem Fall klang es auch zu kompliziert, da ich meine Abfragen an die Zeitzonen der Benutzer anpassen musste. Das würde die Voraggregation sehr kompliziert machen (ein Materialized View pro Zeitzone). Außerdem sahen meine realen Abfragen viel komplizierter aus mit mehr WHERE-Klauseln.

Kurz gesagt: Materialized Views waren in meinem Fall nicht möglich.

Spezialisierte Erweiterungen wie TimescaleDB

Ich habe mehrfach über Timescale gelesen. Es ist eine Erweiterung für PostgreSQL, die etwas Magie hinzufügt, um mit Zeitreihendaten in PostgreSQL besser umzugehen.

Klingt cool. Ich kann all mein Wissen über PostgreSQL wiederverwenden und es mit Timescale aufladen. Und das ist etwas für den nächsten Blogbeitrag!

Zusammenfassung

Für viele Zeitreihen-Anwendungsfälle könnte PostgreSQL schnell genug sein. Die wichtigste Empfehlung ist, Ihre Abfragen so zu gestalten, dass sie Index-Only-Scans nutzen. Im obigen Beispiel führte dies zu einer 6-fachen Beschleunigung.

Wichtig: Führen Sie die Leistungsmessungen selbst durch, stellen Sie sicher, dass Sie während der Leistungstests keine Caches treffen, und verwenden Sie EXPLAIN ausgiebig zur Optimierung.

Am Ende ist es auch Ihre wichtigste Aufgabe als technische Führungskraft, das Produkt zu hinterfragen. Das Ziel ist es, clevere technische Lösungen zu schaffen, die technisch machbar (also günstig genug) sind und die Produktvision erfüllen.

Mehr: