La suposición que hace todo reloj

El reloj de un ordenador funciona contando algo regular y confiando en que ese algo sigue contando. Un cristal oscila, un contador se incrementa, y el software calcula cuánto tiempo ha pasado.

La virtualización rompe la parte de confiar.

La documentación KVM del kernel sobre la medida del tiempo resume el problema en una frase: «the virtual operating system does not run with 100% usage of the CPU, despite the fact that it may very well make that assumption» — el sistema operativo virtual no corre usando el 100 % de la CPU, aunque bien puede suponer que sí. Todo lo que sigue se deriva de ahí.

Tu vCPU no está siempre corriendo

Una vCPU es un hilo en el host. Corre cuando el planificador del host lo dice.

Cuando no corre, el invitado no está simplemente ocioso — está ausente. No puede contar, no puede atender una interrupción de temporizador, y no tiene forma de saber cuánto tiempo estuvo fuera. El host lo contabiliza como steal time, que es el nombre honesto para «tiempo que te ocurrió a ti en vez de servirte».

Las interrupciones de temporizador son donde esto duele más. Un invitado que pide un tick periódico le está pidiendo al host que entregue interrupciones a cadencia fija, y el host no siempre puede obedecer. De nuevo la documentación del kernel: «the host virtualization engine may not be able to deliver the proper number of interrupts per second, and so guest time may fall behind» — el motor de virtualización del host puede no ser capaz de entregar el número correcto de interrupciones por segundo, así que la hora del invitado se queda atrás.

Por qué los ticks de temporizador de un invitado dejan de estar regularmente espaciadosHardware nativo — la CPU es siempre tuyaen marchaticks de temporizador, regularmente espaciados — contarlos te da la horaEn una VM — la vCPU es un hilo en el planificador de otroen marchadesplanificadodesplanificadolos ticks que tocaban durante los huecos llegan tarde, a montones, o no lleganEl invitado no ve los periodos punteados. Desde dentro, el reloj simplemente produjo menosticks de los que debía — por eso la documentación del kernel dice que la hora de un invitado«may fall behind» cuando el host no puede entregar las interrupciones que pidió.El host llama a las regiones punteadas steal time. El invitado no las llama nada, porque no estabaahí.
La vista propia del invitado es la fila de abajo: ticks que llegan tarde, ticks que llegan a ráfagas, y huecos que no sabe explicar. Mide tanto el planificador del host como el paso del tiempo.

Cuanto más alta la cadencia de tick, peor, y un host sobresuscrito lo empeora todavía más. Por eso también un host cargado degrada la hora de invitados tranquilos. Todos hacen cola para los mismos núcleos físicos.

Los contadores tampoco son tuyos

Si los ticks periódicos no son fiables, la respuesta evidente es leer un contador en su lugar. Eso tiene sus propios problemas.

El TSC es el rápido, y la documentación del kernel es franca al respecto: «The TSC is a CPU-local clock in most implementations… the TSCs of different CPUs may start at different times» — el TSC es un reloj local a la CPU en la mayoría de implementaciones, y los TSC de CPU distintas pueden arrancar en momentos distintos. Su cadencia puede variar con los estados de energía del procesador, y en piezas antiguas se detiene por completo cuando el núcleo se pone en reposo. Una vCPU que migra entre núcleos físicos puede por tanto leer un contador que no coincide con el que leyó un microsegundo antes.

Las alternativas son malas de otra forma. El HPET, el PIT y el temporizador ACPI PM son todos dispositivos emulados, así que cada lectura es una trampa hacia el hipervisor. Correcto, y lo bastante caro como para que un invitado que lea el reloj en un bucle apretado lo note.

Por eso existen los relojes paravirtuales. En KVM, kvm-clock deja que el host publique su propia medida del tiempo en una estructura compartida que el invitado lee directamente: sin trampa, sin conteo, sin suponer que el invitado estaba despierto. También por eso deberías dejar en paz la clocksource del invitado en vez de forzar tsc o hpet porque un mensaje de foro dijera que era más rápido.

Migración, instantáneas y suspensión

La migración en vivo, la restauración de instantánea y la suspensión/reanudación le hacen todas lo mismo al reloj de un invitado: lo paran, y luego lo arrancan de nuevo en otro sitio.

Lo que el invitado ve no es deriva, es un salto. El reloj valía una cosa, y ahora vale otra, sin nada en medio. Migrar a un host cuyo TSC corre a otra frecuencia lo agrava.

Los saltos importan porque el software que corrige relojes está hecho para corregir deriva, no teletransporte.

Esto no es un problema de KVM

Es tentador leer todo lo anterior como un defecto de KVM. No lo es.

