Die Annahme, die jede Uhr macht

Die Uhr eines Computers arbeitet, indem sie etwas Regelmäßiges zählt und darauf vertraut, dass es weiterzählt. Ein Quarz schwingt, ein Zähler steigt, und Software rechnet aus, wie viel Zeit vergangen ist.

Virtualisierung zerbricht den Teil mit dem Vertrauen.

Die KVM-Dokumentation des Kernels zur Zeitmessung bringt das Problem in einen Satz: „the virtual operating system does not run with 100% usage of the CPU, despite the fact that it may very well make that assumption“ — das virtuelle Betriebssystem läuft nicht mit 100 % der CPU, obwohl es genau das durchaus annehmen kann. Alles Weitere folgt daraus.

Deine vCPU läuft nicht immer

Eine vCPU ist ein Thread auf dem Host. Sie läuft, wenn der Scheduler des Hosts es sagt.

Wenn sie nicht läuft, ist der Gast nicht bloß unbeschäftigt — er ist abwesend. Er kann nicht zählen, er kann keinen Timer-Interrupt bedienen, und er hat keine Möglichkeit zu wissen, wie lange er weg war. Der Host führt das als Steal Time, was der ehrliche Name für „Zeit, die dir passiert ist statt für dich“ ist.

Timer-Interrupts sind die Stelle, an der das am meisten weh tut. Ein Gast, der einen periodischen Tick verlangt, verlangt vom Host, Interrupts mit einer festen Rate zuzustellen, und der Host kann das nicht immer leisten. Wieder aus der Kernel-Dokumentation: „the host virtualization engine may not be able to deliver the proper number of interrupts per second, and so guest time may fall behind“ — die Virtualisierung des Hosts kann die richtige Zahl an Interrupts pro Sekunde vielleicht nicht liefern, und dann fällt die Gast-Zeit zurück.

Warum die Timer-Ticks eines Gasts aufhören, gleichmäßig zu liegenBlech — die CPU gehört immer dirläuftTimer-Ticks, gleichmäßig — sie zu zählen gibt dir die ZeitIn einer VM — die vCPU ist ein Thread auf dem Scheduler eines anderenläuftweggenommenweggenommenTicks, die in den Lücken fällig waren, kommen spät, im Schwung oder gar nichtDer Gast sieht die gestrichelten Abschnitte nicht. Von innen hat die Uhr einfach weniger Tickserzeugt als sie sollte — weshalb die Kernel-Dokumentation sagt, Gast-Zeit könne „may fall behind“,also zurückfallen, wenn der Host die verlangten Interrupts nicht liefern kann.Der Host nennt die gestrichelten Bereiche Steal Time. Der Gast nennt sie überhaupt nicht,denn er war nicht da.
Die eigene Sicht des Gasts ist die untere Reihe: Ticks, die zu spät kommen, Ticks, die im Schwung kommen, und Lücken, die er nicht erklären kann. Er misst genauso den Scheduler des Hosts wie den Lauf der Zeit.

Je höher die Tick-Rate, desto schlimmer, und ein überbuchter Host macht es noch schlimmer. Deshalb verdirbt ein beschäftigter Host auch die Zeitmessung ruhiger Gäste. Sie stehen alle in derselben Schlange für dieselben physischen Kerne.

Die Zähler sind auch nicht deine

Wenn periodische Ticks unzuverlässig sind, ist die naheliegende Antwort, stattdessen einen Zähler zu lesen. Das hat seine eigenen Probleme.

Der TSC ist der schnelle, und die Kernel-Dokumentation ist unmissverständlich: „The TSC is a CPU-local clock in most implementations… the TSCs of different CPUs may start at different times“ — der TSC ist in den meisten Umsetzungen eine CPU-lokale Uhr, und die TSCs verschiedener CPUs können zu verschiedenen Zeiten losgelaufen sein. Seine Rate kann mit den Energiezuständen des Prozessors schwanken, und auf älteren Teilen hält er ganz an, wenn der Kern ruht. Eine vCPU, die zwischen physischen Kernen wandert, kann daher einen Zähler lesen, der dem widerspricht, den sie eine Mikrosekunde vorher gelesen hat.

