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

Распределённые блокировки

Цели урока

  • Понимать когда нужна распределённая блокировка и когда от неё следует отказаться
  • Реализовать безопасный Redis-лок с уникальным токеном и Lua-скриптом
  • Знать алгоритм Redlock и его ограничения при GC-паузах
  • Применять fencing tokens для защиты от устаревших lock holders
  • Выбирать между Redis, Redlock, ZooKeeper и etcd по требованиям задачи

Предварительные знания

  • Понимание Redis: команды SET NX PX, Lua eval
  • Концепция race condition и critical section
  • Базовые знания о кворуме и консенсусе в распределённых системах

Один Redis-лок без уникального токена - и GC-пауза в 2 секунды превращает mutex в решето: два процесса одновременно в критической секции, двойное списание, потеря данных.

  • **Stripe, PayPal** - idempotency keys на каждый платёж фактически распределённая блокировка на уровне бизнес-логики
  • **Kubernetes** - etcd-блокировки для leader election controller-manager и scheduler
  • **Apache Kafka** - ZooKeeper (исторически) для координации брокеров и partition leadership
  • **Cron в кластере** - без distributed lock каждая задача запускается на всех Pod-ах одновременно
  • **GitHub Actions self-hosted runners** - блокировки на уровне workflow для предотвращения параллельных деплоев

Дискуссия Antirez vs Kleppmann (2016)

В феврале 2016 Martin Kleppmann опубликовал статью 'How to do distributed locking', где доказал что Redlock небезопасен при GC-паузах и clock skew. Antirez (автор Redis и Redlock) ответил детальным возражением. Дискуссия длилась несколько недель, привлекла Kyle Kingsbury (aphyr) и других. Итог: Redlock достаточно безопасен для большинства задач, но не для систем где корректность критична - там нужны fencing tokens. Эта дискуссия стала учебником по тому, как думать о safety в distributed systems.

Зачем нужны распределённые блокировки

**2012 год. Stripe обрабатывает платежи на нескольких инстансах. Два Pod-а одновременно получают webhook от банка об успешной оплате - и оба начинают начислять бонусы пользователю. Двойное начисление. 100 000 транзакций в день. Цена ошибки - миллионы долларов.** Локальный mutex не поможет: Pod-ы не видят память друг друга. Нужен mutex на уровне кластера.

**Распределённая блокировка** - механизм, гарантирующий что только один процесс в кластере выполняет критическую секцию в любой момент времени. В отличие от локального mutex, живёт в общем хранилище (Redis, ZooKeeper, etcd).

Когда нужна распределённая блокировка

СценарийПроблема без блокировкиРешение
Обработка платежейДвойное списание/начисление при race conditionЛок на идентификатор транзакции
Cron-задачи в кластере5 Pod-ов запускают одну задачу одновременноЛок на имя задачи перед запуском
Доступ к внешнему API с rate limitСуммарный rate кластера превышает лимит APIЛок или distributed rate limiter
Leader electionНесколько сервисов считают себя лидеромКто захватил лок - тот лидер

**Главное правило:** распределённая блокировка - последний выбор, не первый. Сначала проверить: операция идемпотентна? Можно использовать CRDT или optimistic locking? Нужен один writer - рассмотреть leader election. Лок добавляет latency, single point of failure и сложность.

Распределённые блокировки работают как обычный mutex - захватил, сделал, отпустил, всё безопасно

В распределённой системе между захватом лока и его использованием может произойти GC-пауза, network partition или crash - и другой процесс захватит тот же лок

Локальный mutex живёт в памяти процесса - нет паузы между проверкой и захватом. В сети каждый шаг - отдельный запрос с потенциальной задержкой. Поэтому нужны дополнительные механизмы (fencing tokens, TTL, уникальные идентификаторы) чтобы обнаружить устаревший holder.

Два Pod-а одновременно обрабатывают один и тот же webhook события оплаты. Какой механизм корректнее всего предотвращает двойную обработку?

Redis-блокировка: от простой к безопасной

**Redis - первый выбор для distributed locks в 90% случаев.** Команда `SET key value NX PX ttl` атомарна: записывает значение только если ключа нет, с автоматическим истечением. Один round-trip вместо транзакции. Но простая реализация содержит опасный баг.

