Approach: keep incremental, outlier-resistant summaries per small time bucket (e.g., 1-minute). Use t-digest per bucket (good for streaming percentiles, mergeable, robust to extreme outliers). Maintain a fixed-size circular buffer for last 7 days of buckets, merge t-digests on demand (or maintain an incremental merged digest updated as oldest buckets expire) to compute rolling p99 quickly. Persist raw digests (not raw samples) to durable store for recovery.Code (sketch using tdigest package):python
from tdigest import TDigest
from collections import deque, defaultdict
import time
import threading
BUCKET_SECONDS = 60
WINDOW_SECONDS = 7 * 24 * 3600
NUM_BUCKETS = WINDOW_SECONDS // BUCKET_SECONDS
class RollingSLI:
def __init__(self, bucket_seconds=BUCKET_SECONDS):
self.bucket_seconds = bucket_seconds
self.num_buckets = NUM_BUCKETS
self.buckets = deque(maxlen=self.num_buckets) # each item: (bucket_ts, TDigest)
self.lock = threading.Lock()
self.current_bucket_ts = None
def _bucket_ts(self, ts):
return int(ts // self.bucket_seconds) * self.bucket_seconds
def ingest(self, latency_ms, ts=None):
ts = ts or time.time()
bts = self._bucket_ts(ts)
with self.lock:
if not self.buckets or self.buckets[-1][0] != bts:
# append new bucket
self.buckets.append((bts, TDigest()))
# ensure last bucket matches
if self.buckets[-1][0] != bts:
# gap handling: create empty buckets until bts
while not self.buckets or self.buckets[-1][0] < bts:
next_ts = (self.buckets[-1][0] + self.bucket_seconds) if self.buckets else bts
self.buckets.append((next_ts, TDigest()))
self.buckets[-1][1].update(latency_ms)
def compute_p99(self):
with self.lock:
merged = TDigest()
for _, td in self.buckets:
merged = TDigest.merge(merged, td) # uses tdigest's efficient merge
return merged.percentile(99)
def availability_sli(self, threshold_ms):
p99 = self.compute_p99()
return p99 < threshold_ms, p99
Key points:- Per-minute TDigest: memory ~ O(NUM_BUCKETS * size_per_digest). Typical t-digest small (hundreds of bytes) so total memory manageable (MBs).- Merge cost: O(B * k) where B=num buckets, k = centroids per digest. Computing p99 is O(B*k). Incremental optimization: maintain a running merged digest; on ingest only merge new bucket into running and subtract expired bucket by rebuilding (or maintain count-based reference — subtraction not trivial for t-digest) — practical approach: recompute merged digest periodically (e.g., every minute) or maintain hierarchical digests (tree) to reduce merge cost to O(log B * k).- Storage: persist per-bucket serialized t-digests to object store or TSDB (compressed blobs). For HA, replicate recent buckets.- Time/Space complexity: - Ingest: amortized O(log k) per update (t-digest update) - Compute p99 (naive): O(B * k) - Space: O(B * k)- Edge cases: sparse traffic (empty buckets), clock skew (use ingestion timestamps), very high cardinality/memory — increase bucket size or sample before digesting.- Alternatives: HDR Histogram for fixed-range fast merges; counting sketches if only binary availability needed.This design yields outlier-resistant, mergeable, incremental summaries suitable for SRE-grade rolling SLIs.