Skip to content

Commit 178edc6

Browse files
committed
feat(monitoring): hold each shared-central tenant to a write rate
The design has the per-tenant collector rate-limit its tenant, so that a noisy or hostile tenant cannot exhaust a shared backend. The disk cap alone does not do that: VictoriaTraces always keeps the last two days, so a tenant writing faster than the cap allows over two days fills the disk for every tenant on the store. tenant-root publishes the rate on its vmauth, from tracingCentralTenantBytesPerSecond or, by default, so that one tenant alone cannot fill the generic store's cap within two days. Each central tenant's collector reads it, splits it across its replicas and refuses data above it before the batch processor, so the sender sees the refusal. An OTLP/HTTP sender gets a 429 and retries; the limiter answers gRPC with RESOURCE_EXHAUSTED and no RetryInfo, which the OTLP specification makes final, so central tenants should export over HTTP. Without a published rate it falls back to that of a default store rather than to no limit. Signed-off-by: Alexey Artamonov <[email protected]>
1 parent 23749d1 commit 178edc6

10 files changed

Lines changed: 435 additions & 19 deletions

File tree

‎packages/extra/monitoring/README.md‎

Lines changed: 12 additions & 11 deletions
Large diffs are not rendered by default.

‎packages/extra/monitoring/values.schema.json‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -382,10 +382,16 @@
382382
}
383383
},
384384
"tracingCentralHost": {
385-
"description": "tenant-root only: accept spans from tenants that turned on shared-central tracing (the Tenant's tracingCentral) into this Monitoring's tracingStorages entry named `generic`, which must be in `cluster` mode. Every central tenant then shares that store's disk and retention with tenant-root, and nothing reserves space or limits the rate per tenant: once the store's disk cap is reached (retentionDiskUsageBytes, or 80% of its filesystem) the oldest spans of all of them are evicted together, and a tenant sending faster than the cap allows for the two days VictoriaTraces always keeps can fill the disk for all of them. Ignored outside tenant-root. The tenant-root namespace is inside the trust boundary of the shared store, as it is for the metrics of tenants that rely on a parent's monitoring: vmauth pins each central tenant to its own account, but workloads in tenant-root reach the store directly, and one that learns a tenant's account ID (a hash of its namespace name and UID, which its VMUser carries) can read and write that account. Whoever tenant-root admits to its Grafana is inside it too, since tenant-root's own datasource for the store reaches vtselect without vmauth; who that is follows its Grafana's oidc mode (with System, tenant-root's own groups, which already hold the same access level in every tenant under tenant-root). The render refuses to stop hosting while central tenants remain, but removing the tenant-root Monitoring altogether is not a render and is not refused: turn tracingCentral off on the central tenants first, since until then every Monitoring render of theirs fails, their metrics, logs and alerting changes included. The refusals read each other's objects when they render, so a tenant turning tracingCentral on while tenant-root stops hosting can pass both: that tenant's collector then exports to a vmauth that is gone while every release reports Ready, until its Monitoring renders again. Recover by hosting again; turning that tenant's tracingCentral off instead ends the silent drop, and turning it on again is then refused until tenant-root hosts.",
385+
"description": "tenant-root only: accept spans from tenants that turned on shared-central tracing (the Tenant's tracingCentral) into this Monitoring's tracingStorages entry named `generic`, which must be in `cluster` mode. Every central tenant then shares that store's disk and retention with tenant-root, and nothing reserves space per tenant: once the store's disk cap is reached (retentionDiskUsageBytes, or 80% of its filesystem) the oldest spans of all of them are evicted together. Each tenant is held to tracingCentralTenantBytesPerSecond, which by default lets no single tenant fill the cap within the two days VictoriaTraces always keeps; several tenants writing at their limit together still can. Ignored outside tenant-root. The tenant-root namespace is inside the trust boundary of the shared store, as it is for the metrics of tenants that rely on a parent's monitoring: vmauth pins each central tenant to its own account, but workloads in tenant-root reach the store directly, and one that learns a tenant's account ID (a hash of its namespace name and UID, which its VMUser carries) can read and write that account. Whoever tenant-root admits to its Grafana is inside it too, since tenant-root's own datasource for the store reaches vtselect without vmauth; who that is follows its Grafana's oidc mode (with System, tenant-root's own groups, which already hold the same access level in every tenant under tenant-root). The render refuses to stop hosting while central tenants remain, but removing the tenant-root Monitoring altogether is not a render and is not refused: turn tracingCentral off on the central tenants first, since until then every Monitoring render of theirs fails, their metrics, logs and alerting changes included. The refusals read each other's objects when they render, so a tenant turning tracingCentral on while tenant-root stops hosting can pass both: that tenant's collector then exports to a vmauth that is gone while every release reports Ready, until its Monitoring renders again. Recover by hosting again; turning that tenant's tracingCentral off instead ends the silent drop, and turning it on again is then refused until tenant-root hosts.",
386386
"type": "boolean",
387387
"default": false
388388
},
389+
"tracingCentralTenantBytesPerSecond": {
390+
"description": "tenant-root only, with tracingCentralHost: the rate, in bytes per second, each central tenant may write into the shared store, counted uncompressed. 0 derives it from the `generic` store so that one tenant alone cannot fill the store's disk cap within the two days VictoriaTraces always keeps: twice the per-replica cap (retentionDiskUsageBytes, or 80% of the volume) over 172800 seconds, which for a 10Gi volume is about 97 KiB/s. Spans are stored compressed, so the default errs low. Each tenant's collector enforces it, split across its replicas, and refuses data above it: an OTLP/HTTP sender gets a 429 and retries, while an OTLP/gRPC sender gets RESOURCE_EXHAUSTED without the RetryInfo a retry needs and drops those spans, so applications of central tenants should export over HTTP.",
391+
"type": "integer",
392+
"default": 0,
393+
"minimum": 0
394+
},
389395
"tracingCollector": {
390396
"description": "OpenTelemetry Collector for trace ingestion.",
391397
"type": "object",
@@ -410,7 +416,7 @@
410416
}
411417
},
412418
"replicas": {
413-
"description": "Number of collector replicas. Head sampling is trace-id-deterministic, so replicas\u003e1 are safe and remove the single-ingest-path SPOF. With shared-central tracing each replica sends up to two requests at a time against this tenant's limit of four per vmauth replica, which its Grafana reads share, so more replicas do not raise the tenant's throughput and can slow its reads.",
419+
"description": "Number of collector replicas. Head sampling is trace-id-deterministic, so replicas\u003e1 are safe and remove the single-ingest-path SPOF. With shared-central tracing each replica sends up to two requests at a time against this tenant's limit of four per vmauth replica, which its Grafana reads share, so more replicas do not raise the tenant's throughput and can slow its reads; the tenant's write rate (tenant-root's tracingCentralTenantBytesPerSecond) is split across them as well.",
414420
"type": "integer",
415421
"default": 1,
416422
"minimum": 1

‎packages/extra/monitoring/values.yaml‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,16 +111,20 @@ logsStorages:
111111
## @param {[]TracingStorage} tracingStorages - Configuration of traces storage instances. Empty by default (tracing off): no backend and no trace collector are rendered. Adding an entry provisions a VictoriaTraces backend and, while tracingCollector.enabled is true, deploys the OTLP collector in front of it. A tenant with shared-central tracing (the Tenant's tracingCentral) gets the collector, while tracingCollector.enabled is true, without an entry, which then exports to the shared store; entries still listed keep their store and datasource, so traces stored before the switch stay readable until the entry is removed.
112112
tracingStorages: []
113113

114-
## @param {bool} tracingCentralHost - tenant-root only: accept spans from tenants that turned on shared-central tracing (the Tenant's tracingCentral) into this Monitoring's tracingStorages entry named `generic`, which must be in `cluster` mode. Every central tenant then shares that store's disk and retention with tenant-root, and nothing reserves space or limits the rate per tenant: once the store's disk cap is reached (retentionDiskUsageBytes, or 80% of its filesystem) the oldest spans of all of them are evicted together, and a tenant sending faster than the cap allows for the two days VictoriaTraces always keeps can fill the disk for all of them. Ignored outside tenant-root. The tenant-root namespace is inside the trust boundary of the shared store, as it is for the metrics of tenants that rely on a parent's monitoring: vmauth pins each central tenant to its own account, but workloads in tenant-root reach the store directly, and one that learns a tenant's account ID (a hash of its namespace name and UID, which its VMUser carries) can read and write that account. Whoever tenant-root admits to its Grafana is inside it too, since tenant-root's own datasource for the store reaches vtselect without vmauth; who that is follows its Grafana's oidc mode (with System, tenant-root's own groups, which already hold the same access level in every tenant under tenant-root). The render refuses to stop hosting while central tenants remain, but removing the tenant-root Monitoring altogether is not a render and is not refused: turn tracingCentral off on the central tenants first, since until then every Monitoring render of theirs fails, their metrics, logs and alerting changes included. The refusals read each other's objects when they render, so a tenant turning tracingCentral on while tenant-root stops hosting can pass both: that tenant's collector then exports to a vmauth that is gone while every release reports Ready, until its Monitoring renders again. Recover by hosting again; turning that tenant's tracingCentral off instead ends the silent drop, and turning it on again is then refused until tenant-root hosts.
114+
## @param {bool} tracingCentralHost - tenant-root only: accept spans from tenants that turned on shared-central tracing (the Tenant's tracingCentral) into this Monitoring's tracingStorages entry named `generic`, which must be in `cluster` mode. Every central tenant then shares that store's disk and retention with tenant-root, and nothing reserves space per tenant: once the store's disk cap is reached (retentionDiskUsageBytes, or 80% of its filesystem) the oldest spans of all of them are evicted together. Each tenant is held to tracingCentralTenantBytesPerSecond, which by default lets no single tenant fill the cap within the two days VictoriaTraces always keeps; several tenants writing at their limit together still can. Ignored outside tenant-root. The tenant-root namespace is inside the trust boundary of the shared store, as it is for the metrics of tenants that rely on a parent's monitoring: vmauth pins each central tenant to its own account, but workloads in tenant-root reach the store directly, and one that learns a tenant's account ID (a hash of its namespace name and UID, which its VMUser carries) can read and write that account. Whoever tenant-root admits to its Grafana is inside it too, since tenant-root's own datasource for the store reaches vtselect without vmauth; who that is follows its Grafana's oidc mode (with System, tenant-root's own groups, which already hold the same access level in every tenant under tenant-root). The render refuses to stop hosting while central tenants remain, but removing the tenant-root Monitoring altogether is not a render and is not refused: turn tracingCentral off on the central tenants first, since until then every Monitoring render of theirs fails, their metrics, logs and alerting changes included. The refusals read each other's objects when they render, so a tenant turning tracingCentral on while tenant-root stops hosting can pass both: that tenant's collector then exports to a vmauth that is gone while every release reports Ready, until its Monitoring renders again. Recover by hosting again; turning that tenant's tracingCentral off instead ends the silent drop, and turning it on again is then refused until tenant-root hosts.
115115
tracingCentralHost: false
116116

117+
## @param {int} tracingCentralTenantBytesPerSecond - tenant-root only, with tracingCentralHost: the rate, in bytes per second, each central tenant may write into the shared store, counted uncompressed. 0 derives it from the `generic` store so that one tenant alone cannot fill the store's disk cap within the two days VictoriaTraces always keeps: twice the per-replica cap (retentionDiskUsageBytes, or 80% of the volume) over 172800 seconds, which for a 10Gi volume is about 97 KiB/s. Spans are stored compressed, so the default errs low. Each tenant's collector enforces it, split across its replicas, and refuses data above it: an OTLP/HTTP sender gets a 429 and retries, while an OTLP/gRPC sender gets RESOURCE_EXHAUSTED without the RetryInfo a retry needs and drops those spans, so applications of central tenants should export over HTTP.
118+
## @minimum 0
119+
tracingCentralTenantBytesPerSecond: 0
120+
117121
##
118122
## @section Traces collector configuration
119123
##
120124

121125
## @typedef {struct} TracingCollector - Per-tenant OpenTelemetry Collector that fronts the traces backend, the tenant's own or, with shared-central tracing, the shared store in tenant-root. It is the app-facing OTLP ingest gateway and the point where sampling/redaction are enforced. Tenant isolation does not come from this workload: for a local backend it comes from the tenant NetworkPolicies, and with shared-central tracing from the vmauth in tenant-root, which pins the tenant's account to its credentials whatever the collector sends.
122126
## @field {bool} enabled=true - Deploy the OTLP collector. When disabled, applications must send OTLP straight to the traces backend (no central sampling/redaction); with shared-central tracing they have no path to it, so no new spans arrive, while the traces already stored stay readable. The collector fronts a single backend, and the limit holds with shared-central tracing too, since turning that off makes it export to the local entry again: while it is enabled, tracingStorages must hold at most one entry, and adding a second one fails the whole Monitoring render until the extra entry is removed or the collector is disabled.
123-
## @field {int} replicas=1 - Number of collector replicas. Head sampling is trace-id-deterministic, so replicas>1 are safe and remove the single-ingest-path SPOF. With shared-central tracing each replica sends up to two requests at a time against this tenant's limit of four per vmauth replica, which its Grafana reads share, so more replicas do not raise the tenant's throughput and can slow its reads.
127+
## @field {int} replicas=1 - Number of collector replicas. Head sampling is trace-id-deterministic, so replicas>1 are safe and remove the single-ingest-path SPOF. With shared-central tracing each replica sends up to two requests at a time against this tenant's limit of four per vmauth replica, which its Grafana reads share, so more replicas do not raise the tenant's throughput and can slow its reads; the tenant's write rate (tenant-root's tracingCentralTenantBytesPerSecond) is split across them as well.
124128
## @minimum 1
125129
## @field {int} samplingPercentage=10 - Head-sampling percentage applied at the collector before export.
126130
## @minimum 0

‎packages/system/monitoring-rd/cozyrds/monitoring.yaml‎

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎packages/system/monitoring/templates/_helpers.tpl‎

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,72 @@
302302
{{- index .Values "_tracingCentralPassword" -}}
303303
{{- end -}}
304304

305+
{{- /*
306+
Bytes in a Kubernetes quantity (10Gi, 1.5Gi, 500M), as a float. Only the
307+
plain forms the storage field is written in are accepted; anything else fails
308+
the render rather than sizing the tenant rate on a guess.
309+
*/ -}}
310+
{{- define "monitoring.quantityBytes" -}}
311+
{{- $units := dict "" 1.0 "k" 1e3 "M" 1e6 "G" 1e9 "T" 1e12 "P" 1e15 "Ki" 1024.0 "Mi" 1048576.0 "Gi" 1073741824.0 "Ti" 1099511627776.0 "Pi" 1125899906842624.0 -}}
312+
{{- $q := toString . -}}
313+
{{- $m := regexFind "^[0-9]+(\\.[0-9]+)?" $q -}}
314+
{{- $unit := trimPrefix $m $q -}}
315+
{{- if or (not $m) (not (hasKey $units $unit)) -}}
316+
{{- fail (printf "monitoring: cannot read %q as a volume size to derive the shared-central tenant rate from; write it as a number with an optional k/M/G/T/P or Ki/Mi/Gi/Ti/Pi suffix, or set tracingCentralTenantBytesPerSecond." $q) -}}
317+
{{- end -}}
318+
{{- mulf (float64 $m) (get $units $unit) -}}
319+
{{- end -}}
320+
321+
{{- /* Bytes in a VictoriaMetrics BytesString (8GB, 8GiB), as a float. The
322+
format is validated with the tracingStorages entry. */ -}}
323+
{{- define "monitoring.bytesStringBytes" -}}
324+
{{- $units := dict "" 1.0 "kb" 1e3 "mb" 1e6 "gb" 1e9 "tb" 1e12 "KB" 1e3 "MB" 1e6 "GB" 1e9 "TB" 1e12 "KiB" 1024.0 "MiB" 1048576.0 "GiB" 1073741824.0 "TiB" 1099511627776.0 -}}
325+
{{- $s := toString . -}}
326+
{{- $m := regexFind "^[0-9]+" $s -}}
327+
{{- mulf (float64 $m) (get $units (trimPrefix $m $s)) -}}
328+
{{- end -}}
329+
330+
{{- /*
331+
The rate, in bytes per second, at which one tenant alone would fill the
332+
shared store's disk cap within the two days VictoriaTraces always keeps,
333+
from a per-vtstorage-replica cap in bytes. Spans are counted uncompressed
334+
by the limiter and stored compressed, so the bound errs low.
335+
*/ -}}
336+
{{- define "monitoring.tracingCentralRateFromCap" -}}
337+
{{- printf "%.0f" (floor (divf (mulf 2 .) 172800)) -}}
338+
{{- end -}}
339+
340+
{{- /* The rate a central tenant's collector falls back to when tenant-root
341+
publishes none: that of a default 10Gi store without a byte cap. */ -}}
342+
{{- define "monitoring.tracingCentralDefaultTenantRate" -}}
343+
{{- include "monitoring.tracingCentralRateFromCap" (mulf 0.8 (include "monitoring.quantityBytes" "10Gi")) -}}
344+
{{- end -}}
345+
346+
{{- /*
347+
The rate each central tenant may write to the shared store, which tenant-root
348+
publishes on its vmauth: tracingCentralTenantBytesPerSecond when set, else
349+
derived from the generic store's byte cap or, without one, the 80% of its
350+
volume the store is bounded at.
351+
*/ -}}
352+
{{- define "monitoring.tracingCentralTenantRate" -}}
353+
{{- $explicit := int64 (.Values.tracingCentralTenantBytesPerSecond | default 0) -}}
354+
{{- if gt $explicit 0 -}}
355+
{{- $explicit -}}
356+
{{- else -}}
357+
{{- range .Values.tracingStorages -}}
358+
{{- if eq .name "generic" -}}
359+
{{- $cap := 0.0 -}}
360+
{{- if .retentionDiskUsageBytes -}}
361+
{{- $cap = include "monitoring.bytesStringBytes" .retentionDiskUsageBytes | float64 -}}
362+
{{- else -}}
363+
{{- $cap = mulf 0.8 (include "monitoring.quantityBytes" .storage) -}}
364+
{{- end -}}
365+
{{- include "monitoring.tracingCentralRateFromCap" $cap -}}
366+
{{- end -}}
367+
{{- end -}}
368+
{{- end -}}
369+
{{- end -}}
370+
305371
{{- /* tenant-root hosts the shared store only when it opts in with
306372
tracingCentralHost, since the store is also its own, and lists a
307373
cluster-mode entry named generic, the store every central VMUser routes

0 commit comments

Comments
 (0)