Warum wir iowait nicht mehr messen

linux monitoring-plugins

Unser Monitoring-Plugin disk-io misst kein iowait mehr. Was nach einem Rückschritt klingt, ist ein Fortschritt: iowait sieht wie ein Mass für einen überlasteten Storage aus, ist aber umetikettierte Idle-Zeit, und auf jedem Mehrkern-System laut den Kernel-Entwicklern selbst schlicht unbrauchbar. Ausgelöst durch ein GitHub-Issue für disk-io auf ZFS haben wir uns den Quellcode von Kernel, sysstat, psutil, glances und OpenZFS zu Gemüte geführt - mit eindeutigem Ergebnis.

Das Mysterium iowait

Wir hatten iowait in disk-io als Zusatzsignal drin, wurden aber mit dessen Aussage im Datacenter-Betrieb selbst nie ganz warm - der Wert war schwer zu fassen. Der Anlass, das endlich mal genauer anzuschauen, kam mit Issue #1371: Hier meldet sich disk-io auf zwei frisch gestarteten Proxmox-Servern in verschiedenen Rechenzentren mit Werten wie iowait 377%. Beide Server waren praktisch im Leerlauf, die Platten hatten mit ein paar Kilobyte pro Sekunde nichts zu tun, die VMs liefen sauber. Gemeinsamkeit der beiden Hosts: ZFS. Die korrekte Einschätzung des Reporters: "These high iowaits must be fantasy numbers."

Was iowait wirklich ist

Der Linux-Kernel sagt eindeutig: iowait ist keine Zeit, die eine CPU mit I/O verbringt, sondern Idle-Zeit unter anderem Namen. Der Kernel betitelt die zuständige Sektion in kernel/sched/core.c genau so und definiert den Wert direkt darunter:

IO-wait accounting, and how it's mostly bollocks (on SMP).

The idea behind IO-wait account is to account the idle time that we could
have spend running if it were not for IO. That is, if we were to improve the
storage performance, we'd have a proportional reduction in IO-wait time.

iowait ist damit definitionsgemäss Idle-Zeit, nur anders etikettiert. Das zeigt auch der Programmcode: die zuständige Routine bekommt Idle-Zeit übergeben und entscheidet nur noch, in welches Bucket sie wandert. Der Kernel bucht die Idle-Zeit einer CPU auf iowait statt auf idle, sobald mindestens ein Task, der zuletzt auf dieser CPU lief, im I/O-Wartezustand schläft (kernel/sched/cputime.c):

void account_idle_time(u64 cputime)
{
    u64 *cpustat = kcpustat_this_cpu->cpustat;
    struct rq *rq = this_rq();

    if (atomic_read(&rq->nr_iowait) > 0)
        cpustat[CPUTIME_IOWAIT] += cputime;
    else
        cpustat[CPUTIME_IDLE] += cputime;
}

Der Zähler nr_iowait wird inkrementiert, sobald ein Task über io_schedule() schlafen geht, was etwas völlig anderes bedeutet als "die Platte war ausgelastet". iowait misst also "verlorene Rechenzeit durch I/O-Warten", nicht "wie ausgelastet ist die Platte". Der erfrischend ehrliche Kommentar in kernel/sched/core.c über den Wert dieser Zahl auf Multicore-Systemen:

Worse, since the numbers are provided per CPU, they are sometimes
interpreted per CPU, and that is nonsensical. A blocked task isn't strictly
associated with any one particular CPU, it can wake to another CPU than it
blocked on. This means the per CPU IO-wait number is meaningless.

Sauber interpretierbar ist iowait wenn überhaupt nur auf einem Einzelkern-System, und selbst dort hilft der Wert nicht dabei, Aussagen über Disks zu treffen. Grund genug für uns, den Wert aus disk-io (wieder) zu entfernen.

%util

Was ist mit %util von iostat als Ersatz? Auch hier lohnt der Blick in den Code. iostat berechnet %util aus dem Zähler io_ticks in /proc/diskstats (sysstat, rd_stats.c). io_ticks zählt laut Kernel (block/blk-core.c, update_io_ticks) die Wall-Clock-Zeit, in der mindestens ein I/O-Request in Bearbeitung war, nicht die Queue-Tiefe.

