GH GambleHub

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

1) Зачем (и когда) нужны распределенные блокировки

Распределенная блокировка — механизм, гарантирующий взаимоисключение для критической секции между несколькими узлами кластера. Типовые задачи:
  • Лидерство (leader election) для фоновой задачи/шедулера.
  • Ограничение единственного исполнителя над общим ресурсом (перемещение файлов, миграции схемы, эксклюзивный платежный шаг).
  • Последовательная обработка агрегата (wallet/order) если невозможно добиться идемпотентности/упорядочивания иначе.
Когда лучше не использовать замок:
  • Если можно сделать идемпотентный upsert, CAS (compare-and-set) или очередь по ключу (per-key ordering).
  • Если ресурс допускает коммутативные операции (CRDT, счетчики).
  • Если проблема решается транзакцией в одном хранилище.

2) Модель угроз и свойства

Отказы и сложности:
  • Сеть: задержки, разделение (partition), потеря пакетов.
  • Процессы: пауза GC, stop-the-world, крэш после взятия замка.
  • Время: дрейф часов и смещение ломают TTL-подходы.
  • Повторное владение: «зомби»-процесс после сети может думать, что все еще владеет замком.
Желаемые свойства:
  • Безопасность: не более одного действительного владельца (safety).
  • Живучесть: замок освобождается при сбое владельца (liveness).
  • Справедливость: отсутствует голодание.
  • Независимость от часов: корректность не зависит от wall-clock (или компенсируется fencing tokens).

3) Основные модели

3.1 Lease (арендный замок)

Замок выдается с TTL. Владелец обязан продлеваць его до истечения (heartbeat/keepalive).

Плюсы: самоснятие при крэше.
Риски: если владелец «завис» и продолжает работать, но потерял продление, может возникнуть двойное владение.

3.2 Fencing token (токен ограждения)

При каждом успешном захвате выдается монотонно растущий номер. Потребители ресурса (БД, очередь, файловое хранилище) проверяют токен и отвергают операции со старым номером.
Это крайне важно при TTL/lease и сетевых разделениях — защищает от «старого» владельца.

3.3 Quorum-замки (CP-системы)

Используют распределенный консенсус (Raft/Paxos; etcd/ZooKeeper/Consul), запись связана с логом консенсуса → нет сплит-брейна при большинстве узлов.

Плюс: сильные гарантии безопасности.
Минус: чувствительность к кворуму (при его потере живучесть хромает).

3.4 AP-замки (in-memory/кэш + репликация)

Например, Redis-кластер. Высокая доступность и скорость, но без жестких гарантий безопасности при сетевых разделениях. Требуют fencing на стороне синка.

4) Платформы и паттерны

4.1 etcd / ZooKeeper / Consul (рекомендуемые для strong locks)

Эфемерные узлы (ZK) или сессии/leases (etcd): ключ существует, пока жива сессия.
Сессионный keepalive; потеря кворума → сессия истекает → замок освобождается.
Порядковые узлы (ZK `EPHEMERAL_SEQUENTIAL`) для очереди ожидания → справедливость.

Эскиз на etcd (Go):
go cli, _:= clientv3. New(...)
lease, _:= cli. Grant(ctx, 10)            // 10s lease sess, _:= concurrency. NewSession(cli, concurrency. WithLease(lease. ID))
m:= concurrency. NewMutex(sess, "/locks/orders/42")
if err:= m. Lock(ctx); err!= nil { / handle / }
defer m. Unlock(ctx)

4.2 Redis (аккуратно)

Классика — `SET key value NX PX ttl`.

Проблемы:
  • Репликация/фейловер могут допустить одновременных владельцев.
  • Redlock из нескольких инстансов уменьшает риск, но не устраняет; спорен в средах с ненадежной сетью.

Безопаснее применять Redis как быстрый координационный слой, но всегда дополнять fencing token в целевом ресурсе.

Пример (Lua-unlock):
lua
-- release only if value matches if redis. call("GET", KEYS[1]) == ARGV[1] then return redis. call("DEL", KEYS[1])
else return 0 end

4.3 БД-замки

PostgreSQL advisory locks: лок в рамках кластера Postgres (процесс/сессия).
Хорошо, когда все критические секции и так в одной БД.

SQL:
sql
SELECT pg_try_advisory_lock(42); -- take
SELECT pg_advisory_unlock(42); -- let go

4.4 Файловые/облачные «замки»

