Многие уверены, что если сервис поднят в кластере — значит, он защищён от любых потрясений инфраструктуры, и, если что-то случится, Kubernetes "сам всё поднимет". Но на деле есть нюанс. Реальная устойчивость и грамотный disaster recovery появляются только на стыке платформы, клиентской логики и конфигурации сервисов. А обнаружить узкие места возможно только во время инцидентов или плановых аварийных учений. Так мы и поступили: выключили 30% нод в production кластере и посмотрели, что будет.

В статье разберём — почему именно 30%, какие сбои и узкие места всплыли в ходе учений, а также какие сделали выводы и мы, как команда Kubernetes, и прикладные команды.

Навигация по статье

Архитектура кластера

Для лучшего понимания стоит привести архитектуру кластера, чтобы было понятно, что конкретно мы проверяем.

Мы используем OKD, но на механики процессов это не влияет. Все kind-ы, сущности и т.д., рассматриваемые в данной статье, есть также в "ванильном" Kubernetes и в любом другом его дистрибутиве.

Общая архитектура кластера Kubernetes
Общая архитектура кластера Kubernetes

На схеме видно:

  • Облако с 3 зонами доступности. Каждая зона — это отдельный ЦОД, соединённый с остальными высокоскоростным каналом по полносвязной топологии.

    • Задержка между зонами доступности в среднем < 1мс.

    • Благодаря такой архитектуре облака все ноды взаимодействуют друг с другом так, как будто они расположены в одной локальной сети.

  • Балансировщик нагрузки для трафика пользовательских приложений, у которого в качестве бэкендов используются 3 Infra-ноды.

  • Балансировщик нагрузки для API кластера. У него в качестве бэкендов используются 3 Master-ноды.

  • 3 Master-ноды — на них располагаются Control Plane компоненты.

  • 3 Infra-ноды — на них располагаются инфраструктурные компоненты:

    • Pod-ы Ingress-ов (именно на них приходит пользовательский трафик).

    • Persistent Volume-ы, а также контроллер, который их обслуживает.

  • N-ое количество worker-нод, равномерно распределённых по 3-м зонам доступности.

  • Пользователи, которые взаимодействуют с консолью и приложениями в кластере. Могут быть как внешними, так и внутренними. Обращаются через балансировщик нагрузки для трафика пользовательских приложений.

Подготовка

План аварийных учений

Если кратко, то план был такой:

  1. Проверка работоспособности кластера и сервисов в нём. Очевидно, что нельзя "ломать" кластер, который уже "поломан".

  2. Создание снэпшотов master-нод и infra-нод (самый важный пункт). Если аварийные учения пойдут не по плану, и починить кластер не получается, то в дело идёт самый крайний вариант — восстановление из снэпшотов или бэкапов.

  3. Выключение всех нод, находящихся в одной любой зоне доступности. Либо отключение 1 master-ноды, 1 infra-ноды и 30% worker-нод, если в облаке таких зон доступности нет.

  4. Наблюдение, фиксирование сбоев, сбор статистики, помощь командам, у которых расположены сервисы в данном кластере. Предполагается, что данный пункт должен длиться 1-2 часа. За это время сервисы в кластере должны успеть восстановиться, алерты — вернуться в статус resolved, трафик — нормализоваться, а мы должны успеть всё зафиксировать и восстановить — что пошло не по плану. Также сервисы в кластере должны какое-то время поработать в таком режиме, чтобы мы могли убедиться, что всё работает стабильно.

  5. Возврат всех нод в строй.

  6. Проверка работоспособности кластера и сервисов в нём.

  7. Завершение аварийных учений.

  8. Фиксирование поручений по итогам.

Почему мы делаем снэпшоты, а не бэкапы etcd?

Бэкап etcd штатными средствами — это правильный механизм бэкапа etcd, который у нас делается автоматически по расписанию, но восстановление из него — это отдельные танцы с бубном чувствительная процедура со своими ограничениями, в которой легко накосячить. Для регулярных аварийных учений нам удобнее иметь именно снэпшоты виртуальных машин: так быстрее вернуть стенд в исходное состояние и продолжить работу.

Почему не делаем снэпшоты worker-нод, но делаем их для infra-нод?

Потому что worker-ноды не являются источником истины для состояния кластера. Состояние всех сущностей кластера хранится в etcd, а control plane постоянно приводит фактическое состояние к желаемому. Worker-ноды являются просто исполняющим слоем, а хранение их снэпшотов увеличивает стоимость содержания кластера без какой-либо ощутимой выгоды.

На infra-нодах у нас расположены persistent volume-ы, поэтому их, в отличие от worker-нод, мы бэкапим, так как содержимое volume-ов, очевидно, не хранится в etcd. Для persistent volume-ов у нас так же есть другой основной механизм бэкапов — встроенные бэкапы на S3, но для аварийных учений нам удобнее иметь именно снэпшоты.

