Case study: Metrics monitoring and alerting
Design a planet-scale time-series metrics and alerting system with Gorilla chunk compression, cardinality protection, and resilient alerting.
35 min9 steps
By the end you can
- Estimate active time-series cardinality, sample ingestion rate, and storage savings from Gorilla delta-of-delta and XOR compression
- Explain why label cardinality — not sample rate — is the primary scaling limit of a time-series database
- Design a decoupled ingestion, in-memory head chunk, and step-aligned query caching pipeline
- Predict how the monitoring and alerting pipeline survives incident query storms, ingester restarts, and alert flapping
Pin down what a metrics system must survive
Features
- 10 s samples; label/range queries; rollups; alert ticks 15-30 s
Targets
- 85-95% writes; millions samples/s; query p99 <500 ms
Isolation
- Never block production; alerts survive dashboard storms
Calculate cardinality, sample rate, and Gorilla compression
Active series
50M
200k hosts * 250 series
Samples
5M/s
50M / 10 s; 1,000/batch gives 5k requests/s
Raw
6.9 TB/day
5M * 16 B * 86,400
Compressed
590 GB/day
1.37 B/sample; 12x reduction
Head samples
49 GB
5M * 7,200 s * 1.37 B; labels/index extra
Design batched ingestion and step-aligned queries
Ingest
POST /v1/metrics/write; Snappy Protobuf; tenant quota returns 429
Query
GET /v1/query_range?query=&start=&end=&step=15s
Cache
Snap bounds to step; reuse immutable history
Alert
POST /v1/rules; expr, for, labels, annotations
Model series IDs, the inverted label index, and chunks
Series
Canonical labels map to seriesId; label=value postings
Intersect
job=api AND status=500 finds matching series IDs
Chunks
seriesId maps to compressed two-hour blocks; label metadata separate
Draw the ingestion, storage, and query pipeline
Order the lifecycle of a metric sample from host collection to long-term range query.
- 1Local agent scrapes or collects metrics and sends a compressed batch of ~1,000 samples to a Distributor
- 2Every 2 hours, the ingester compacts and flushes immutable chunks and the inverted label index to long-term block storage
- 3Ingester consumes the batch and compresses `(ts, value)` into the seriesId's in-memory 2-hour Gorilla head chunk
- 4Distributor checks cardinality quotas and appends the batch to the seriesId's partition in the durable metric stream
Deep dive: Gorilla compression and resilient alerting
Timestamps
- Regular interval: delta-of-delta zero, one bit
Values
- Unchanged XOR: one bit; otherwise changed middle bits
Alerts
- Live head; true every tick for 5 minutes before firing
Heartbeat
- External switch pages after 3 missed minutes
Compare the alternatives
metrics-sql-rows
- Indexed row/sample; goodput <70%
Pull
- Target-down detection; central rate control
Push
- Firewalls/ephemeral jobs; batched remote-write
Retention
- 10 s/15 days; 1 min/90 days; 1 h/2 years; min/max/sum/count
Run the failure scenarios
Inject
Ingest + queries x2
Observe
Partitioned log; step cache
Recover
>99% goodput
Inject
Workers down 1.3-1.8 s
Observe
walqueue accepts batches
Recover
Workers replay and catch up
Inject
Flush at 1.5 s
Observe
Read TSDB; refill cache
Recover
Writes unaffected
Wrap up and self-assess
Guard
Tenant cardinality limits; reject unbounded labels
Batch
500-2,000 samples/request; two-hour chunks
Isolate
Independent failure domain; external heartbeat
Answer this step to continue.