Die Alternativen sind auf andere Weise schlechter. Der HPET, der PIT und der ACPI-PM-Timer sind alle emulierte Geräte, jedes Lesen fällt also in den Hypervisor. Korrekt, und teuer genug, dass ein Gast, der die Uhr in einer engen Schleife liest, es merkt.

Deshalb gibt es paravirtuelle Uhren. Auf KVM lässt kvm-clock den Host seine eigene Zeitmessung in eine gemeinsame Struktur schreiben, die der Gast direkt liest: kein Hineinfallen, kein Zählen, keine Annahme, dass der Gast wach war. Und deshalb solltest du die Clocksource des Gasts in Ruhe lassen, statt tsc oder hpet zu erzwingen, weil ein Forumsbeitrag sagte, das sei schneller.

Migration, Snapshots und Suspend

Live-Migration, Wiederherstellung eines Snapshots und Suspend/Resume machen mit der Uhr eines Gasts alle dasselbe: sie halten sie an und starten sie woanders wieder.

Was der Gast sieht, ist kein Weglaufen, es ist eine Stufe. Die Uhr hatte einen Wert, und jetzt hat sie einen anderen, mit nichts dazwischen. Auf einen Host zu migrieren, dessen TSC mit anderer Frequenz läuft, kommt obendrauf.

Stufen sind wichtig, weil die Software, die Uhren korrigiert, dafür gebaut ist, Weglaufen zu korrigieren, nicht Teleportation.

Das ist kein KVM-Problem

Es ist verlockend, all das als Schwäche von KVM zu lesen. Ist es nicht.

Jeder Hypervisor liefert eine paravirtuelle Uhr, weil jeder Hypervisor dasselbe strukturelle Problem hat: KVM hat kvm-clock, Hyper-V hat seine Reference-TSC-Seite, VMware hat einen Pseudo-Leistungszähler plus Abgleich über die Tools, Xen hat seine pvclock. Das sind vier unabhängige Umsetzungen eines Behelfs.

Der eigene Schluss der Kernel-Dokumentation ist, dass es hier keine perfekte Lösung gibt. Nur Abwägungen zwischen Genauigkeit, Leistung und Komplexität.

Wenn du es lieber von einem Anbieter als von Kernel-Entwicklern hörst: Microsofts Grenze der Unterstützung für hochgenaue Zeit ist bemerkenswert offen. Um auf einem virtualisierten Windows-System 50 ms Genauigkeit zu behaupten, lautet eine der genannten Bedingungen, dass „the one-day average CPU utilization of the host must not exceed 90%“ — die über einen Tag gemittelte CPU-Auslastung des Hosts darf 90 % nicht übersteigen. Für 1 ms muss der Host unter 80 % bleiben.

Lies das noch einmal: die Genauigkeit der Uhr des Gasts ist dokumentiert als abhängig davon, wie beschäftigt der Host ist. Das ist keine Windows-Eigenart, es ist dieselbe Physik, die die Kernel-Dokumentation beschreibt, niedergeschrieben als Grenze der Unterstützung.

Die drei Schichten der Zeitmessung im Gast, und welche der Gast wirklich besitztAbgleich der Wanduhrwie viel Uhr es wirklich istNTP über das NetzZittern, Asymmetrie, Stratumptp_kvm hypercallfragt den Host direktParavirtuelle Uhrder Host veröffentlicht seine eigene ZeitmessungJeder Hypervisor bringt eine mit,weil sie alle dieses Problem haben:kvm-clockHyper-V-TSC-SeiteVMware pseudo-perfXen pvclockvier unabhängige Umsetzungen eines BehelfsHardware-Zählerin einer VM nicht deineTSCHPETPITACPI-PM-TimerCPU-lokal und mit schwankender Rate, oder emuliert — jedes Lesen fällt also in den HypervisorDer Gast besitzt oben seine Wahl. Die Mitte ist der Hypervisor, der ihm eine Uhr leiht, die dieganze Zeit lief.
Drei Schichten, und der Gast besitzt wirklich nur die obere. Die paravirtuelle Uhr jedes Hypervisors gibt es, um dieselbe fehlende Zusage in der Schicht darunter zu umgehen.

