Выбор лидера
Некоторым задачам нужен ровно один координатор. Выбор лидера его назначает — через консенсус (Raft/ZAB) или lease/lock в ZooKeeper, etcd или Redis. Ловушка — split-brain: приостановленный лидер, думающий, что он главный. Починка — fencing token, а не длинный таймаут.
Биллинговая система гоняла ночную задачу для списания за подписки. Чтобы пережить крах, команда запускала планировщик на трёх машинах и использовала распределённый лок, чтобы задачу запускал лишь держатель. Однажды ночью у держателя лока JVM словил 25-секундную паузу сборки мусора сразу после захвата лока. Lease лока истёк, вторая машина схватила его и начала списывать с клиентов — а затем первая машина проснулась, всё ещё веря, что держит лок, и списала со всех снова. Два координатора, одна задача, двойные списания. Баг был не в локе; он был в допущении, что «я захватил лок» остаётся правдой вечно. Выбрать одного координатора легко. Гарантировать, что их никогда не больше одного — даже когда узел замерзает — это сложная часть.
Почему иногда нужен ровно один
Бо́льшая часть системного дизайна толкает к statelessness и «любой узел делает что угодно» — это масштабируется и терпит отказы. Но некоторые задачи по сути единичны: их должен делать ровно один узел за раз, или они ломаются. Планировщик, что должен запускать каждую cron-задачу один раз (не трижды); единственный писатель/primary в реплицированной базе; один узел, назначающий партиции или диапазоны sequence; координатор, компактящий общий лог. Запусти два таких сразу — и получишь дублирующие сайд-эффекты, конфликтующие записи или повреждённое состояние.
Выбор лидера — механизм, назначающий один узел — лидера — делать единичную работу, пока остальные в готовности перехватить, если он умрёт. Два сложных требования — уникальность (никогда не два лидера одновременно) и живость (liveness) (когда лидер умирает, новый избирается быстро). Уникальность — то, что недооценивают, и то, что вызывает худшие сбои.
Два способа избрать: консенсус против lease
Прежде чем выбирать сервис координации, задай себе один вопрос: дубликат операции — лишь расточителен, или катастрофичен? Именно этот вопрос определяет, сколько механизмов корректности тебе реально нужно — и именно его инженеры систематически пропускают, отсюда и двойные списания.
Есть два семейства механизма, и большинство реальных систем используют второй, построенный поверх первого.
Протоколы консенсуса (Raft, ZAB, Paxos). Кластер узлов гоняет протокол, где большинство (кворум) должно согласиться, кто лидер. В Raft узлы голосуют в нумерованных термах; кандидат, выигравший голоса большинства, становится лидером на этот терм, и поскольку требуется большинство, два лидера не могут быть избраны в один терм — любые два большинства пересекаются хотя бы в одном узле, который не проголосует дважды. Лидер шлёт heartbeat’ы; если фолловеры перестают их слышать, они начинают новый терм и избирают заново. Так избирают etcd, Consul и контроллер Kafka (KRaft), и так реплицированный автомат состояний остаётся консистентным.
Lease или lock в сервисе координации. Вместо запуска консенсуса самому ты опираешься на сервис, который уже это делает — ZooKeeper, etcd или lock в Redis — и трактуешь «держу lock/lease» как «являюсь лидером». Узел захватывает ограниченный по времени lease; пока держит lease, он лидер; он должен продлевать до истечения, иначе теряет лидерство, и другой узел может его захватить. Это проще строить (тяжёлый консенсус живёт в проверенном сервисе) и это частый паттерн для «мне просто нужен один воркер для этой задачи».
Split-brain: отказ, определяющий проблему
Причина, почему выбор лидера сложен, — split-brain: ситуация, где два узла оба верят, что они лидер, в один момент. Это происходит не потому, что код выборов багован, а из-за зазора между быть лидером и знать, что ты всё ещё им:
- Сетевое разделение (partition) изолирует лидера от кластера. Остальные не слышат его heartbeat’ов, объявляют его мёртвым и избирают нового лидера — но старый лидер на своей стороне разделения всё ещё думает, что он главный.
- Пауза GC, приостановка VM или долгий затык замораживает лидера за истечение его lease. Кластер избирает нового лидера. Замороженный узел просыпается всё ещё с истёкшим убеждением и действует как лидер — ровно вступление с биллингом.
Наивная «починка» — сделать lease длиннее — лишь расширяет окно, в котором реальный отказ не обрабатывается, меняя одну проблему на другую. Длинный таймаут делает split-brain реже, но failover медленнее; короткий — failover быстрым, но split-brain вероятнее. Настройка таймаута не может устранить split-brain, потому что никакой таймаут не отличит «мёртв» от «заморожен и вот-вот проснётся». Нужен механизм, делающий действия устаревшего лидера безвредными, даже если он действует.
▸Почему это работает
Почему кворум большинства предотвращает двух лидеров в консенсусе, а один lease — нет? В Raft, чтобы стать лидером, нужны голоса строгого большинства узлов, и каждый узел голосует не более раза за терм. Любые два большинства одного множества должны делить хотя бы один узел — и этот общий узел не проголосует за двух разных кандидатов в один терм — так что не более одного лидера может выиграть за терм. Математика запрещает двух лидеров в один терм. У голого lease нет такого аргумента пересечения по всей системе: сервис координации выдаёт один lease, но лидер, что приостановился за истечение и затем возобновился, всё ещё думает, что держит его, и если он напрямую говорит с ресурсом (базой, файлом) без перепроверки, ничто его не остановит. Поэтому lease нужен fencing token: он переносит свойство «монотонно, нельзя назад» термов консенсуса вниз к защищаемому ресурсу.
Fencing token: настоящая починка
Корректная защита от split-brain — fencing token: монотонно растущее число, которое сервис координации выдаёт с каждой выдачей лидерства. Lease #1 несёт token 33; когда он истекает и новый лидер перехватывает, та выдача несёт token 34. Ключевое правило живёт на защищаемом ресурсе: каждая операция включает свой token, и ресурс отвергает любой token ниже наибольшего, что он уже видел.
1. Лидер A захватывает lease, получает fencing token = 33
2. A шлёт «write X» с token 33 → ресурс принимает (33 ≥ 33), помнит 33
3. A замерзает (пауза GC); lease истекает
4. Лидер B захватывает lease, получает token = 34
5. B шлёт «write Y» с token 34 → ресурс принимает (34 ≥ 34), помнит 34
6. A просыпается, всё ещё думает, что лидер, шлёт «write Z» с token 33
→ ресурс ОТВЕРГАЕТ: 33 < 34. Устаревшая запись A отгорожена. ✓Теперь даже если лидер-зомби возобновляется и пытается действовать, его операции несут старый token и отклоняются — вред структурно невозможен, независимо от таймаутов. Поэтому «используй более длинный таймаут лока» — неверный ответ, а «ресурс должен применять fencing token» — верный: корректность идёт из отвержения ресурсом устаревших token’ов, а не из надежды, что старый лидер останется спящим.
▸Частая ошибка
Трактовка лока Redis как гарантии корректности без fencing. Лок на одном узле Redis SET key value NX PX <ttl> удобен и отлично подходит для эффективности (избежать, чтобы два воркера делали одну работу — дубликат расточителен, но не катастрофичен). Он не безопасен для корректности (где дубликат вызывает двойное списание или порчу данных), потому что у него нет fencing token, и его TTL может истечь под паузой GC ровно как во вступлении. Если два воркера, гоняющих задачу, лишь расточительны, голый лок в порядке. Если два воркера — катастрофа, нужны fencing token’ы, применяемые на ресурсе — и сервис локов, чьи token’ы линеаризуемы (etcd/ZooKeeper). Реши, в каком ты случае, до выбора лока, а не после инцидента.
Лидер захватывает лок, затем страдает 30-секундную паузу GC. Его lease истекает, избирается новый лидер, а старый просыпается, всё ещё веря, что он лидер. Оба теперь действуют. Как это зовётся и что на деле предотвращает урон?
В Raft почему два узла не могут быть оба избраны лидером в один терм?
Чтобы сделать split-brain безвредным, сервис координации выдаёт монотонно растущий _______ с каждой выдачей лидерства, а защищаемый ресурс отвергает любую операцию с меньшим — так записи ожившего устаревшего лидера отклоняются, какими бы ни были таймауты.
- 01Когда нужен выбор лидера и каковы его два требования?
- 02Сравни выбор на консенсусе с lease/lock и почему большинство предотвращает двух лидеров.
- 03Что такое split-brain, почему более длинный таймаут его не чинит и что чинит?
Выбор лидера назначает ровно один узел делать по сути единичную работу — однократный планировщик, единственный write-primary, назначатель партиций — где запуск двух лидеров вызывает дублирующие сайд-эффекты или порчу. Два его требования — уникальность и живость, и уникальность — опасное. Избираешь либо через протокол консенсуса (Raft/ZAB/Paxos), где кворум большинства математически запрещает двух лидеров в один терм, ведь любые два большинства делят голосующего, либо через продлеваемый lease/lock в сервисе координации (ZooKeeper, etcd, Redis), уже гоняющем консенсус за тебя. Отказ, определяющий всю проблему, — split-brain — два лидера сразу, вызванный сетевым разделением или паузой GC/затыком VM, переживающим lease, после чего замороженный узел просыпается, всё ещё действуя как лидер (двойное списание в биллинге). Критично: более длинный таймаут это не чинит — никакой таймаут не отличит «мёртв» от «заморожен». Настоящая починка — fencing token (монотонно растущий токен, выдаваемый с каждой выдачей лидерства): защищаемый ресурс применяет его, отвергая любой меньший token, так что записи ожившего устаревшего лидера структурно отклоняются. Корректность живёт на ресурсе, а не в длине lease — поэтому же голый лок Redis в порядке для эффективности, но небезопасен для корректности без fencing. Теперь, когда встретишь инцидент, где одна задача сработала дважды или однокластерный процесс поднялся на двух узлах — сразу спрашивай: ресурс проверяет fencing token, или доверяет лидеру оставаться живым?
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.