Почему отключаем именно 30% нод?

TL; DR. Для нас — это разумный компромисс между реалистичностью и безопасностью эксперимента.

С одной стороны — такой объём отказа действительно проверяет, насколько сервисы и кластер устойчивы к деградации инфраструктуры: хватает ли запаса ресурсов, корректно ли работают anti-affinity, PDB, репликация и механизмы планировщика pod-ов.

С другой стороны — это всё ещё не катастрофический сценарий потери всего кластера или целого облака. Мы специально выбрали именно такой масштаб, чтобы смоделировать серьёзную, но правдоподобную аварию, после которой платформа оркестрации со всеми пользовательскими сервисами должна продолжать работать.

Также немаловажный фактор — при схеме с 3 master-нодами возможна потеря только 1 мастера (как раз 33%) для поддержания кворума.

Даже если зон доступности формально нет, похожую модель отказа можно воспроизвести через распределение по стойкам, гипервизорам или другим доменам отказа.

Что мы ожидали увидеть?

В первую очередь мы ожидали увидеть узкие места, которые в обычной эксплуатации могут долго оставаться незамеченными: просчёты в архитектуре кластера, неочевидные настройки платформы (а таких историй у нас достаточно!), а также проблемы на стороне пользовательских приложений.

При этом мы не шли в учения с ощущением, что кластер обязательно начнёт разваливаться на глазах. Наоборот, мы рассчитывали, что бОльшая часть базовых сценариев будет отрабатываться штатно. За последний год мы серьёзно вложились в стандартизацию кластеров, выравнивание конфигураций и приведение платформы к более "зрелому" и предсказуемому состоянию. Поэтому основной вопрос был не в том, "упадёт кластер или нет", а в том, какие именно слабые места проявятся при отказе 30% нод.

Кстати, для команд сопровождения сервисов, которые расположены в наших кластерах, мы создали рекомендации по настройке их сервисов, если они планируют или уже расположены в наших кластерах. Я считаю неправильным просто "бить по рукам" людей за неправильную настройку манифестов их приложений. Гораздо лучше — показать как правильно. Часть рекомендаций есть в конце статьи.

Что именно мы считали успехом учений?

  • API кластера остаётся доступным и продолжает отвечать на запросы.

  • Pod-ы продолжают корректно создаваться, изменяться и удаляться.

  • Persistent Volume-ы остаются доступными и работоспособными.

  • Прикладные сервисы сохраняют работоспособность и продолжают обрабатывать клиентские запросы.

  • Полное восстановление сервисов до исходного количества реплик происходит в течение 10-15 минут после отключения нод.

  • В течение 10-15 минут все алерты возвращаются в штатное состояние, за исключением алертов, связанных с недоступностью преднамеренно отключённых нод.

Аварийные учения №1

Первые АУ мы проводили, естественно, в development кластере (небольшой, ≈ 250 ядер суммарно на worker-нодах). Как и планировалось, мы отключили все ноды в 1-й зоне доступности:

  • 1 Master-ноду;

  • 1 Infra-ноду;

  • 1/3 Worker-нод.

Отключили треть кластера.
Отключили треть кластера.

Что мы увидели?

Балансировщики нагрузки довольно быстро (менее минуты) отключили нерабочие бэкенды.

API кластера продолжал отвечать. Значит, по меньшей мере, работает цепочка:
Load Balancer -> (как минимум одна) Master-нода -> Pod API -> etcd (живой; как минимум в режиме read-only).
В теории могли быть кратковременные лаги при обращении к API на время перевыборов лидера etcd, но у нас такого не было.

Примерно через 50 секунд (мы с секундомером не сидели, но примерно так и получилось) ноды из 1-й зоны доступности перешли в статус Unknown.

Ноды находятся в таком статусе 300 секунд пока контроллер не пометит их taint-ами node.kubernetes.io/not-ready и node.kubernetes.io/unreachable. Такой таймаут стоит в Kubernetes по умолчанию, и мы его не меняем. Он нужен, чтобы, например, при небольших "флапах" сети pod-ы не шли сразу пересоздаваться и перегружать API кластера частыми и лавинообразными пересозданиями.

Всё это время мы сидели и ждали этого таймаута — когда же произойдёт "магия", и pod-ы начнут пересоздаваться.

После примерно 10 минут ожидания мы поняли, что что-то идёт не так, и начали проверять создание pod-ов, так как треть всё ещё висела в Terminating-е, но замена для них так и не начала создаваться.
Первая мысль была, что по какой-то причине не собрался кворум, и etcd работал в режиме read-only. Но при создании pod-а через kubectl всё сразу стало понятно — вылезла ошибка mutation webhook-а:

