Не подскажете, сколько времени?

Не подскажете, сколько времени?

Vanya Khodor

Когда мы говорим про время, мы обычно имеем в виду физическое время -- время, которое идёт от какой-то общей точки отсчёта и для всех одно и то же. Правда уже тут начинаются проблемы. Например, человечество построило себе несколько разных осей времени:

http://leapsecond.com/java/gpsclock.htm

Мы живём по UTC (Coordinated Universal Time). В основе UTC, GPS и TAI (международное атомное время) лежит атомное время (Loran сейчас уже не прям актуален, т.к. GPS задавил; в Loran там использовались наземные станции, большинство из которых уже не действуют). Если понаблюдать за UTC, GPS и TAI, можно заметить, что разница между первыми двумя может меняться (но всегда в целое кол-во секунд), а между 2м и 3м фиксирована. В статье мы разберёмся с причинами таких расхождений, с используемыми в языках программирования часами, с GPS (которое почему-то помогает нам измерять время) и с подходами к синхронизации времени в распределённых системах.

Изначально время измерялось по каким-то периодическим процессам. Например вы знаете, что солнце встаёт и садится -> можно зафиксировать разницу во времени и сказать, что это день. А ещё можно заметить цикличность в астрономических наблюдениях и сказать, что есть год, в котором условно 365 дней. А правда ли в году целое количество дней? Или верно ли, что в году 365 дней по 24 часа? Оказывается, что нет, т.к. период вращения Земли вокруг Солнца не делится нацело на это кол-во дней. В обороте есть ещё лишние 6 часов, которые накапливаются, но про которые мы вспоминаем только раз в 4 года на 29е февраля, чтобы календарь в другие годы не ломался.

С сутками так же. Хотя тут мы знаем, что сутки = 24 часа, час = 60 минут, а минута = 60 секунд. Но что такое секунда? Когда-то это могла быть 1/(365*24*60*60) часть года, которую вычислили на основе наблюдений. Но чуть больше 50 лет назад определение секунды зафиксировали (тут что-то сложное и непонятное):

Секунда — время, равное 9 192 631 770 периодам излучения, соответствующего переходу между двумя сверхтонкими уровнями основного состояния атома цезия-133.

Такой эталон секунды определяется механизмом атомных часов. Константа (9 192 631 770) кол-ва периодов излучений была выбрана так, чтобы значение секунды, которое соответствовало определению из наблюдений, сходилось с вот этим новым определением. По факту просто подогнали чиселко, чтобы старая и новая секунды были равны.

Теперь у нас есть точная секунда и мы точно зафиксировали, сколько этих точных секунд в наших сутках. Но, неожиданно, проблема! Земля вращается вокруг своей чуть медленнее, чем за 86400 секунд. Потому наши точные сутки начинают по чуть-чуть расходится с астрономическими наблюдениями. Решили, что надо бы синхронизировать. Потому тут возникает такой же эффект, как и с високосным годом: иногда у нас накапливается лишняя секунда, -- и нам нужно куда-то её вставить, чтобы всё снова сходилось. Так появляется високосная секунда (на часах, которые про неё знают):

Хотя иногда у вас может просто залипнуть время на 23:59:59 на две секунды. Ну или ваши часы пойдут дальше, а далее на синхронизации они подвинутся назад. Не каждый код конечно готов к таким ситуациям.

Теперь к разгадке про отличия временных осей. В UTC високосные секунды есть. В TAI их нет: это просто время, которое считается по атомным часам, -- потому они немного опережают UTC. GPS тоже атомные часы, но в момент запуска они были синхронизированы с UTC (т.е. уже имели какие-то високосные секунды в себе) и потому они изначально расходились с TAI и постепенно расходятся с UTC.

Постойте. Но GPS это же global positioning system. Почему это ещё и шкала измерения времени? Вот тут есть небольшая миленькая статья про то, как работает GPS. Добавим один факт: вместе со своими координатами спутники могут послать время со своих атомных часов. Конец.

Время [и код]

Обычно в языках программирования есть два разных интерфейса к часам: настенные часы (в случае C++ std::system_clock) и монотонные часы (std::steady_clock). 

Кстати с C++20 также появились std::utc_clock, std::tai_clock и std::gps_clock.

Wall clock показывают гражданское время, но мы не должны ожидать от них монотонности. От монотонных часов можем ожидать монотонности : )