Warum NTP im Gast das falsche Werkzeug ist

NTP ist gut in dem, wofür es entworfen wurde: eine Maschine mit einem echten Schwinger, der etwas zu schnell oder zu langsam läuft, korrigiert dadurch, dass die Umlaufzeit zu einem entfernten Server gemessen und die lokale Uhr sanft nachgezogen wird.

Jede dieser Annahmen wackelt in einer VM.

Der lokale Schwinger ist nicht etwas falsch, er ist zeitweise abwesend. Die Messung der Umlaufzeit macht ein Prozess, der zwischen dem Lesen der Uhr und dem Senden des Pakets vom Scheduler weggenommen werden kann, was die Messung selbst verdirbt. Und die Korrekturen, die nach einer Migration nötig sind, sind Stufen, mit denen ein nachziehendes Verfahren schlecht zurechtkommt oder die es ganz ablehnt.

chrony kommt hier weit besser zurecht als ntpd. Es zieht schneller nach, verträgt Stufen und ist ehrlich über seine eigene Unsicherheit. Aber es löst noch immer das falsche Problem: Zeit über ein Netz von einem Stratum-2-Server 20 ms weit zu holen, während die richtige Zeit im Hypervisor auf der anderen Seite einer einzigen Speichergrenze liegt.

Was du in der Praxis bekommst, ist ein Gast, der meistens richtig liegt, gelegentlich Dutzende oder Hunderte Millisekunden daneben, und nie richtig sagen kann, welches von beidem gerade gilt.

Was Versatz tatsächlich kaputt macht

Niemand kümmert sich um Uhren um ihrer selbst willen. Man kümmert sich, wenn etwas aufhört zu funktionieren.

Kerberos und Active Directory

Kerberos ist von Entwurf her zeitabhängig, weil die Gültigkeit eines Tickets als Zeitfenster ausgedrückt ist.

Die Dokumentation zu krb5.conf von MIT bestimmt clockskew als „the maximum allowable amount of clockskew in seconds that the library will tolerate before assuming that a Kerberos message is invalid“ — den größten zulässigen Uhrenversatz in Sekunden, den die Bibliothek verträgt, bevor sie eine Kerberos-Nachricht für ungültig hält —, und die Voreinstellung sind 300 Sekunden, also fünf Minuten.

Überschreite das, und die Anmeldung wird nicht schlechter, sie scheitert. Da die Anmeldung an Active Directory Kerberos ist, heißt das Domänenanmeldungen, net use, Verbindungen zum SQL Server, Exchange, Dateifreigaben — alles. Fünf Minuten klingen großzügig, bis ein Gast nach der Wiederherstellung eines Snapshots rückwärts springt.

TLS

Ein Zertifikat trägt ein Gültigkeitsfenster: notBefore und notAfter. Ein Gast, dessen Uhr nachläuft, weist ein Zertifikat ab, das heute Morgen ausgestellt wurde, weil es nach seinem Kenntnisstand noch nicht gültig ist. Ein Gast, dessen Uhr vorläuft, weist eines ab, das noch nicht wirklich abgelaufen ist.

Dieselbe Rechnung regiert die Frische von OCSP und CRL, die Ansprüche nbf und exp in einem JWT und TOTP-Codes für die Mehrfaktoranmeldung, die in Fenstern von 30 Sekunden leben. Eine Uhr, die 45 Sekunden daneben liegt, ist ein Anmeldeausfall mit einer sehr verwirrenden Fehlermeldung.

Ceph

