На первый взгляд точное время на серверах выглядит мелочью из разряда "настроил и забыл". В кластере же эта мелочь превращается в тихий разрушитель: etcd отказывается принимать записи, Kerberos-билеты внезапно протухают, а распределённые логи складываются в мозаику, где события будто происходят задом наперёд. Всё это начинается с ухода часов на несколько секунд, который никто не заметил, потому что никто не смотрел на вывод chronyc tracking. Ниже разбор того, как устроена борьба за миллисекунды на практике: почему старый ntpd продолжает жить в углах инфраструктуры, как читать выводы chronyc до строки, что означают makestep и iburst в конфиге, и какой минимум профилактики избавляет от средненочных инцидентов с аутентификацией.

Откуда берётся уход часов на виртуалках и железе и чем это грозит кластеру

Системные часы на обычном железе опираются на кварцевый генератор, который меняет частоту от температуры. В дата-центре с его стабильным микроклиматом это не так страшно, но стойкие стойки всё равно живут с перепадами, и типичное отклонение дешёвого кварца составляет от 10 до 40 ppm, то есть от секунды до полуминуты в сутки. На виртуальных машинах ситуация хуже: гипервизор отнимает vCPU, таймерные прерывания опаздывают, а после live migration часы гостя могут прыгнуть сразу на несколько секунд.

Цена вопроса известна любому, кто обслуживал распределённые системы. В Kerberos допустимый перекос между клиентом и KDC по умолчанию равен 300 секундам: параметр clockskew в krb5.conf, и превышение даёт ошибку "Clock skew too great" на ровном месте. В etcd требование ещё жёстче: лидер шлёт heartbeat, узлам нужно держать расхождение в пределах сетевой задержки, а погрешность в секунды провоцирует выборы нового лидера и недоступность кластера.

Чем chronyd отличается от ntpd и почему на новых системах выбирают первое

Классический ntpd работает по старинке: опрашивает источники, усредняет и крутит частоту часов малыми поправками через adjtime. Подход честный, но медлительный: устойчивое слежение после рестарта занимает десятки минут. Chrony хранит статистику дрейфа в driftfile и после старта сразу применяет накопленную коррекцию частоты. Схождение занимает секунды, а с опцией iburst у директивы server первый ответ от источника приходит в течение пары секунд после запуска.

Проверить, какой клиент работает на машине, просто:

systemctl status chronyd --no-pager | head -n 5
systemctl status ntpd --no-pager | head -n 5

Здоровая система показывает "active (running)" у chronyd. Одновременная работа обоих клиентов - классическая находка на серверах, которые администрировали разные люди в разные годы: ntpd оставили как legacy, chronyd приехал с обновлением, и оба молча тянут время в свои стороны. Решение одно: выключить лишнего маской, чтобы пакетный менеджер случайно не поднял его снова:

# Полностью отключаем ntpd навсегда: маска свяжет юнит с /dev/null
systemctl stop ntpd && systemctl disable ntpd && systemctl mask ntpd

Разбор chronyc tracking и сопутствующих диагностических команд

Основной диагностический инструмент - chronyc, интерактивный клиент, который спрашивает состояние у работающего chronyd через сокет. Вызов без оболочки выглядит так:

chronyc tracking

Типичный вывод на выровненном сервере:

Reference ID    : C023FC01 (ntp.company.local)
Stratum         : 3
Ref time (UTC)  : Wed Oct 07 13:20:14 2026
System time     : 0.000312492 seconds slow of NTP time
Last offset     : -0.000128641 seconds
RMS offset      : 0.000415920 seconds
Frequency       : 12.382 ppm slow
Residual freq   : -0.004 ppm
Skew            : 0.072 ppm
Root delay      : 0.009812123 seconds
Root dispersion : 0.001202488 seconds
Update interval : 520.0 seconds
Leap status     : Normal

