|
| 1 | +# Design: Replica Peak Resource Usage |
| 2 | + |
| 3 | +## Summary |
| 4 | + |
| 5 | +Replica resource usage is visible only as periodic samples taken by the orchestrator, roughly one |
| 6 | +per minute, landing in `mz_cluster_replica_metrics_history`. A memory spike between two samples is |
| 7 | +invisible, so the usage of a hydration episode that starts and finishes inside one sampling gap |
| 8 | +cannot be recovered at all. |
| 9 | + |
| 10 | +This adds a measured high-water mark instead: each replica process tracks the peak memory, heap |
| 11 | +and disk usage it has reached, and compute introspection exposes those peaks as |
| 12 | +`mz_introspection.mz_cluster_peak_usage`. |
| 13 | + |
| 14 | +## Semantics |
| 15 | + |
| 16 | +Peaks are monotonic for the lifetime of the process, and are never reset. |
| 17 | + |
| 18 | +The alternative, max-since-last-flush, makes a window's peak recoverable by taking the maximum of |
| 19 | +the rows in that window. It also makes every reader destructive: whoever reads first consumes the |
| 20 | +value, a retried read loses a peak, and two consumers cannot both see the same episode. In a |
| 21 | +system where the peak is read by ad-hoc SQL, that is not workable. |
| 22 | + |
| 23 | +Monotone peaks still answer the question hydration visibility asks. A replica starts fresh, so at |
| 24 | +the moment it finishes hydrating, its since-start peak *is* its hydration peak. More generally, |
| 25 | +for a monotone series `M`, the peak over a window `(t1, t2]` is `M(t2)` whenever `M(t2) > M(t1)`, |
| 26 | +and is otherwise bounded above by `M(t1)`. |
| 27 | + |
| 28 | +The cost is that peaks live and die with the process. A restarted replica reports the peaks of its |
| 29 | +new process, and the old ones are gone. Persisting peaks across replica lifetimes is deliberately |
| 30 | +not part of this work. |
| 31 | + |
| 32 | +## Precision |
| 33 | + |
| 34 | +`memory_bytes` is exact. `getrusage`'s `ru_maxrss` is a high-water mark the kernel maintains |
| 35 | +itself, so no spike can pass between two of our observations unseen. |
| 36 | + |
| 37 | +`heap_bytes` and `disk_bytes` have no such kernel-side counter, so they are maxima over samples |
| 38 | +and are therefore lower bounds: a spike shorter than the sampling interval can be missed. The |
| 39 | +sampling interval is `mz_metrics_peak_usage_refresh_interval` (5s by default), separate from |
| 40 | +`memory_limiter_interval`, which governs OOM-kill behavior and must not be retuned for |
| 41 | +introspection's sake. |
| 42 | + |
| 43 | +`heap_bytes` folds in the `memory_bytes` peak, since peak heap is at least peak memory. That keeps |
| 44 | +`heap_bytes >= memory_bytes` true even when `ru_maxrss` catches a spike that sampling missed. |
| 45 | + |
| 46 | +## Where the peaks are measured |
| 47 | + |
| 48 | +In `mz_metrics::usage`, on the periodic task that already samples `rusage` and lgalloc stats. That |
| 49 | +task runs on the tokio runtime, independent of the timely workers. |
| 50 | + |
| 51 | +The tempting alternative, folding the maximum inside the compute logging operator, samples exactly |
| 52 | +where sampling is least reliable: a logging operator only runs when its worker schedules it, and a |
| 53 | +worker saturated by hydration is precisely the case whose peak we want. Keeping the fold in the |
| 54 | +sampler also means a slow reader cannot lose a peak, because it reads an already-monotone value |
| 55 | +rather than a series of instantaneous ones. |
| 56 | + |
| 57 | +## How the peaks reach SQL |
| 58 | + |
| 59 | +A new `ComputeLog::PeakUsage` logging dataflow reads the process-global peaks and emits one row |
| 60 | +per process, following `ComputeLog::PrometheusMetrics`: the usage is per-process, not per-worker, |
| 61 | +so one worker per process reports and the rest drop their capability. |
| 62 | + |
| 63 | +The peaks are also registered as Prometheus gauges (`mz_metrics_peak_*_bytes`), which costs |
| 64 | +nothing extra since the sampler already holds the values, and which makes them scrapable without |
| 65 | +going through SQL. |
| 66 | + |
| 67 | +## Alternatives considered |
| 68 | + |
| 69 | +- **Extend `/api/usage-metrics` and `mz_cluster_replica_metrics_history`.** Gets 30-day retention |
| 70 | + and after-the-fact queryability for free. Rejected as the primary surface because the transport |
| 71 | + is a poll of an HTTP endpoint by the orchestrator, so reporting a peak means either making the |
| 72 | + endpoint destructive or duplicating peaks across polls, and because the peaks then inherit the |
| 73 | + orchestrator's poll cadence. Worth revisiting as the persistence story. |
| 74 | +- **Reuse `mz_cluster_prometheus_metrics`.** The gauges appear there automatically, so this needs |
| 75 | + no catalog change at all. Rejected as the primary surface: values are untyped `double`, the |
| 76 | + relation is a debugging escape hatch rather than a documented contract, and it is gated by its |
| 77 | + own scrape-interval dyncfg, so a config change would silently remove the peaks. |
0 commit comments