Todos los hipervisores llevan un reloj paravirtual, porque todos los hipervisores tienen el mismo problema estructural: KVM tiene kvm-clock, Hyper-V tiene su página TSC de referencia, VMware tiene un contador de rendimiento ficticio más una sincronización por las Tools, Xen tiene su pvclock. Son cuatro implementaciones independientes de un mismo apaño.

La conclusión de la propia documentación del kernel es que aquí no hay solución perfecta. Solo compromisos entre precisión, rendimiento y complejidad.

Si lo prefieres de labios de un fabricante en vez de desarrolladores del kernel, la frontera de soporte de Microsoft para la hora de alta precisión es notablemente franca. Para reclamar una precisión de 50 ms en un sistema Windows virtualizado, uno de los requisitos declarados es que «the one-day average CPU utilization of the host must not exceed 90%» — la utilización media de CPU del host en un día no debe superar el 90 %. Para 1 ms, el host debe mantenerse por debajo del 80 %.

Léelo otra vez: la precisión del reloj del invitado está documentada como condicionada por lo cargado que esté el host. Eso no es una rareza de Windows, es la misma física que describe la documentación del kernel, puesta por escrito como una frontera de soporte.

Las tres capas de la medida del tiempo en el invitado, y cuáles posee de verdadSincronización de hora de paredqué hora es en realidadNTP sobre la redjitter, asimetría, estratohypercall ptp_kvmpregunta al host directamenteReloj paravirtualel host publica su propia medida del tiempoTodos los hipervisores llevan uno,porque todos tienen este problema:kvm-clockpágina TSC de Hyper-VVMware pseudo-perfXen pvclockcuatro implementaciones independientes de un mismo apañoContadores hardwareno son tuyos en una VMTSCHPETPITtemporizador ACPI PMlocales a la CPU y de cadencia variable, o emulados — leerlos es una trampa al hipervisorEl invitado posee su elección arriba del todo. La capa del medio es el hipervisor prestándole un reloj quesí estuvo corriendo todo el rato.
Tres capas, y el invitado solo posee de verdad la de arriba. El reloj paravirtual de cada hipervisor existe para sortear la misma garantía que falta en la capa de debajo.

Por qué NTP dentro del invitado es la herramienta equivocada

NTP es bueno en aquello para lo que se diseñó: una máquina con un oscilador real que va un poco rápido o lento, corregida midiendo el ida y vuelta hacia un servidor remoto y ajustando suavemente el reloj local.

Cada una de esas suposiciones es frágil en una VM.

El oscilador local no está ligeramente mal, está intermitentemente ausente. La medida del ida y vuelta la toma un proceso que puede ser desplanificado entre leer el reloj y enviar el paquete, lo que corrompe la propia medida. Y las correcciones necesarias tras una migración son saltos, que un algoritmo de ajuste gradual maneja mal o rechaza de plano.

chrony se las apaña mucho mejor que ntpd aquí. Ajusta más rápido, tolera saltos, y es honesto sobre su propia incertidumbre. Pero sigue resolviendo el problema equivocado: tirar de la hora a través de una red desde un servidor de estrato 2 a 20 ms de distancia, cuando la hora correcta está posada en el hipervisor, al otro lado de una simple frontera de memoria.

Lo que obtienes en la práctica es un invitado casi siempre correcto, de vez en cuando desviado decenas o cientos de milisegundos, y nunca del todo capaz de decirte cuál.

Qué rompe de verdad el desfase

A nadie le importan los relojes por sí mismos. Le importan cuando algo deja de funcionar.

Kerberos y Active Directory

Kerberos depende del tiempo por diseño, porque la validez de un ticket se expresa como una ventana temporal.

La documentación del krb5.conf del MIT define clockskew como «the maximum allowable amount of clockskew in seconds that the library will tolerate before assuming that a Kerberos message is invalid» — el desfase máximo en segundos que la biblioteca tolerará antes de dar por inválido un mensaje Kerberos, y el valor por defecto son 300 segundos, cinco minutos.

Cruza eso y la autenticación no se degrada, falla. Como la autenticación de Active Directory es Kerberos, eso significa los inicios de sesión del dominio, net use, las conexiones a SQL Server, Exchange, los recursos compartidos de archivos — todo el lote. Cinco minutos parecen generosos hasta que un invitado retrocede tras una restauración de instantánea.

TLS

Un certificado lleva una ventana de validez: notBefore y notAfter. Un invitado cuyo reloj va atrasado rechazará un certificado emitido esta mañana porque, por lo que él sabe, el certificado aún no es válido. Un invitado cuyo reloj va adelantado rechazará uno que en realidad no ha caducado.

La misma aritmética gobierna la frescura de OCSP y CRL, las reclamaciones nbf/exp de los JWT, y los códigos TOTP de la autenticación multifactor, que viven en ventanas de 30 segundos. Un reloj desfasado 45 segundos es una caída de autenticación con un mensaje de error muy confuso.

