Обзор
Время — та зависимость, которую замечают только когда она ломается, и ломается она не там, где её ищут. Сертификат отвергается с формулировкой «ещё не действителен», хотя выдан вчера. Код из приложения-аутентификатора не подходит, хотя вводится верно. Логи с двух машин при разборе инцидента не сходятся, и восстановить порядок событий невозможно. Во всех трёх случаях виноваты часы, но ни одно сообщение об ошибке этого не говорит.
Причина в том, что время в системе — не одна величина, а несколько разных, живущих по разным правилам: аппаратные часы на плате, системные часы в ядре, монотонный счётчик для измерений и слой часовых поясов поверх всего этого. Большинство проблем возникает на стыках: значение берут из одного механизма, а ожидают свойств другого.
Статья разбирает эти механизмы, два демона синхронизации и, отдельно, диагностику — потому что чтение вывода chronyc требует понимания, что означают его поля, а без этого он выглядит набором чисел.
Версии-якоря: chrony 4.9 (август 2026), systemd 261.
Что такое «время» в системе
RTC и системные часы
Аппаратные часы (RTC, real-time clock) — микросхема на плате с собственным питанием от батарейки. Их единственная задача — помнить время, пока машина выключена. Ядро читает их один раз при загрузке, устанавливает по ним системные часы и дальше к ним практически не обращается: точность RTC невелика, разрешение обычно в секундах.
Системные часы ведёт ядро, отсчитывая от прочитанного при старте значения с помощью счётчика процессора. Именно они возвращаются программам, и именно их правит демон синхронизации.
В виртуальных машинах и на облачных VPS картина другая: RTC там эмулируется гипервизором, а точность после миграции или паузы гостя может быть какой угодно. На таких машинах демон синхронизации не «желателен для точности», а обязателен для работоспособности.
Отдельный случай — машины вовсе без батарейки RTC (одноплатники, часть встраиваемых устройств). Для них systemd-timesyncd держит файл /var/lib/systemd/timesync/clock и при каждой успешной синхронизации обновляет его время модификации; при следующей загрузке системные часы подтягиваются как минимум к этой отметке. Точным временем это не является, но обеспечивает главное — время не идёт назад между перезагрузками, а с ним не ломаются проверки сроков действия и сборка по временным меткам.
UTC или локальное время в RTC
В самих часах не записано, в какой шкале они идут — это соглашение, которое операционная система должна знать заранее. В Linux оно хранится третьей строкой файла /etc/adjtime: UTC или LOCAL.
Правильный ответ — UTC, и он же по умолчанию. Локальное время в RTC создаёт проблему, точно описанную в документации hwclock: если система работает в момент перехода на летнее время, записанное в RTC значение будет скорректировано; если система в этот момент была выключена — прочитанное из RTC значение скорректировано не будет. Одна и та же машина получает разное время в зависимости от того, работала ли она в ночь перехода.
Классический источник боли — дуалбут с Windows, которая исторически держит RTC в локальном времени. Симптом узнаваем: после каждой смены системы часы уезжают ровно на величину смещения пояса. Правильное лечение — приучить Windows к UTC (параметр реестра RealTimeIsUniversal), а не переводить Linux на LOCAL: во втором случае вы получаете описанную выше несогласованность в обмен на видимость порядка.
Проверить и переключить:
timedatectl # строка «RTC in local TZ»
timedatectl set-local-rtc 0 # перевести RTC в UTC
Какие часы читать: REALTIME против MONOTONIC
Ядро отдаёт несколько разных часов, и выбор между ними — не оптимизация, а вопрос корректности.
| Часы | Может ли прыгнуть | Учитывает NTP-подстройку | Идёт во время suspend |
|---|---|---|---|
CLOCK_REALTIME |
да, назад и вперёд | да | да |
CLOCK_MONOTONIC |
нет | да, плавно | нет |
CLOCK_MONOTONIC_RAW |
нет | нет | нет |
CLOCK_BOOTTIME |
нет | да, плавно | да |
CLOCK_REALTIME — настоящее календарное время, и его можно переставить: администратором, демоном синхронизации при шаге, гипервизором. Для «который час» это правильный источник и единственно возможный.
Для измерения длительности он не годится совсем. Классическая ошибка — засечь CLOCK_REALTIME до операции и после, вычесть. Если в промежуток попал шаг часов назад, разность получится отрицательной; если вперёд — операция «займёт» лишние секунды. Код, где таймаут вычисляется таким способом, при переводе часов либо срабатывает мгновенно, либо не срабатывает никогда, и воспроизвести это в тестах почти невозможно. Для длительностей нужен CLOCK_MONOTONIC: он не переставляется никогда.
Две тонкости, которые обычно упускают. Во-первых, CLOCK_MONOTONIC всё же не равномерен: он подвержен плавным подстройкам частоты от adjtime() и NTP — просто никогда не идёт назад. Если нужна именно аппаратная шкала без вмешательства демона синхронизации, есть CLOCK_MONOTONIC_RAW. Во-вторых, CLOCK_MONOTONIC не считает время, проведённое системой в suspend: на ноутбуке, закрытом на ночь, он «простоит» эту ночь. Если требуется монотонность с учётом сна — CLOCK_BOOTTIME.
Демоны синхронизации
В Linux сегодня два реальных кандидата, и разница между ними принципиальная, а не в удобстве.
systemd-timesyncd реализует только SNTP — упрощённый клиентский протокол. Он опрашивает сервер, получает время, правит часы. Ни серверной части, ни сложной дисциплины часов, ни работы с несколькими источниками одновременно у него нет, и документация systemd это оговаривает прямо: сценарии, требующие полноценного NTP, им не покрываются.
chrony — полная реализация NTP: несколько источников с оценкой качества каждого, отсев источников с неверным временем, статистическая фильтрация, собственная модель дрейфа часов, режим сервера для других машин, аппаратные штампы времени и NTS.
| systemd-timesyncd | chrony 4.9 | |
|---|---|---|
| Протокол | SNTP (клиент) | NTP полностью |
| Несколько источников одновременно | нет | да, с комбинированием |
| Отсев источника с неверным временем | нет | да (falseticker) |
| Модель дрейфа часов | нет | да, компенсирует уход между опросами |
| Работа сервером для парка машин | нет | да |
| Плохая сеть, редкая связь, ноутбук | посредственно | целевой сценарий |
| Диагностика | timedatectl show-timesync |
chronyc tracking, sources, sourcestats |
Когда чего хватает. Для десктопа или ноутбука, которому нужно «примерно верное время», timesyncd достаточен, и он уже установлен. Для сервера — chrony, и причина не в точности как таковой, а в отсеве плохих источников: SNTP-клиент верит единственному серверу, к которому обратился, и если тот отдаёт неверное время, поедет вслед за ним без единого возражения. Для машины, выдающей TOTP-коды, подписывающей что-либо или участвующей в Kerberos, это неприемлемый режим.
Одновременно запускать оба нельзя — они будут драться за системные часы. Установка chrony в большинстве дистрибутивов маскирует systemd-timesyncd автоматически, но проверить стоит: systemctl status systemd-timesyncd должен показывать masked или inactive.
Дисциплина часов: подтягивать или переставлять
Исправить отставшие часы можно двумя способами, и они не равноценны.
Slew (подтягивание) — изменить скорость хода: часы идут чуть быстрее, пока не догонят. Время при этом остаётся монотонным, ни одна отметка не повторяется и не пропадает. Плата — медленно: скорость подтягивания ограничена, и на Linux chronyd может править частоту в пределах до 100000 ppm (10%), то есть даже теоретический максимум даёт лишь несколько секунд в минуту, а реальный предел задаётся директивой maxslewrate и обычно много ниже.
Step (шаг) — переставить часы одномоментно. Быстро, но время скачет: при шаге назад отметки повторяются, при шаге вперёд возникает разрыв. Всё, что опирается на монотонность CLOCK_REALTIME, в этот момент может сломаться — сработать разом или зависнуть.
Отсюда правило: подтягивать всегда, переставлять только на старте, пока прикладной софт ещё не запущен. Именно это и делает директива chrony:
makestep 1.0 3
Первый аргумент — порог в секундах, второй — предел по числу обновлений. Читается так: переставить часы шагом, если расхождение больше секунды, но только в первых трёх обновлениях после запуска chronyd. Дальше — только плавное подтягивание. Документация chrony формулирует смысл прямо: на большинстве систем часы желательно переставлять только при загрузке, до старта программ, которые полагаются на монотонное движение времени вперёд.
Отрицательное значение второго аргумента снимает ограничение — то есть разрешает шаг в любой момент. Это то, что иногда советуют в интернете как лекарство от «часы всё равно уезжают», и это лечение симптома ценой создания нового класса багов. Если часы уезжают настолько, что подтягивание не справляется, проблема в источнике времени или в железе, а не в осторожности демона.
Выбор источников
Источник времени задаётся директивами server (конкретный хост) и pool (имя, за которым скрывается набор адресов):
pool 2.pool.ntp.org iburst
iburst ускоряет первую синхронизацию, отправляя серию запросов вместо одного, — на старте это разница между секундами и минутами.
Stratum — расстояние от эталона: stratum 1 берёт время напрямую от источника (GPS, атомные часы), stratum 2 синхронизирован по stratum 1 и так далее. Обращаться к stratum 1 без надобности не нужно: их немного, они перегружены, а разница для обычной машины неразличима.
Для парка машин правильная схема — один-два собственных NTP-сервера, синхронизированных наружу, и все остальные машины по ним. Это даёт две вещи: во-первых, машины парка согласованы между собой, что важнее абсолютной точности, когда вы сравниваете их логи; во-вторых, наружу ходят двое, а не сотня.
Диагностика
Что показывает chronyc
chronyc tracking
Из вывода практически значимы четыре величины.
Last offset — расхождение на последнем обновлении. Мгновенное значение, скачет; само по себе мало о чём говорит.
RMS offset — усреднённое расхождение за длительный срок. Вот это и есть ответ на вопрос «насколько точны часы»: значения в единицы миллисекунд для машины в интернете нормальны, устойчивые сотни миллисекунд означают проблему.
Frequency — на сколько ppm часы уходили бы без коррекции. Это свойство железа, а не сети. Величина в единицы-десятки ppm обычна; сотни означают либо плохой генератор, либо, чаще, виртуализацию.
Root dispersion вместе с Root delay дают верхнюю оценку погрешности: сумма накопленной по цепочке неопределённости и половины сетевой задержки. Если требуется утверждение «наше время верно с точностью до X» — оно строится отсюда, а не из Last offset.
chronyc sources
Первые два символа строки — самое информативное в выводе. Первый обозначает тип источника (^ — сервер, = — peer, # — локальные часы), второй — состояние выбора:
| Символ | Значение |
|---|---|
* |
выбран основным источником |
+ |
участвует в комбинировании с основным |
- |
пригоден, но не выбран |
x |
falseticker: время расходится с большинством остальных |
~ |
слишком большой разброс измерений |
? |
непригоден: недоступен, не синхронизирован или мало измерений |
x напротив источника — не мелочь, а сообщение о том, что кто-то отдаёт неправильное время; именно эту ситуацию SNTP-клиент распознать не способен в принципе.
Колонка Reach читается как восьмеричное число: каждый бит — успех или неуспех одного из последних восьми опросов. 377 — все восемь удались, 0 — источник недоступен, промежуточные значения означают потери. Это самый быстрый способ отличить «сервер отвечает через раз» от «сервер не отвечает вообще».
timedatectl
timedatectl # общая картина: пояс, RTC, статус синхронизации
timedatectl show-timesync --all # детали, если работает timesyncd
timedatectl set-ntp true # включить синхронизацию
Строка System clock synchronized: yes означает лишь, что демон однажды успешно синхронизировался, а не что часы верны прямо сейчас. Для реального ответа нужен chronyc tracking.
Симптом → причина
| Что видно | Где искать |
|---|---|
| TLS: сертификат «ещё не действителен» или «истёк», хотя срок в порядке | часы клиента ушли; сверить timedatectl с внешним источником |
| TOTP-коды не принимаются, хотя вводятся верно | расхождение больше окна (обычно ±30 с) на любой из двух сторон |
| Kerberos: «clock skew too great» | расхождение больше допуска (по умолчанию 5 минут) |
| Логи двух машин не сходятся по порядку событий | машины синхронизированы с разными источниками или одна не синхронизирована вовсе |
| Таймер systemd отработал дважды или пропустил запуск | переход на летнее время в локальном поясе |
| Часы уезжают, несмотря на работающий демон | chronyc sources: все источники ? или x, либо блокируется UDP/123 |
Огромный Frequency, часы плывут после каждой паузы |
виртуализация без корректной синхронизации с гипервизором |
Часовые пояса
Пояс — это слой отображения поверх системных часов: ядро оперирует UTC, а преобразование в локальное время делает библиотека по данным из tzdata. /etc/localtime — символьная ссылка на файл зоны в /usr/share/zoneinfo.
timedatectl list-timezones | grep Moscow
timedatectl set-timezone Europe/Moscow
Практическое следствие: tzdata — это данные, которые устаревают. Правила перехода на летнее время и смещения поясов меняются решениями правительств, иногда за недели до вступления в силу. Машина с неактуальным пакетом tzdata показывает верное UTC и неверное локальное время, причём молча. Для сервера это ещё один довод держать всё в UTC: серверу локальное время нужно ровно в одном месте — при показе человеку.
Переход на летнее время — регулярный источник багов в расписаниях. Осенью час повторяется, весной час не существует; задание, назначенное на этот час, соответственно либо выполняется дважды, либо не выполняется. Для systemd-таймеров лечение простое: OnCalendar= понимает суффикс UTC.
OnCalendar=*-*-* 02:30:00 UTC
Для заданий, которые обязаны идти именно раз в сутки, это правильный выбор; привязка к локальному времени оправдана только там, где смысл задания связан с человеческим распорядком.
Отдельная категория — високосная секунда: вставка лишней секунды в UTC для согласования с вращением Земли. Для системы это разрыв монотонности CLOCK_REALTIME со всеми вытекающими, и крупные инфраструктуры давно обходят его «размазыванием» секунды по суткам (leap smearing). Резолюцией 4 Генеральной конференции по мерам и весам 2022 года принято решение прекратить добавление високосных секунд — не позднее 2035 года; окончательные детали ещё дорабатываются, и до тех пор секунды добавляются по прежним правилам. Практический вывод на сегодня прежний: если инфраструктура зависит от непрерывности времени, источники NTP должны быть согласованы в том, как они этот момент обрабатывают — смешивать «размазывающие» и обычные серверы нельзя.
Сравнение с FreeBSD
В базовой системе FreeBSD лежит ntpd — референсная реализация от ntp.org, та самая, на смену которой chrony пришёл в большинстве Linux-дистрибутивов. Включается он в rc.conf:
ntpd_enable="YES"
ntpd_sync_on_start="YES"
Второй переключатель — прямой аналог решения задачи «на старте часы могут быть далеко»: он запускает ntpd с флагом -g, разрешающим однократную большую коррекцию при запуске. Документация rc.conf(5) называет его предпочтительной заменой утилите ntpdate, которая выводится из состава дистрибутива NTP. Если вы встречаете совет «добавьте ntpdate в загрузку» — он устарел.
Различия по существу:
- Идеология та же, что и везде во FreeBSD: в базе лежит рабочий и консервативный вариант, а более современный инструмент берётся из портов. chrony есть в портах и работает так же, как в Linux, включая конфигурацию.
- Предел скорости подтягивания у ядра ниже: 5000 ppm против 100000 ppm в Linux, причём, как отмечает документация chrony, установка
maxslewrateво FreeBSD в диапазоне от 500 до 5000 ppm фактически даёт 500 ppm. Практического значения для обычной эксплуатации это не имеет, но объясняет, почему сильно ушедшие часы там подтягиваются дольше. - Управление RTC — через
sysctl machdep.wall_cmos_clockиmachdep.adjkerntzвместо/etc/adjtime; смысл настройки тот же, и правильный ответ тот же — держать RTC в UTC. - Слой часовых поясов идентичен: тот же tzdata, тот же
/etc/localtime, настраивается черезtzsetup.
Источники
- chrony documentation —
makestep,maxslewrate, поляtrackingи легендаsources - chrony NEWS — состав релизов, версия 4.9
- systemd-timesyncd.service(8) — «implements SNTP only», файл
clockдля машин без RTC - clock_gettime(2) — поведение
CLOCK_REALTIME,CLOCK_MONOTONIC,CLOCK_MONOTONIC_RAW,CLOCK_BOOTTIME - hwclock(8) —
/etc/adjtime, несогласованность локального времени в RTC при переходе на летнее - rc.conf(5), FreeBSD —
ntpd_sync_on_startи статусntpdate - Резолюции CGPM 2022 — решение о прекращении добавления високосных секунд
Связанные заметки
- Linux — systemd: init, сервисы, таймеры — таймеры, которые ломает переход на летнее время, и
OnCalendar=в UTC - Linux — мониторинг и отладка — сведение логов нескольких машин, ради которого нужны согласованные часы
- Linux — безопасность: hardening, SELinux/AppArmor — TOTP, сроки действия сертификатов и метки времени в аудите
- Обратный прокси и TLS — сроки действия сертификатов с другой стороны: почему расхождение часов выглядит как отзыв сертификата
- Linux — пользователи, группы, PAM и systemd-logind — где в стеке аутентификации оказывается TOTP-модуль, чувствительный к часам
Из библиотеки института уносят только знания. Читателей здесь не считают: посещение не регистрируется, отметок в издании не остаётся, счётчиков нет ни у одного издания фонда.
Издание пополняется и пересверяется. Текст доступен на условиях CC BY-SA 4.0 — унесённое можно передавать дальше на тех же условиях.