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).

Натискаючи кнопку, ви погоджуєтесь на обробку даних.