Перейти к содержанию

Time Matters

1. Что по поводу проблем со временем написано в "Кабанчике"

В книге «Высоконагруженные приложения» Мартин Клеппман подробно разбирает фундаментальные проблемы, которые время создает в распределенных системах. Основные идеи можно свести к трем ключевым аспектам:

1. Нет единого глобального времени

В распределенной системе невозможно иметь абсолютно точные и синхронизированные часы на всех узлах. Из-за задержек сети, дрейфа часов (clock drift) и других факторов время на разных машинах всегда расходится.

Это делает бессмысленными вопросы вроде «Что произошло раньше?» для событий на разных узлах.

2. Проблема определения порядка событий (Causality)

Если два события произошли на одном узле, их порядок установить легко. Но если они произошли на разных узлах, из-за отсутствия точного времени невозможно достоверно определить, какое из них было раньше.

Это критически важно для поддержания причинно-следственных связей (например, ответ на сообщение должен прийти после самого сообщения).

3. Неоднозначность временных меток (Timestamps)

Использование временных меток (например, для разрешения конфликтов или упорядочивания данных) ненадежно, так как они основаны на локальных часах, которые могут быть неточными или даже произвольно перескакивать (например, из-за коррекции NTP или администратора).

Слепая опора на временные метки может привести к потере данных или нарушению логической целостности.

4. Вывод и решение

Клеппман подчеркивает, что нельзя полагаться на физическое время для обеспечения корректности работы распределенного приложения. Вместо этого он предлагает использовать:

  1. Логические часы (например, счетчики Lamport или векторные часы) для отслеживания причинно-следственных связей.
  2. Детерминированные алгоритмы для разрешения конфликтов, которые не зависят от ненадежных временных меток (например, Last Write Wins — рискованный шаблон).
  3. Явное проектирование систем с учетом того, что временные метки — это всего лишь приблизительные метаданные, а не источник истины.

В итоге: Главная проблема со временем — это его неопределенность и ненадежность в распределенных системах, что вынуждает инженеров искать обходные пути и более надежные логические конструкции для обеспечения согласованности и корректности данных.

2. Выбор формата меток времени для заголовков Kafka

1. Контекст, цели и ограничения

  1. Kafka, микросервисы на Java/Go/Python, Kubernetes в ЦОДах Москвы, клиенты в РФ.
  2. Цель: единообразные, однозначные и человекочитаемые временные метки в заголовках Kafka для анализа latency, с точностью до миллисекунд.
  3. Заголовки времени:
    1. origin-created-at;
    2. created-at;
    3. chain-processing-ms;
    4. chain-wait-ms;
    5. chain-total-ms;
    6. stage-duration-ms.
  4. Требования: удобство чтения людьми (ELK, Jaeger, Kafka Tool), интеграция с Grafana/Prometheus, NTP-синхронизация, невысокая критичность к размеру/скорости парсинга.

2. Критерии выбора формата меток времени

  1. Однозначность во времени и зоне: отсутствие двусмысленности часовых поясов и переходов на летнее время.
  2. Человекочитаемость и узнаваемость инструментами APM/лог-стека.
  3. Стандартность и межъязыковая совместимость (Java/Go/Python).
  4. Лексикографическая сортировка ≡ хронологической (упрощает фильтрацию/поиск).
  5. Ровно миллисекундная точность (без «ложной» наносекундной).
  6. Простые, неизменяемые правила сериализации (фиксированное количество знаков в дробной части).
  7. Адекватная поддержка в Kafka UI и CLI (отображение строковых header’ов).

3. Рекомендованный формат меток времени

  1. Для timestamp меток (origin-created-at, created-at): строковый UTC-timestamp формата RFC 3339 (ISO 8601) с фиксированной миллисекундной точностью:
    1. Формат: YYYY-MM-DDTHH:MM:SS.SSSZ.
    2. Характеристики:
      1. Всегда UTC (символ ‘Z’ в конце, никаких смещений ±hh:mm).
      2. Ровно 3 цифры дробной части секунды (миллисекунды), нули дополняются слева при необходимости.
      3. Кодировка UTF-8 в Kafka headers.
  2. Для длительностей (chain-processing-ms, chain-wait-ms, chain-total-ms, stage-duration-ms): целое количество миллисекунд в виде десятичной строки (без плавающей запятой), также UTF-8.