Ceph-Monitore kümmern sich darum mehr als fast alles andere im Stapel, weil ihr Konsens davon abhängt.

Die Gesundheitsprüfung MON_CLOCK_SKEW schlägt an, wenn „the clocks on hosts running Ceph Monitor daemons are not well-synchronized“ — die Uhren auf Hosts mit Ceph-Monitor-Diensten nicht gut abgeglichen sind. Genauer: wenn der Versatz mon_clock_drift_allowed übersteigt. Der Rat der Dokumentation ist, mit ntpd oder chrony gegen mehrere Quellen abzugleichen, und sie hält fest, dass besonders der Abgleich der Monitore untereinander zählt.

Du kannst mon_clock_drift_allowed hochsetzen, aber die Dokumentation ist klar, dass es „significantly below the mon_lease interval“ bleiben muss, also deutlich unter dem mon_lease-Intervall. Damit ist es ein kleines Budget, und es dafür auszugeben, ein Zeitproblem des Hypervisors zu übertapezieren, ist kein guter Handel.

Alles Übrige

Protokolle über Hosts hinweg passen nicht mehr zusammen, was aus der Zeitachse eines Vorfalls Rätselraten macht. Datenbank-Replikation und verteilter Konsens — etcd, Galera, alles mit Leader-Leases — werden unglücklich. Fenster für Sicherungen und Überwachung laufen aus dem Takt mit dem, was sie beobachten sollten.

Windows-Gäste laufen anders aus dem Takt

Windows verdient eine eigene Anmerkung, weil sein Zeitdienst mit anderen Zielen gebaut wurde, und das merkt man.

Microsoft sagt klar, dass Versionen vor Windows 10 1607 / Server 2016 „can’t guarantee highly accurate time“ — hochgenaue Zeit nicht zusichern können. Was der Windows-Zeitdienst in diesen Ausgaben lieferte, war „the necessary time accuracy to satisfy Kerberos version 5 authentication requirements“ — die Genauigkeit, die Kerberos Version 5 zum Anmelden braucht — und „loosely accurate time“, also lose genaue Zeit, für Maschinen in einem gemeinsamen AD-Forest. Enger als das war „outside of the design specification… and weren’t supported“ — außerhalb der Entwurfsvorgaben und nicht unterstützt.

Mit anderen Worten: älteres Windows zielt darauf, im Fünf-Minuten-Fenster von Kerberos zu bleiben, nicht darauf, richtig zu liegen. Was in Ordnung ist, bis etwas in deinem Bestand es besser braucht. Eine virtualisierte Windows-Kiste, die 90 Sekunden daneben liegt, meldet sich fröhlich an und schreibt dabei Protokolle, die zu nichts passen.

Windows 10 und Server 2016 und neuer können 1 s, 50 ms oder sogar 1 ms — aber nur unter den oben zitierten Bedingungen, samt der Grenzen für die CPU-Auslastung des Hosts. Microsoft hält außerdem fest, dass „anything that introduces network asymmetry, such as a one-way satellite connection or high CPU load on the target system, will negatively influence accuracy“ — alles, was Asymmetrie ins Netz bringt, etwa eine einseitige Satellitenverbindung oder hohe CPU-Last auf dem Zielsystem, verschlechtert die Genauigkeit. Eine umkämpfte vCPU ist hohe CPU-Last auf dem Zielsystem unter anderem Namen.

