Failover и отказоустойчивость
Failover — перенос трафика с мёртвого компонента на здоровый. Отказоустойчивость — пережить отказ вовсе без видимого перерыва. Ошибись в обнаружении, split-brain и взаимодействии ретраев с таймаутами — и failover вызовет сбой, что должен был предотвратить.
У кластера БД был готовый резерв и настроен автоматический failover — устойчивость по учебнику. Потом сеть между primary и резервом икнула на восемь секунд. Резерв не смог достучаться до primary, объявил его мёртвым и продвинул себя. Но primary не был мёртв; он был жив и всё ещё принимал записи. Эти восемь секунд у системы было два primary, оба принимали записи, расходились. Когда сеть зажила, две истории записей конфликтовали, и инженер провёл ночь, вручную сверяя заказы. Резервирование сработало. Именно failover вызвал сбой — потому что единственное, что он не мог различить, — это разницу между «primary мёртв» и «я не могу достучаться до primary».
Два слова, которые не одно и то же
Люди говорят «отказоустойчивый», когда имеют в виду «есть failover», и зазор между ними — там, где ломаются ожидания.
- Failover — это реактивная последовательность: компонент умирает, что-то это замечает, и трафик переносится на здоровую замену. Есть окно — обнаружение плюс переключение — в течение которого запросы отказывают или висят. Failover даёт тебе высокую доступность: система восстанавливается быстро, но не мгновенно. Продвижение БД в active-passive — это failover.
- Отказоустойчивость (fault tolerance) — это маскировка отказа, так что видимого перерыва нет вообще. Работу сбойного компонента впитывают другие с нулевым разрывом, ведь резервирование уже было живым и обслуживало. Active-active за балансировщиком отказоустойчив к потере одного узла — выжившие уже принимали трафик, так что потеря одного невидима пользователям.
Практическое различие: у отказоустойчивой системы нет окна восстановления для отказов, которые она терпит; у высокодоступной системы с failover оно короткое. Настоящая отказоустойчивость дороже (резервную ёмкость гоняешь и оплачиваешь вживую, непрерывно), так что большинство систем высокодоступны через failover для большинства компонентов и отказоустойчивы лишь там, где разрыв недопустим.
Механика: обнаружить → продвинуть → перенаправить → откатиться
Failover — это конвейер из четырёх шагов, и у каждого свой режим отказа:
- Обнаружить. Что-то должно заметить, что компонент пропал. Два частых механизма: health checks (наблюдатель периодически зондирует «ты в порядке?» и ждёт здорового ответа) и heartbeats (компонент периодически объявляет «я жив», и тишина значит смерть). Обнаружение — это трейдофф: зондируй слишком редко — медленно реагируешь; зондируй слишком агрессивно — мгновенный сбой выглядит как смерть, и ты переключаешься зря.
- Продвинуть. Резерв возвышают, чтобы взять на себя — реплика становится новым primary, пассивный узел — активным. Для stateful-систем это опасный шаг: ты должен быть уверен, что старый реально пропал.
- Перенаправить. Трафик направляют на новую цель — обнови DNS, переконфигурируй балансировщик, переключи виртуальный IP. Шаг ограничен тем, как быстро распространится перенаправление (TTL DNS, дренаж соединений).
- Откатиться (fail back). Когда оригинал восстанавливается, ты решаешь, возвращаться ли к нему (fail back) или оставить продвинутый узел новым primary. Откат сам по себе — управляемый failover и частое место, где вызывают второй сбой, делая его небрежно под нагрузкой.
Все четыре шага вместе означают: каждая деталь инфраструктуры — TTL DNS, интервал health-check, время прогрева резерва — должна быть спроектирована до инцидента, а не придумываться в его ходе. Пропустишь шаг fencing — рискуешь split-brain; пропустишь шаг перенаправления — трафик не найдёт нового primary.
Split-brain: отказ, который вызывает failover
У сбоя из вступления есть имя: split-brain (расщепление мозга). Он случается, когда failover продвигает новый primary, пока старый ещё жив — обычно потому что сетевое разделение лишило их возможности видеть друг друга, и каждая сторона заключила, что другая мертва. Теперь два узла оба верят, что они primary, оба принимают записи, и данные расходятся. Когда разделение залечивается, у тебя две конфликтующие истории и нет автоматического способа их слить. Split-brain уникально мерзок, потому что не теряет доступность — обе половины подняты и обслуживают — он теряет корректность, что часто хуже и труднее обнаружить.
Корень — фундаментальная неоднозначность, которую обнаружение никогда не разрешит полностью: с точки зрения резерва «primary упал» и «я потерял сеть до primary» выглядят одинаково — тишина в любом случае. Все защиты сводятся к тому, чтобы максимум один узел мог победить:
- Кворум / консенсус. Требуй согласия большинства узлов перед продвижением. Разделение, оставившее старый primary в меньшинстве, значит, что он не может продолжать быть primary, и продвигает только сторона большинства. Поэтому системам консенсуса (Raft, ZooKeeper, etcd) нужно нечётное число узлов — большинство должно быть однозначным.
- Fencing (STONITH — «выстрели другому узлу в голову»). Перед продвижением принудительно отрежь старый primary — отзови его аренду хранилища, выключи питание, заблокируй в сети — чтобы, даже будь он жив, он не мог продолжать писать. Ты не просишь его уйти; ты делаешь уход физически принудительным.
▸Почему это работает
Почему нельзя просто сделать обнаружение умнее — длиннее таймауты, больше зондов — и избежать всей проблемы? Потому что никакой таймаут не разрешает неоднозначность; он лишь меняет один плохой исход на другой. Короткий таймаут обнаружения переключается быстро, но принимает кратковременный сбой за смерть и рискует split-brain (и лишней суетой). Длинный таймаут избегает ложных срабатываний, но значит, что реальная смерть оставит тебя лежащим весь таймаут. Нет настройки, что и быстра, и безопасна, потому что «нет ответа» по-настоящему не отличить от «медленный ответ» или «мёртв» ожиданием. Поэтому надёжная починка — не лучшее обнаружение, а механизм, что делает неверное обнаружение безвредным: кворум, чтобы меньшинство не могло продвигаться, или fencing, чтобы ошибочно объявленный мёртвым узел не мог продолжать писать. Проектируй так, чтобы обнаружение ошибалось, а не было идеальным.
Graceful degradation: отказать частично вместо целиком
Отказоустойчивость не всегда всё-или-ничего. Graceful degradation (плавная деградация) — это проектирование системы так, чтобы под отказом сбрасывать неважную функциональность вместо полного потемнения. Если сервис рекомендаций лежит — покажи общий список популярного вместо 500. Если кэш персонализации недостижим — отдай страницу без персонализации. Если записи отказывают — оставайся поднятым в режиме только-чтение. Пользователь получает деградировавший, но работающий опыт, и отказ сдержан в сломавшейся фиче. Это практическое дополнение к резервированию: резервирование держит критический путь поднятым, graceful degradation делает некритические пути отказывающими мягко, так что смерть одной зависимости — это пропавшая фича, а не пустая страница.
Взаимодействие ретраи/таймаут/failover — где failover превращается в сбой
Это ловушка senior-уровня, и поэтому AWS посвящает ей целую статью Builders’ Library. Failover, таймауты и ретраи взаимодействуют, и при неверной настройке усиливают малый отказ в каскадный:
- Таймауты надо ставить осознанно. Слишком длинные — и одна медленная зависимость держит потоки в заложниках, пока весь пул не исчерпан, и ты превратил один медленный бэкенд в полный сбой. Слишком короткие — и ты бросаешь запросы, что прошли бы, производя отказы.
- Ретраи помогают с кратковременными сбоями, но опасны при перегрузке. Когда зависимость борется, наивные ретраи умножают нагрузку на неё ровно тогда, когда она меньше всего может это вынести — петля обратной связи, что AWS зовёт retry storm (шторм ретраев), что гонит просадку в сбой. Защиты: бюджет ретраев (ограничь ретраи как долю трафика, а не на запрос), экспоненциальный backoff (жди дольше между попытками) и jitter (рандомизируй backoff, чтобы все клиенты не ретраили в унисон, создавая синхронные всплески). И ретраи уместны только на идемпотентных операциях — ретрай неидемпотентной записи может дважды списать с клиента.
- Во время failover всё это складывается: в момент смерти primary каждый запрос в полёте таймаутится разом, каждый клиент ретраит разом, и этот синхронный всплеск приземляется на резерв, что ещё разогревается — так что само событие failover может опрокинуть то, что ты только что продвинул. Рецепт AWS (ограниченные таймауты, ограниченные ретраи, экспоненциальный backoff с jitter, circuit breakers) существует именно ради того, чтобы восстановление не стало следующим инцидентом.
▸Ещё практика
Два числа, что суммируют всё это, — MTBF и MTTR. MTBF (среднее время между отказами) — как часто вещи ломаются; MTTR (среднее время восстановления) — как долго ты лежишь, когда ломаются. Доступность ≈ MTBF / (MTBF + MTTR), так что поднять доступность можно двумя путями: отказывать реже (поднять MTBF) или восстанавливаться быстрее (снизить MTTR). Senior-инсайт в том, что снижение MTTR обычно лучшая инвестиция. Отказы неизбежны, и погоня за высоким MTBF упирается в убывающую отдачу, но быстрое, автоматическое, отрепетированное восстановление — хорошее обнаружение, протестированный failover, fencing, graceful degradation, runbook’и — сжимает каждый инцидент. Система, что отказывает вдвое чаще, но восстанавливается в десять раз быстрее, гораздо доступнее. Поэтому зрелые команды одержимы MTTR: отрепетированное восстановление, а не фантазия о том, чтобы никогда не падать.
Сетевое разделение изолирует primary от его резерва. Резерв не может достучаться до primary, объявляет его мёртвым и продвигает себя — но primary жив и всё ещё принимает записи. Что это и что это предотвращает?
Нижестоящий сервис замедляется. У клиентов длинный таймаут, и они ретраят до 3× без backoff. Каков вероятный исход и верная починка?
Доступность ≈ MTBF / (MTBF + MTTR), так что можно отказывать реже или восстанавливаться быстрее. Зрелые команды вкладываются больше всего в снижение _______ — среднего времени восстановления — потому что отказы неизбежны, но быстрое, отрепетированное восстановление сжимает каждый инцидент.
- 01Различи failover и отказоустойчивость и назови шаги failover.
- 02Что такое split-brain, почему лучшее обнаружение его не предотвратит и что предотвращает?
- 03Как взаимодействуют ретраи, таймауты и failover и как не дать восстановлению стать следующим сбоем?
Failover и отказоустойчивость не синонимы: failover — реактивная последовательность обнаружить → fencing → продвинуть → перенаправить → откат, что даёт высокую доступность с коротким окном восстановления, тогда как отказоустойчивость маскирует отказ с видимым перерывом ноль (живая резервная ёмкость, вроде active-active) дороже. Трудное — обнаружение, ведь «узел упал» и «я не могу достучаться до узла» неразличимы ожиданием — и failover, что продвигает новый primary, пока старый жив, вызывает split-brain: два primary принимают записи и расходятся без автоматического слияния. Это не чинится умнее таймаутами (они лишь меняют ложные срабатывания на медленное восстановление); чинится тем, что неверный вызов делают безвредным — кворум, чтобы продвигалось лишь большинство, или fencing/STONITH, чтобы старый primary не мог продолжать писать. Graceful degradation сжимает отказы дальше, сбрасывая неважные фичи (список популярного, режим только-чтение) вместо потемнения. Senior-ловушка — взаимодействие ретраи/таймаут/failover: длинные таймауты исчерпывают пулы, наивные ретраи при перегрузке становятся retry storm, а на failover каждый клиент таймаутится и ретраит в унисон на разогревающийся резерв — так что ограничивай таймауты и используй ограниченные ретраи с экспоненциальным backoff и jitter (только на идемпотентных операциях), чтобы восстановление не было следующим инцидентом. Наконец, доступность ≈ MTBF/(MTBF+MTTR): раз отказы неизбежны, вложение в быстрое, автоматическое, отрепетированное восстановление (ниже MTTR) обычно бьёт погоню за всё более высоким MTBF — ровно дисциплина, которой не хватало команде из вступления, когда их failover, а не отказ, вызвал сбой. Теперь, когда увидишь failover в постмортеме, проверяй, на каком шаге произошёл сбой: обнаружение дало ложный сигнал, отсутствующий fencing вызвал split-brain или ретраи превратили икание сети в шторм?
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.
Примени это
Примени этот урок в реальном проекте.