RFC 3339: Date and Time on the Internet: Timestamps

"Z": A suffix which, when applied to a time, denotes a UTC offset of 00:00; often spoken "Zulu" from the ICAO phonetic alphabet representation of the letter "Z".

4.1. Coordinated Universal Time (UTC). Because the daylight saving rules for local time zones are so convoluted and can change based on local law at unpredictable times, true interoperability is best achieved by using Coordinated Universal Time (UTC).

5.2. Human Readability. Human readability has proved to be a valuable feature of Internet protocols. Human readable protocols greatly reduce the costs of debugging... On the other hand, human readability sometimes results in interoperability problems.

4. Почему это оптимально для поставленной задачи

  1. Универсальность и однозначность: UTC+RFC 3339 без локальных смещений исключает путаницу часовых поясов и переходов на летнее время.
  2. Человекочитаемость: разработчики и дежурные мгновенно распознают формат; Kafka Tool/ELK отображают без декодирования; grep/поиск работают интуитивно.
  3. Согласованность инструментов:
    1. ELK/Logstash/Elastic Ingest из коробки распознают ISO 8601 UTC и корректно нормализуют в хранилище.
    2. Grafana понимает ISO 8601 в аннотациях/логах; преобразование в локальную зону делается на уровне UI.
    3. Jaeger/Tracing: для корреляции с логами и событиями ISO 8601 удобнее, чем голый epoch, при этом на стороне трейсинга внутренние epoch-значения не страдают.
  4. Лексикографическая сортировка совпадает с временной, что упрощает анализ в текстовых логах и в UI, показывающих «сырые» заголовки.
  5. Межъязыковая совместимость: стандарт присутствует в Java (java.time), Go (time.RFC3339), Python (datetime ISO). Правило «ровно 3 цифры» устраняет типичную несогласованность фреймворков с nano/micro precision.
  6. Не вредит производительности и размеру: даже при большом трафике накладные расходы строковых header’ов минимальны и некритичны для Kafka.

5. Сравнение альтернатив и их недостатков

Ниже — практическое сравнение популярных кандидатов.

Альтернатива Что это Плюсы Ключевые минусы для поставленной задачи
Unix epoch в миллисекундах (int64) Целое число мс с 1970-01-01 UTC Компактен; машинное сравнение/арифметика Не человекочитаемо в Kafka Tool/логах; сложнее в ручной отладке; требуется форматирование практически всегда; легко перепутать секунды/миллисекунды
RFC 3339/ISO 8601 с локальным смещением (±hh:mm) Строка с часовым поясом Формально корректно Путает людей и инструменты при нескольких TZ; усложняет агрегации; визуально «шумит» offset’ом
RFC 3339 с переменной точностью (до nanos) Строка, но дробная часть 0–9 знаков Больше точность теоретически Непредсказуемый вид; разные языки дадут разное число знаков; ложная «точность» выше стабильности NTP и Kafka таймеров; ломает сортировку как «фиксированный шаблон»
RFC 2822/HTTP-date Человекоориентированный текст (Mon, 02 Jan …) Читаемо человеком Плохо парсится, неоднородно поддерживается; нет фиксированной доли секунд; избыточно многословно
Кастом dd.MM.yyyy HH:mm:ss,SSS Локальный шаблон Привычно части команды Локалезависимость; нет «T»/Z; неоднозначность часового пояса; парсеры ELK/графаны требуют доп. настройки
Protobuf/Avro logical timestamp (binary) Бинарные типы Эффективно в payload В Kafka headers ухудшает читаемость; инструменты показывают «мусор»; усложняет ручную отладку
TAI/GPS-время Без leap seconds Теоретически точнее Не поддерживается стандартными парсерами; лишняя сложность; не требуется для NTP и мс-точности