Für Windows gibt es kein ptp_kvm. Was du stattdessen hast:

  • Hyper-V-Uhren-Enlightenments. Proxmox zeigt die Windows-Gästen schon. PVE::QemuServer::CPUConfig setzt hv_time neben hv_vapic, hv_spinlocks, hv_relaxed und hv_synic. hv_time ist die paravirtuelle Uhr und das Windows-seitige Gegenstück zu kvm-clock. Das ist für VMs vom Typ Windows standardmäßig an; es gibt nichts einzuschalten.
  • Der QEMU-Gast-Agent. Mit installiertem Agenten kann der Host seine Zeit nach einem Resume oder der Wiederherstellung eines Snapshots in den Gast schieben, und das deckt genau den Stufenfall ab, mit dem NTP am schlechtesten zurechtkommt.
  • Wähle eine Autorität. Der klassische Fehlerfall von Windows in einer VM sind zwei Zeitquellen, die sich streiten: Abgleich vom Host in den Gast und Abgleich über die Domänenhierarchie, die beide dieselbe Uhr in verschiedene Richtungen korrigieren. Lass bei einem Gast in der Domäne die Domänenhierarchie gewinnen und hör auf, dem Host Zeit hineinschieben zu lassen. Bei einem eigenständigen Gast ist Abgleich vom Host in Ordnung. Nie beides.

Willst du genau wissen, was dein Hypervisor einer bestimmten VM über die Zeit erzählt, frag ihn, statt zu raten:

# Everything Proxmox actually passes to QEMU for this VM, including -rtc and CPU flags
qm showcmd <vmid> --pretty

Die Lösung auf QEMU/KVM: ptp_kvm

Für Linux-Gäste auf KVM gibt es eine richtige Antwort, und sie lautet nicht „mehr NTP-Server“.

ptp_kvm lässt den Gast den Host direkt über einen Hypercall nach der Zeit fragen — KVM_HC_CLOCK_PAIRING auf x86 und ein entsprechender Firmware-Aufruf auf arm64. Der Kernel zeigt das als PTP-Hardware-Uhr, aus dem Userspace sieht es also aus wie jede andere Präzisionsuhr, und chrony kann sie als Referenzuhr nutzen.

Die Eigenschaften, auf die es ankommt:

  • Kein Netz. Kein Zittern, keine Asymmetrie, kein Stratum, keine Pakete. Der Weg ist eine Speichergrenze.
  • Unter einer Mikrosekunde. Die Genauigkeit wird vom Hypercall begrenzt, nicht von einem Umlauf durch ein Rechenzentrum.
  • Es umgeht den kaputten Teil. Der Gast zählt nichts und schätzt keinen Umlauf. Er liest einen Wert, den der Host mit einer Uhr berechnet hat, die die ganze Zeit lief.
NTP über das Netz gegen ptp_kvm über eine Speichergrenzechrony gegen Netz-PoolsGastUhr hält dauernd anNetzZittern, AsymmetrieStratum-2-ServerDutzende ms weitden Umlauf misst die Uhr, die korrigiert wird— und dem messenden Prozess kann mitten in der Messung der Kern weggenommen werdenchrony gegen ptp_kvmGastliest /dev/ptp_kvmHost-Uhrhat nie angehaltenHypercallKVM_HC_CLOCK_PAIRINGeine Speichergrenze, kein Netzunter 1 µsKeine Pakete, kein Stratum, keine Asymmetrie, und nichts wird geschätzt. Der Gast rechnet nicht aus,wie viel Uhr es ist — es wird ihm gesagt, vom einzigen Beteiligten, der die ganze Zeit wach war.
Beide Wege enden damit, dass der Gast seine Uhr stellt. Einer davon misst einen Netzumlauf mit einer Uhr, die dauernd anhält; der andere fragt den Hypervisor.

Umsetzung

Wende das auf jede Linux-VM an, die den Treiber hat. Der Host-Hypervisor braucht sein eigenes funktionierendes NTP oder PTP. ptp_kvm gibt dem Gast die Zeit des Hosts, er erbt also den Fehler des Hosts.

1. Das Kernel-Modul beim Start laden.

# /etc/modules-load.d/ptp_kvm.conf
ptp_kvm

2. Ihm einen stabilen Namen geben und chrony lesen lassen.

# /etc/udev/rules.d/90-ptp-kvm.rules
ACTION=="add", SUBSYSTEM=="ptp", ATTR{clock_name}=="kvm", SYMLINK+="ptp_kvm", GROUP="chrony", MODE="0660"