Für eine klassische Einzel-Queue-Platte wie eine HDD ist der Wert brauchbar, aber für alles, was Requests parallel abarbeitet (NVMe, SSDs, dm/md-RAID und ZFS-zvols) ist es das nicht: %util steht bei 100%, sobald durchgehend ein Request offen ist, egal wie weit das Gerät von seinem echten Limit entfernt ist. Wir behalten %util als reine Trend-Metrik (Linux, per Device), aber alarmieren nicht darauf.

Warum ZFS so absurde Zahlen liefert

Warum ausgerechnet ZFS disk-io auf 377% getrieben hat, beantwortet der Code zu OpenZFS: ZFS leitet seine synchronen I/O-Waits über cv_wait_io() respektive cv_timedwait_io(), und die enden im Kernel bei io_schedule() (module/os/linux/spl/spl-condvar.c). Ein Thread, der auf eine physische I/O wartet (zio_wait()), oder auf das Commit einer Transaction-Group (txg_wait_synced()), schläft also genau über den Pfad, der nr_iowait hochzählt. OpenZFS kommentiert das in module/zfs/txg.c sogar ausdrücklich:

Callers setting should_quiesce will use cv_wait_io() and be accounted for
as iowait time. Otherwise, the caller is understood to be idle and
cv_wait_sig() is used to prevent incorrectly inflating the system load average.

Über viele vCPUs verteilt schiebt das einen überproportionalen Anteil der ansonsten leeren Idle-Zeit in die iowait-Statistik. Ein fast leerer Pool mit ein paar Kilobyte pro Sekunde landet so bei mehreren "gesättigten Cores", während die Platten faktisch nichts tun. Die Zahlen sind echt vom Kernel gemessen. Sie bedeuten nur nicht das, was man vermutet.

Was glances macht

Wir haben zum Vergleich den glances-Code hinzugezogen, da es wie wir die psutil-Bibliothek nutzt. glances macht es nicht besser, was zeigt, dass die Annahme weit verbreitet ist: glances alarmiert per Default auf iowait, und zwar gegen genau die Schwelle, die auf Multicore-Systemen am ehesten zu Fehlalarmen führt. Der Default-Schwellwert ist 1 / Anzahl CPU-Kerne, ab dann wird das Feld eingefärbt, ein Event "High CPU I/O waiting" in die Events-Liste geschrieben und optional eine hinterlegte Action ausgelöst (glances/config.py, glances/events_list.py).

Fazit

iowait

  • misst keine Disk-Sättigung, sondern umetikettierte Idle-Zeit,
  • ist laut Kernel auf allen Mehrkern-Systemen unzuverlässig,
  • liefert auf ZFS regelmässig Fantasiewerte,
  • und taugt damit weder als Alarm- noch als verlässliche Trend-Metrik.

Damit haben wir es aus disk-io entfernt (in cpu-usage trägt es auch nicht mehr zur Alarmierung bei). disk-io misst den realen Durchsatz pro Gerät und vergleicht ihn mit dem je beobachteten Maximum, und es liefert %util als Linux-Trend-Metrik mit klar dokumentierter Grenze. Die alten Parameter --iowait-warning und --iowait-critical bleiben aus Kompatibilitätsgründen erhalten, werden aber ignoriert.

Ausblick

Wir liefern neu stattdessen das Signal, das die Frage "ist mein Storage langsam" tatsächlich beantwortet: await, die durchschnittliche I/O-Latenz pro Gerät. Anders als iowait ist das eine physikalische Grösse, robust gegen Geräte-Parallelismus, und genau die Zahl, die man als Admin im Ernstfall braucht.

await kommt aus denselben Feldern in /proc/diskstats, die auch iostat auswertet: die pro Request aufsummierte Bearbeitungszeit inklusive Wartezeit in der Queue, geteilt durch die Anzahl abgeschlossener Requests (sysstat, rd_stats.c). Das Ergebnis ist die mittlere Zeit in Millisekunden, die eine I/O vom Absetzen bis zur Quittung braucht, bei Bedarf getrennt nach Lesen und Schreiben (r_await und w_await).

await ist ein Mittelwert ("average wait") und verdeckt damit Ausreisser. Eine Platte kann einen harmlosen Durchschnitt zeigen, während einzelne Zugriffe sekundenlang hängen bleiben. Die Zahl allein sagt daher nicht, ob eine hohe Latenz von der Last oder von einer sterbenden Platte kommt. Wir lesen await deshalb zusammen mit dem Durchsatz pro Gerät, nicht als isolierten Einzelwert. Aber im Gegensatz zu iowait misst await wenigstens das, was draufsteht. :-)

Vorheriger Beitrag

DE · EN