Разбор по строкам. Reference ID - адрес источника, к которому привязаны часы; видна корпоративная станция ntp.company.local. Stratum - дистанция до эталонного источника: stratum 1 получает время по GPS, каждая прослойка добавляет единицу; 3 нормально для внутреннего сервера, 16 означает, что источник недостижим. System time - текущая ошибка локальных часов: 312 микросекунд отставания, отличный показатель. Last offset - последняя разность, RMS offset - среднеквадратичное отклонение последних измерений; микросекунды говорят о здоровом канале. Frequency - скорректированная ошибка частоты кварца в ppm, именно это значение chronyd пишет в driftfile; 12.382 ppm без коррекции дали бы примерно 1.07 секунды ухода в сутки. Residual freq - остаток ошибки частоты после коррекции, должен стремиться к нулю. Skew - неопределённость оценки frequency, меньше - лучше. Root delay и Root dispersion - накопленная задержка и дисперсия до эталона; их сумма задаёт потолок точности, и с 9 мс задержки точность выше миллисекунды недостижима физически. Update interval - текущий интервал опроса, здесь chronyd дорос до стабильных 520 секунд. Leap status - статус високосной секунды, норма "Normal".

Дальше смотрим, откуда берётся время:

chronyc sources -v

Вывод содержит таблицу источников, а флаг -v добавляет шапку с расшифровкой символов. Ключевая первая колонка: символ "^" означает сервер, "=" - peer, "#" - локальные часы; вторая колонка с "*" отмечает текущий выбранный источник, "+" - приемлемый резерв, "-" - потенциальный, "x" - некорректный, "?" - недоступный. Типичная беда выглядит как строка с "?" против каждого источника: чаще всего виноват закрытый UDP 123 на сетевом экране или корпоративный прокси.

Третью лапу диагностики дает команда chronyc sourcestats:

chronyc sourcestats

Для каждого источника она показывает число измерений NP, частоту коррекции и стандартное отклонение std dev. Значение std dev в десятках миллисекунд намекает на перегруженный uplink источника: такой сервер разумно заменить ближним, даже если формально он ещё выбран.

Поучительно сравнить с выводом старого доброго ntpq -p, который всё ещё работает там, где живёт ntpd:

ntpq -p

Таблица показывает remote, refid, stratum, delay, offset и jitter. Звёздочка перед адресом отмечает выбранный источник, offset в миллисекундах трактуется так же. Логика одна, инструменты разные: мигрируя с ntpd на chronyd, администратор пересаживается с одной приборной панели на другую, не учась читать заново.

Практический конфиг chronyd для кластера виртуальных машин

Для кластера характерны резкие скачки часов при миграциях и рестартах, поэтому конфиг пишется под быстрое схождение и честную коррекцию больших расхождений. Комментарии кусочек за кусочком:

# /etc/chrony.conf на RHEL-подобной системе
# Внутренний сервер времени, iburst ускоряет первый ответ после старта:
# клиент шлёт 4 пакета за первые ~2 секунды вместо стандартного ожидания
server ntp.company.local iburst minpoll 6 maxpoll 10
# Резервный источник на вырост
server 192.168.10.5 iburst

# Файл, где chronyd копит скорректированный дрейф кварца между рестартами
driftfile /var/lib/chrony/drift

# Если расхождение больше 1.0 секунды в первые 3 измерения, шагнуть время сразу,
# а не полировать его часами; именно это спасает VM после live migration
makestep 1.0 3

# Включить синхронизацию RTC с системным временем через ядро Linux
rtcsync

# Каталог для логов измерений
logdir /var/log/chrony
log measurements statistics tracking

Директива makestep заслуживает отдельной реплики. Пара ограничений там последовательная: порог в секундах и число первых обновлений, в течение которых порог действует. "makestep 1.0 3" переводится как "пока машина стартовала и chronyd получил меньше трёх измерений, любое расхождение больше секунды решаем скачком; потом работаем плавно". Скачки времени нелюбимы некоторыми приложениями, которые обижаются на часы, пошедшие назад, поэтому на уже устоявшемся сервере шаги отключены, а на холодном старте здорово сокращают время до консистентности с двух часов до пары минут. Для Kerberos-инфраструктуры порог иногда опускают до 0.1 секунды, если машины регулярно просыпаются из suspend или мигрируют.

После правки конфига штатный ритуал:

# Перечитываем конфиг мягко
systemctl reload chronyd
# Принудительная немедленная пересинхронизация в обход интервалов
chronyc burst 4/10
# Проверяем, что схождение пошло
chronyc tracking

Burst с параметром "4/10" означает: четыре хороших измерения из десяти попыток, после чего вернуться к нормальному ритму.

Мониторинг расхождения и аппаратных часов как регулярная дисциплина