Der Symlink zählt, weil die Numerierung der PTP-Geräte nicht stabil ist. /dev/ptp0 kann bei einem Start die Uhr einer NIC sein und beim nächsten die KVM-Uhr. Über clock_name zu greifen holt jedes Mal die richtige.

3. chrony darauf zeigen lassen, und die Pools entfernen.

Bearbeite /etc/chrony/chrony.conf auf Debian und Ubuntu oder /etc/chrony.conf in der RHEL-Familie. Lösche die pool-Zeilen und nutze:

refclock PHC /dev/ptp_kvm poll 2 stratum 1 delay 0.0004

Die Pools zu entfernen ist keine optionale Ordnungsliebe. Sie stehen zu lassen heißt, chrony zu bitten, eine lokale Referenz unter einer Mikrosekunde mit Internet-Servern Dutzende Millisekunden weit in Einklang zu bringen, und die Internetquellen können die Antwort nur verschlechtern.

4. Neu starten und prüfen.

systemctl restart chronyd    # or chrony, on Debian/Ubuntu

Prüfen

# The symlink exists and points at the KVM clock
ls -l /dev/ptp_kvm
cat /sys/class/ptp/ptp*/clock_name

# chrony should be using PHC0 as its selected source
chronyc sources -v

# and the offset should be microseconds, not milliseconds
chronyc tracking

In chronyc sources erscheint die PHC-Referenzuhr als #* PHC0, sobald sie gewählt ist. Das # markiert eine lokale Hardware-Referenz statt eines Netz-Partners, und das * markiert sie als die genutzte. Siehst du sie aufgeführt, aber nicht gewählt, hat chrony sie nicht angenommen: prüf die Rechte am Gerät und dass die Gruppe chrony in der udev-Regel zu dem Nutzer passt, unter dem chrony auf deiner Distribution tatsächlich läuft.

Vorbehalte

  • Der Host muss richtig liegen. Das bringt den Gast dazu, mit dem Host übereinzustimmen, was nur nützt, wenn der Host mit der Wirklichkeit übereinstimmt. Gib den Hypervisoren echtes NTP oder PTP.
  • Jeder Gast braucht es. Ein Bestand, in dem die Hälfte der VMs ptp_kvm nutzt und die andere Hälfte Internet-Pools, ist ein Bestand mit zwei Zeitautoritäten.
  • Live-Migration ist in Ordnung, und das ist der Punkt. Nach der Migration liest der Gast die Uhr seines neuen Hosts. Solange die Hosts miteinander übereinstimmen, sieht der Gast nie eine Stufe.
  • Nur KVM. Es ist ein KVM-Hypercall. Geschachtelte oder fremde Hypervisoren zeigen das Gerät nicht, und die udev-Regel greift einfach nicht. Das ist ein sauberes Scheitern statt einer stillen falschen Antwort.

Was man nicht tun sollte

  • Erzwing die Clocksource nicht. Lass kvm-clock in Ruhe. tsc oder hpet auf der Kernel-Kommandozeile des Gasts zu erzwingen tauscht eine paravirtuelle Uhr, die für genau diese Lage entworfen wurde, gegen einen Zähler, der dir nie gehörte.
  • Lass ntpdate oder hwclock nicht aus cron laufen. Das ist eine Stufe nach Zeitplan, und genau das hassen Datenbanken und Kerberos.
  • Behalte die Pools nicht „als Rückfall“. Mit einer funktionierenden Referenzuhr sind sie kein Rückfall, sie sind eine zweite Meinung aus schlechterer Quelle.
  • Setz nicht mon_clock_drift_allowed hoch und nenn es behoben. Du hast dann einen Teil eines Budgets ausgegeben, das für die Wirklichkeit im Netz da ist, um ein Problem zu vertragen, für das es eine bekannte Lösung gibt.

Der letzte Punkt ist es wert, klar gesagt zu werden: die Schwelle zu weiten ist nicht, die Uhr zu richten. Es ist, den Alarm zu verschieben, damit er aufhört zu gehen.

Quellen