6. Негативные последствия, если не использовать предложенный формат

  1. Диагностика и инциденты:
    1. binary/epoch в заголовках усложнит инцидент-менеджмент: дежурные не смогут быстро «на глаз» сопоставить события между сервисами и зонами;
    2. Дольше MTTR: инженерам приходится «переводить» форматы и проверять единицы измерения.
  2. Аналитика и визуализация:
    1. Непредсказуемые результаты фильтров и агрегаций в Kibana/Grafana при смешанных TZ/форматах.
    2. Неверные алерты в Grafana, если парсер времени не распознал формат и упал в локаль/дефолт.
    3. Grafana/ELK потребуют больше кастомных парсеров и маппингов, возрастут трудозатраты поддержки.
  3. Интеграционный «зоопарк»:
    1. Java/Go/Python начнут отдавать разное число дробных знаков, кто-то — offset, кто-то — Z, что разрушит унификацию.
    2. Форматы с локальными смещениями/нестабильной дробной частью приведут к:
    3. дублирующимся/плавающим представлениям времени (хуже парсинг и фильтрация);
    4. ошибочным сортировкам и неверным окнам корреляции в логах;
    5. разночтениям в метриках, SLO/SLA и алертах (skew latency).
  4. Операционные риски:
    1. Сезонные/региональные изменения часовых поясов вносят ретро-ошибки в отчёты и реплеи.
    2. Ошибки округления и отрицательные дельты при перескоках системного времени вероятнее проявятся и будут хуже диагностироваться.

7. Соглашения по использованию заголовков и измерению

  1. Типы и формат:
    1. origin-created-at: точка рождения события у источника, формат YYYY-MM-DDTHH:MM:SS.SSSZ (UTC, ровно 3 дробных знака).
    2. created-at: момент создания текущего сообщения данным продьюсером (например, при републикации/агрегации), тот же формат.
    3. chain-processing-ms, stage-duration-ms: длительности активной обработки, целые миллисекунды в десятичной строке.
    4. chain-wait-ms: суммарное ожидание «в очередях/сети», целые миллисекунды.
    5. chain-total-ms: от origin-created-at до момента отправки текущего сообщения, целые миллисекунды.
  2. Правила измерения:
    1. Длительности вычислять на монотонных часах процесса (monotonic), чтобы исключить прыжки системного времени; при передаче в заголовки — конвертировать в миллисекунды (integer).
    2. Для chain-total-ms на отправке брать разницу между «текущим UTC» и origin-created-at; при отрицательном результате (редкие NTP-коррекции) — приравнивать к 0 и логировать предупреждение.
    3. Округление: .5 и выше — вверх; иначе — вниз; не использовать плавающую точку в заголовках.
  3. Транзит и обновление:
    1. Всегда пробрасывать существующие заголовки; не менять origin-created-at.
    2. created-at перезаписывать при каждом новом «рождении» сообщения данным сервисом.
    3. stage-duration-ms относится к текущему этапу; после отправки может быть переименован/суммирован в chain-processing-ms.
  4. Сериализация:
    1. Только UTF-8; без локализации, без пробелов, без смещения зоны, только Z.
    2. Фиксированная длина дробной части (ровно 3 цифры).
  5. Валидация и наблюдаемость:
    1. На входе: если формат невалиден — пометить событие и логировать; не «лечить» значения молча.
    2. Алерты на аномалии: отрицательные длительности, слишком большие значения (например, > 7 дней), расхождение между chain-total-ms и суммой компонент > заданного допуска.

8. Совместимость с инструментами (ELK, Jaeger, Kafka Tool, Grafana/Prometheus)

  1. ELK/Logstash: готовые шаблоны и процессоры парсят ISO 8601 UTC без кастомизации; отображение и агрегации корректные, часовой пояс пользователь выбирает в Kibana UI.
  2. Kafka Tool (Offset Explorer): строковые заголовки читаются напрямую; timestamp распознаваем «на глаз», упрощая дебаг.
  3. Jaeger: внутренние времена — epoch, но для контекстных полей/логов ISO 8601 понятен людям и легко сопоставим со span’ами.
  4. Grafana/Prometheus: метрики длительностей в мс как числа — нативно; аннотации/лог-панели хорошо отображают ISO 8601; преобразование в локальное время — на стороне клиента.