Хотя конечно у каждого CPU обычно свой таймер, ОС старается сглаживать часы от разных CPU, чтобы в многопоточных программах было корректно.

Зато у всех настенных часов точка отсчёта на разных машинах одинаковая, а значит у вас есть общая временная ось (хотя конечно пользоваться настенными часами для подобных задач обычно не стоит). Когда монотонные часы могут перезапускаться с каждым стартом машины. Т.е. вы можете сравнивать значения друг с другом, вычитать и мерять интервалы времени, но смысла делать с этими показаниями что-то на другой машины в районе нуля.

В блоге Cloudfare есть статья про то, как високосная секунда привела к падению нескольких DNS, что зааффектило примерно 1% запросов к провайдеру.

Давай взглянем на их код:

// Update upstream sRTT on UDP queries, penalize it if it fails
if !start.IsZero() {
 rtt := time.Now().Sub(start)
 if success && rcode != dns.RcodeServerFailure {
  s.updateRTT(rtt)
 } else {
  // The penalty should be a multiple of actual timeout
  // as we don't know when the good message was supposed to arrive,
  // but it should not put server to backoff instantly
  s.updateRTT(TimeoutPenalty * s.timeout)
 }
}

Переменная rtt может стать отрицательной, если time.Now() меньше, чем start (который был получен с помощью time.Now() чуть ранее). Однако на тот момент часы в Go не гарантировали монотонность, что и привело к инциденту. Фикс был простым:

Сейчас struct Time в Go выглядит так:

type Time struct {
  wall uint64 // настенные часы
  ext int64 // монотонные часы

  loc *Location 
}

Т.е. при вызове time.Now() вы получаете сразу две метки. И в зависимости от того, как вы используете эти метки, будет использована либа настенная, либо монотонная.

Время [и распределённые системы]

Временные метки широко используются в различных распределённых систем. Например:

  • Истекло ли время ожидания запроса?
  • Чему равен 99й перцентиль времени ожидания запросов?
  • Когда истекает время хранения записи в кеше?
  • В какой момент времени был записан лог (и правда ли, что на разных машинах причинность появления логов будет соответствовать их временным меткам)?

Сообщения от одной машинки к другой доставляются с некоторой непредсказуемой задержкой в сети, что иногда затрудняет определение последовательности событий. А ещё у каждой машинки есть свои часы и они не всегда синхронизированы с часами на других машинах, т.к. обычно это локальные кварцевые часы, которые оставляют желать лучшего, если мы говорим про точность (сдвиг часов в точности относительно некоторого референсного значения называют дрейфом часов или clock drift). Хотя я слышал, что иногда к машинках по PCI подключают напрямую атомные часы в дц, чтобы соптимизировать.

Одним из наиболее популярных решений для синхронизации часов на различных серверах является network time protocol (NTP). Про то, как это работает, можно почитать тут.

В том числе из-за NTP могут быть скачки во времени у wall clock. Если часы разъехались в точности вперёд/назад, после очередного цикла синхронизации они могут скакнуть вперёд/назад во времени. В том числе потому они не подходят для измерения промежутков времени. С монотонными часами NTP может только подкручивать частоту хода, т.е. ускорять/замедлять их, но не может заставить их перепрыгнуть во времени.

Понятно, что точность NTP-синхронизации тоже может страдать из-за сетевых задержек. Минимальная ошибка, которую получилось достичь, составляет порядка 35 мс, но отдельные крупные пики нагрузки в сети могут привести к отклонению в секунду. 

Ещё не стоит доверять часам на устройствах, которые вы не можете контролировать. Когда-то в детстве, я играл в мобильные игры, в которых иногда нужно было подождать, чтобы что-то произошло. Перевод часов на телефоне вперёд на нужное время помогал сократить ожидание и ускорить процесс игры, что вряд ли подразумевалось геймдизайном. 

Иногда для синхронизации времени используют не конкретную метку, а какой-то доверительный интервал времени. Например система на 95% уверена, что время сейчас находится в промежутке [t - 100мс; t + 100мс]. Конечно в этом примере не имеет смысла говорить про микросекунды. Примерно так работает TrueTime в Google Spanner (какая-то статья на хабре про это). Он возвращает два значения: [earliestTime; latestTime]. Подобные подходы помогают эффективно синхронизировать время на глобально распределённых системах, когда сетевые задержки по дефолту огромные (сигналы отправляются с одного континента на другой).



Report Page