awesome-everything EN
↑ Обратно к восхождению

Распределённые системы

Время и порядок: почему таймстемпы по настенным часам врут

Суть Настенные часы дрейфуют и прыгают назад при синхронизации NTP, поэтому упорядочивание записей по клиентскому таймстемпу молча теряет данные. Логические часы дают порядок без доверия к стене; TrueTime в Spanner покупает его железом и ожиданием.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на junior-высоте — поверхность
◷ 17 min

Пользователь меняет адрес доставки, видит тост об успехе, делает рефреш — и старый адрес вернулся. Ни ошибки, ни строки в логе, ни исключения. Кластер Cassandra использовал last-write-wins по клиентскому таймстемпу, а часы на узле, обработавшем апдейт, отставали на 1.2 секунды. Поэтому «новая» запись пришла со штампом раньше, чем строка, которую должна была заменить. База сравнила два числа, оставила большее и выбросила изменение пользователя как устаревшее. Про таймстемпы она говорила правду, про реальность — врала.

Настенные часы — не те часы, по которым можно упорядочивать

У каждой машины есть кварцевый кристалл, который дрейфует — обычно на десятки частей на миллион, поэтому свободные часы уплывают на миллисекунды в минуту и секунды в сутки. NTP это правит, периодически подгоняя (slew) или прыгая (step) к эталону. Прыжок — опасная часть: когда локальные часы достаточно сильно ушли, ntpd прыгает ими, и этот прыжок может пойти назад. На несколько сотен миллисекунд «сейчас» оказывается раньше, чем «сейчас», которое ты уже прочитал. По флоту из десятков узлов даже здоровый NTP оставляет skew порядка миллисекунд; упавший демон NTP или VM, приостановленная гипервизором, разгоняют это до секунд или минут.

Поэтому таймстемп, сгенерированный на машине A, и таймстемп с машины B несравнимы как порядок. У них общие единицы измерения и больше ничего. В тот момент, когда твоя корректность зависит от того, что tsA < tsB означает «A произошло раньше B», ты построил на песке.

Last-write-wins — потеря данных в ожидании сдвига часов

Баг с адресом — каноничный продакшен-провал, и он встроен в любую систему, которая разрешает конфликты сравнением настенных таймстемпов: Cassandra, Riak, хранилища в стиле DynamoDB в режиме LWW. Правило — «оставить запись с наибольшим таймстемпом». Когда часы разъехались, наибольший таймстемп — это не самая поздняя запись, а запись с узла, чьи часы спешат. По-настоящему более новая запись проигрывает, и, что критично, клиент получает успешный ответ. Нет ошибки, которую можно поймать, нет метрики для алерта. Данные просто исчезли, и узнаёшь ты об этом, когда жалуется клиент.

С удалениями хуже. Tombstone, записанный с таймстемпом в далёком будущем — баговый клиент, узел с часами, прыгнувшими вперёд, — подавит любую реальную запись ниже себя, пока compaction не соберёт tombstone мусором, а это могут быть дни. Один сдвинувшийся узел способен стереть ключ на неделю. Анализ Jepsen сформулировал прямо: настенные таймстемпы — фундаментально небезопасный конструкт для упорядочивания.

Почему это работает

«Просто запусти NTP» тебя не спасёт. NTP держит skew малым в среднем, но LWW — это не среднее, это худший случай. Ты теряешь данные на том одном узле, в ту одну минуту, когда часы сдвинулись. И провал тихий: запись возвращает 200 OK, строка выброшена, и ничто в логах не отличает её от записи, которой просто никогда не было.

Логические часы: упорядочивать события без доверия к стене

Фикс — перестать измерять физическое время и начать измерять причинность. Таймстемп Лампорта — это один счётчик на процесс: инкремент на каждое локальное событие, а при получении сообщения выставляешь счётчик в max(local, received) + 1. Это гарантирует: если событие A happens-before B (A причинно предшествует B), то L(A) < L(B). Получаешь согласованный полный порядок бесплатно, за O(1) памяти на событие.

Чего часы Лампорта не умеют — это обратное: L(A) < L(B) не означает, что A вызвало B. Два события на узлах, которые никогда не общались, могут быть по-настоящему конкурентными, но Лампорт всё равно навязывает им произвольный порядок — он не умеет детектировать конкурентность, только навязать порядок. Для системы разрешения конфликтов эта слепота — и есть вся проблема: тебе нужно знать, что две записи конкурентны, чтобы их слить, а не молча выбрать одну.

Векторные часы это возвращают. Каждый узел несёт вектор — по счётчику на каждый узел системы — и шлёт весь вектор с каждым сообщением. Теперь можно сравнить два события: если каждая компонента A ≤ компоненты B и хотя бы одна строго меньше, то A happens-before B; если ни один вектор не доминирует над другим, события конкурентны, и система может поднять обе версии (siblings), чтобы приложение их слило. Цена — вот загвоздка: O(n) памяти на штампованное значение, где n — число узлов, плюс метаданные едут с каждой записью. На кластере в 200 узлов это 200 счётчиков, прицепленных к данным, и вектор растёт вместе с кластером.

ПодходДетектирует конкурентность?Цена на штампПродакшен-подвох
Настенный таймстемп (LWW)НетO(1), одно числоМолчаливая потеря данных при skew; ошибка не возвращается
Таймстемп ЛампортаНет — только полный порядокO(1), один счётчикУпорядочивает конкурентные записи произвольно; не слить конфликты
Векторные часыДа — помечает конкурентные событияO(n), счётчик на каждый узелМетаданные растут с кластером; нужна обрезка
TrueTime (Spanner)Н/Д — даёт реальный порядокGPS + атомные часы на датацентрCommit-wait добавляет латентность; нужно спецжелезо

TrueTime: заплати железом, чтобы настенные часы стали честными

Spanner от Google делает противоположную ставку: вместо отказа от физического времени — сделать его надёжным и квантифицировать своё незнание. TrueTime ставит в каждый датацентр GPS-приёмники и атомные часы, синхронизируемые примерно каждые 30 секунд, и отдаёт TT.now() не как один момент, а как интервал [earliest, latest]. Ширина этого интервала — неопределённость ε — обычно около 1мс (меньше 1мс на 99-м перцентиле в опубликованных Google числах, иногда расширяясь до единиц миллисекунд, когда time master отвечает медленно).

Хитрая часть — commit-wait. Spanner назначает таймстемп коммита s, затем намеренно спит, пока TT.now().earliest > s — пока не станет уверен, что таймстемп лежит в прошлом везде. Это ожидание — размером с ε, несколько миллисекунд, и оно идёт параллельно с репликацией Paxos, поэтому часто стоит мало настенного времени. Выигрыш — внешняя согласованность (линеаризуемость по всей базе): если транзакция T1 закоммитилась до старта T2, таймстемп T1 гарантированно меньше таймстемпа T2 — глобально, без оговорок про skew. Spanner превращает неопределённость часов из тихого бага корректности в явную ограниченную цену по латентности.

Выбери лучший вариант

Мультирегиональному key-value хранилищу нужно разрешать конкурентные записи в один ключ, не теряя молча апдейт пользователя. Какой механизм упорядочивания подходит?

Викторина

Кластер Cassandra использует last-write-wins по клиентским таймстемпам. Часы одного узла отстают на 1.5с. Апдейт пользователя, прошедший через этот узел, исчезает после рефреша. Почему клиент не увидел ошибки?

Викторина

Что на самом деле делает commit-wait в Spanner и что он покупает?

Расставь шаги по порядку

Расставь цепочку событий, превращающую skew часов в молчаливую потерю данных при last-write-wins:

  1. 1 Часы одного узла дрейфуют (или NTP прыгает ими) и отстают от остального флота
  2. 2 По-настоящему более новая запись идёт через этот узел и штампуется таймстемпом из прошлого
  3. 3 Разрешение конфликта сравнивает таймстемпы и оставляет существующую (с бóльшим таймстемпом) строку
  4. 4 Более новая запись выброшена — но клиент всё равно получает успешный ответ
  5. 5 Через недели клиент сообщает, что изменение пропало; ни лог, ни метрика этого не пометили
Вспомните перед уходом
  1. 01
    Коллега говорит: «у нас на каждом узле работает NTP, значит last-write-wins по таймстемпу безопасен». Объясни, почему это рассуждение неверно и что на самом деле происходит.
  2. 02
    Когда ты возьмёшь векторные часы вместо таймстемпов Лампорта и чего это стоит?
Итог

По всему флоту настенное время — не примитив упорядочивания: дрейф кварца плюс прыжки NTP (которые могут сдвинуть часы назад) оставляют skew в миллисекунды в норме и в секунды-минуты, когда что-то ломается. Любая система, разрешающая конфликты сравнением клиентских таймстемпов — last-write-wins в Cassandra, Riak, режиме LWW DynamoDB — превращает этот skew в молчаливую потерю данных, потому что наибольший таймстемп принадлежит самым быстрым часам, а не самой поздней записи, и клиент всё равно получает успех. Логические часы чинят порядок, измеряя причинность вместо физического времени: таймстемпы Лампорта дают дешёвый полный порядок за O(1), но не детектируют конкурентность, поэтому навязывают конкурентным записям произвольный порядок; векторные часы несут счётчик на каждый узел, детектируют конкурентность и позволяют хранилищу держать конфликтующие siblings — ценой O(n) метаданных, растущих с кластером. Google Spanner идёт третьим путём: TrueTime квантифицирует неопределённость часов как ограниченный интервал ε (около 1мс через GPS и атомные часы), а commit-wait намеренно пережидает это окно, чтобы дать внешнюю согласованность, превращая тихий баг корректности в явную ограниченную цену по латентности. Инстинкт сеньора прост: никогда не доверяй стене упорядочивание распределённых записей.

Продолжить восхождение ↑Часы: тест с множественным выбором
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.