9. Точность, NTP и реалистичные ожидания

  1. При NTP-«смиринге» и типичных дрейфах ВМ в Kubernetes точность миллисекунд реальна; наносекунды — ложная точность и даёт межъязыковую несогласованность.
  2. Для сквозных длительностей используйте монотонные часы для локальных интервалов и UTC-времена — только для межузловых разниц; так вы минимизируете эффект редких коррекций времени.
  3. При мульти-хостовой обработке допускайте небольшой «технический люфт» (например, 2–5 мс) между суммой этапов и итоговой длительностью.

10. Краткие требования к разработчикам микросервисов

  1. Всегда писать моментные метки в заголовках в формате YYYY-MM-DDTHH:MM:SS.SSSZ (UTC, фиксированные 3 мс-цифры, UTF-8).
  2. Все длительности — целые миллисекунды в виде десятичной строки; без плавающей точки.
  3. Использовать монотонные часы для измерения длительностей; UTC-часы — для моментных меток.
  4. Не использовать локальные часовые пояса или смещения; всегда Z.
  5. Пробрасывать origin-created-at неизменным; корректно обновлять created-at при повторном издании.
  6. Валидация входящих заголовков; логирование аномалий; не «исправлять» молча.
  7. Писать автотесты на кросс-языковую совместимость сериализации/парсинга (Java/Go/Python) и на нварианты сортировки/точности (ровно 3 мс-цифры, строгое UTC).

11. Источники и материалы для дальнейшего изучения

  1. RFC #3339: Date and Time on the Internet: Timestamps.
  2. RFC #9557: Date and Time on the Internet: Timestamps with Additional Information

3. Wall-clock vs. Monotonic Time

Вот основные различия в виде таблицы:

Критерий now_wall (Wall-clock time) t_start_mono (Monotonic time)
Суть Календарное или "реальное" время. Аналог настенных часов. Абстрактный, постоянно растущий счетчик. Аналог секундомера.
Связь с реальным миром Привязано к временным зонам, UTC, переводу часов (летнее/зимнее время). Не привязано, абстрактно.
Изменения Может идти назад или прыгать вперед (из-за корректировок NTP, смены часового пояса пользователем). Только увеличивается с постоянной скоростью. Не может уменьшиться.
Основное использование Измерение и отображение реального времени (логи, время события, таймстампы в БД). Измерение продолжительности и интервалов (таймауты, бенчмаркинг, профилирование).
Примеры в коде time.Now() (Go), datetime.now() (Python), System.currentTimeMillis() (Java) time.Since(start) (Go), time.monotonic() (Python), System.nanoTime() (Java)
Синхронизация Обычно синхронизируется с серверами точного времени (NTP). Основано на системном таймере (например, счётчике процессора), независимо.
Портативность Единицы понятны человеку (секунды, миллисекунды с эпохи Unix). Единицы и точка отсчета специфичны для системы. Сравнивать значения между разными машинами бессмысленно.

Простая аналогия

Представьте, что вы проводите эксперимент и засекаете время.

  • now_wall — это чтобы посмотреть на настенные часы и записать, что эксперимент начался в 14:30:05.
  • t_start_mono — это чтобы включить секундомер в момент начала эксперимента. Его показания (например, 0:00) сами по себе не несут информации о реальном времени, но идеально подходят для замера длительности.

Что дальше?

  1. Нашли эту статью полезной? Поделитесь ею и помогите распространить знания!
  2. Нашли ошибку или есть идеи 💡 о том, что и как я могу улучшить? Напишите мне в Telegram.
  3. Хотите узнать обо мне больше? Читайте здесь.