Distributed Locks и Leader Election
Зачем знать: В распределённой системе нельзя использовать
sync.Mutex— он защищает только память одного процесса. Когда нужно ровно один воркер обрабатывал scheduled job, ровно один лидер записывал в БД, ровно один сервис выпускал инвойс — нужны distributed locks. Middle Go-разработчик должен знать Redis Redlock, etcd lease, Zookeeper sequential nodes, PostgreSQL advisory locks и понимать, почему “lock” в распределённой системе — это всегда trade-off между safety и liveness.
Содержание
Заголовок раздела «Содержание»- Концепция: зачем distributed locks, безопасность
- Реализации: Redis, etcd, ZooKeeper, Consul, БД
- Gotchas
- Real cases
- Вопросы (25)
- Practice
- Источники
1. Концепция
Заголовок раздела «1. Концепция»1.1 Что такое distributed lock
Заголовок раздела «1.1 Что такое distributed lock»Mutual exclusion across nodes. Несколько процессов на разных машинах хотят выполнить критическую секцию (CS) так, чтобы ровно один процесс находился в CS в любой момент.
Невозможно гарантировать абсолютную безопасность из-за асинхронной сети, GC pauses, clock drift. Поэтому distributed lock = best-effort + защита от наиболее частых race conditions, а для критичных операций — fencing tokens.
1.2 Use cases
Заголовок раздела «1.2 Use cases»Leader election: один процесс становится лидером и принимает решения, остальные — followers. При падении лидера выбирается новый.
- Примеры: Kubernetes scheduler, controller-manager; Kafka controller; Cassandra coordinator; Elasticsearch master.
Scheduled jobs (cron): запланированная задача не должна выполниться дважды.
- Запустил cron на 3 нодах для HA → нужно, чтобы только один выполнил.
Single-writer: в шардированной БД к одному ключу пишет только один сервис.
- Реплицируется через write-through cache: invalidation должна быть последовательной.
Resource pool exhaustion: одновременно деплоить только один сервис, чтобы не упасть в OOM.
Workflow orchestration: одна saga обрабатывается одним worker.
Distributed mutex для legacy кода: где невозможно переписать на event-driven, нужна блокировка на ресурс.
1.3 Свойства правильного distributed lock
Заголовок раздела «1.3 Свойства правильного distributed lock»Из статьи Martin Kleppmann “How to do distributed locking” (2016):
- Safety property: Mutual exclusion. В любой момент времени lock держит ровно один client.
- Liveness property A: Deadlock free. Если client упал, lock в конце концов освободится (TTL).
- Liveness property B: Fault tolerance. Кластер работает при падении меньшинства нод.
Trade-off: safety vs liveness. С TTL мы рискуем double-execution (старый владелец думает что владеет lock, новый владелец уже взял).
1.4 Fencing tokens (защита от stale leaders)
Заголовок раздела «1.4 Fencing tokens (защита от stale leaders)» Client 1 Lock service Storage | acquire lock | | | ----------------------> | | | <--- token=33 --------- | | | (GC pause 30s) | | | | lock expires | | Client 2 acquires | | | <--- token=34 --------- | | | Client 2: write(34) | | | ---------------------> | -----write token=34--->| | Client 1 wakes up | | | Client 1: write(33) | | | ---------------------> | -----write token=33--->| | | [REJECT, stale]|Fencing token — монотонно возрастающее число, выдаваемое lock service при acquire. Storage отказывает на любой write с token меньше последнего увиденного.
Это единственный способ безопасно работать с unreliable lock service + retries.
1.5 Lease (аренда времени)
Заголовок раздела «1.5 Lease (аренда времени)»Lock даётся на время T (TTL). Если client не renew — lock автоматически освобождается. Без TTL — fault tolerance невозможен (упавший процесс заблокирует ресурс навсегда).
Trade-off TTL:
- Слишком короткий: client делает GC pause / сетевой лаг → теряет lock, продолжает работать → race.
- Слишком длинный: при падении ресурс заблокирован надолго → liveness страдает.
Типично: 10-60 секунд + heartbeat renewal каждые 1/3 TTL.
2. Реализации
Заголовок раздела «2. Реализации»2.1 Redis lock (single instance)
Заголовок раздела «2.1 Redis lock (single instance)»Атомарный acquire:
SET key value NX PX 30000NX— set only if not existsPX 30000— TTL 30 секунд
value — уникальный токен (UUID). Это критично для безопасного release.
Release через Lua script (атомарно):
if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1])else return 0endБез проверки токена при release мы можем удалить чужой lock (наш TTL истёк, новый владелец взял lock, мы делаем DEL — удаляем его lock).
Go реализация:
package redislock
import ( "context" "crypto/rand" "encoding/hex" "errors" "time"
"github.com/redis/go-redis/v9")
type Lock struct { rdb *redis.Client key string value string ttl time.Duration}
func New(rdb *redis.Client, key string, ttl time.Duration) (*Lock, error) { buf := make([]byte, 16) _, _ = rand.Read(buf) return &Lock{rdb: rdb, key: key, value: hex.EncodeToString(buf), ttl: ttl}, nil}
func (l *Lock) TryAcquire(ctx context.Context) (bool, error) { return l.rdb.SetNX(ctx, l.key, l.value, l.ttl).Result()}
var releaseScript = redis.NewScript(`if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1])else return 0end`)
func (l *Lock) Release(ctx context.Context) error { res, err := releaseScript.Run(ctx, l.rdb, []string{l.key}, l.value).Int() if err != nil { return err } if res == 0 { return errors.New("lock not held or expired") } return nil}
func (l *Lock) Renew(ctx context.Context) error { script := redis.NewScript(` if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("pexpire", KEYS[1], ARGV[2]) else return 0 end `) res, err := script.Run(ctx, l.rdb, []string{l.key}, l.value, l.ttl.Milliseconds()).Int() if err != nil { return err } if res == 0 { return errors.New("lock lost") } return nil}2.2 Redlock (Antirez 2014)
Заголовок раздела «2.2 Redlock (Antirez 2014)»Распределяет lock на N независимых Redis instances (master без replication между ними). Цель: устойчивость к падению меньшинства.
Алгоритм:
- Получи current time T1 (millisecond precision).
- Попытайся acquire lock на всех N instances последовательно, с малым timeout на каждый (~5-50 ms).
- После прохода вычисли elapsed = T_now - T1.
- Lock получен если: acquired на N/2+1 instances И elapsed < TTL.
- Effective TTL = TTL - elapsed.
- Если не получили — release на всех instances.
Go библиотека:
import "github.com/go-redsync/redsync/v4"
pool := goredis.NewPool(rdb)rs := redsync.New(pool)mutex := rs.NewMutex("my-lock", redsync.WithExpiry(10*time.Second), redsync.WithTries(3),)
if err := mutex.Lock(); err != nil { return err}defer mutex.Unlock()// CSRedsync поддерживает Redlock, если в pool несколько independent Redis.
2.3 Критика Redlock (Kleppmann vs Antirez, 2016)
Заголовок раздела «2.3 Критика Redlock (Kleppmann vs Antirez, 2016)»Martin Kleppmann “How to do distributed locking”:
- Clock dependence. Redlock полагается на bounded clock drift между nodes. При NTP step (jump time) lock может быть одновременно у двух clients.
- GC pauses. Если client делает long GC pause (Java 30s), lock истечёт, другой возьмёт. После pause client продолжит работу, не зная о потере lock.
- Network delays. Аналогично GC — задержка ответа от Redis может означать, что lock уже истёк.
Вывод Kleppmann: Redlock не safe для correctness. Используй для эффективности (avoid duplicate work), но критичные операции защищай fencing tokens.
Antirez ответ: Redlock safe при правильном использовании (фиксированный clock skew assumption). Но он не предотвращает long pauses.
Практика:
- Redis Redlock — best-effort lock для some-loss-acceptable use cases (cache warming, rate limiting).
- Для correctness — etcd / ZooKeeper + fencing.
2.4 etcd-based locks
Заголовок раздела «2.4 etcd-based locks»etcd предоставляет linearizable KV store с Raft consensus. Это даёт сильные гарантии safety.
Concurrency package (etcd client v3):
import ( "github.com/coreos/etcd/clientv3" "github.com/coreos/etcd/clientv3/concurrency")
cli, _ := clientv3.New(clientv3.Config{Endpoints: []string{"etcd:2379"}})defer cli.Close()
session, _ := concurrency.NewSession(cli, concurrency.WithTTL(10))defer session.Close()
mutex := concurrency.NewMutex(session, "/my-lock/")if err := mutex.Lock(ctx); err != nil { return err}defer mutex.Unlock(ctx)// CSКак работает:
- Session — это lease в etcd. Auto-renewed через keepalive.
- Mutex создаёт ephemeral key с lease.
- При acquire: получает key с lock prefix, ставит watch на предыдущий key.
- Если предыдущий упал (lease истёк) — наш становится первым → owner lock.
Преимущества:
- Linearizable (Raft).
- Fencing tokens доступны (Revision из etcd response).
- Auto-cleanup при крахе client (lease expire).
Trade-off:
- Latency 10-50ms (Raft).
- Кластер сложнее поднять чем Redis.
2.5 ZooKeeper locks
Заголовок раздела «2.5 ZooKeeper locks»Sequential ephemeral nodes pattern:
/locks/job-1/ /locks/job-1/lock-00001 (client A, ephemeral) /locks/job-1/lock-00002 (client B, ephemeral) /locks/job-1/lock-00003 (client C, ephemeral)- Каждый client создаёт ephemeral sequential node под
/locks/job-1/. - Получает список children, проверяет: моя node имеет наименьший номер?
- Если да — lock acquired.
- Если нет — ставит watch на предыдущий по номеру (lock-00001 для B).
- Когда предыдущий удалён — повтори проверку.
Это fair lock (FIFO порядок).
Используется в Kafka (controller election), HBase, Hadoop NameNode HA.
Go library: github.com/go-zookeeper/zk.
2.6 Consul
Заголовок раздела «2.6 Consul»Consul поддерживает sessions с TTL и health checks.
import "github.com/hashicorp/consul/api"
client, _ := api.NewClient(api.DefaultConfig())session, _, _ := client.Session().Create(&api.SessionEntry{TTL: "15s", Behavior: "delete"}, nil)
kv := &api.KVPair{Key: "locks/job", Value: []byte("locked"), Session: session}acquired, _, _ := client.KV().Acquire(kv, nil)if acquired { defer client.KV().Release(kv, nil) // CS}Менее популярен для locks. Больше используется для service discovery + KV.
2.7 PostgreSQL advisory locks
Заголовок раздела «2.7 PostgreSQL advisory locks»PostgreSQL имеет встроенные advisory locks — не относящиеся к таблицам:
Session-level:
SELECT pg_advisory_lock(123);-- CSSELECT pg_advisory_unlock(123);Transaction-level (auto-release on commit/rollback):
BEGIN;SELECT pg_advisory_xact_lock(123);-- CS, авто-release при COMMIT/ROLLBACKCOMMIT;Try-lock (non-blocking):
SELECT pg_try_advisory_lock(123); -- returns true/falseComposite key (намного полезнее):
SELECT pg_advisory_lock(123, 456); -- два int32Преимущества:
- Бесплатно (если уже используете PostgreSQL).
- Linearizable (single primary).
- Auto-cleanup при disconnect.
- Lock keys — числовой (нужно hash от string:
hashtext('user_42')).
Trade-off:
- Single point of failure (если primary упал — failover задерживает locks).
- Lock keys не human-readable.
- Connection-bound (если используешь pool, нужно держать connection).
2.8 SELECT FOR UPDATE pattern
Заголовок раздела «2.8 SELECT FOR UPDATE pattern»BEGIN;SELECT * FROM jobs WHERE id=$1 FOR UPDATE SKIP LOCKED;-- если возвращает row, мы получили lock-- обработкаUPDATE jobs SET status='done' WHERE id=$1;COMMIT;FOR UPDATE блокирует выбранные строки до конца транзакции.
SKIP LOCKED — пропускает уже залоченные (для worker pool: каждый worker берёт свою задачу).
Use case: message-queue-like работа с БД. Outbox worker, batch processor.
2.9 INSERT с UNIQUE constraint trick
Заголовок раздела «2.9 INSERT с UNIQUE constraint trick»Простейший lock без сторонних систем:
INSERT INTO locks (resource_key, owner, acquired_at) VALUES ($1, $2, NOW())ON CONFLICT DO NOTHINGRETURNING owner;Если RETURNING вернул row — мы owner. Иначе — кто-то уже залочил. TTL + cron для cleanup.
Минус: lock orphaned при крахе owner до cleanup.
2.10 Leader election
Заголовок раздела «2.10 Leader election»Kubernetes leader election (используется kube-scheduler, kube-controller-manager):
Реализован через Lease.coordination.k8s.io resource:
apiVersion: coordination.k8s.io/v1kind: Leasemetadata: name: my-controller namespace: kube-systemspec: holderIdentity: pod-123 leaseDurationSeconds: 15 acquireTime: "2026-01-01T00:00:00Z" renewTime: "2026-01-01T00:00:10Z"Holder обновляет renewTime каждые leaseDuration / 3 секунд. Если не обновляется > leaseDuration — другой может взять.
Go код для leader election (k8s client-go):
import "k8s.io/client-go/tools/leaderelection"
leLock := &resourcelock.LeaseLock{ LeaseMeta: metav1.ObjectMeta{Name: "my-controller", Namespace: "default"}, Client: clientset.CoordinationV1(), LockConfig: resourcelock.ResourceLockConfig{Identity: podName},}
leaderelection.RunOrDie(ctx, leaderelection.LeaderElectionConfig{ Lock: leLock, LeaseDuration: 15 * time.Second, RenewDeadline: 10 * time.Second, RetryPeriod: 2 * time.Second, Callbacks: leaderelection.LeaderCallbacks{ OnStartedLeading: func(ctx context.Context) { // мы лидер: выполняем работу }, OnStoppedLeading: func() { // мы потеряли лидерство: останавливаем работу os.Exit(0) // часто проще выйти и дать поду перезапуститься }, OnNewLeader: func(identity string) { log.Printf("new leader: %s", identity) }, },})Pattern: при потере лидерства — exit, не пытаться продолжать. Это упрощает жизнь.
etcd для leader election:
session, _ := concurrency.NewSession(cli, concurrency.WithTTL(10))election := concurrency.NewElection(session, "/leader/my-svc")if err := election.Campaign(ctx, podName); err != nil { return err}// мы лидерdefer election.Resign(ctx)2.11 Fencing tokens с etcd
Заголовок раздела «2.11 Fencing tokens с etcd»etcd возвращает Revision для каждой операции — это монотонно возрастающее число.
resp, _ := cli.Put(ctx, "/leader/svc", "pod-123", clientv3.WithLease(leaseID))fencingToken := resp.Header.Revision
// при write в storage:storage.WriteWithFence(data, fencingToken)Storage хранит last_fence_token для каждого ресурса и отказывает на стейл tokens.
3. Gotchas
Заголовок раздела «3. Gotchas»⚠️ Redis lock без unique value опасен. При DEL key без проверки можно удалить чужой lock. Всегда используйте уникальный токен и Lua script для release.
⚠️ TTL слишком короткий → race. Если client делает GC pause / I/O block / network lag дольше TTL — теряет lock, не зная об этом. Решение: fencing tokens + защита на storage.
⚠️ TTL слишком длинный → liveness. Упавший client держит lock до истечения. 60+ секунд = 60+ секунд простоя задачи.
⚠️ Redlock не safe для correctness. Используй для best-effort. Критичные операции — etcd/ZooKeeper + fencing.
⚠️ Clock skew ломает Redlock. Если NTP сделал step (jump), TTL рассчитывается неверно. Подключай chrony, не systemd-timesyncd.
⚠️ etcd lease не renewed → orphan workload. Если client сделал GC pause на 30s, lease истёк, кто-то другой стал лидером. После pause client думает что он лидер. Решение: проверять status lease периодически.
⚠️ PostgreSQL advisory lock и pgbouncer transaction mode. В transaction mode pgbouncer переключает connections между транзакциями. Advisory lock session-level потеряется. Используй xact-level или session pooling.
⚠️ SELECT FOR UPDATE без SKIP LOCKED — блокировка всего worker pool. Все workers ждут на одной строке. Используй SKIP LOCKED + LIMIT.
⚠️ Leader election в k8s: split brain при network partition. Если etcd partition, обе половины могут думать, что они лидеры. Lease с monotonic clock не решает (см. fencing).
⚠️ OnStoppedLeading не означает остановку работы. Goroutines могут продолжать. Лучше — exit процесса (через os.Exit или возврат main).
⚠️ TryLock vs Lock. TryLock — быстро вернуть false если занят. Lock — ждать. Не путай: блокирующий Lock в HTTP handler — DoS-уязвимость.
⚠️ Lock granularity. Lock на “users” слишком крупный (все операции сериализуются). Lock на “user:42” — нормально. Lock на “user:42:profile” — слишком мелкий (overhead).
⚠️ Async lock в Go. В Go нельзя сделать “lock из горутины, unlock из другой” безопасно — используй channels или explicit ownership. RWMutex.RLock работает только если все читают.
⚠️ Lock внутри транзакции БД. Если БД lock внутри транзакции, длинная транзакция = длинный lock. Старайся брать lock после быстрых BEGIN/COMMIT.
⚠️ Distributed lock — это не synchronization. Если нужно дождаться события — используй pub/sub, condition variables, channel. Distributed lock — для mutual exclusion CS.
⚠️ Watch storms. В ZooKeeper если все clients watchаt одну node, при изменении — все срабатывают. Используй sequential nodes (каждый watch’ит предыдущего) — линейная стоимость.
4. Real cases
Заголовок раздела «4. Real cases»Kafka Controller election (ZooKeeper до 2.8, KRaft после). Один broker — controller, отвечает за partition assignment, leader election. Старый pattern через ZooKeeper sequential nodes. После Kafka 2.8 — встроенный Raft (KRaft) убрал зависимость от ZooKeeper.
Kubernetes kube-controller-manager / kube-scheduler. Обычно деплоятся в HA (3 реплики), но активен только один — лидер. Использует Lease объект через client-go leaderelection. Failover за 15-30 секунд.
Elasticsearch master election. До 7.x — Zen Discovery (плох при split-brain). После 7.x — Raft-based coordination layer. Проблема “split-brain ES” в 5.x — known issue.
Cassandra coordinator. Любой node может стать coordinator для запроса (нет постоянного лидера). Но репликация Gossip + read repair. Locks через Lightweight Transactions (Paxos) — дорого, используется редко (например, для unique constraint).
Stripe distributed locks. Внутренний lock service на базе MySQL с advisory-like locks. Stripe ушли от Redis Redlock после анализа Kleppmann.
Cloudflare DurableObjects. Single-writer гарантия per-object. Реализована через consistency layer (Workers KV). Pattern: для конкретного key операции сериализуются на одном instance.
Uber Cherami / Workflow scheduling. Использовали Cassandra для leader election в очередях. Перешли на Temporal (использует Cadence-style sharding).
Yandex Object Storage / S3 multi-part upload. Multipart upload — атомарная финализация через S3 API. Locks не используются, идемпотентность через unique upload-id.
GitLab Runner — concurrent jobs lock. GitLab Runner использует advisory locks в PostgreSQL для distributed CI runners. Один runner берёт one job at a time.
Prometheus Alertmanager — clustering. Alertmanager 0.15+ использует gossip-based clustering (HashiCorp memberlist). Дедупликация alerts через gossip, не через lock.
Cron в k8s vs distributed cron. K8s CronJob — на каждой ноде может создаться pod. Для критичных задач — нужна leader election: каждый pod проверяет, является ли он лидером, лидер выполняет. Альтернатива: Hangfire (.NET), Quartz (Java), gocron + Redis lock.
Yandex.Tank load tester. Использует Redis для координации distributed load test (несколько генераторов с одного контрольного pane).
Vitess (YouTube) — MySQL sharding. Vitess использует topology service (etcd/ZooKeeper) для leader election на shard master. Failover — automatic.
CockroachDB — distributed locks через Raft. В CockroachDB нет explicit locks как в Redis. Все операции — через Raft consensus per range. Это инкапсулирует locking автоматически и safer.
MongoDB replica set election. MongoDB primary election — Raft-based (с 4.0+). Старый протокол — был slower и менее robust. Failover за 10-30 секунд.
Patroni для PostgreSQL HA. Patroni — HA tool для Postgres, использует etcd/Consul/ZooKeeper для leader election. Один node = primary, остальные = replicas. Auto failover при крахе primary.
Atlassian Jira distributed cron. Jira Datacenter использует database-backed locks (cluster_lock table) для координации scheduled jobs между nodes. Простой, но работает.
Apache Spark — driver vs executors. Driver — координатор (по сути leader). Если упадёт — job фейлится. Spark Standalone не делает leader election driver, но Kubernetes mode позволяет HA через k8s API.
Pgbouncer + advisory locks.
В pgbouncer transaction mode session-level advisory locks теряются между запросами. Это reported issue. Workaround: transaction-level locks (pg_advisory_xact_lock), или session mode.
Twemproxy / Codis для Redis. Twemproxy — proxy для Redis sharding, без built-in HA. Codis — с topology в ZooKeeper и rebalancing. Lock на одну shard работает локально.
Дополнительные patterns
Заголовок раздела «Дополнительные patterns»Read-write locks.
Многие readers OR один writer. Через etcd: writer acquires, readers waitiren. PostgreSQL: SELECT FOR SHARE (reader) vs SELECT FOR UPDATE (writer).
Reentrant locks.
В Go нет рекурсивного sync.Mutex — это deadlock. В Java/C# есть. Для distributed — обычно не нужно. Если нужно — track owner_id и count.
Lock-free алгоритмы distributed. Lock-free структуры (CRDTs, versioned data) не требуют locks. Подходят для high-contention scenarios. Trade-off: сложнее, eventual consistency.
Lease renewal patterns.
- Periodic renewal: heartbeat каждые TTL/3. Простой, но падение между renewals — потеря lock.
- Adaptive renewal: ускорять renewals при approach to expiry.
- Pre-emptive renewal: renew сразу после critical operations.
5. Вопросы
Заголовок раздела «5. Вопросы»Q1: Зачем нужен distributed lock? A: Mutual exclusion между процессами на разных машинах. Use cases: leader election, scheduled jobs, single-writer, workflow orchestration.
Q2: Почему sync.Mutex не подходит?
A: sync.Mutex защищает память одного процесса. Между процессами/нодами нужен external coordinator (Redis, etcd, БД).
Q3: Какие свойства должен иметь distributed lock? A: Mutual exclusion (safety), deadlock-free (liveness A), fault tolerance (liveness B). Trade-off между safety и liveness через TTL.
Q4: Зачем TTL у lock? A: Защита от deadlock при крахе owner. Без TTL упавший процесс заблокирует ресурс навсегда. TTL — компромисс: liveness ценой возможной потери mutual exclusion.
Q5: Почему важен уникальный value у Redis lock? A: При release мы проверяем, что value совпадает — это значит мы owner. Без проверки можем удалить чужой lock (наш TTL истёк, другой клиент взял, мы DEL — удаляем его).
Q6: Как атомарно проверить и удалить lock в Redis? A: Lua script: GET key, сравнить с ARGV[1], если совпадает — DEL. Скрипт исполняется атомарно.
Q7: Что такое Redlock? A: Алгоритм Antirez для распределённого lock на N независимых Redis. Acquire на N/2+1 → lock получен. Tolerant к падению меньшинства.
Q8: Почему Kleppmann критикует Redlock? A: Redlock не safe из-за clock dependence, GC pauses, network delays. Stale leader может продолжать write после потери lock. Решение — fencing tokens.
Q9: Что такое fencing token? A: Монотонно возрастающее число, выдаваемое lock service при acquire. Storage отказывает на write с token < last_seen. Защита от stale leaders.
Q10: Как etcd обеспечивает safer locking? A: Linearizable (Raft). Session с lease auto-renewed. Mutex использует ephemeral keys + watches. Revision = fencing token.
Q11: Как работает ZooKeeper sequential nodes lock? A: Каждый client создаёт ephemeral sequential node. Owner = с наименьшим номером. Остальные watch’ат предыдущего. FIFO порядок.
Q12: Что такое PostgreSQL advisory lock? A: Встроенные locks, не относящиеся к таблицам. Идентифицируются числовым ключом. Session-level или transaction-level (auto-release на COMMIT).
Q13: Как использовать SELECT FOR UPDATE для worker queue?
A: SELECT * FROM jobs WHERE status='pending' FOR UPDATE SKIP LOCKED LIMIT 1. Каждый worker берёт свою задачу, другие пропускают залоченные.
Q14: В чём разница между leader election и distributed lock? A: Leader election — long-lived ownership с автоматическим renewal. Distributed lock — обычно short-lived для CS. По сути lock — это special case leader election на коротком интервале.
Q15: Как Kubernetes делает leader election? A: Через Lease объект в API server. Holder обновляет renewTime каждые ~5s. Если не обновляется > leaseDuration — другой может взять.
Q16: Что делать при потере лидерства? A: Остановить всю работу, не пытаться продолжать. Обычно — exit процесса для перезапуска. Goroutines могут не успеть остановиться вовремя.
Q17: Чем отличаются TryLock и Lock? A: TryLock — non-blocking, возвращает false если занят. Lock — блокирующий, ждёт. TryLock — для optimistic паттернов, Lock — когда нужно дождаться.
Q18: Что такое split brain в context лидер-выборов? A: При network partition обе половины думают, что они лидеры → одновременные writes → corruption. Защита: quorum-based election (Raft, Paxos) — лидер только если majority.
Q19: Можно ли использовать Redis Cluster для Redlock? A: Нет, Redlock требует независимые Redis (не реплицирующиеся между собой). Redis Cluster — это шардинг + репликация, не подходит.
Q20: Зачем jitter при retry acquire lock? A: Чтобы избежать thundering herd: при освобождении lock все waiters одновременно атакуют. Jitter (random delay) распределяет атаки во времени.
Q21: Как защититься от GC pause при locking? A: 1) Fencing tokens на storage. 2) Watchdog который kill процесс если он не отвечает дольше TTL/2. 3) Использовать языки без GC pauses для критичного кода.
Q22: Зачем OnStoppedLeading в k8s leader election?
A: Callback при потере лидерства. Нужно остановить все long-running tasks, закрыть connections. Часто проще os.Exit чем graceful shutdown.
Q23: Lock granularity — крупный или мелкий? A: По бизнес-сущности. “users:42” вместо “users”. Слишком мелкий — overhead на acquire. Слишком крупный — низкий concurrency.
Q24: Можно ли использовать Kafka для distributed lock? A: Не для real-time locks (latency высокий). Для leader election через consumer group — да: один consumer assignment получает партицию.
Q25: Какой lock использовать для shop inventory (одна единица товара)? A: Strong consistency + fencing. etcd / ZooKeeper. Или PostgreSQL row-level lock с serializable isolation. Redis Redlock — не подходит для денежно-критичных операций.
Q26: Reentrant lock в Go — как сделать?
A: sync.Mutex не reentrant, рекурсивный Lock — deadlock. В distributed lock track owner_id и count. Но обычно архитектурно лучше избежать reentrant locks.
Q27: Что такое read-write lock в distributed?
A: Множество readers OR один writer. В etcd / ZooKeeper — через две очереди. В Postgres — SELECT FOR SHARE vs SELECT FOR UPDATE.
Q28: Зачем нужен heartbeat при долгих locks? A: Renewal lease. TTL ограничен (10-60s). Если работа длится дольше — нужно renew, иначе lock истекёт. Renewal каждые TTL/3.
Q29: Что произойдёт при крахе heartbeat goroutine? A: Lock не renewable → TTL истечёт → другой может взять. Реализация: heartbeat goroutine с panic recovery, защита процесса (supervisor).
Q30: Можно ли использовать gRPC streaming для locks? A: Можно (Coordination сервис). Stream open = lock держится. Stream close = release. Используется в HashiCorp Consul, некоторых internal Google systems.
6. Practice
Заголовок раздела «6. Practice»-
Реализуй Redis lock с автоматическим renewal. Heartbeat каждые TTL/3, Lua script для acquire/release.
-
Сравни latency lock acquire: Redis SETNX vs etcd Mutex vs PostgreSQL pg_advisory_lock. Benchmark под нагрузкой 1000 concurrent acquire/release.
-
Реализуй fencing tokens. Lock service + fake storage. Симулируй GC pause: после lock сделай sleep > TTL, попробуй write. Storage должен отказать.
-
Leader election на etcd. Запусти 3 worker pods. Покажи, что только один обрабатывает задачи. Убей лидера — failover.
-
PostgreSQL job queue.
SELECT FOR UPDATE SKIP LOCKED LIMIT 1. Несколько workers конкурентно. Tracking lag, throughput. -
Redlock vs single-instance Redis. Симулируй падение 1 из 3 Redis. Redlock должен продолжать работать.
-
Bench Kubernetes leader election. Запусти 5 реплик. Замерь время failover при kill лидера.
-
Cron на 3 нодах с distributed lock. Имитируй сценарий: только один из 3 cron-pods выполняет job. Используй Redis lock или PostgreSQL advisory.
-
Heartbeat с graceful shutdown. Lock с TTL 30s, heartbeat каждые 10s. При SIGTERM — explicit release lock, не дожидаться TTL.
-
Read-write lock в etcd. Реализуй: множество readers OR один writer. Метрики: max wait readers, writer throughput.
-
Lock granularity test. Сравни throughput при locks разной гранулярности: global, per-table, per-row. На 100 concurrent workers.
-
Fairness test. Запусти 10 clients, каждый много раз acquire/release одного lock. Проверь: ZooKeeper sequential (FIFO) vs Redis (no guarantee).
-
Симулируй split-brain. 5 etcd nodes, partition 2+3. Покажи: minority cannot elect leader, majority — может.
-
Lock TTL tuning. Замерь impact различных TTL (5s, 30s, 60s, 300s) на recovery time vs liveness risk при crashes.
7. Источники
Заголовок раздела «7. Источники»- Martin Kleppmann. “How to do distributed locking.” 2016. https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html
- Salvatore Sanfilippo (Antirez). “Distributed locks with Redis.” Redis docs. https://redis.io/docs/manual/patterns/distributed-locks/
- etcd documentation. “Distributed Locks.” https://etcd.io/docs/v3.5/learning/api/
- Kubernetes client-go leaderelection package. https://pkg.go.dev/k8s.io/client-go/tools/leaderelection
- Apache ZooKeeper Recipes. “Locks, Leader Election.” https://zookeeper.apache.org/doc/current/recipes.html
- PostgreSQL Documentation. “Explicit Locking — Advisory Locks.” https://www.postgresql.org/docs/current/explicit-locking.html
- Jepsen analyses on Redis, etcd, Consul. https://jepsen.io/analyses
- Antirez vs Kleppmann debate (2016). http://antirez.com/news/101
- Heidi Howard. “ARC: Analysis of Raft Consensus.” University of Cambridge thesis.
- Diego Ongaro, John Ousterhout. “In Search of an Understandable Consensus Algorithm (Raft).” USENIX ATC 2014.