Keep virtio and Xen disks in host disk I/O collection

99ad8c2c4 routed collectDiskIO through fsfilters.IsVirtualBlockDevice to
stop ZFS zvols being collected. That helper answers "can this device report
SMART", which is the wrong question for I/O accounting: it also matches
vd* and xvd*, so every agent running inside a KVM, Proxmox or Xen VM lost
host disk I/O entirely, since vda is the real disk on those machines. It
also dropped md, nbd, rbd, drbd and pmem, none of which were part of the
report.

Add IsNonPhysicalDiskIODevice for the I/O question and use it here. It
covers only loop, ram, zram, dm- and zd, so the zvol fix for #1671 stands
while everything that was collected before 99ad8c2c4 is collected again.
Tests assert vda and xvda survive the I/O filter while still being excluded
from SMART, so the two predicates cannot be conflated again.

Contract-Neutral: behavioural regression fix, no contract delta

Refs #1671
This commit is contained in:
rcourtman 2026-08-03 17:39:55 +01:00
parent 6be2af1c19
commit 4e67a93d49
4 changed files with 87 additions and 15 deletions

View file

@ -412,13 +412,16 @@ func collectDiskIO(ctx context.Context, diskExclude []string) []agentshost.DiskI
if isPartition(name) {
continue
}
// Skip virtual/pseudo block devices (loop, ram, dm-, md, zvols,
// zram, nbd, rbd, drbd, ...). ZFS-backed Proxmox hosts expose one
// zd<N> device per zvol, so without this a host with a few hundred
// guests reports a few hundred phantom disks on every collection
// cycle (issue #1671). The prefix list is shared with SMART
// collection and the server-side resource registry.
if fsfilters.IsVirtualBlockDevice(name) {
// Skip devices whose counters are not host physical disk activity
// (loop, ram, zram, dm- aggregates, and ZFS zvols). ZFS-backed
// Proxmox hosts expose one zd<N> device per zvol, so without this a
// host with a few hundred guests reports a few hundred phantom
// disks on every collection cycle (issue #1671).
//
// Deliberately NOT fsfilters.IsVirtualBlockDevice: that answers
// "can this report SMART" and would drop vd*/xvd*, which are the
// real disks of any agent running inside a VM.
if fsfilters.IsNonPhysicalDiskIODevice(name) {
continue
}
// Skip user-excluded devices (issue #1142)

View file

@ -189,30 +189,31 @@ func TestCollectDiskIOFiltersAndError(t *testing.T) {
"ram0": {ReadBytes: 7},
"dm-0": {ReadBytes: 8},
"sdb": {ReadBytes: 9},
// Aggregate virtual devices whose counters restate the I/O of
// the physical members underneath them.
"md0": {ReadBytes: 10},
"md0": {ReadBytes: 10},
// ZFS zvols. A Proxmox host with ZFS-backed guest storage
// exposes one of these per zvol (issue #1671).
"zd0": {ReadBytes: 11},
"zd16": {ReadBytes: 12},
"zd160": {ReadBytes: 13},
"zram0": {ReadBytes: 14},
"nbd0": {ReadBytes: 15},
"rbd0": {ReadBytes: 16},
// Real disks of a virtualised host. These cannot report SMART
// but their I/O counters are genuine host disk activity, so
// they must survive the filter.
"vda": {ReadBytes: 15},
"xvda": {ReadBytes: 16},
}, nil
}
disks := collectDiskIO(context.Background(), []string{"sdb"})
if len(disks) != 2 {
t.Fatalf("expected 2 disk I/O entries after filtering, got %d: %+v", len(disks), disks)
if len(disks) != 5 {
t.Fatalf("expected 5 disk I/O entries after filtering, got %d: %+v", len(disks), disks)
}
gotDevices := make([]string, 0, len(disks))
for _, disk := range disks {
gotDevices = append(gotDevices, disk.Device)
}
wantDevices := []string{"nvme0n1", "sda"}
wantDevices := []string{"md0", "nvme0n1", "sda", "vda", "xvda"}
if !reflect.DeepEqual(gotDevices, wantDevices) {
t.Fatalf("unexpected disk I/O devices: got %v, want %v", gotDevices, wantDevices)
}

View file

@ -269,6 +269,44 @@ var virtualBlockDevicePrefixes = []string{
"zram",
}
// nonPhysicalIODevicePrefixes are device-name prefixes whose kernel I/O
// counters do not describe host physical disk activity, either because the
// device is purely virtual (loop, ram, zram), because it is guest storage
// layered on the real disks (ZFS zvols), or because it is a mapper
// aggregate that restates the I/O of its own members (dm-).
//
// This list is deliberately NARROWER than virtualBlockDevicePrefixes, and
// the two must not be conflated. That list answers "can this device report
// SMART", which is a different question: a virtio (vd*) or Xen (xvd*) disk
// cannot report SMART, but its I/O counters are the real disk activity of a
// virtualised host, so excluding those from I/O collection blanks disk I/O
// for every agent running inside a VM.
var nonPhysicalIODevicePrefixes = []string{
"dm-",
"loop",
"ram",
"zd",
"zram",
}
// IsNonPhysicalDiskIODevice reports whether a block-device name should be
// left out of host disk I/O collection because its counters do not
// represent physical disk activity on this host. Use this for I/O
// accounting. Use IsVirtualBlockDevice for SMART capability.
func IsNonPhysicalDiskIODevice(name string) bool {
trimmed := strings.ToLower(strings.TrimSpace(name))
trimmed = strings.TrimPrefix(trimmed, "/dev/")
if trimmed == "" {
return false
}
for _, prefix := range nonPhysicalIODevicePrefixes {
if strings.HasPrefix(trimmed, prefix) {
return true
}
}
return false
}
// IsVirtualBlockDevice reports whether a block-device name (with or
// without the /dev/ prefix) looks like a virtual or pseudo device that
// cannot provide SMART data or meaningful physical-disk metrics.

View file

@ -364,3 +364,33 @@ func TestIsVirtualBlockDevice(t *testing.T) {
})
}
}
// Regression guard: IsNonPhysicalDiskIODevice answers a different question
// from IsVirtualBlockDevice. Conflating them once blanked host disk I/O for
// every agent running inside a VM, because virtio and Xen disks cannot
// report SMART but their I/O counters are real host disk activity.
func TestIsNonPhysicalDiskIODevice(t *testing.T) {
excluded := []string{"loop0", "ram0", "zram0", "dm-0", "zd0", "zd16", "zd160", "/dev/zd0"}
for _, device := range excluded {
if !IsNonPhysicalDiskIODevice(device) {
t.Errorf("IsNonPhysicalDiskIODevice(%q) = false, want true", device)
}
}
kept := []string{"sda", "nvme0n1", "vda", "vdb", "xvda", "md0", "pmem0", "nbd0", "rbd0", "/dev/vda"}
for _, device := range kept {
if IsNonPhysicalDiskIODevice(device) {
t.Errorf("IsNonPhysicalDiskIODevice(%q) = true, want false", device)
}
}
// vd*/xvd* must stay excluded for SMART while staying included for I/O.
for _, device := range []string{"vda", "xvda"} {
if !IsVirtualBlockDevice(device) {
t.Errorf("IsVirtualBlockDevice(%q) = false, want true", device)
}
if IsNonPhysicalDiskIODevice(device) {
t.Errorf("IsNonPhysicalDiskIODevice(%q) = true, want false", device)
}
}
}