Тюнинг без наблюдения мертвецки бесполезен. Первое, что хочется видеть в дашборде - абсолютное значение System time из chronyc tracking по всем узлам. Для этого не нужен агент, хватает cron-задачи раз в минуту, которая парсит вывод и кладёт метрику в Node Exporter textfile:

# /usr/local/bin/chrony-offset.sh
#!/bin/bash
OFFSET=$(chronyc tracking | awk '/System time/ {print $4}')
cat > /var/lib/node_exporter/textfile_collector/chrony.prom <<EOF
chrony_system_time_offset_seconds $OFFSET
EOF

Алерт разумно повесить на 200 миллисекунд для обычных сред и 100 миллисекунд там, где живут распределённые базы данных. На уровне 150 мс ещё можно вмешаться руками, на 4 минутах 59 секундах перед Kerberos-пределом в 300 секунд уже поздно.

Второй незаметный пункт профилактики - аппаратные часы RTC. Когда сервер выключают или перезагружают, системное время улетает, и при включении ядро стартует с показаниями RTC. Если RTC отстаёт на 40 секунд, chronyd потребуется пересинхронизация на каждом апгарде. Директива rtcsync периодически синхронизирует RTC из ядра, но проверить вручную не мешает:

# Сверяем системные часы и аппаратный RTC
date; hwclock --show
# При расхождении больше пары секунд записываем системное время в RTC
hwclock --systohc
# Проверяем результат обратным чтением
hwclock --show

В systemd-мире диагностика дополняется timedatectl status со строками "System clock synchronized: yes" и "NTP service: active". Timedatectl, честно говоря, смотрит на systemd-timesyncd, а не на chronyd напрямую, и при здоровом chronyd статус там может выглядеть подозрительно скромно. Об этом забывают и пугаются зря.

Из практики кластерного вмешательства каноничный алгоритм действий при жалобах на рассинхронизацию выглядит так:

  1. Прогнать chronyc tracking на каждом узле и зафиксировать System time и Stratum: значения выше секунды или stratum 16 - кандидат на разбор первым;
  2. Посмотреть chronyc sources -v и убедиться, что на всех узлах выбран тот же источник с меткой "*", а не "?": расхождение источников даёт три разных "правильных" времени;
  3. Проверить chronyc sourcestats: источник с дрожащей задержкой менять на ближний соседний, а не на дальний публичный пул;
  4. Сверить часы с эталоном через chronyd -Q 'server ntp.company.local iburst' в одноразовом режиме без применения времени;
  5. После правок записать системное время в RTC через hwclock --systohc, чтобы следующий рестарт не начался со старой ошибки;
  6. Включить метрику System time в мониторинг с алертом на 200 мс, иначе тикет вернётся через квартал.

Типичные причины рассинхронизации и что с ними делать

Первый класс - сетевая сегментация. UDP 123 заблокирован новым правилом сетевого экрана, sources показывает "?" на всех строках, а перестроенную две недели назад маршрутизацию уже никто не помнит. Лечение очевидно: выяснить, что закрыли, и пробросить NTP в обе стороны. Второй класс - уход после гипервизора: live migration и массовые evacuation прыгают часы, а конфиг без makestep не справляется с полминутой расхождения. Третий класс - конфликт клиентов: молча поднятый ntpd параллельно с chronyd или включённый timesyncd, и два механизма крутят частоту по-разному. Четвёртый класс - деградация источника: публичный пул подменил ближнего исправного сервера на дальний перегруженный, std dev выросла с двух до сорока миллисекунд. Пятый класс - високосные секунды: у корпоративных станций желателен один upstream, чтобы половина парка не сместила время, а половина проигнорировала.

Дисциплина здесь простая. Один источник времени на сервер - лучше двух самостоятельных. Конфиг с makestep на старте и driftfile, переживающим ребут. Мониторинг System time и автоматический алерт на сотые доли секунды. Раз в квартал лёгкий скрипт, который гоняет chronyc tracking по узлам и кидает отчёт почтой, находит больше честных проблем, чем кажется: в одном таком прогоне на 60-узловом кластере обнаруживаются и убитые источники, и забытый ntpd на пяти старых машинах, и один узел, который ушёл на минуту в будущее из-за залипшего RTC после переезда стойки. Время быстро прощает тем, кто за ним следит, и мстит тем, кто думал, что синхронизация - штука "настроил один раз на всю жизнь".