Monitoring¶
Volume health in miroir is reported per node. Each agent exports what its own leg of every volume sees, so a problem shows up as a metric on the node that has it. The controller adds the few cluster-level signals the agents can't know, like RWX gateway health.
monitoring.podMonitor.enabled: true creates a Prometheus Operator
PodMonitor scraping the controller and every agent on their
metrics ports (the per-volume gauges are exported by the agent on
each storage node; a node label is added to every series). The
diskful per-volume gauges also carry a pool label naming the pool
backing that node's leg. Pools are per-node, so two legs of one
volume can report different pools, which lets you scope volume
health to a pool (the shipped dashboard's pool variable does
exactly that). miroir_volume_diskless_primary and
miroir_volume_wedged are the exceptions: a diskless leg holds no
backing device in any pool, and a wedged teardown's pool can be
unknowable, so both carry no pool label.
Every volume series also carries pvc and pvc_namespace: the
PersistentVolumeClaim the volume serves, so dashboards and alerts
read the claim's name instead of the opaque pvc-<uuid> volume
name (which stays available as the volume label). The pair is
recorded on the MiroirVolume at provisioning time and backfilled
onto pre-existing volumes from their PV; a volume whose claim is
unknown falls back to its volume name in pvc, with an empty
pvc_namespace. The agent exports, per volume on that node:
| Metric | Meaning |
|---|---|
miroir_volume_up_to_date |
1 when this node's replica is UpToDate (unreplicated volumes are always 1 once created) |
miroir_volume_connected |
1 when all replication links to diskful peers are established (tie-breaker links excluded) |
miroir_volume_split_brain |
1 when DRBD refused to reconnect after divergence; manual resolution required |
miroir_volume_suspended |
1 while the snapshot write barrier freezes IO; sustained means a stranded barrier |
miroir_volume_resync_ratio |
fraction (0-1) in sync of the least-synced diskful peer; 1 when fully in sync |
miroir_volume_quorum |
0 while a freeze volume has lost quorum and refuses writes, the "workloads are failing I/O" signal (always 1 under last-man-standing) |
miroir_volume_disk_failed |
1 when this leg's disk was detached after an I/O error and latched failed; replace the disk, then remove and re-add the replica |
miroir_volume_out_of_sync_bytes |
worst per-peer out-of-sync bytes: the exposure if the healthiest peer is lost; also counts online-verify findings |
miroir_volume_primary |
1 while this node's diskful leg is Primary: the consumer pod or the RWX gateway runs here and this leg serves the I/O |
miroir_volume_diskless_primary |
1 while a diskless leg (client or tie-breaker) is Primary here: the consumer pays network I/O; see auto-diskful |
miroir_volume_verify_last_timestamp_seconds |
unix time of the last completed scheduled verify; alert on staleness to catch a schedule that stopped firing |
miroir_volume_verify_out_of_sync_bytes |
out-of-sync bytes the last scheduled verify found (0 = clean) |
miroir_volume_wedged |
1 when the kernel can no longer tear down this volume's DRBD resource (LINBIT/drbd#137); only a node reboot clears it |
Each agent additionally exports its pool capacities
(miroir_pool_capacity_bytes / miroir_pool_allocated_bytes /
miroir_pool_meta_used_ratio, one series per named pool via the
pool label), the same sample that feeds capacity-aware placement
and the PoolUsageHigh condition. Pool exhaustion is alertable,
and two pools on one node stay distinguishable, not just an Event.
It also exports miroir_node_drbd_kernel_info (always 1): the DRBD
kernel module version probed at startup (version label) plus the
agent image's drbd-utils version (utils_version label), from
client-only nodes too (which have no MiroirNode status). Query it
for fleet version skew before a release raises the kernel floor.
Three further node-scoped series cover the failure mode that
miroir_volume_wedged only ever sees one volume of at a time:
| Metric | Meaning |
|---|---|
miroir_node_stranded_children |
host commands killed at their deadline whose task is still in uninterruptible sleep, so the kernel will neither run nor reap them |
miroir_node_wedged |
1 while the node-scoped breaker is open: miroir has stopped spawning storage commands because each new one only strands too |
miroir_node_drbd_assertions_total |
fatal DRBD kernel assertions (put_ldev refcount underflows, LINBIT/drbd#137) sighted in the kernel log since the agent started |
A sustained non-zero miroir_node_stranded_children is the earliest
signal that a node's storage stack is jamming: it climbs while the
per-volume gauges still look healthy, because the stuck tasks hold
kernel locks that commands against healthy volumes then block on.
At the breaker's limit miroir_node_wedged goes to 1 and the agent
fails further lvm/zfs/drbdsetup calls and unmounts on that
node with node storage stack wedged: node reboot required.
Refusing is not a recovery — nothing in userspace can reap a task
the kernel holds. It bounds the pile so the node stays drainable:
unbounded, every retry (including kubelet's NodeUnstageVolume
retries) adds a stuck task until kubelet's own shutdown cannot
complete and the node needs an out-of-band power cycle. The shipped
MiroirNodeStorageWedged rule alerts on it; drain and reboot that
node. The gauge clears by itself if the stuck commands drain. Note
that the count lives in the agent process, so restarting the agent
resets it — the stuck tasks remain and the breaker re-trips once
enough new children strand.
The breaker also opens on a fatal DRBD kernel assertion in the node's
kernel log (a put_ldev refcount underflow, LINBIT/drbd#137): the
damaged refcount makes the next detach a hang or a use-after-free, so
the agent stops initiating DRBD state changes on the node. Unlike the
stranded-children trip, this latch never clears on its own and
survives agent restarts — the agent replays the kernel ring on start —
so miroir_node_wedged holds 1 until the node reboots. Unmounts are
not refused on a latch: the filesystem layer still works, and draining
the node is exactly the remedy.
For RWX volumes the controller exports miroir_export_ready: 1
while the volume's NFS gateway is serving (gateway pod available,
export address published). This is the signal the per-volume gauges
cannot give you: DRBD replicas stay healthy while a dead gateway
leaves every NFS client hanging.
Each gateway pod additionally serves its own metrics endpoint
(scraped by a second PodMonitor, with node and volume labels):
miroir_gateway_nfs_healthy is the result of the last liveness
probe's NFS NULL call against the pod's local ganesha. The same
probe backs the pod's /healthz, so a ganesha that still accepts
TCP connections but has stopped answering NFS fails liveness and is
restarted. Previously that failure mode was invisible.
Prometheus is where you alert on all of this, and for volume health it
is currently the only place. The driver also answers the CSI health
RPCs (ControllerGetVolumeHealth, ControllerListVolumeHealth,
NodeGetVolumeHealth), reporting a split-brain that needs an operator
as DATA_LOSS, a failed disk or lagging replica as DEGRADED, and a
volume with no backing device left as INACCESSIBLE. The controller
RPCs answer cluster-wide; NodeGetVolumeHealth answers for the node
that asks, which is the only place a node-local quorum loss shows up.
These replace the VolumeCondition API that CSI v1.13 removed, but on
most clusters nothing consumes them yet: kubelet calls
NodeGetVolumeHealth only on Kubernetes 1.37+ with the alpha
CSIVolumeHealth feature gate enabled (default off), and
external-health-monitor has no release that speaks them, so the
health-monitor sidecar is gone from the chart (its released versions
refuse to start against a driver that no longer advertises
VOLUME_CONDITION). Until a consumer runs in your cluster, the
miroir_volume_* gauges are what to alert on.
Starter alerts and dashboard¶
monitoring.prometheusRule.enabled: true ships starter alerts for
all of the above (split-brain, quorum lost, stranded barrier, disk
failed, a wedged teardown, degraded replication, sustained
out-of-sync, an unavailable RWX export, a stale verify schedule,
pool and thin-metadata usage, and a down agent). A node whose agent
stops answering scrapes loses every miroir_* series, so none of
its per-volume alerts can fire; the kernel-floor refusal to start
looks exactly like this.
monitoring.dashboards.enabled: true installs a Grafana
dashboard, either a sidecar-labelled ConfigMap or a grafana-operator
GrafanaDashboard CR via monitoring.dashboards.grafanaOperator.
The per-volume alerts inherit the pool label and name the pool in
their summaries, so Alertmanager routes and silences can target a
single pool. The wedged-teardown alert is the exception: a pool can
be unknowable mid-teardown, so its metric carries only volume.
The dashboard's pool variable defaults to All; narrowing it
filters the volume-health and pool panels together.