~> kubectl apply -f pod.yaml
Error from server (InternalError): error when creating "pod.yaml": Internal error occurred: failed calling webhook "mutate.kyverno.svc-fail": failed to call webhook: Post "https://infra-kyverno-svc.infra-kyverno.svc:443/mutate/fail?timeout=30s": dial tcp 10.128.4.174:9443: connect: connection refused

В качестве инструмента управления политиками Admission/Mutation/Validation webhook-ов у нас используется Kyverno, который у нас оказался развёрнут по одной pod-е для каждого вида webhook-ов! И, конечно, 2 из 3 контроллеров оказались в 1 зоне доступности и теперь висели в статусе Terminating.

На всякий случай скажу, что ваши запросы после обращения к API кластера (на изменение) идут в etcd совсем не напрямую. На их пути стоят несколько контроллеров (см. схему ниже). И если один из этих контроллеров не работает, то изменения в etcd не произойдёт, так как запрос просто не дойдёт до него из-за отсутствия элемента в цепочке.

В нашем случае это означает, что в кластере не может произойти никакого изменения. В том числе и создания новых pod-ов.

Как работает Kyverno

Первое поручение по итогам аварийных учений уже есть :)

Осталось масштабировать количество реплик Kyverno, чтобы новые pod-ы смогли подняться, а наши аварийные учения — продолжиться.

Следует отметить, что все системные неймспейсы (в том числе неймспейс Kyverno) у нас внесены в исключения для вебхуков Kyverno, чтобы не попасть в ловушку курицы-и-яйца, и pod-ы самой Kyverno могли развернуться.

Увеличив число реплик, мы стали ждать восстановления, но тут нас ждало второе и самое главное поручение по итогам этих аварийных учений. Обычно новые реплики создаются сразу же, а тут мы ждали уже целую минуту. Первый шаг — просмотр event-ов — сразу же выявил корневую причину всех наших бед: в кластере банально закончились ресурсы.

То есть причинно-следственная связь примерно такая:
Отключили ноды -> упали pod-ы Kyverno + уменьшилось общее количество ресурсов в кластере -> контроллер попытался поднять ещё одну реплику Kyverno и получил отказ, так как свободных ресурсов в кластере нет -> все остальные пострадавшие сервисы даже не доходили до аллоцирования ресурсов, так как их запрос не доходил в etcd из-за отсутствия звена в цепи валидации/изменения запроса.

После оперативного создания новых нод и добавления их в кластер (не из отключенной зоны доступности, всё по-честному) кластер успешно восстановился.

Оказалось, что после объявления работ о проведении аварийных учений, многие инженеры подняли количество реплик своих приложений, а также немного увеличили квоты своих проектов. Для такого небольшого кластера это оказалось критичным, так как в нём был не такой большой запас ресурсов, чтобы ещё дополнительно выдержать падение 30% worker-нод.
У нас есть алерты на высокие реквесты в кластере, но у них довольно длительное время срабатывания во избежание ложных алертов, а также для повышения их ценности. Они ещё просто не успели нам прилететь.

Также в данном кластере был всего один проект-потребитель persistent volume-ов, что было для нас совсем не показательным.

Свои ошибки мы учли и пошли проводить учения на development кластере побольше.

Поручения по итогам первых аварийных учений:

  1. Провести RnD High Availability режима работы Kyverno. Внедрить во все кластеры при удачном проведении RnD.

  2. Проверить запас ресурсов во всех кластерах. При необходимости — добавить worker-нод до нужного запаса.

Аварийные учения №2

Следующие аварийные учения мы проводили в development кластере покрупнее (2000 ядер суммарно на worker-нодах).

Тут всё уже прошло относительно неплохо:

  • Балансировщики нагрузки снова отработали быстро и правильно.

  • API кластера "лагнула" буквально на пару секунд.

  • Выключенные ноды перешли в статус Unknown.

  • Контроллер пометил ноды taint-ами node.kubernetes.io/not-ready и node.kubernetes.io/unreachable.

  • После этого начали массово пересоздаваться pod-ы прикладных команд.

  • Persistent Volume-ы успешно создали новые реплики. При это сами Volume-ы были доступны всё время проведения АУ. В качестве провайдера Persistent Volume-ов мы используем Longhorn.

  • Всё это время было возможно создание/изменение/удаление pod-ов.

Релизы не остановились, консоль кластера доступна, сервисы в большинстве своём остались так же доступны (за исключением неудачных сервисов с единственной репликой, которые оказались на выключенных нодах).

Следующий этап — production кластер.

Аварийные учения №3 (production)

Как и в прошлые разы, перед началом учений мы сверили план, согласовали окно проведения АУ с прикладными командами и в назначенное время приступили к учениям.

