Как NTP подводит часы сервера, ни разу их не переводя
Если посмотреть на график chronyc tracking за сутки, легко решить, что часы сервера либо идут точно, либо их кто-то дёрнул на секунду вперёд или назад. На деле почти всё время происходит третье: демон синхронизации незаметно меняет скорость хода часов, чтобы расхождение исчезло само, без единого видимого скачка. Это не побочный эффект, а осознанная конструкция протокола — и если вы пишете код, который меряет интервалы обычным временем или полагается на монотонность меток, стоит понимать, почему часы «подводятся», а не «переводятся».
Содержание
Почему часы сервера вообще расходятся сами по себе
Аппаратные часы любого компьютера — это кварцевый генератор, который считает колебания и превращает их в тики. Кварц не идеален: частота немного плывёт от температуры, возраста кристалла, электрических наводок и разброса при производстве. В результате часы почти любой машины либо немного спешат, либо немного отстают от эталонного времени — обычно на небольшую, но ненулевую величину в сутки, которая зависит от конкретного железа и условий.
На виртуальных машинах эта проблема обычно ощутимее, чем на физическом сервере: гипервизор делит время процессора между гостями, и виртуальные часы могут «застревать» на время такта, а потом наверстывать разницу. Мы разбирали это отдельно в статье о том, почему часы виртуалки убегают — там же есть примеры паравиртуальных источников времени, которые сглаживают часть этой погрешности.
Задача NTP (Network Time Protocol) и его более современной реализации chrony — постоянно сверять локальные часы с несколькими внешними источниками времени и держать расхождение в разумных пределах. Ключевое слово — «постоянно»: демон не запускается раз в сутки что-то поправить, а крутится в фоне и реагирует на дрейф частоты и накопленное смещение непрерывно.
Два инструмента правки времени: step и slew
У демона синхронизации есть, по сути, два способа исправить расхождение между локальными часами и эталоном.
Step (скачок) — это мгновенная установка часов на правильное значение, как будто вы вручную набрали date -s. Время до этого момента и после него в системных часах не связано непрерывной линией — оно просто «прыгает».
Slew (подстройка хода) — это изменение скорости, с которой идут часы, а не самого показания. Если часы отстают, ядро на время начинает считать секунды чуть быстрее обычного — условно говоря, реальная секунда проходит, а системные часы за это время «проходят» чуть больше секунды. Если часы спешат — наоборот, ход немного замедляется. Показание часов при этом всегда монотонно возрастает и никогда не прыгает назад и вперёд рывком.
По умолчанию и ntpd, и chrony в подавляющем большинстве повседневных ситуаций выбирают именно slew. Это принципиальный момент конструкции протокола, а не мелкая деталь реализации.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверМеханика slew: как ядро незаметно меняет ход часов
Подстройка хода — не хак поверх системных часов, а встроенный механизм ядра Linux, который живёт там ещё со времён David Mills и оригинальной спецификации NTP. Демон синхронизации не трогает системное время напрямую каждую секунду — вместо этого он периодически сообщает ядру через системный вызов adjtimex() (или его обёртку ntp_adjtime) два параметра: текущее накопленное смещение и требуемую поправку частоты хода часов.
Дальше в дело вступает дисциплина часов ядра — по сути, контур с обратной связью (в литературе по NTP его называют PLL/FLL — phase-locked loop и frequency-locked loop). Он не пытается закрыть всё расхождение одним движением, а плавно меняет частоту, с которой системный таймер инкрементирует счётчик времени, — обычно на очень небольшую величину относительно номинальной скорости хода. Часы в это время всё ещё «настоящие» системные часы: любой процесс, читающий время, видит непрерывно возрастающие значения, просто секунда «системного» времени чуть короче или чуть длиннее секунды по стенным часам.
Если смещение небольшое, вся эта работа укладывается в фоновые циклы опроса демона (обычно от нескольких секунд до нескольких минут между итерациями, в зависимости от настроек minpoll/maxpoll) и происходит настолько плавно, что снаружи заметить момент коррекции почти невозможно — вы не увидите ни скачка в логах, ни аномалии в метках времени, только то, что через какое-то время chronyc tracking покажет offset, стремящийся к нулю. Именно поэтому в заголовке этой статьи не преувеличение: «подводит» часы демон буквально ни разу их не «переводя» — показание никогда не меняется дискретным прыжком, оно просто накапливает нужную разницу за счёт временно другой скорости хода.
Практически это означает, что коррекция всегда растянута во времени. Чем больше накопленное расхождение, тем дольше демону нужно «доехать» до нуля за счёт ограниченной скорости подстройки — резкое смещение частоты тоже запрещено, потому что оно точно так же ломало бы приложения, чувствительные к скачкам, только не в показании времени, а в его производной.
Когда NTP всё-таки идёт на скачок
Slew — это стратегия по умолчанию, но не единственная. Есть ситуации, где демон осознанно переключается на step:
- Старт демона на «холодных» часах. Если сервер только что загрузился, а его аппаратные часы (RTC) сильно разошлись с реальностью — например, простояли выключенными или виртуалка только что поднялась из снапшота, — держать процесс подстройки хода до сходимости может быть непрактично долго. И ntpd, и chrony по умолчанию допускают один или несколько шаговых скачков именно в первые минуты после старта, пока расхождение большое, а сервисы ещё не успели опереться на текущее время.
- Расхождение выше порога, за которым slew считается небезопасным. У обеих реализаций есть настраиваемый порог (в chrony это директива
makestep, у классического ntpd — параметр step threshold), выше которого демон решает, что растягивать коррекцию на часы или дни не имеет смысла, и делает шаг. Конкретные значения порога различаются между дистрибутивами и версиями пакетов, поэтому их стоит проверять в собственном конфиге, а не считать одинаковыми везде. - Ручной вызов одноразовой синхронизации — например,
chronyd -qили устаревшийntpdate— по определению не «подстраивает ход», а один раз выставляет часы, то есть всегда работает через step. - Расхождение настолько велико, что демон вообще отказывается его тихо чинить — это уже не штатный режим, а защитный, когда система скорее сообщит об ошибке (или потребует ручного вмешательства), чем молча передвинет время на часы или дни в любую сторону.
Вне этих случаев — то есть в подавляющем большинстве времени жизни уже запущенного и синхронизированного сервера — исправление идёт исключительно через slew.
Почему тихая правка — это осознанный выбор, а не полумера
Резкий скачок системных часов назад ломает предположение, на котором держится огромный пласт кода: что время между двумя последовательными измерениями time()/gettimeofday() не может быть отрицательным. Если между двумя строками кода часы «прыгнули» на минуту назад, разница времён может стать отрицательной, таймаут — сработать раньше, чем должен, TTL записи в кеше — неожиданно продлиться, а лог-файл — получить события «из будущего» раньше «прошлых».
Скачок вперёд не менее опасен: он может мгновенно просрочить токены и сессии, которые ещё должны были жить, спровоцировать ложное срабатывание задач по расписанию или создать дыру в интервале, за который якобы «ничего не произошло», хотя на деле прошли реальные секунды или минуты.
Мы разбирали похожий по духу случай, когда авторизация ломалась именно из-за резкого расхождения часов — см. «Часы ушли на минуту и сломали авторизацию». NTP через slew фактически убирает целый класс таких проблем: пока расхождение укладывается в штатный порог, время всегда идёт вперёд и никогда не прыгает, значит любой код, который просто вычисляет разницу между двумя последовательными чтениями системного времени, не увидит парадоксов причинности из-за самой синхронизации.
Это не значит, что slew полностью безобиден. Пока идёт подстройка, часы временно идут быстрее или медленнее номинала — то есть если вы измеряете длительность операции через обычные wall-clock метки, вы получите слегка искажённое значение именно в эти моменты. Это осознанный компромисс: NTP жертвует идеальной точностью хода в узком окне коррекции ради того, чтобы вообще не было разрывов и скачков назад.
Что это значит для ваших приложений и как проверить синхронизацию
Из механики slew следует несколько практических выводов, которые стоит учитывать при проектировании и эксплуатации серверного кода.
Не измеряйте длительность обычными часами. Для интервалов и таймаутов используйте монотонные часы — в Linux это clock_gettime(CLOCK_MONOTONIC), в большинстве языков есть готовая обёртка (time.monotonic() в Python, System.nanoTime() в Java, Instant::now() в связке с Duration в Rust, hrtime/performance.now() в Node.js). Монотонные часы не подчиняются NTP-коррекции по показанию вообще — они просто не могут пойти назад, и обычно их не переводит вручную ни один процесс. Обычные «настенные» часы (CLOCK_REALTIME) годятся для меток времени, логов и всего, что должно соответствовать календарной дате, но не для измерения «сколько прошло».
Проектируйте операции идемпотентными, если они зависят от сравнения меток времени между разными узлами — двум серверам, даже синхронизированным по NTP, нужно закладывать разумный допуск (обычно единицы–десятки миллисекунд при исправно работающей синхронизации, но это ориентир, а не гарантия — конкретная величина зависит от качества источников времени и сети), а не считать их часы идентичными до микросекунды.
Мониторьте сам факт синхронизации, а не только текущее время. На системах с chrony это chronyc tracking (поля System time — текущее смещение, Frequency — на сколько подстроен ход, Root dispersion — накопленная неопределённость) и chronyc sources -v — список источников и то, кого демон считает надёжным. На классическом ntpd — аналогично ntpq -p. Если offset стабильно не сходится к нулю или источники помечены как недоступные, это тревожный сигнал задолго до того, как расхождение станет заметно приложениям.
Проверяйте, кто ещё трогает время на сервере. Если на машине одновременно работают chrony и, например, гостевой агент гипервизора, который тоже пытается синхронизировать часы (частая ситуация на некоторых виртуальных платформах), они могут конфликтовать — один пытается плавно подстроить ход, другой время от времени скачком выставляет своё значение. Разумная практика — оставить синхронизацию времени за одним источником и явно отключить остальные.
Если вы настраиваете синхронизацию времени с нуля на новом сервере, у нас есть отдельный практический разбор — «Настройка NTP: синхронизация времени сервера», там показаны конкретные конфиги chrony и типовые команды проверки.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
NTP всегда работает через slew, скачков вообще не бывает?
Нет. Скачок (step) — штатная часть протокола для больших расхождений и для старта на «холодных» часах. Slew — стратегия по умолчанию для небольших, регулярных расхождений на уже работающем сервере, но не единственная.
Если часы почти всегда идут «неточно» из-за slew, стоит ли на это полагаться в высокочастотных измерениях?
Для интервалов и таймаутов — нет, используйте монотонные часы (CLOCK_MONOTONIC), они не подвержены подстройке показания и не идут назад. Обычные часы годятся для меток и календарных значений, но не для точного измерения длительности внутри узкого окна.
Чем chrony отличается от ntpd в этом вопросе?
Оба используют одну и ту же дисциплину часов ядра Linux и одинаковый принцип «slew по умолчанию, step по порогу», но по-разному управляют опросом источников и порогами — например, у chrony есть гибкая настройка makestep с указанием, сколько первых обновлений можно делать через скачок. Конкретное поведение стоит смотреть в конфиге вашей системы, а не считать одинаковым везде.
Как понять, что мои часы прямо сейчас находятся в процессе подстройки хода, а не просто синхронизированы?
По chronyc tracking — поле Frequency показывает текущую поправку хода в ppm, а System time — оставшееся расхождение. Если Frequency далека от нуля, а System time постепенно уменьшается от опроса к опросу, значит идёт активная подстройка.
Может ли slew когда-нибудь «не успеть» и накопить слишком большое расхождение?
Теоретически да, если источники времени недоступны долгое время или сеть до них деградировала — тогда локальные часы дрейфуют без коррекции. Именно поэтому важно мониторить не только offset, но и доступность источников и не полагаться на то, что синхронизация «просто работает» без контроля.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →