Распределённая блокировка с TTL: какой механизм не позволяет остановившемуся на паузе владельцу выполнить устаревшую операцию после истечения аренды?
Нужны токены ограждения — монотонно возрастающие номера, выдаваемые вместе с арендой. Защищаемый ресурс должен принимать операцию только с токеном, не меньшим последнего принятого; поэтому старый владелец после паузы будет отвергнут, даже если считает блокировку своей.
Одного TTL недостаточно: он позволяет службе блокировок выдать аренду другому владельцу, но не может физически остановить прежний процесс.
Блокировки с ограниченным временем жизни появились как практический компромисс для систем, где узел может отказать, потерять связь или навсегда прекратить продлевать блокировку. В отличие от бессрочной блокировки, аренда позволяет автоматически освободить ресурс после исчезновения владельца.
Однако распределённая система не может надёжно отличить длительную паузу процесса от его отказа в момент принятия решения. Поэтому возникла необходимость защищать не только саму блокировку, но и операции на ресурсе от запоздалых владельцев.
Процесс получил аренду, затем остановился на длительной паузе сборщика мусора, вытеснении или другой остановке. За это время TTL истёк, сервис выдал ту же блокировку новому процессу, а старый после возобновления продолжил запись.
Если ресурс проверяет только факт получения блокировки, обе операции могут быть приняты. Это приводит к повреждению данных, откату более новой версии, повторной отправке команды или нарушению бизнес-инварианта.
Сервис блокировок при каждой выдаче аренды назначает новый fencing token: например, 41, затем 42. Новый владелец получает 42, а старый сохраняет 41. При обращении к защищаемому ресурсу владелец передаёт токен.
Ресурс хранит максимальный уже принятый токен и применяет операцию только если входящий токен не устарел. После принятия токена 42 запрос с токеном 41 отклоняется, независимо от того, считает ли старый процесс свою аренду действующей.
Проверка должна выполняться внутри атомарной операции самого ресурса либо его согласованного слоя доступа. Простая проверка в клиентском коде ненадёжна: между проверкой и записью другой процесс может изменить состояние.
Токены должны быть монотонными в рамках защищаемого ресурса и не теряться при перезапуске сервиса, выдающего блокировки. Для этого генератор токенов должен опираться на согласованное состояние, а не на локальные часы отдельных узлов.
TTL и fencing token решают разные задачи. TTL обеспечивает восстановление доступности: после исчезновения владельца ресурс можно передать другому. Токен обеспечивает безопасность от запоздалых действий прежнего владельца.
Подход не спасает ресурс, который не умеет проверять токены, например внешнюю систему с неконтролируемым API. В таком случае нужны её собственные версии, условные записи, транзакции или другой механизм защиты от устаревших операций.
Есть и компромисс: строгая проверка токенов повышает безопасность, но усложняет протокол доступа к ресурсу. Кроме того, отказ сервиса блокировок или потеря его согласованного состояния может временно остановить выдачу новых аренд.
Сервис распределяет задания между обработчиками. Обработчик A получил аренду задания и начал обновлять его состояние, после чего надолго остановился. TTL истёк, задание получил обработчик B и завершил его. Затем A возобновился и записал старый результат поверх результата B.
Рассматривались три варианта. Увеличение TTL уменьшало вероятность столкновения, но не устраняло длительные паузы и увеличивало время восстановления после отказа. Повторная проверка блокировки перед записью тоже не давала гарантии: пауза могла произойти сразу после проверки. Полагаться на локальные часы было опасно из-за рассинхронизации и задержек сети.
Выбран вариант с fencing token. Сервис блокировок выдавал каждой аренде возрастающий токен, а хранилище задания принимало обновление только при наличии токена не меньше сохранённого. После записи B с токеном 18 обновление A с токеном 17 было отклонено, поэтому устаревший обработчик не повредил результат.
Нет. Продление уменьшает вероятность истечения аренды при нормальной работе, но не защищает от паузы, которая длится дольше оставшегося времени. Старый процесс всё равно может возобновиться после передачи ресурса другому владельцу, поэтому необходима проверка fencing token на стороне ресурса.
На границе, которая фактически выполняет опасную операцию: в хранилище, координаторе записи или другом защищаемом сервисе. Если токен проверить только в клиенте, между проверкой и записью возможна пауза, а другой владелец успеет выполнить более новую операцию. Надёжная реализация объединяет сравнение токена и изменение состояния атомарно.
Обычно это небезопасно. Часы узлов могут расходиться, корректироваться назад, иметь одинаковые значения для разных выдач или не отражать порядок событий. Нужен источник, гарантирующий монотонный порядок выдачи токенов в нужной области; физическое время можно применять только при явно обеспеченных свойствах, которых локальные часы сами по себе не дают.