Ceph

Los monitores de Ceph se preocupan por esto más que por casi nada más en la pila, porque su consenso depende de ello.

El chequeo de salud MON_CLOCK_SKEW salta cuando «the clocks on hosts running Ceph Monitor daemons are not well-synchronized» — los relojes de los hosts que ejecutan demonios Ceph Monitor no están bien sincronizados. En concreto, cuando el desfase supera mon_clock_drift_allowed. El consejo de la documentación es sincronizar con ntpd o chrony contra varias fuentes, y señala que la sincronización entre monitores importa de forma particular.

Puedes subir mon_clock_drift_allowed, pero la documentación es clara: tiene que quedar «significantly below the mon_lease interval» — muy por debajo del intervalo mon_lease. Como tal, es un presupuesto pequeño, y gastarlo para tapar un problema de medida del tiempo del hipervisor no es un buen trato.

Todo lo demás

Los registros de varios hosts dejan de correlacionar, lo que convierte la cronología de un incidente en adivinanza. La replicación de bases de datos y el consenso distribuido — etcd, Galera, cualquier cosa que haga arrendamientos de líder — se ponen de mal humor. Las ventanas de copia de seguridad y de supervisión se desalinean de aquello que debían observar.

Los invitados Windows se desfasan de otra forma

Windows merece su propia nota, porque su servicio de tiempo se construyó con otros objetivos y se nota.

Microsoft dice sin rodeos que las versiones anteriores a Windows 10 1607 / Server 2016 «can’t guarantee highly accurate time» — no pueden garantizar una hora de alta precisión. Lo que el servicio de tiempo de Windows ofrecía en esas versiones era «the necessary time accuracy to satisfy Kerberos version 5 authentication requirements» — la precisión de hora necesaria para satisfacer los requisitos de autenticación de Kerberos versión 5, y una hora «loosely accurate», vagamente precisa, para las máquinas de un mismo bosque AD. Más ajustado que eso quedaba «outside of the design specification… and weren’t supported» — fuera de la especificación de diseño, y sin soporte.

Dicho de otro modo, el Windows antiguo aspira a quedarse dentro de la ventana Kerberos de cinco minutos, no a ser exacto. Lo cual va muy bien hasta que algo en tu parque necesita más. Una máquina Windows virtualizada desfasada 90 segundos se autenticará sin rechistar mientras escribe registros que no se pueden correlacionar con nada.

Windows 10 y Server 2016 en adelante saben hacer 1 s, 50 ms o incluso 1 ms — pero solo bajo las condiciones citadas antes, incluidos los límites de utilización de CPU del host. Microsoft también señala que «anything that introduces network asymmetry, such as a one-way satellite connection or high CPU load on the target system, will negatively influence accuracy» — cualquier cosa que introduzca asimetría de red, como un enlace por satélite unidireccional o una carga de CPU alta en el sistema de destino, perjudicará la precisión. Una vCPU en disputa es una carga de CPU alta en el sistema de destino con otro nombre.

No hay ptp_kvm para Windows. Lo que tienes en su lugar:

  • Los enlightenments de reloj de Hyper-V. Proxmox ya se los expone a los invitados Windows. PVE::QemuServer::CPUConfig pone hv_time junto a hv_vapic, hv_spinlocks, hv_relaxed y hv_synic. hv_time es el reloj paravirtual, y es el equivalente del lado Windows de kvm-clock. Está activo por defecto para las VM tipadas como Windows; no hay nada que habilitar.
  • El agente invitado de QEMU. Con el agente instalado, el host puede empujar su hora dentro del invitado tras una reanudación o una restauración de instantánea, lo que cubre el caso del salto, el que NTP maneja peor.
  • Elige una sola autoridad. El fallo clásico de Windows en una VM son dos fuentes de tiempo peleándose: la sincronización host-a-invitado y la sincronización por la jerarquía del dominio, ambas corrigiendo el mismo reloj en sentidos contrarios. Para un invitado unido al dominio, deja que gane la jerarquía del dominio y detén al host de empujarle la hora. Para un invitado autónomo, la sincronización por el host va bien. Nunca las dos.

Si quieres saber exactamente qué le cuenta tu hipervisor a una VM dada sobre el tiempo, pregúntaselo en vez de adivinar:

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

El arreglo en QEMU/KVM: ptp_kvm

Para los invitados Linux en KVM hay una respuesta como es debido, y no es «más servidores NTP».

ptp_kvm deja que el invitado le pregunte al host qué hora es, directamente, a través de un hypercall — KVM_HC_CLOCK_PAIRING en x86, y una llamada de firmware equivalente en arm64. El kernel lo presenta como un dispositivo de reloj hardware PTP, así que desde el espacio de usuario parece cualquier otra fuente de reloj de precisión, y chrony puede usarlo como reloj de referencia.

