Суть Читай реальные сниппеты лока, lease и fencing-токена плюс лог split-brain, предскажи поведение и выбери фикс, который сеньор сделает первым.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 14 min
Баги выбора лидера прячутся в зазоре между «я держу лок» и «моя запись приземлилась». Читай код и лог, затем выбери фикс, который закрывает зазор, — а не тот, что лишь сужает его.
Цель
Отработай петлю, которую ты запускаешь в каждом инциденте координации: прочитай путь лока/lease, найди момент, где пауза или partition может вставить второго писателя, и потянись за фиксом на стороне ресурса раньше, чем за подкруткой таймаута.
Сниппет 1 — захват, затем запись
func runJob(lock *LockService, store *ObjectStore) error { if err := lock.Acquire("job-leader", 10*time.Second); err != nil { return err // лидирует кто-то другой } defer lock.Release("job-leader") // ... минуты работы, включая возможную долгую GC-паузу ... return store.Put("result.csv", data) // запись в общее хранилище}
Викторина
Completed
Лок корректен и эксклюзивен. Почему это всё равно может породить двух одновременных писателей в result.csv?
Heads-up defer выполняется при возврате из функции, после завершения Put — порядок верный. Проблема в паузе ПЕРЕД Put, пока лок держится, но lease истёк под ним.
Heads-up Acquire эксклюзивен по построению — его держит лишь один вызывающий. Баг не в конкуренции; он в приостановленном держателе, проснувшемся после истечения lease.
Heads-up Любой конечный TTL может быть превышен достаточно долгой паузой. Поднятие снижает частоту ложных истечений, но никогда не убирает возможность устаревшей записи.
Токен теперь проброшен в store.Put. При каком условии это реально останавливает устаревшую запись — и что обязан делать store.Put?
Heads-up Передача токена не делает ничего, если store.Put не сравнивает его с наибольшим виденным и не отвергает меньшие. Хранилище, которое просто логирует токен, всё равно принимает устаревшую запись.
Heads-up Токены сравниваются как монотонные целые, а не времена. Отметки времени возвращают проблему доверия часам, ради избегания которой fencing и существует.
Heads-up Put бьёт во внешнее объектное хранилище, вне любой транзакции лок-сервиса. Навязывание должно быть проверкой токен-против-наибольшего на каждом ресурсе, а не общей транзакцией.
Сниппет 3 — петля keep-alive для lease
func keepLeadership(c *etcd.Client, leaseID etcd.LeaseID, onLost func()) { ka, _ := c.KeepAlive(ctx, leaseID) // канал подтверждений продления for { select { case resp, ok := <-ka: if !ok { // канал закрыт = продление сорвалось onLost() // мы больше не лидер return } _ = resp } }}
Викторина
Completed
onLost() срабатывает, когда продления keep-alive срываются. Почему действие по onLost необходимо, но НЕ достаточно для безопасности?
Heads-up Приостановленный процесс не исполняет горутины, поэтому onLost не может сработать во время паузы. К моменту, когда он выполнится, устаревшая запись может уже быть на лету. Самоуведомление не может быть механизмом безопасности.
Heads-up Подкрутка интервала меняет частоту продлений, а не факт, что остановленный процесс ничего не наблюдает. Зазор структурный, а не значение тюнинга.
Heads-up Закрытый канал надёжно сигнализирует о срыве продления; ложные подтверждения не проблема. Проблема в том, что колбэк зависит от того, что подозреваемый узел остаётся живым, чтобы сработать.
Читая этот лог, какое утверждение — верное прочтение сеньора?
Heads-up Lease отработал как задумано: он истёк и запустил перевыбор. Lease не может остановить приостановленный процесс от пробуждения и записи — именно поэтому нужна проверка токена, которая это и поймала.
Heads-up Инкремент term при каждом выборе — корректное поведение Raft, а не баг. Скачок term — это то, как новое лидерство делается однозначным.
Heads-up Ожидание подтверждения от приостановленного узла — это как навсегда потерять доступность: он может никогда не ответить. Выбор на кворуме намеренно не ждёт подозреваемого-мёртвым узла.
Итог
Каждый баг выбора лидера читается одинаково: лок или lease гарантирует исключение лишь пока держатель исполняется, поэтому пауза между «захватом» и «записью» позволяет смещённому лидеру проснуться и записать устаревшие данные. Проброс монотонного fencing-токена в запись необходим, но именно РЕСУРС, отвергающий любой токен ниже своего наибольшего, реально навязывает безопасность. Колбэки keep-alive и более короткие TTL не помогут процессу, который не исполняется. Диагностируй по логу, найди зазор паузы-или-partition и чини его там, где приземляется запись, — а не там, где выдан лок.