Балансировщики, API, ноды — всё отработало чётко. Репликация Persistent Volume-ов повела себя так, как и ожидалось. "Магия" Kubernetes работала.

Но примерно через пять минут после начала АУ мы увидели 5-кратный рост входящего трафика. Причина оказалась в ретраях со стороны внешних клиентов: как только сервисы начали отвечать медленнее, клиенты по своей внутренней логике стали активнее повторять запросы. То есть в момент деградации (недостаточного количества реплик) сервис получил то, что ему было нужно меньше всего — дополнительную нагрузку.

Так как этот трафик по сути легальный — защита его не резала. В целом инфраструктура оказалась готова к повышенной нагрузке (спасибо повышенному количеству запросов в праздничные дни), и примерно через 10 минут трафик начал спадать: сервисы вернулись в строй, отскейлились и "переварили" запросы.

Главный вывод по итогам третьих учений был уже не про Kubernetes и не про железо, а про клиентскую логику: повторные запросы должны выполняться с exponential backoff. Иначе в момент частичной деградации клиенты начинают не помогать системе восстановиться, а добивать её.

Итоги

Номер АУ

Кластер

Размер кластера, vCPU

Восстановление, мин

Выявленные проблемы

План исправления

1

dev

≈ 250

≈ 60

1. Не работали Admission webhook-и, так как deployment-ы Kyverno были развёрнуты по одной реплике.

2. Недостаточный запас ресурсов в кластерах.

3. Слишком долгое срабатывание алертов на утилизацию ресурсов в кластерах.

1. Провести RnD High Availability режима работы Kyverno. Внедрить во все кластеры при удачном проведении RnD.
2. Проверить запас ресурсов во всех кластерах. При необходимости — добавить worker-нод до нужного запаса.

3. Уменьшение времени срабатывания алерта.

2

dev

2000

≈ 15

Проблем не выявлено.

Действий не требуется.

3

prod

≈ 800

≈ 18

1. "Легитимный" клиентский DDOS сервисов в кластере.

1. Настройка exponential backoff со стороны клиентов.

Что стоит проверить прикладным командам перед АУ?

Чек-лист не только для проведения учений, но и для развёртывания любого сервиса в кластере Kubernetes:

  • У приложения должно быть больше одной реплики. Минимально допустимый вариант — 2 реплики, но для действительно важных сервисов лучше ориентироваться на 3 и более.

  • Pod-ы должны быть распределены по отказоустойчивым доменам. В идеале — по зонам доступности, в крайнем случае — хотя бы по разным нодам. Для этого можно использовать podAntiAffinity и/или topologySpreadConstraints.

  • У приложения должны быть корректно настроены requests и limits. Requests должны соответствовать реальному потреблению. CPU limits стоит задавать только там, где они действительно нужны или требуются политикой платформы.

  • Должны быть настроены корректные probes. Как минимум readinessProbe и livenessProbe, а для приложений с долгим стартом — ещё и startupProbe. Иначе Kubernetes либо слишком рано начнёт слать трафик в неготовый pod, либо будет преждевременно его перезапускать.

  • Клиентская логика должна быть устойчивой к деградации сервиса. Ретраи, таймауты и backoff должны быть настроены аккуратно. Без этого даже кратковременная деградация может превратиться в лавину повторных запросов и усугубить восстановление.

Это, конечно, не полный список рекомендаций, которые мы используем на практике. Но даже такой набор уже заметно повышает шансы сервиса спокойно пережить деградацию части кластера. Если будет интересно — готов в отдельной статье подробнее разобрать каждый из этих пунктов на конкретных примерах. А также рассказать об остальных рекомендациях.

Что стоит проверить администраторам Kubernetes перед АУ?

  • Есть согласованный план проведения учений и коммуникации. Все участники должны понимать окно проведения, сценарий, роли, критерии остановки и порядок действий в случае выхода за допустимые рамки. Также у вас всегда должен быть аналогичный план для нештатных ситуаций и инцидентов.

  • В кластере есть достаточный запас ресурсов. После отключения части нод должно оставаться достаточно CPU, RAM и storage ресурсов для восстановления кластерных и прикладных сервисов.

  • Ключевые компоненты Kubernetes развёрнуты отказоустойчиво. Это касается ingress, DNS, monitoring, admission webhook-ов, CNI и других компонентов, от которых зависит работа кластера.

  • Алерты настроены корректно. Команда должна быстро увидеть нехватку ресурсов, деградацию control plane, проблемы с scheduling, сетью и т.д.

Вывод

В ходе учений мы ещё раз убедились, что Kubernetes действительно хорошо справляется со своей задачей. Но настоящая отказоустойчивость появляется только когда к нештатной ситуации готовы и сама платформа, и конфигурация сервисов, и клиентская логика.