Repository navigation
Add metrics representing silo quota limits and utilization. - #11500
Conversation
2fac930 to
71ecb55
Compare
|
I'm going to look at the implementation tomorrow, but I feel like the naming could use a little bikeshedding. Maybe something like |
| configured period: every <REDACTED_DURATION>m | ||
| last completed activation: <REDACTED ITERATIONS>, triggered by <TRIGGERED_BY_REDACTED> | ||
| started at <REDACTED_TIMESTAMP> (<REDACTED DURATION>s ago) and ran for <REDACTED DURATION>ms | ||
| warning: unknown background task: "capacity_metrics" (don't know how to interpret details: Object {"samples_queued": Number(0)}) |
There was a problem hiding this comment.
we should probably add an implementation for this in omdb::nexus::print_task_details, rather than letting it fall through to the "warning: unknown background task..." behavior.
Nitpicking is welcome! Conceptually, I was thinking of a set of quota metrics capturing:
I haven't added project-scoped metrics here, in the interest of keeping the patch small, but I'd like to get to that in a follow-up. So the full set of metric names under the proposed scheme would eventually look like:
So what you're proposing would probably look like:
I've been staring at these names for too long, but I think yours make sense, and it feels more correct to capture capacity/provisioned in the metric name instead of the target name. I had started all the names with |
This patch adds two groups of metrics: silo_quota:{memory,cpu,disk}_capacity,
which represents resource quota limits per silo; and
silo_quota:{memory,cpu,disk}_provisioned, which represents resources charged
against each quota.
We currently have two features that almost solve this problem, but aren't quite
adequate: the `collection_target` metric, and the /v1/system/utilization/silos
endpoint. The `collection_target` metric exposes quota utilization, mixing
silos and projects. However, it's sparse, emitting values only when utilization
changes, which doesn't play nicely with use cases that expect regular
timeseries. It also doesn't expose quota limits. The silo utilization endpoint
exposes quota limits, but isn't a metric.
We work around this problem in the [OpenTelemetry metrics
receiver](https://github.com/oxidecomputer/opentelemetry-collector-components/tree/main/receiver/oxidemetricsreceiver),
which can optionally emit synthetic capacity and utilization metrics by
periodically querying Nexus. However, this requires users to run our otel
collector, or implement their own synthetic metrics--neither of which is an
ideal experience.
The metrics proposed here allow users to query silo quota capacity and
utilization via OxQL or other metrics backends, without post-processing sparse
metrics or deriving their own custom quota metrics.
Note: we collect these metrics using a new `CapacityMetrics` background task,
which runs on each Nexus instance. As a result, we emit those metrics one per
Nexus--in triplicate, by default. To handle that duplication, users should
aggregate (max, mean) over `nexus_id`.
Part of oxidecomputer/customer-support#1383.
71ecb55 to
133c4ad
Compare
Did this--and sorry, I force-pushed out of habit, although I also wanted to change the commit message so that it would get the new names right. |
bnaecker
left a comment
There was a problem hiding this comment.
Nice, this is great. I have a few comments, though overall looks solid to me.
| ) | ||
| .await?; | ||
|
|
||
| let mut metrics = self.metrics.lock().unwrap(); |
There was a problem hiding this comment.
I think we can move this lock to after the loop. Compute all the metrics, lock once, and move them all onto the queue, then drop the lock.
| description = "Silo quota capacity and provisioned resources" | ||
| authz_scope = "fleet" | ||
| versions = [ | ||
| { version = 1, fields = [ "nexus_id", "silo_id" ] }, |
There was a problem hiding this comment.
I'm not sure about putting the nexus_id field on the timeseries. On the one hand, I get it that it's useful to dedupe the data. OTOH, it doesn't really provide any useful context for the data itself.
I think we have a few options:
- Leave it
- Remove it, relying on the timestamps themselves for deduplication. E.g, use something like
mean_within()for alignment or the equivalent in a 3p tool / language. - Remove it, and do something complicated to try to avoid reporting values from different Nexus "close by" in time.
I'm inclined toward the 2nd option, but WDYT?
There was a problem hiding this comment.
- Remove it, relying on the timestamps themselves for deduplication. E.g, use something like
mean_within()for alignment or the equivalent in a 3p tool / language.
IIUC some external metrics systems (like Prometheus) get Very Sad when we have multiple data points in one timeseries with no labels to disambiguate them. I guess here, that would only happen if two nexuses HAPPENED to emit such a data point with exactly the same timestamp, which seems unlikely, but...
There was a problem hiding this comment.
some external metrics systems (like Prometheus) get Very Sad when we have multiple data points in one timeseries with no labels to disambiguate them.
Yes--for example, oxidecomputer/hubris#2634. Prometheus also represents timestamps in ms, not ns as in oximeter, so collisions are more likely there (although still not at all likely, and users who want to export to prometheus-like stores can deduplicate on their end if necessary).
The robots also helpfully pointed out that the oximeter read path can panic on duplicate timestamps--although again, with nanosecond precision, this is extremely unlikely, and I believe we can fix that by changing a > 0 to >= 0, and it's not an architectural assumption in oximeter.
Anyway, I have a slight preference for keeping the nexus_id label, both because it avoids potential sadness in downstream metrics stores related to duplicate samples, and because--and I know this is a weak argument--labeling by nexus zone gives us n regularly-spaced series per silo, and omitting the label gives us a single series with weird and variable intervals between samples. I don't think either of these is particularly strong, which is why I'm calling it a slight preference, and I'm happy to drop that label if others prefer.
| description = "ID of the silo" | ||
|
|
||
| [[metrics]] | ||
| name = "memory_capacity" |
There was a problem hiding this comment.
Is there a difference between "capacity" and "quota"? Should we use the same term in the name and description?
There was a problem hiding this comment.
I think we have a few ways of describing these quantities already:
- In /v1/utilization, we say
capacityfor the amount of resource available to be provisioned, andprovisionedfor the amount actually provisioned. - In /v1/system/utilization/silos, we use
allocatedandprovisioned, respectively. - In /v1/system/silo-quotas, we just use an object of
cpus,memory,storage, without referring toquota,capacity, etc.
My thinking here was to try and stay consistent with existing APIs, although they're not entirely consistent as they are. I personally prefer capacity/provisioned to allocated/provisioned, so that's what I used to begin with. I think quota makes sense as well, but I think it's less consistent with existing APIs, and arguably that would mean overloading quota to mean both the resource defined in /v1/system/silo-quotas, as well as each per-resource limit within that resource. So I think I'd lean toward leaving the metric names as they are, and changing the descriptions to match.
There was a problem hiding this comment.
Makes sense to me. I was mostly asking about the different terms in the names and descriptions. I agree the rest of the API is a little confused, but it's good this is all self-consistent.
|
|
||
| [[metrics]] | ||
| name = "disk_provisioned" | ||
| description = "Provisioned disk charged against the quota" |
There was a problem hiding this comment.
Does this also include the snapshot usage? The description for disk_capacity makes it seem like it should, which we should make clear here too.
There was a problem hiding this comment.
Yes, fixing this. I'm also realizing that I should have called these metrics storage and not disk to match existing APIs, so I'll change that too.
| //! Note: because background tasks run on each Nexus instance, these metrics are | ||
| //! emitted once for each Nexus--as of this writing, in triplicate. Callers | ||
| //! should take the mean or maximum datum value over Nexus instance IDs to | ||
| //! deduplicate. |
There was a problem hiding this comment.
These instructions seem like they're intended for someone consuming the metrics through the API, but they aren't going to be visible in customer docs as written. Should this advice exist somewhere else?
Without this, it seems possible that the values will be jittery, as each Nexus races to insert values?
There was a problem hiding this comment.
Yes, adding a note in silo-quota.toml.
| //! deduplicate. | ||
| //! | ||
| //! Note: the `silo_quota` metrics are related to, but distinct from, the | ||
| //! `collection_target` metric. The `silo_quota` metrics are emitted on a |
There was a problem hiding this comment.
At the cost of fine-pointed accuracy here - we COULD make the collection_target metric a background polling task too, if it would be more useful to have regularly-emitted values. I'm not asking for that change in this PR, but rather, offering that it seems fine to me to change that behavior.
There was a problem hiding this comment.
Right, I'll make the comment more precise. In some sense, we could also think about turning collection_target:* into the new proposed metrics: sample it on an interval in a background task, add capacity, separate silo collections from project collections--but at least while oximeter metric labels are immutable, it seems easier to add a new metric than to reshape the existing one.
|
|
||
| let silo_utils = self | ||
| .datastore | ||
| .silo_utilization_list( |
There was a problem hiding this comment.
This filters out the default silo + non-discoverable silos, as an FYI. Is that intentional?
There was a problem hiding this comment.
It's intentional, but not necessarily correct! My thought was to match /v1/system/utilization/silos. It sounds like the default silo concept is meant to be retired, so I didn't think too hard about it. And it does look like non-discoverable silos will be returned here, if they have non-zero quota capacity:
.filter(
dsl::silo_discoverable
.eq(true)
.or(dsl::cpus_allocated.gt(0))
.or(dsl::memory_allocated.gt(0))
.or(dsl::storage_allocated.gt(0)),
)
.filter(dsl::silo_id.ne(DEFAULT_SILO_ID))
My weakly-held opinion here is that the new metric should match /v1/system/utilization/silos, and I'm agnostic about whether we should show non-discoverable silos without quotas set--but I would lean toward showing those silos for both views, or neither.
| .datastore | ||
| .silo_utilization_list( | ||
| opctx, | ||
| &PaginatedBy::Id(DataPageParams::max_page()), |
There was a problem hiding this comment.
Do we want to be using max_page, rather than paginating? This could make very expensive queries if we have a lot of silos
There was a problem hiding this comment.
Switching to pagination.
| now, | ||
| &target, | ||
| &silo_quota::CpuCapacity { | ||
| datum: util.cpus_allocated.cast_unsigned(), |
There was a problem hiding this comment.
I believe that cast_unsigned would convert a negative value into an extremely large value - would it be better to clamp this at zero?
|
|
||
| /// Create a project in the given silo. | ||
| async fn project_create_in_silo( | ||
| pub async fn project_create_in_silo( |
There was a problem hiding this comment.
I know you're making this pub for a test, but it's doing the same thing as project_create, just skipping an authz lookup.
This seems a little sketchy to expose as pub - is it really not possible to use project_create in that test?
There was a problem hiding this comment.
Reverting. I wound up creating a test user and giving it silo access, which is boilerplate I was trying to avoid, but seems better than changing method access just for the test.
|
|
||
| impl Metrics { | ||
| pub(crate) fn push_samples(&mut self, samples: Vec<Sample>) { | ||
| self.samples.extend(samples); |
There was a problem hiding this comment.
You might want to look into oximeter::queue::BoundedQueue. Otherwise, if oximeter goes down, this will grow indefinitely
There was a problem hiding this comment.
Switched to a BoundedQueue<Sample>.
| "warning: failed to interpret task details: {:?}: {:?}", | ||
| error, details |
There was a problem hiding this comment.
turbo nit: perhaps:
| "warning: failed to interpret task details: {:?}: {:?}", | |
| error, details | |
| "warning: failed to interpret task details: {error:?}: {details:?}", |
| async move { | ||
| match self.fetch_samples(opctx, self.nexus_id).await { | ||
| Ok(sample_count) => { | ||
| serde_json::json!({"samples_queued": sample_count}) |
There was a problem hiding this comment.
this is a nitpick, and it's not a huge deal here because the status object only has one field in it, but it's worth noting that a lot of the other background tasks have a single Rust type defined in nexus_types::internal_api::background that is used both when serializing the status object in Nexus and when deserializing it in omdb. We could do that here, and it might be a little bit nicer --- though that being nicer would matter a bit more if there was more data here...
There was a problem hiding this comment.
Done. Maybe we'll add more data later--I do want to add more metrics here once we agree on the basics.
| description = "Silo quota capacity and provisioned resources" | ||
| authz_scope = "fleet" | ||
| versions = [ | ||
| { version = 1, fields = [ "nexus_id", "silo_id" ] }, |
There was a problem hiding this comment.
- Remove it, relying on the timestamps themselves for deduplication. E.g, use something like
mean_within()for alignment or the equivalent in a 3p tool / language.
IIUC some external metrics systems (like Prometheus) get Very Sad when we have multiple data points in one timeseries with no labels to disambiguate them. I guess here, that would only happen if two nexuses HAPPENED to emit such a data point with exactly the same timestamp, which seems unlikely, but...
* Use consistent-ish names and descriptions throughout. * Use `BoundedQueue` to prevent unbounded sample growth. * Paginate utilization records. * Fix other nits from code review.
1d50e0b to
0bc1a69
Compare
|
Ready for another look--I think everything is addressed. I think the only open question is about whether or not to include the |
|
I chatted with @lgfa29 about this, and continue to think including the Anyway, let me know if the above is unconvincing, or if the patch needs other changes. |
|
By the way, a quick sanity check that the new metrics work. Thanks @fakemonster for the new metrics explorer!
|
| ] | ||
|
|
||
| [[metrics]] | ||
| name = "storage_capacity" |
There was a problem hiding this comment.
Since you're asking for naming nitpicking: One thing I'm realizing is that the usage of "storage" here refers to "virtual storage", not "physical storage". If we add metrics for "physical storage" later, it'll be hard to detangle these two.
As an example: Crucible storage being distributed, so asking for "1 GiB virtual" actually uses ~3.75 GiB physical storage.
Do we want to label this virtual_storage_capacity, or otherwise have some way of making clear that it isn't physical?
There was a problem hiding this comment.
Yes, I think this is right, especially given we're talking about changing and/or adding storage quotas and quota metrics in https://rfd.shared.oxide.computer/rfd/0624. I'm going to rename the storage metrics, then merge.
Analogous to #11500, but covering projects rather than silos. Project have utilization but not capacity, so we only emit utilization metrics. Note: to parallel the existing `DataStore::silo_utilization_list`, and the `silo_utilization` view that it queries, we add a `DataStore::project_utilization_list` method and `project_utilization` view. We don't add a `/v1/system/utilization/projects` endpoint to parallel `/v1/system/utilization/silos`, but we could in a future patch if useful. Part of oxidecomputer/customer-support#1383.
Analogous to #11500, but covering projects rather than silos. Project have utilization but not capacity, so we only emit utilization metrics. Note: to parallel the existing `DataStore::silo_utilization_list`, and the `silo_utilization` view that it queries, we add a `DataStore::project_utilization_list` method and `project_utilization` view. We don't add a `/v1/system/utilization/projects` endpoint to parallel `/v1/system/utilization/silos`, but we could in a future patch if useful. Part of oxidecomputer/customer-support#1383.
Analogous to #11500, but covering projects rather than silos. Project have utilization but not capacity, so we only emit utilization metrics. Note: to parallel the existing `DataStore::silo_utilization_list`, and the `silo_utilization` view that it queries, we add a `DataStore::project_utilization_list` method and `project_utilization` view. We don't add a `/v1/system/utilization/projects` endpoint to parallel `/v1/system/utilization/silos`, but we could in a future patch if useful. Part of oxidecomputer/customer-support#1383.
Analogous to #11500, but covering projects rather than silos. Project have utilization but not capacity, so we only emit utilization metrics. Note: to parallel the existing `DataStore::silo_utilization_list`, and the `silo_utilization` view that it queries, we add a `DataStore::project_utilization_list` method and `project_utilization` view. We don't add a `/v1/system/utilization/projects` endpoint to parallel `/v1/system/utilization/silos`, but we could in a future patch if useful. Part of oxidecomputer/customer-support#1383.


This patch adds two groups of metrics: silo_quota:{memory,cpu,disk}_capacity, which represents resource quota limits per silo; and silo_quota:{memory,cpu,disk}_provisioned, which represents resources charged against each quota.
We currently have two features that almost solve this problem, but aren't quite adequate: the
collection_targetmetric, and the /v1/system/utilization/silos endpoint. Thecollection_targetmetric exposes quota utilization, mixing silos and projects. However, it's sparse, emitting values only when utilization changes, which doesn't play nicely with use cases that expect regular timeseries. It also doesn't expose quota limits. The silo utilization endpoint exposes quota limits, but isn't a metric.We work around this problem in the OpenTelemetry metrics receiver, which can optionally emit synthetic capacity and utilization metrics by periodically querying Nexus. However, this requires users to run our otel collector, or implement their own synthetic metrics--neither of which is an ideal experience.
The metrics proposed here allow users to query silo quota capacity and utilization via OxQL or other metrics backends, without post-processing sparse metrics or deriving their own custom quota metrics.
Note: we collect these metrics using a new
CapacityMetricsbackground task, which runs on each Nexus instance. As a result, we emit those metrics one per Nexus--in triplicate, by default. To handle that duplication, users should aggregate (max, mean) overnexus_id.Part of oxidecomputer/customer-support#1383.
cc @smklein for quota context, @bnaecker for metrics context, @hermanol because we've had some support requests for this and related features. The new task is roughly modeled on
InstanceWatcher, and hopefully isn't very interesting at a code level. I'd like to add some related metrics in the future, but wanted to establish a pattern for emitting capacity metrics first.