compute: expose replica resource usage observations - #38250
Conversation
767b1a7 to
5ce94b3
Compare
b53e4fe to
40e0406
Compare
e27eda1 to
11d5bec
Compare
|
|
||
| ## `mz_cluster_resource_usage` | ||
|
|
||
| The `mz_cluster_resource_usage` source reports the resource usage of each process of a cluster |
There was a problem hiding this comment.
Some of the text here gets a bit lost in the weeds. Plus, could we have a table that lists the sources and what metric they expose, so that I can have an overview of what's there?
There was a problem hiding this comment.
Restructured. The section now leads with two short paragraphs, then a table of the four sources and what each one measures, then a table of every metric with its source and meaning. The nuance that was buried in prose moved into the meaning column, and what is left as prose is a short "Interpreting" list of the questions people actually ask and which metric answers them.
I also added an example query, since a couple of people have asked for the same thing: how close each process came to a memory-limiter kill, comparing proc_status heap and heap_peak against mz_cluster_replica_metrics.heap_limit. Verified against staging.
Posted by Claude Code
| @@ -0,0 +1,243 @@ | |||
| # Design: Replica resource usage observations | |||
There was a problem hiding this comment.
To me, most of the explanation is a bit to dense to read with this measly human brain. I think the design that matters is mostly:
- we're adding a builtin collection, it lives on the replica
- it reports different metrics from different sources, so would be good to have a table of the sources and the metrics they report, maybe a matrix/2d table even?
- and we say that we have useful peak metrics because they prevent problems with sampling
and a lot of the other discussion can go into a technical appendix or be removed to a degree
But that depends on how useful we want the design doc to be for humans later on.
There was a problem hiding this comment.
Resolved by dropping the design doc entirely: the change is not contentious enough to need one. What you asked for as the readable core is now split by audience. The three decisions (builtin collection living on the replica, multiple sources, peaks reported rather than sampled downstream) are in the PR description, and the source and metric tables plus the interpretation guidance are a user-facing page, manage/monitor/replica-resource-usage.md.
Posted by Claude Code
11d5bec to
2949d94
Compare
DAlperin
left a comment
There was a problem hiding this comment.
LGTM. Agree with Aljoscha on the design doc notes though if we can clean that up first
| <!-- RELATION_SPEC_UNDOCUMENTED mz_introspection.mz_compute_operator_durations_histogram_per_worker --> | ||
| <!-- RELATION_SPEC_UNDOCUMENTED mz_introspection.mz_compute_operator_durations_histogram_raw --> | ||
|
|
||
| ## `mz_cluster_resource_usage` |
There was a problem hiding this comment.
A better structure could be to extract the detailed description and interpretation to a new page, which we link here. What stays is the description of the relation itself.
There was a problem hiding this comment.
Done. mz_introspection.md now carries only what the relation is: two short paragraphs plus the column table. The source table, the metric table, the interpretation list and the example query moved to a new page, manage/monitor/replica-resource-usage.md, linked from the section.
Posted by Claude Code
| They are sampled roughly once a minute, so a spike shorter than the sampling interval is not | ||
| visible here at all. For a higher-cadence view of one replica, including high-water marks that | ||
| survive a spike the sampling missed, see | ||
| [`mz_introspection.mz_cluster_resource_usage`](/reference/system-catalog/mz_introspection/#mz_cluster_resource_usage). |
There was a problem hiding this comment.
If documented, link the feature flag that controls the sampling frequency.
There was a problem hiding this comment.
It is not documented, and I did not add it. There is no user-facing page for dyncfgs at all, and mz_metrics_usage_refresh_interval is replica-scoped and not settable by a user, so naming it here would point readers at a knob they cannot reach. Instead the cross-reference now states the cadence: "sampled every few seconds" in both mz_cluster_replica_metrics and _history, against "roughly once a minute" for the orchestrator samples, which is the part a reader is actually choosing between.
Posted by Claude Code
There was a problem hiding this comment.
I think we can do without a design doc. This change isn't contentious enough to need one.
There was a problem hiding this comment.
Removed. The parts worth keeping are already elsewhere: the three design decisions and the syscall cost table are in the PR description, and the reader-facing half is the new manage/monitor/replica-resource-usage.md page.
Posted by Claude Code
| // Advance the capability to the next logging interval boundary, and schedule the next | ||
| // activation there, so the output frontier progresses at the logging rate without | ||
| // drifting from wall-clock elapsed time. The capability is downgraded on this | ||
| // operator's own timer rather than on the sampler's, so a sampler that stops ticking | ||
| // cannot freeze this collection's frontier. | ||
| let elapsed = now.elapsed().as_millis(); | ||
| let time_ms: u128 = | ||
| ((elapsed + start_offset.as_millis()) / interval_ms + 1) * interval_ms; | ||
| let ts: Timestamp = time_ms.try_into().expect("must fit"); | ||
| cap.downgrade(&ts); | ||
|
|
||
| let next_boundary_ms = time_ms - start_offset.as_millis(); | ||
| let next_activation = | ||
| now + Duration::from_millis(next_boundary_ms.try_into().expect("must fit")); | ||
| activator.activate_after(next_activation.saturating_duration_since(Instant::now())); |
There was a problem hiding this comment.
I think this is the same logic we have for the prometheus collector? If yes, extract a common function.
There was a problem hiding this comment.
Yes, twice over. Both were extracted into mz_compute::logging:
downgrade_to_interval_boundary(cap, activator, now, start_offset, interval_ms) -> Timestampfor the capability downgrade plus the boundary-aligned reactivation.emit_snapshot_diff(session, packer, prev, current, ts, pack)for the retract-changed-and-removed, insert-changed-and-new loop, generic over the key and value types.
pack takes the packer as an argument instead of closing over it, because it returns rows borrowed from it and a closure cannot lend from its own captures.
Posted by Claude Code
2949d94 to
959bb91
Compare
Replica resource usage is visible only as periodic samples taken by the orchestrator, roughly one per minute, so a spike between two samples is invisible. A hydration episode that starts and finishes inside one sampling gap cannot be recovered at all. Add `mz_introspection.mz_cluster_replica_resource_usage`, reporting what each measurement source says about each replica process, one row per `(process_id, source, metric)`. `mz_metrics::usage` reads cgroup v2 interface files, `getrusage`, `/proc/self/status` and `statvfs` on the existing periodic metrics task, and a new `ComputeLog::ResourceUsage` logging dataflow reports the observations. The relation surfaces uninterpreted observations. It does not decide which number is "the" memory usage of a replica, and it never combines two sources into a third figure. Sources measure overlapping but distinct quantities: cgroup memory charges page cache, kernel memory and socket buffers to the replica while `VmRSS` charges none of them, and the disagreement is the diagnostic signal rather than noise to be averaged away. Combining sources belongs in views, where it can change without a migration and where a reader can see what was combined. Columns are `(process_id, source, metric, value)` rather than one column per metric. A wide row retracts and re-inserts on any field's change, so its churn is set by its noisiest field, which would drag a memory peak that has not moved through a retraction every sample. Long rows also make a missing observation representable as an absent row rather than as an in-band sentinel, and let a new metric be added without a catalog migration. Peaks are observations too, not derived values. A peak cannot be recovered downstream from this relation, because each sample retracts the previous one and a reduce over the result sees a single value per key; retaining the samples instead is not an option, since a compute log collection is an arrangement with no retention mechanism available to it. Most peaks cost nothing to report because the kernel maintains them: `cgroup memory.peak` and `memory.swap.peak` are exact high-water marks of the cgroup accounting, and `ru_maxrss` is already one. Only a source with no kernel-side peak gets one folded in the sampler, and those are reported under a distinct `_peak` name so a reader can tell an exact peak from a sampled one. One of those matters more than the rest. The memory limiter enforces `vm_rss + vm_swap` against a limit that is the replica's memory limit plus its disk limit, because swap is not paged back in eagerly, so a RAM ceiling alone bounds nothing and Kubernetes cannot express a swap limit. The kernel maintains no peak of that sum, so "how close did this replica come to a limiter kill" is answerable only from below: `heap_peak` is folded from samples of the sum and bounds its peak from below. No upper bound is available. The cgroup peaks describe the cgroup's charge, which excludes the replica's resident file-backed pages and is a systematically smaller quantity, and `ru_maxrss` is refreshed at kernel checkpoints rather than on every fault and has been observed reading below the concurrent `vm_rss` in the same sample. The logging operator downgrades its capability on its own timer rather than on the sampler's, so disabling sampling cannot freeze the collection's frontier and wedge queries over the relation. No source read here is proportional to heap size. `VmRSS` and `VmSwap` come from per-mm counters and the cgroup figures are `page_counter` reads. Measured on a 32-core Linux host, `/proc/self/status` costs 4.67 us with an empty heap and 4.74 us with 32 GiB resident. The neighbouring `smaps_rollup` and `numa_maps` walk page tables and cost 137 ms and 142 ms at 32 GiB, so they must stay out of this path. Incidental refactors: `ProcStatus` and the `statvfs` helper move from `mz_compute::memory_limiter` to `mz_metrics::usage` so the limiter, the clusterd usage collector and the sampler share one reader. The cgroup helpers move from `mz_ore::memory` to `mz_ore::cgroup` and lose their `target_os = "linux"` gate, since a second crate now uses them and the file is pure `std`; the gate expressed which platform the readings are meaningful on, which the readers already handle by returning `None`. The module gains a `CgroupV2` handle that resolves the cgroup directory once and reads its interface files, instead of a fourth copy of the `/proc/self/cgroup` parse. It exposes the resolved path rather than logging it, so that the module keeps its property of depending on nothing outside `std`: it is compiled unconditionally now, including into the wasm32 build where `mz_ore`'s optional dependencies are absent. The Prometheus metrics operator reports a sampled source in the same shape, so the interval-boundary capability downgrade and the snapshot diff become `downgrade_to_interval_boundary` and `emit_snapshot_diff` in `mz_compute::logging`, and both operators call them. Adding a builtin log changes the inlined-VALUES fingerprints of `mz_catalog.mz_indexes` and `mz_catalog.mz_sources`, so both get a `MigrationStep::replacement` at the current dev version.
959bb91 to
1764cd4
Compare
|
Renamed the relation from The old name was wrong on the facts. The data is one replica's processes, the row carries
One wrinkle worth stating: every existing OID stays 17123. Posted by Claude Code |
Motivation
Replica resource usage is only visible as periodic orchestrator samples, roughly one per minute, landing in
mz_cluster_replica_metrics_history. A spike between two samples is invisible, so the usage of a hydration episode that starts and finishes inside one sampling gap cannot be recovered at all. This is the replica-local, higher-cadence half of the Improved Hydration Visibility work.Description
mz_metrics::usagereads cgroup v2 interface files,getrusage,/proc/self/statusandstatvfson the existing periodic metrics task. A newComputeLog::ResourceUsagelogging dataflow reports them asmz_introspection.mz_cluster_replica_resource_usage(process_id, source, metric, value), following theComputeLog::PrometheusMetricsshape: usage is per-process, so one worker per process reports and the rest drop their capability.Three decisions the diff cannot show.
The relation surfaces uninterpreted observations. It does not decide which number is "the" memory usage of a replica, and it never combines two sources into a third figure. Sources measure overlapping but distinct quantities:
cgroup memory.currentis the accounting that limit enforcement and the OOM killer act on, whileVmRSSis the process's resident set. They disagree by a measured ~96 MiB on staging, in the direction ofVmRSSbeing the larger, because a page is charged to whichever cgroup first faulted it in and this binary's resident text was faulted in by the runtime that unpacked the image. That disagreement is the diagnostic signal, and it is not recoverable once two sources have been folded into one number. Combining sources belongs in views, where it can change without a migration and where a reader can see what was combined. For what fusing costs, note thatmz_cluster_replica_metrics.disk_bytescurrently reports disk plus swap because a consumer wanted it displayed that way, and the fix is blocked on porting that consumer.Long rows, not one column per metric. A wide row retracts and re-inserts on any field's change, so its churn is set by its noisiest field: a filesystem-usage number that moves every sample would drag a memory peak that has not moved in an hour through a retraction every time. Long rows churn only what changed. They also make a missing observation representable as an absent row rather than an in-band sentinel, and let a new metric be added as kernels gain interface files without a catalog migration.
Peaks are reported, not derived. A peak cannot be recovered downstream from this relation: each sample retracts the previous one, so a
reducecomputing a maximum sees one value per key and returns it. Retaining the samples instead is not an option, because a compute log collection is an arrangement with no retention, window or TTL mechanism available to it, so a retained history would grow unbounded for the process's lifetime. Most peaks cost nothing to report because the kernel maintains them:cgroup memory.peakandmemory.swap.peakare exact high-water marks of the cgroup accounting, kept per cgroup for the container's lifetime and therefore unaffected by how rarely we read them, by the sampler being disabled and restarted, or byenvironmentdrestarting;ru_maxrssis already a high-water mark. Only a source with no kernel-side peak gets one folded in the sampler, currently the scratch filesystem and swap below the kernel version providingmemory.swap.peak, and those are reported under a distinct_peakmetric name so a reader can tell an exact peak from a sampled one.The logging operator downgrades its capability on its own timer rather than on the sampler's. Were the sampler to drive progress, disabling it (which
mz_metrics_usage_refresh_interval = 0is meant to allow) would freeze the collection's frontier and wedge every query over the relation along with themz_catalog_serverindexes on it.Cadence is a new dyncfg,
mz_metrics_usage_refresh_interval(5s), kept separate frommemory_limiter_interval, which governs OOM-kill behavior and must not be retuned for introspection's sake.Cost
No source read here is proportional to heap size.
VmRSSandVmSwapcome from per-mm counters and the cgroup figures arepage_counterreads. Measured on a 32-core Linux host, p50 over 2000 iterations:getrusage(RUSAGE_SELF)statvfs/proc/self/status/proc/self/smaps_rollup/proc/self/numa_mapsThe last two rows are not read by this path and must not be added to it. They walk page tables, so their cost scales with resident pages and rises superlinearly. Held at 16 GiB resident,
numa_mapscosts 28.9 ms over one fully-touched VMA, 11.0 ms over 8192 VMAs, and 1.5 ms when only every 64th page is written, confirming the cost tracks page-table entries rather than VMA count.At the 5s default the whole sampler is roughly 0.0002% of one core.
Incidental refactors
ProcStatusand thestatvfshelper move frommz_compute::memory_limitertomz_metrics::usage, so the limiter, the clusterd usage collector and the sampler share one reader. The cgroup helpers move frommz_ore::memorytomz_ore::cgroupand lose theirtarget_os = "linux"gate, since a second crate now uses them and the file is purestd. The module gains aCgroupV2handle that resolves the cgroup directory once and reads its interface files, rather than this becoming a fourth copy of the/proc/self/cgroupparse. It exposes the resolved path rather than logging it, so the module keeps its property of depending on nothing outsidestd: it is compiled unconditionally now, including into the wasm32 build wheremz_ore's optional dependencies are absent. The three existing cgroup limit readers (usage_metrics.rs,ore::cgroup,service::boot) are deliberately left alone; consolidating them is worth doing separately.The Prometheus metrics operator reports a sampled source in the same shape, so the interval-boundary capability downgrade and the snapshot diff become
downgrade_to_interval_boundaryandemit_snapshot_diffinmz_compute::logging, and both operators call them.Docs: the relation is described in
mz_introspection.md, and the sources, the metrics they report, how to interpret them and an example query live on a new page,manage/monitor/replica-resource-usage.md.Adding a builtin log changes the inlined-VALUES fingerprints of
mz_catalog.mz_indexesandmz_catalog.mz_sources, so both get aMigrationStep::replacementat the current dev version.Verification
mz_metrics::usage: a sample is non-empty, a derived peak rises with a higher observation and survives a lower one, an unreadable source is absent rather than zero, and a derived peak is only published for a source that was actually read.test-resource-usageintest/cluster/mzcompose.py: asserts both processes of a 2-process replica report, that they report the same metric set, thatrusageandproc_statusare present, and that no peak goes backwards across a workload. cgroup metrics are deliberately not asserted, sincememory.peakandmemory.swap.peakdepend on the kernel version.autogenerated/mz_introspection.sltneededci/test/lint-docs-catalog.sh --rewrite, not just--rewrite-results: the relation name is baked into the query text, and a plain rewrite silently produced an empty result.memory_peakandswap_peakare both present on the node kernel, andcgroup memory_currentmatches cAdvisor'scontainer_memory_working_set_bytesfor the same container to within 1.5% across every pod, so the cgroup directory resolution is correct.memory_current1.20 GiB /swap_current695 MiB whilememory_peakwas 3.79 GiB andswap_peak3.98 GiB: the instantaneous readings understate the episode by 3.2x and 5.9x. Sampled again minutes later,memory_currenthad risen 2.46 GiB andswap_currentfallen 616 MiB as pages swapped back in, with both peaks byte-identical. That is the monotonicity property under a bidirectional swing.VmRSSruns a roughly constant 96 MiB above the replica's own cgroup charge (7% spread across replicas spanning 36 MiB to 400 MiB ofmemory_current), because this binary's resident text is charged to the runtime that unpacked the image. Takingru_maxrssas the replica's footprint, as the previous design did, overstates the enforced figure by 2.75x on an idle replica and 1.20x on a loaded one.memory.events'maxstayed 0 even with the cgroup pinned at its ceiling, because reclaim succeeded by swapping rather than failing. It is therefore not a reliable limit-hit signal where swap is configured;memory_peakreachingmemory_maxis. Both are reported without interpretation.Release notes
This release adds
mz_introspection.mz_cluster_replica_resource_usage, reporting the resource usage of each cluster replica process as measured by each available source, including cgroup memory and swap usage and their kernel-maintained peaks.