Las propiedades que importan:

  • Sin red. Sin jitter, sin asimetría, sin estrato, sin paquetes. El camino es una frontera de memoria.
  • Por debajo del microsegundo. La precisión la acota el hypercall, no un ida y vuelta a través de un centro de datos.
  • Sortea la parte rota. El invitado no cuenta nada ni estima ningún ida y vuelta. Lee un valor que el host calculó con un reloj que sí estuvo corriendo todo el rato.
NTP a través de la red frente a ptp_kvm a través de una frontera de memoriachrony contra pools de redinvitadoel reloj no para de pararseredjitter, asimetríaservidor de estrato 2a decenas de msel ida y vuelta lo mide el reloj que se está corrigiendo— y el proceso que mide puede ser desplanificado en plena medidachrony contra ptp_kvminvitadolee /dev/ptp_kvmreloj del hostnunca dejó de correrhypercallKVM_HC_CLOCK_PAIRINGuna frontera de memoria, no una redsub-µsSin paquetes, sin estrato, sin asimetría, y nada estimado. El invitado no está averiguandoqué hora es — se la dicen, y es el único participante que estuvo despierto todo el intervalo.
Ambos caminos terminan con el invitado ajustando su reloj. Uno mide un ida y vuelta de red con un reloj que no para de pararse; el otro le pregunta al hipervisor.

Implementación

Aplica esto a toda VM Linux que tenga el controlador. El hipervisor host necesita un NTP o un PTP que funcione, por su parte. ptp_kvm le entrega al invitado la hora del host, así que hereda el error del host.

1. Cargar el módulo del kernel al arrancar.

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

2. Darle un nombre estable y dejar que chrony lo lea.

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

El enlace simbólico importa porque la numeración de dispositivos PTP no es estable. /dev/ptp0 puede ser el reloj de una NIC en un arranque y el reloj KVM en el siguiente. Filtrar por clock_name atrapa el correcto cada vez.

3. Apuntar chrony hacia él, y quitar los pools.

Edita /etc/chrony/chrony.conf en Debian y Ubuntu, o /etc/chrony.conf en la familia RHEL. Borra las líneas pool y usa:

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

Quitar los pools no es una limpieza opcional. Dejarlos le pide a chrony que reconcilie una referencia local por debajo del microsegundo contra servidores de internet a decenas de milisegundos, y las fuentes de internet solo pueden empeorar la respuesta.

4. Reiniciar y comprobar.

systemctl restart chronyd    # or chrony, on Debian/Ubuntu

Verificación

# 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

En chronyc sources, la refclock PHC aparece como #* PHC0 una vez seleccionada. El # marca una referencia hardware local en vez de un par de red, y el * marca la que está en uso. Si la ves listada pero no seleccionada, chrony no la ha aceptado: comprueba los permisos del dispositivo y que el grupo chrony de la regla udev coincide con el usuario bajo el que chrony corre realmente en tu distribución.

Salvedades

  • El host tiene que estar bien. Esto pone al invitado de acuerdo con el host, lo cual solo sirve si el host está de acuerdo con la realidad. Dales a los hipervisores NTP o PTP de verdad.
  • Todos los invitados lo necesitan. Un parque donde la mitad de las VM usan ptp_kvm y la otra mitad pools de internet es un parque con dos autoridades de tiempo.
  • La migración en vivo va bien, y es el objetivo. Tras migrar, el invitado lee el reloj de su nuevo host. Siempre que los hosts estén de acuerdo entre sí, el invitado nunca ve un salto.
  • Solo KVM. Es un hypercall de KVM. Los hipervisores anidados o ajenos no presentarán el dispositivo, y la regla udev simplemente no se disparará. Eso es un fallo limpio en vez de una respuesta silenciosamente equivocada.

Qué no hacer

  • No fuerces la clocksource. Deja kvm-clock en paz. Forzar tsc o hpet en la línea de comandos del kernel del invitado cambia un reloj paravirtual diseñado para esta situación por un contador que nunca fue tuyo.
  • No lances ntpdate ni hwclock desde cron. Eso es un salto de reloj a hora fija, exactamente lo que odian las bases de datos y Kerberos.
  • No mantengas los pools «como respaldo». Con una refclock que funciona no son un respaldo, son una segunda opinión de una fuente peor.
  • No subas mon_clock_drift_allowed y lo des por arreglado. Has gastado parte de un presupuesto que existe para la realidad de la red, con tal de tolerar un problema cuya solución es conocida.

Ese último merece decirse claro: ensanchar el umbral no arregla el reloj. Mueve la alarma para que deje de sonar.

Referencias