**Сценарий атаки (GC pause):** Process A захватывает лок. JVM делает GC-паузу на 2 секунды. TTL истекает. Process B захватывает тот же лок. Process A выходит из паузы, думает что держит лок, и при `release()` удаляет лок Process B. Теперь оба процесса одновременно работают в критической секции.

Безопасная реализация: уникальный токен

**Почему Lua-скрипт?** GET + DEL - два отдельных запроса. Между ними другой процесс может захватить лок и TTL истечёт именно тогда. Lua-скрипт выполняется атомарно в одном Redis-command.

Почему при release() нужно использовать Lua-скрипт вместо последовательных GET + DEL?

Redlock: кворум на 5 нодах

**Единственная Redis-нода - single point of failure.** Если она упала или делает AOF-запись с fsync, лок недоступен. Antirez (автор Redis) в 2013 предложил Redlock - алгоритм блокировки на N независимых Redis-нодах. Захват считается успешным если лок получен на большинстве (N/2 + 1) нод за время меньше TTL.

ШагДействиеУсловие продолжения
1Запомнить startTime = Date.now()Всегда
2Последовательно SET NX на каждой ноде с коротким timeoutСобрать все ответы
3Подсчитать successCount и elapsed = Date.now() - startTimeВсегда
4Проверить: successCount >= N/2+1 AND ttl - elapsed > clockDriftЛок захвачен - можно работать
5Если условие не выполнено - DEL на всех нодахНет лока - retry или error

**Критика Martin Kleppmann (2016):** Redlock не безопасен при GC-паузах и clock skew. Сценарий: процесс A захватывает Redlock, уходит в GC-паузу на 10 секунд, TTL истекает, процесс B захватывает тот же Redlock, процесс A выходит из паузы и оба работают в критической секции. Antirez возразил, но дискуссия не закрыта. Для финансовых систем - использовать fencing tokens.

Redlock на 5 нодах полностью безопасен - даже если 2 ноды упали, большинство держит лок

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

Проблема не в том, сколько нод держат лок, а в том что между захватом лока и реальным использованием ресурса может пройти произвольное время (GC, CPU scheduling). За это время TTL может истечь на всех нодах и другой клиент захватит тот же лок. Fencing tokens решают это независимо от механизма блокировки.

Redlock на 5 нодах: захвачено 2 из 5. Что происходит?

Fencing Tokens и ZooKeeper

**Martin Kleppmann предложил fencing tokens в 2016 - единственный способ гарантировать корректность при GC-паузах.** Идея: сервер блокировок выдаёт монотонно возрастающий номер при каждом захвате. Ресурс отклоняет запросы со старым номером. Процесс A захватил лок с token=33, завис, token=34 выдан процессу B, A проснулся - ресурс видит 33 < 34 и отклоняет запрос A.

ZooKeeper: ephemeral sequential nodes

ZooKeeper предоставляет примитивы для блокировок на уровне консенсуса (ZAB протокол). Клиент создаёт **эфемерный последовательный узел** - ZooKeeper автоматически добавляет монотонный суффикс (lock-0000001, lock-0000002). Кто создал узел с наименьшим суффиксом - тот держит лок. При потере сессии эфемерный узел удаляется автоматически.

МеханизмНадёжностьLatencyFencing tokensКогда использовать
Redis single nodeНизкая (SPOF)< 1 мсНет (нужно реализовать)Dev/staging, некритичные задачи
Redlock (5 нод)Средняя (кворум)3-10 мсНетВысокая доступность, не финансы
ZooKeeperВысокая (ZAB)5-20 мсДа (sequential nodes)Координация кластера, leader election
etcd (Raft)Высокая (Raft)5-15 мсДа (revision)Kubernetes-окружения, service mesh

В чём преимущество ZooKeeper ephemeral sequential nodes перед Redis-блокировкой для реализации fencing tokens?

Вопросы для размышления

  • Представьте систему обработки финансовых транзакций на 10 Pod-ах. Определите: 1. для каких операций достаточно идемпотентности без блокировки 2. для каких нужен Redis-лок 3. для каких нужен ZooKeeper или etcd с fencing tokens.

Связанные уроки

  • dist-07-transactions — Locks implement pessimistic transaction isolation
  • dist-09-raft — Lock services are built on consensus (etcd/ZooKeeper)
  • dist-12-consistency — Locks provide mutual exclusion underpinning strong consistency
  • db-13-transactions
Распределённые блокировки

0

1

Войти