Розподілені блокування
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') для черги очікування → справедливість.
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
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-підходи без огорожі - ризик спліт-брейну. Думайте спочатку про каузальність та ідемпотентність, використовуйте блокування там, де без них не можна, вимірюйте, тестуйте відмовні режими - і ваші «критичні секції» залишаться критичними тільки за змістом, а не за кількістю інцидентів.