S3/GCS + объектный метаданный лок с условиями `If-Match` (ETag) → по сути CAS.
Подходит для бэкапов/миграций.

5) Дизайн безопасного замка

5.1 Идентичность владельца

Храните `owner_id` (узел#процесс#pid#start_time) + случайный токен для сверки при unlock.
Повторный unlock не должен снимать чужой замок.

5.2 TTL и продление

TTL < T_fail_detect (время обнаружения сбоя) и ≥ p99 работы критической секции × запас.
Продление — периодически (например, каждые `TTL/3`), с deadline.

5.3 Fencing token на синке

Секция, изменяющая внешний ресурс, должна передать `fencing_token`.

Синк (БД/кэш/хранилище) хранит `last_token` и отвергает меньшие:
sql
UPDATE wallet
SET balance = balance +:delta, last_token =:token
WHERE id =:id AND:token > last_token;

5.4 Очередь ожидания и справедливость

В ZK — `EPHEMERAL_SEQUENTIAL` и наблюдатели: клиент ждет освобождение ближайшего предшественника.
В etcd — ключи с ревизией/версионированием; очередность по `mod_revision`.

5.5 Поведение при split-brain

CP-подход: без кворума нельзя взять замок — лучше простоять, чем сломать safety.
AP-подход: допускается прогресс в разделенных островах → необходим fencing.

6) Лидерство (leader election)

В etcd/ZK — «лидер» это эксклюзивный эпемерный ключ; остальные подписаны на изменения.
Лидер пишет heartbeats; потеря — переизбрание.
Все операции лидера сопровождайте fencing token (номер эпохи/ревизии).

7) Ошибки и их обработка

Клиент взял замок, но крэш до работы → норм, никто не пострадает; TTL/сессия освободят.

Замок истек посередине работы:
  • Обязателен watchdog: если продление неуспешно — прервать критическую секцию и откатить/компенсировать.
  • Никаких «доделать потом»: без замка критическую секцию продолжать нельзя.

Долгая пауза (GC/stop-the-world) → продление не случилось, другой взял замок. Рабочий процесс должен обнаружить потерю владения (канал keepalive) и прервать.

8) Дедлоки, приоритеты и инверсия

Дедлоки в распределенном мире редки (замок обычно один), но если замков несколько — придерживайтесь единого порядка взятия (lock ordering).
Инверсия приоритетов: низкоприоритетный владелец держит ресурс, пока высокоприоритетные ждут. Решения: TTL-лимиты, preemption (если бизнес допускает), sharding ресурса.
Голодание: используйте очереди ожидания (ZK-подпорядковые узлы) для справедливости.

9) Наблюдаемость

Метрики:
  • `lock_acquire_total{status=ok|timeout|error}`
  • `lock_hold_seconds{p50,p95,p99}`
  • `fencing_token_value` (монотонность)
  • `lease_renew_fail_total`
  • `split_brain_prevented_total` (сколько попыток отказано из-за отсутствия кворума)
  • `preemptions_total`, `wait_queue_len`
Логи/трейсинг:
  • `lock_name`, `owner_id`, `token`, `ttl`, `attempt`, `wait_time_ms`, `path` (для ZK), `mod_revision` (etcd).
  • Спаны «acquire → critical section → release» с результатом.
Алерты:
  • Рост `lease_renew_fail_total`.
  • `lock_hold_seconds{p99}` > SLO.
  • «Орфанные» замки (без heartbeat).
  • Раздутые очереди ожидания.

10) Практические примеры

10.1 Безопасный Redis-замок с fencing (псевдо)

1. Храним счетчик токенов в надежном сторе (например, Postgres/etcd).
2. При успешном `SET NX PX` читаем/инкрементируем токен и все изменения ресурса делаем с проверкой токена в БД/сервисе.

python acquire token = db. next_token ("locks/orders/42") # monotone ok = redis. set("locks:orders:42", owner, nx=True, px=ttl_ms)
if not ok:
raise Busy()

critical op guarded by token db. exec("UPDATE orders SET... WHERE id=:id AND:token > last_token",...)
release (compare owner)

10.2 etcd Mutex + watchdog (Go)

go ctx, cancel:= context. WithCancel(context. Background())
sess, _:= concurrency. NewSession(cli, concurrency. WithTTL(10))
m:= concurrency. NewMutex(sess, "/locks/job/cleanup")
if err:= m. Lock(ctx); err!= nil { /... / }

