Langweilig ist besser: 67k Telemetrie-Events pro Sekunde in Postgres
Auf dieser Seite
Wir sammeln enorme Mengen an Telemetriedaten. Prompts, Nutzungsmetriken, Sessiondaten, Einblicke aus jedem KI-Coding-Assistenten und jeder Desktop-App, die die Engineers unserer Kunden benutzen. Alles läuft von Tausenden Nutzern in unsere Insights-Plattform zusammen.
Bei Flowstate messen wir KI-Ausgaben mit OpenTelemetry. Jeder API-Call, jede Coding-Session, jeder Modellaufruf, zurückverfolgt bis zu Teams, Projekten und Kostenstellen. Das Volumen ist erheblich und reißt nie ab.
Heißt: Wir müssen eine Menge Daten speichern. Keine “Analytics-Dashboard”-Daten. Keine “Monatsbericht”-Daten. Jeder Span, jede Metrik, jede Logzeile von jedem Nutzer bei jedem Kunden, abgelegt, damit unsere Analyse-Pipeline sie später verdauen kann.
Alle haben denselben ersten Reflex: “Dafür brauchst du ein Data Warehouse.”
Ich hab’s versucht. Wirklich.
Auf Einkaufstour
Vorab: Die Produkte, die ich mir angesehen habe, sind alle hervorragend in dem, wofür sie gebaut sind. Sie sind für Organisationen mit großen Data-Teams und komplexen analytischen Workloads gemacht. Unser Problem war einfacher: viele Telemetriedaten schnell schreiben, später abfragen. Dafür waren die meisten dieser Lösungen mehr, als wir brauchten.
Dazu kam ein praktisches Bedenken, das mich nicht losließ. Ich sollte im Flugzeug entwickeln können. Nicht dass ich es wirklich tue, aber das ist der Kern der Sache. Wenn ein Baustein meines Stacks eine Internetverbindung braucht, kann ich nicht lokal bauen, testen und iterieren. Ich kann ihn nicht an einem Samstagmorgen in Docker hochziehen und Daten drauf werfen. Viele dieser Data Warehouses fühlten sich an, als würden sie Probleme lösen, die ein gut getuntes Postgres auch schafft.
BigQuery war die erste Station. Großartig, um im großen Maßstab abzufragen, weniger gut für kontinuierliche Schreiblast in hohem Volumen. Die Streaming-Insert-API kostet pro Zeile, und bei unserem Volumen läppert sich das schnell. Das Latenzprofil passt auch nicht zu einer Ingestion in Beinahe-Echtzeit. Eine phänomenale Analytics-Engine, nur nicht die richtige für eine schreiblastige Telemetrie-Pipeline.
Snowflake ist eine mächtige Datenplattform, aber das Preismodell ist kompliziert (Compute-Credits, Storage, Datentransfer), und für unseren Bedarf war es deutlich mehr Infrastruktur, als das Problem verlangte. Wenn die Alternative ein gut getuntes Postgres ist, fällt der Kostenvergleich brutal aus.
Databricks beeindruckt, wenn du eine eigene Data-Engineering-Abteilung hast. Die Lakehouse-Architektur und die Spark-Integration sind wirklich stark. Aber wir haben kein Team aus zwölf Data Engineers, und eine Plattform dieser Komplexität einzuführen, nur um “Zeilen schreiben, Zeilen abfragen” zu erledigen, fühlte sich an, als würde man mit einer Predator-Drohne auf eine Messerstecherei losgehen.
Amazon Redshift, Azure Synapse und andere Managed-Analytics-Angebote sind alle solide Produkte, bringen aber jeweils Betriebsaufwand mit, der nicht zu unserem Stand passte. Noch ein System zum Überwachen, noch ein Satz Zugangsdaten, noch eine Vendor-Beziehung.
ClickHouse war die überzeugendste Option. Spaltenorientiert, speziell für schreiblastige analytische Workloads gebaut, Open Source und richtig schnell. Ich mochte es sehr. Ich bin nicht wegen der Technik davon abgekommen, sondern wegen des Deployments. ClickHouse in einer Managed-Umgebung zuverlässig zum Laufen zu bringen, war mehr Reibung, als ich wollte. ClickHouse Cloud gibt es zwar, aber das ist eine weitere Vendor-Abhängigkeit, obwohl ich schon ein Managed Postgres auf Google Cloud SQL habe. Und analytische Queries kannst du in Postgres sowieso fahren, ClickHouse fühlte sich also nach unnötigen Zwischenschritten an.
Damit war ich wieder bei der Datenbank, die ich ohnehin schon betrieb.
Ich habe über die Jahre einigen wilden Kram auf Postgres gebaut, und die Frage kommt immer: “Sind wir sicher, dass eine Datenbank reicht?” Aus Erfahrung kann ich sagen: Eine Datenbank steckt einiges weg. Es gibt Geschichten von Leuten, die Postgres im Petabyte-Bereich betreiben. Mit Mühe, aber es geht. Schauen wir also, was wir für unseren bescheidenen Anwendungsfall rausholen können.
Ich habe schon Postgres. Es funktioniert schon. Finden wir heraus, wo es aufhört zu funktionieren.
Die Fehlschläge
Was jetzt folgt, ist eine gekürzte Chronik davon, wie ich Dinge auf die harte Tour gelernt habe.
Versuch 1: Einfach in die Produktionsdatenbank schreiben
Die erste Version war genau so dumm, wie sie klingt. Bei jedem eingehenden Telemetrie-Event feuerten wir ein INSERT auf die Postgres-Instanz in Produktion. Eine Zeile nach der anderen. Einzeln. Wie Briefe einwerfen.
Bei wenig Last ging das. Bei mittlerer Last auch. Um 3 Uhr nachts an einem Dienstag war Schluss, als der Proxy-Service einen Traffic-Peak abbekam und wir plötzlich 10.000 einzelne INSERTs pro Sekunde gegen eine Datenbank fuhren, die nebenbei auch noch die eigentliche Anwendung bedienen sollte.
Ich bekam buchstäblich keinen Write Lock, um eine Migration auszuführen. Der Connection Pool war ausgelastet, jede verfügbare Verbindung hing an einem INSERT, und der Migration Runner wartete auf einen Lock, der nie kommen würde. Am Ende habe ich Verbindungen von Hand gekillt, um die Migration durchzukriegen. Um 3 Uhr nachts. An einem Dienstag.
Versuch 2: Batching (wärmer)
Der naheliegende Fix: nicht mehr Zeile für Zeile schreiben. Events im Speicher puffern und in Batches von 500 bis 1.000 Zeilen per Multi-Row-INSERT in die Datenbank schieben.
Das war dramatisch besser. Statt 10.000 Transaktionen gibt es 10 bis 20 größere. Die Datenbank atmet durch. Der Connection Pool leert sich. Migrationen laufen.
Aber nun gab es ein neues Problem: Was passiert, wenn die Anwendung zwischen zwei Batches abstürzt? Alles, was im Puffer liegt, ist weg.
Versuch 3: Redis als Puffer
Also haben wir Redis als Zwischenpuffer dazugenommen. Events kommen rein und werden an einen Redis Stream angehängt (XADD), und ein Background-Worker liest den Stream (XREADGROUP) und schreibt Batches nach Postgres.
Das funktionierte tatsächlich, und Redis steckt bis heute in der finalen Architektur. Aber nicht als Datenspeicher. Es hält Verweise auf eingehende Objekte, merkt sich, was angekommen ist und was schon geschrieben wurde, und lässt uns klug batchen, ohne dass die Anwendung selbst State halten muss. Wenn der Datenstrom so viel Druck hat, dass er dir die Haut abreißen würde, ist Redis das Ventil, das daraus etwas Handhabbares macht.
Die entscheidende Erkenntnis war, Redis nicht als Datenbank zu benutzen. Es ist eine Koordinationsschicht. Die Telemetriedaten selbst gehen direkt nach Postgres. Redis sagt uns nur, was wartet und was verarbeitet wurde.
Die Frage
Jetzt hatte ich ein System, das funktionierte. Postgres zum Speichern, Redis zum Koordinieren, gebatchte COPY-Writes für den Durchsatz. Aber ich wusste nicht wirklich, wie viel Postgres aushält. Schafft es 10x die Last? 100x? Wo liegt die tatsächliche Decke?
Es gibt nur einen Weg, das herauszufinden.
Testen wir’s einfach
Ich tat, was jeder vernünftige Mensch tun würde: Postgres in einem Docker-Container auf meinem Laptop hochziehen und immer absurdere Mengen Telemetrie draufwerfen, bis etwas bricht.
Das Setup:
- Postgres 16 in Docker mit 512MB Shared Buffers und 500 Max Connections
- pgBouncer im Transaction-Pooling-Modus für die Connection-Pooling-Tests
- Ein Schema im OpenTelemetry-Stil: die Tabellen
spans,metricsundlogsmit JSONB-Attributen - Ein Telemetrie-Generator, der realistische Traces erzeugt: ~1.900 Events pro simuliertem Nutzer mit je ~3KB, verteilt auf ~100 Traces mit 5 bis 12 Spans, Metriken und Logs
- Vier Schreibstrategien, getestet mit steigenden Nutzerzahlen
- Für die Extremtests (10K+ Nutzer) laufen 30-Sekunden-Bursts, weil meine 512GB-SSD bei ~200MB/s Dauerschreiblast schnell voll wäre. Jeder längere Test hätte den Ausfallmodus meines Laptop-Speichers gemessen, nicht Postgres.
Das alles lief auf einem MacBook Air M2 von 2022 mit 24GB RAM und einer 512GB-SSD. Kein Server. Ein Laptop.
Die vier Strategien
Naives INSERT: Ein INSERT-Statement pro Telemetrie-Event. 50 parallele Verbindungen. Die “bitte nicht so machen”-Baseline, und genau das, was ich in jener Dienstagnacht um 3 Uhr in Produktion hatte.
Batched INSERT: Events zu Batches von 500 Zeilen sammeln, in einem einzigen Multi-Row-INSERT schreiben. 20 Verbindungen in einem Pool.
COPY-Protokoll: Postgres hat ein eingebautes Protokoll für Massendaten namens COPY. Es streamt tabgetrennte Daten direkt in die Tabelle und umgeht den SQL-Parser komplett. Das benutzen ETL-Tools. Batches von 5.000 Zeilen, durchgeschoben mit pg-copy-streams.
Pooled + Batched: Dieselbe Batching-Strategie, aber über pgBouncer im Transaction-Pooling-Modus geleitet. Das testet, ob Connection Pooling bei hoher Parallelität spürbar Durchsatz bringt.
Die Zahlen
Bei 1.000 simulierten Nutzern, die je ~1.900 Events mit ~3KB erzeugen, schreiben wir etwa 1,8 Millionen Zeilen realistischer Telemetrie in drei Tabellen. Echte JSONB-Payloads mit Modellnamen, Token-Zahlen, Kostendaten und Session-IDs. Die Art Daten, die du in einer echten KI-Telemetrie-Pipeline sehen würdest.
Das ist passiert.
Writes pro Sekunde
Bei wenig Last sieht alles ungefähr gleich aus. Du erkennst keinen Unterschied zwischen den Strategien, wenn du ein paar tausend Zeilen schreibst.
Skalierst du hoch, laufen die Linien auseinander. Der naive Ansatz stagniert bei etwa 14.000 Writes pro Sekunde und bleibt dort. Der Flaschenhals ist der Overhead pro Statement und der Kampf um Verbindungen.
Das COPY-Protokoll schafft 67.000 Writes pro Sekunde. Auf einem Laptop. In einem Docker-Container. Mit max_wal_size auf 4GB und Standard-Checkpoint-Einstellungen. Bei 3KB-Payloads sind das grob 200MB/s Dauerschreibdurchsatz. Hätten wir Checkpoint-Timeouts und WAL-Kompression getunt, wäre vermutlich noch mehr drin gewesen.
Eine Einschränkung bei COPY: Es gilt ganz oder gar nicht. Hat eine einzige Zeile in einem Batch von 5.000 kaputtes JSON oder verletzt eine Constraint, scheitert der ganze Batch. Batched INSERT lässt dich mit ON CONFLICT-Klauseln sauber mit Duplikaten und unsauberen Daten umgehen. In Produktion validieren wir vor dem COPY vor und weichen bei allem, was zwielichtig aussieht, auf Batched INSERT aus.
Batched INSERT landet mit ~34K Writes pro Sekunde in der Mitte. Solide und praktisch, und du musst dafür keine neue API lernen.
Die Pooled-Strategie über pgBouncer kam auf ~43 bis 49K Writes pro Sekunde, schneller als rohes Batching, weil pgBouncer Verbindungen effizienter wiederverwendet als unser Pool in der Anwendung.
Kopf an Kopf bei 1.000 Nutzern
Bei 1.000 Nutzern (1,8 Mio. Events) schafft COPY 67K Writes pro Sekunde, während der naive Ansatz bei 14K hängt. Das ist fast 5x so schnell für dieselben Daten. Bei diesen Payload-Größen schrieb COPY 916K Events in 13,6 Sekunden. Der naive Ansatz brauchte über eine Minute.
Aber was mich überrascht hat: Keine der Strategien ist umgefallen. Ich hatte erwartet, dass Postgres anfängt zu japsen. Verbindungsfehler, OOM-Kills, aufgeblähtes WAL. Nichts davon ist passiert. Postgres hat es einfach weggesteckt.
Also habe ich natürlich noch eine Schippe draufgelegt.
Ein kurzer Realitätscheck
Damit das klar ist: Wir haben aktuell keine 10 Millionen gleichzeitigen Nutzer, die jeden Tag 1.900 Telemetrie-Events schicken. Hätten wir die, würden wir so viel Geld verdienen, dass wir eine ganze Abteilung einstellen könnten, die sich um Datenbankarchitektur sorgt.
Unser tatsächliches Produktionsvolumen packt das jetzige Postgres-Setup locker, ohne ins Schwitzen zu kommen. Warum also die Simulation auf Millionen Nutzer hochdrehen, die Hunderte Megabyte pro Sekunde erzeugen?
Ausschließlich, um etwas zu beweisen.
Ich wollte wissen, was passiert, wenn die “langweilige” Wahl endlich an die Wand fährt. Und ich habe gesehen: Selbst bei absurden, theoretischen Größenordnungen, in denen Postgres tatsächlich langsamer wird und die Queries schlechter werden, stirbt es nicht einfach. Es degradiert sanft. Und wichtiger noch: Wenn es langsamer wird, hast du Standard-Stellschrauben, ganz ohne Magie, an denen du drehen kannst, um die Geschwindigkeit zurückzuholen.
Der echte Test: Schreiben UND Lesen gleichzeitig
Das Problem mit isolierten Write-Benchmarks: Sie lügen dich an.
In Produktion kannst du das Lesen nicht pausieren, während du Daten aufnimmst. Unsere Analyse-Pipeline fährt Aggregat-Queries gegen diese Daten, während sie geschrieben werden. Perzentilberechnungen über Millionen Spans. Service-Abhängigkeitskarten. Aufschlüsselungen der Fehlerrate. Queries, bei denen Postgres richtig nachdenken muss.
Also habe ich das Naheliegende getan: Streaming-COPY-Writes und 8 analytische Queries gleichzeitig laufen lassen, skaliert von 1.000 Nutzern bis hoch auf 10 Millionen. Für die größeren Tests habe ich 30-Sekunden-Burst-Fenster benutzt, weil bei diesen Schreibraten die 512GB-SSD meines MacBooks in unter 10 Minuten physisch voll wäre.
Bei 1.000 Nutzern (vollständig geschrieben, 1,8 Mio. Events) hielt COPY 39K Writes pro Sekunde durch und bediente gleichzeitig über 7.000 analytische Queries mit einem p95 von 9ms. Die langsamste Query, der Dependency-Map-JOIN, brauchte 1,2 Sekunden.
Bei 10 Millionen simulierten Nutzern, als 30-Sekunden-Burst, hielt Postgres 16.763 Writes pro Sekunde und bediente dabei analytische Queries mit einem p95 unter 200ms. In 30 Sekunden schrieb es 514.536 Zeilen mit 3KB Telemetriedaten. Über alle Skalierungsstufen war die Dependency-Map-Query (ein Self-Join über die Spans-Tabelle) durchgehend die langsamste, mit einem Spitzenwert von etwa 1,2 Sekunden.
Der Write-Durchsatz sinkt, wenn die Tabelle wächst und Lesezugriffe um I/O konkurrieren. Das ist zu erwarten. Aber Postgres ist nie abgestürzt, nie in OOM gelaufen, hat nie etwas korrumpiert. Es wurde langsamer und machte weiter. Jede Query lieferte korrekte Ergebnisse. Jeder Write wurde committet.
Und bedenke: Das lief mit nur 512MB shared_buffers. Die Datenbank war deutlich größer als ihr Cache. Hätte ich ihr auf diesem 24GB-Mac 4GB shared_buffers gegeben, hätte das gesamte Working Set im RAM gelegen, und die Queries wären viel schneller gewesen. Ich habe es absichtlich nicht getan, weil Produktionsdatenbanken nicht immer alles im Speicher halten dürfen.
Okay, aber lassen sich die langsamen Reads fixen?
Die Dependency-Map-Query ließ mir keine Ruhe. Also habe ich Indizes angelegt und den Test gegen ~920K Zeilen wiederholt (die Telemetrie von 500 Nutzern).
Sieben gezielte Indizes: Lookups nach Service-Namen, Filter nach Status-Codes, Trace-ID-Joins, Parent-Span-Beziehungen, zusammengesetzte Metrik-Lookups und Severity-Verteilungen. Danach ANALYZE, um die Statistiken des Query Planners zu aktualisieren.
Das herausragende Ergebnis:
Query für letzte Fehler: 51ms → 1,2ms. Ein 43-facher Speedup. Damit das klar ist: status_code und start_time sind im Schema Spalten auf oberster Ebene und stecken nicht im JSONB-Payload. Die JSONB-Spalte attributes nimmt das Flexible auf (HTTP-Header, Token-Zahlen, Modellnamen, Kostendaten). Die Felder, die wir oft abfragen, sind herausgezogene Spalten mit sauberen Typen. Der Partial Index auf status_code = 2 zusammen mit dem Index auf start_time DESC lässt Postgres den Full Table Scan komplett überspringen. Es läuft einfach den Index entlang und greift die obersten 50.
Log-Severity-Verteilung: 65ms → 33ms. Der zusammengesetzte Index auf (severity, service_name) macht aus einem Sequential Scan einen Index-Only Scan. 2x so schnell.
Die Dependency-Map-Query fiel von 456ms auf 320ms. Eine 1,4-fache Verbesserung. Besser, aber keine Verwandlung. Diese Query macht einen Self-Join über Hunderttausende Zeilen, und kein einzelner Index kann den grundlegenden Aufwand beseitigen, Parent- und Child-Spans in dieser Größe zu korrelieren.
Aber das ist in Ordnung. Es gibt klare Skalierungspfade, wenn du sie brauchst.
Die Skalierungs-Roadmap (wenn du sie wirklich brauchst)
Hier argumentiere ich gegen frühes Überoptimieren. Was wir gebaut haben, funktioniert. Es trägt unsere aktuelle Last bequem, und es trägt 10x, ohne ins Schwitzen zu kommen. Sollten wir aber irgendwann Hunderte Millionen Events verarbeiten, gibt es zwei klare Wege, beide weiter mit Postgres.
Pfad 1: Read Replicas
Der einfachste Skalierungsschritt. Richte ein Streaming-Replication-Replica ein und leite alle analytischen Queries dorthin. Writes gehen an den Primary, Reads an das Replica. Der Primary kann sich voll auf die Ingestion konzentrieren, und das Replica kann komplexe JOINs verdauen, ohne den Write-Durchsatz zu bremsen.
Das ist eine Änderung für einen Dienstagnachmittag. Google Cloud SQL, AWS RDS und Azure unterstützen Read Replicas nativ. Du fügst einen Connection String und eine Routing-Regel hinzu. Dein Write-Durchsatz geht zurück in den Bereich der 67K Writes pro Sekunde der Einzelinstanz, weil er nicht mehr mit Reads kämpft, und deine analytischen Queries dürfen auf dem Replica so lange dauern, wie sie wollen, ohne dass es jemand merkt. Auf richtiger Server-Hardware mit mehr CPU-Kernen und schnellerem Storage läge diese Zahl deutlich höher.
Für unseren Anwendungsfall, in dem die Analyse nicht in Echtzeit laufen muss, ist ein Replica mit ein paar Sekunden Replikationsverzug völlig in Ordnung.
Pfad 2: Table Partitioning
Zeitbasiertes Partitioning zerlegt deine Tabellen in Stücke. Eine Partition pro Tag, pro Woche oder pro Monat. Queries, die nach Zeit filtern, scannen nur die relevanten Partitionen statt der ganzen Tabelle. Die Dependency-Map-Query mit 1,2 Sekunden? Wenn du nur die letzten 24 Stunden statt der gesamten Historie ansiehst, scannst du einen Bruchteil der Daten. Die Query fällt von Sekunden auf Millisekunden.
Partitioning macht auch das Datenlebenszyklus-Management trivial. Du willst Daten löschen, die älter als 90 Tage sind? DROP TABLE spans_2025_q4. Kein Vacuum, kein Bloat, keine gesperrte Tabelle. Sofort.
Die Mega-Skalierung
Müssten wir wirklich komplett durchdrehen, mit Hunderten Millionen Nutzern und Milliarden Telemetrie-Events, hat Postgres auch dafür eine Antwort. Citus ist eine Postgres-Extension (von Microsoft vollständig als Open Source freigegeben), die deine Daten per Hash-Sharding auf mehrere Postgres-Knoten verteilt. Du führst CREATE EXTENSION citus; aus, und es geht los. Dein Schema bleibt gleich. Deine Queries bleiben gleich. Es arbeiten nur mehr Knoten.
Shardest du die Spans-Tabelle nach trace_id, bearbeitet jeder Knoten einen Ausschnitt des Gesamtdatensatzes. Eine Query nach einem bestimmten Trace trifft einen Knoten. Eine Aggregat-Query fächert über alle Knoten auf und führt die Ergebnisse zusammen. Lineare horizontale Skalierung, und du bleibst im Postgres-Ökosystem. Dasselbe Tooling, dasselbe Monitoring, dieselbe Expertise.
Wir brauchen das nicht. Wir werden es wahrscheinlich sehr lange nicht brauchen. Aber dass dieser Weg existiert, ohne Postgres zu verlassen, ist genau der Grund, warum es richtig war, einfach anzufangen.
Nicht zu früh überoptimieren
Eins will ich ganz klar sagen: Die Architektur, die wir gerade betreiben, ist nicht die optimalste für dieses Problem. Sie ist die angemessenste.
Wir haben eine Datenpipeline. Wir können komplexe Queries fahren, während wir Writes drauf feuern. Sie hält parallele analytische Workloads aus. Sie fällt nicht um. Warum sollte ich dafür ein anderes Datenbankprodukt brauchen?
Klar, irgendwann wird vielleicht Disk-I/O zur Grenze. Aber es gibt belastbare Skalierungsoptionen, und wir wissen genau, welche, weil wir gemessen haben, wo die Flaschenhälse sitzen. Das ist der Vorteil, einfach anzufangen: Wenn du skalieren musst, weißt du, was du skalieren musst.
Wir hätten mit ClickHouse starten können. Wir hätten Citus von Tag eins an aufsetzen können. Wir hätten eine Lambda-Architektur mit Kafka bauen können, mit einem Stream Processor und einer Serving Layer und einer Batch Layer und einer… du verstehst schon.
Aber all diese Komplexität hat ihren Preis. Jedes weitere System ist ein weiteres Ding, das überwacht werden muss, das um 3 Uhr nachts ausfallen kann, das dein Team verstehen muss. Wenn du nicht die Größenordnung hast, die das rechtfertigt, zahlst du die Komplexitätssteuer, ohne den Skalierungsvorteil zu bekommen.
Postgres auf Cloud SQL, mit Redis als Koordinationsschicht, und das COPY-Protokoll für die Batch-Ingestion. Das ist unsere Architektur. Sie trägt unsere aktuelle Last. Sie trägt 10x unsere Last. Und wenn sie 100x nicht mehr trägt, wissen wir genau, wo die Flaschenhälse sind, weil wir sie gemessen haben.
Die gute Nachricht: Wir brauchen diesen Service nicht schnell. Die Telemetriedaten werden in Batches analysiert, nicht in Echtzeit an Nutzer ausgeliefert. Eine Query, die 4 Sekunden statt 40 Millisekunden braucht, ist für uns völlig in Ordnung.
Aber müssten wir doch schnell sein… tja, du hast die Zahlen gesehen. Da ist reichlich Luft nach oben.
Was ich wirklich gelernt habe
- Benutze für Writes mit hohem Durchsatz nie einzelne INSERTs. Batch oder COPY. Immer.
- Das COPY-Protokoll gibt es aus gutem Grund. Es ist nicht nur für initiale Datenladungen da. Es ist eine legitime Ingestion-Strategie für Produktion.
- Connection Pooling ist im großen Maßstab Pflicht. pgBouncer im Transaction-Modus. Keine Ausreden.
- Teste Schreiben UND Lesen zusammen. Reine Write-Benchmarks führen in die Irre. Das echte Leistungsprofil siehst du erst, wenn deine Datenbank beides gleichzeitig macht.
- Indizes zählen, aber nicht für alles. Ein gut platzierter Partial Index kann bei gezielten Queries einen 43-fachen Speedup bringen. Aggregat-Queries über Millionen Zeilen bleiben aber so oder so langsam. Dann greifst du zu Replicas und Partitioning.
- Nicht zu früh überoptimieren. Starte mit der einfachsten Architektur, die funktioniert. Miss, wo die Flaschenhälse sind. Skaliere die Teile, die Skalierung brauchen. Nicht alles, nicht alles auf einmal.
- Postgres kann mehr, als die Leute ihm zutrauen. 67K Writes pro Sekunde mit 3KB-Telemetrie-Events. 39K Writes pro Sekunde bei gleichzeitigen analytischen Queries. Hochskaliert auf 10M simulierte Nutzer, und es ist trotzdem nicht abgestürzt. Auf einem Laptop. In Docker.
- Teste deine Annahmen. Ich habe Wochen damit verbracht, Blogposts über Data-Warehouse-Vergleiche zu lesen. Ich hätte einen Nachmittag mit Docker und einem Skript verbringen und echte Zahlen haben können. Die Zahlen erzählten die bessere Geschichte.
Ach ja, und noch etwas. Wir haben Postgres in diesem ganzen Post durch die Hölle geschickt. Millionen Zeilen, parallele Reads, Self-Joins über riesige Spans. Und an das Redis davor haben wir keinen Gedanken verschwendet. Das Redis, das all das koordiniert, den Datenstrom der eingehenden Telemetrie abfängt, Verweise verfolgt, den Batch-Zustand verwaltet. Es hat nicht mal gezuckt. Wir haben es nicht gebenchmarkt, weil es nichts zu benchmarken gab. Es hat einfach funktioniert.
Die meisten Probleme lassen sich wirklich mit einem Webserver, einem Redis und einem Postgres lösen.
Der Benchmark-Code ist in Go geschrieben und liegt unter github.com/willhackett/bench-postgres. docker-compose up -d, go run . -mode=full für die Schreibstrategien, go run . -mode=chaos für den kombinierten Write-plus-Read-Chaos-Test und go run . -mode=optimize für den Indexvergleich.