// Watchdog go func() {
<-sess. Done ()//loss of session/quorum cancel ()//stop working
}()

doCritical (ctx )//must respond to ctx. Done()
_ = m. Unlock(context. Background())
_ = sess. Close()

10.3 Лидерство в ZK (Java, Curator)

java
LeaderSelector selector = new LeaderSelector(client, "/leaders/cron", listener);
selector. autoRequeue();
selector. start(); // listener. enterLeadership() с try-finally и heartbeat

10.4 Postgres advisory lock с дедлайном (SQL+app)

sql
SELECT pg_try_advisory_lock(128765); -- attempt without blocking
-- if false --> return via backoff + jitter

11) Тест-плейбуки (Game Days)

Потеря кворума: отключить 1–2 узла etcd → попытка взять замок должна не проходить.
GC-пауза/stop-the-world: искусственно задержать поток владельца → проверить, что watchdog прерывает работу.
Split-brain: эмуляция сетевого разделения между владельцем и стором замка → новый владелец получает более высокий fencing token, старый — отвергнут синком.
Clock skew/drift: увести часы у владельца (для Redis/lease) → убедиться, что корректность обеспечивается токенами/проверками.
Crash before release: падение процесса → замок освобождается по TTL/сессии.

12) Анти-паттерны

Чистый TTL-замок без fencing при доступе к внешнему ресурсу.
Полагаться на локальное время для корректности (без HLC/fencing).
Раздача замков через один Redis-мастер в среде с фейловером и без подтверждения реплик.
Бесконечная критическая секция (TTL «на века»).
Снятие «чужого» замка без сверки `owner_id`/token.
Отсутствие backoff+jitter → «шторм» попыток.
Единый глобальный замок «на все» — мешок конфликтов; шардинг по ключу лучше.

13) Чек-лист внедрения

  • Определен тип ресурса и можно ли обойтись CAS/очередью/идемпотентностью.
  • Выбран механизм: etcd/ZK/Consul для CP; Redis/кэш — только с fencing.
  • Реализованы: `owner_id`, TTL + продление, watchdog, корректный unlock.
  • Внешний ресурс проверяет fencing token (монотонность).
  • Есть стратегия лидерства и failover.
  • Настроены метрики, алерты, логирование токенов и ревизий.
  • Предусмотрены backoff+jitter и таймауты на acquire.
  • Проведены game days: кворум, split-brain, GC-паузы, clock skew.
  • Документация порядка взятия нескольких замков (если требуется).
  • План деградации (brownout): что делать при недоступности замка.

14) FAQ

Q: Достаточно ли Redis-замка `SET NX PX`?
A: Только если ресурс проверяет fencing token. Иначе при сетевом разделении возможны два владельца.

Q: Что выбрать «по умолчанию»?
A: Для строгих гарантий — etcd/ZooKeeper/Consul (CP). Для легких задач внутри одной БД — advisory locks Postgres. Redis — лишь с fencing.

Q: Какой TTL ставить?
A: `TTL ≥ p99 длительности критической секции × 2` и достаточно короткий для быстрой очистки «зомби». Продление — каждые `TTL/3`.

Q: Как избежать голодания?
A: Очередь ожидания по порядку (ZK sequential) или fairness-алгоритм; лимит попыток и справедливое планирование.

Q: Нужна ли синхронизация времени?
A: Для корректности — нет (используйте fencing). Для эксплуатационной предсказуемости — да (NTP/PTP), но не полагайтесь на wall-clock в логике замка.

15) Итоги

Надежные распределенные блокировки строятся на кворумных сторах (etcd/ZK/Consul) с lease + keepalive, и обязательно дополняются fencing token на уровне изменяемого ресурса. Любые TTL/Redis-подходы без ограждения — риск сплит-брейна. Думайте сначала о каузальности и идемпотентности, используйте блокировки там, где без них нельзя, измеряйте, тестируйте отказные режимы — и ваши «критические секции» останутся критичными только по смыслу, а не по количеству инцидентов.

Contact

Свяжитесь с нами

Обращайтесь по любым вопросам или за поддержкой.Мы всегда готовы помочь!

Начать интеграцию

Email — обязателен. Telegram или WhatsApp — по желанию.

Ваше имя необязательно
Email необязательно
Тема необязательно
Сообщение необязательно
Telegram необязательно
@
Если укажете Telegram — мы ответим и там, в дополнение к Email.
WhatsApp необязательно
Формат: +код страны и номер (например, +380XXXXXXXXX).

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