company_banner

3 истории сбоев Kubernetes в production: anti-affinity, graceful shutdown, webhook

Автор оригинала: Moonlight, Phil Pearl, Jetstack
  • Перевод

Прим. перев.: Представляем вниманию мини-подборку из постмортемов о фатальных проблемах, с которыми столкнулись инженеры разных компаний при эксплуатации инфраструктуры на базе Kubernetes. Каждая заметка рассказывает о самой проблеме, её причинах и последствиях, а также, конечно, о решении, помогающем избегать подобных ситуаций в будущем.

Как известно, учиться на чужом опыте дешевле, а посему — пусть эти истории помогут быть готовыми к возможным неожиданностям. Кстати, большая и регулярно обновляемая подборка ссылок на такие «failure stories» публикуется на этом сайте (по данным из этого Git-репозитория).


№1. Как kernel panic привел к падению сайта


Оригинал: Moonlight.

В период с 18 по 22 января сайт и API Moonlight испытывали периодические сбои в работе. Все началось со случайных ошибок API и закончилось полным отключением. Проблемы были решены, и приложение вернулось в нормальное состояние.

Общие сведения


Moonlight использует программное обеспечение, известное как Kubernetes. Kubernetes запускает приложения на группах серверов. Эти серверы называются узлами. Копии приложения, работающие на узле, зовутся pod'ами. В Kubernetes есть планировщик (scheduler), который динамически определяет, какие pod'ы на каких узлах должны работать.

Хронология


Первые ошибки в пятницу были связаны с проблемами с подключением к базе данных Redis. API Moonlight использует Redis для проверки сессий при каждом аутентифицированном запросе. Наш инструмент для мониторинга Kubernetes оповестил, что некоторые узлы и pod'ы не отвечают. В то же время Google Cloud сообщил о сбоях в работе сетевых служб, и мы решили, что именно они являются причиной наших проблем.

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

Команда техподдержки Google выявила некую закономерность в поведении узлов в нашем кластере Kubernetes. Загрузка CPU отдельных узлов достигала 100%, после чего в виртуальной машине происходил kernel panic и она падала.

Причины


Цикл, вызвавший сбой, оказался следующим:

  • Планировщик Kubernetes размещал несколько pod'ов с высоким потреблением ресурсов CPU на одном и том же узле.
  • Pod'ы съедали все ресурсы CPU на узле.
  • Далее возникал kernel panic, что приводило к периоду простоя, в течение которого узел не отвечал планировщику.
  • Планировщик перемещал все упавшие pod'ы на новый узел, и процесс повторялся, усугубляя общую ситуацию.

Первоначально ошибка возникла в pod'е Redis, но в конечном итоге все pod'ы, работающие с трафиком, упали, что привело к полному отключению. Правила экспоненциальной задержки при повторном планировании приводили ко все более длительным периодам простоя.

Решение


Мы смогли восстановить работу сайта, добавив правила anti-affinity во все основные Deployment'ы. Они автоматически распределяют pod'ы по узлам, повышая отказоустойчивость и производительность.

Сам Kubernetes спроектирован как отказоустойчивая хост-система. Moonlight использует три узла на разных серверах для обеспечения устойчивости, и мы запускаем три копии каждого приложения, обслуживающего трафик. Идея состоит в том, чтобы иметь по одной копии на каждом узле. В этом случае даже отказ двух узлов не приведет к простою. Тем не менее, Kubernetes иногда размещал все три pod'а с сайтом на одном узле, создавая таким образом узкое место в системе. При этом другие приложения, требовательные к мощности процессора (а именно — рендеринг на стороне сервера), оказывались на этом же узле, а не на отдельном.

Правильно настроенный и должным образом функционирующий кластер Kubernetes обязан справляться с продолжительными периодами высокой нагрузки на CPU и размещать pod'ы таким образом, чтобы максимально эффективно использовать доступные ресурсы. Мы продолжаем работать с поддержкой Google Cloud для выявления и устранения основной причины появления kernel panic на серверах.

Заключение


Правила anti-affinity позволяют сделать приложения, работающие со внешним трафиком, более отказоустойчивыми. Если у вас есть подобный сервис в Kubernetes, задумайтесь над тем, чтобы их добавить.

Мы продолжаем работать с ребятами из Google над поиском и устранением причины сбоев в ядре ОС на узлах.

№2. «Грязный» секрет endpoint'а Kubernetes и Ingress


Оригинал: Phil Pearl из Ravelin.

Изящность переоценена


Мы в компании Ravelin мигрировали на Kubernetes (на GKE). Процесс оказался очень успешным. Наши pod disruption budgets полны как никогда, statefulset'ы по-настоящему статны (с трудом переводимая игра слов: «our statefulsets are very stately» ­— прим. перев.), а скользящая замена узлов проходит как по маслу.

Последний кусочек головоломки — перенос слоя API со старых виртуальных машин в кластер Kubernetes. Для этого нам необходимо настроить Ingress, чтобы API был доступен из внешнего мира.

Поначалу задача казалась простой. Мы просто определим Ingress-контроллер, подправим Terraform, чтобы получить некоторое количество IP-адресов, а Google позаботится практически обо всем остальном. И все это заработает как по волшебству. Класс!

Однако со временем стали замечать, что интеграционные тесты периодически получают ошибки 502. С этого и началось наше путешествие. Впрочем, я сэкономлю вам время и перейду сразу к выводам.

Graceful shutdown


Все говорят о graceful shutdown («изящном», постепенном отключении). Но на самом деле не стоит полагаться на него в Kubernetes. Или, по крайней мере, это должен быть не тот graceful shutdown, который вы впитали с молоком матери. В мире Kubernetes подобный уровень «изящности» не нужен и грозит серьезными проблемами.

Идеальный мир


Вот как в представлении большинства проходит удаление pod'а из сервиса или балансировщика нагрузки в Kubernetes:

  1. Контроллер репликации решает удалить pod.
  2. Endpoint pod'а удаляется из сервиса или балансировщика нагрузки. Новый трафик в pod больше не поступает.
  3. Вызывается хук pre-stop, или pod получает сигнал SIGTERM.
  4. Pod «изящно» отключается. Он перестает принимать входящие подключения.
  5. «Изящное» отключение завершено, и pod уничтожается после того, как все его существующие подключения останавливаются или завершаются.

К сожалению, в действительности все обстоит совсем иначе.

Реальный мир


Большая часть документации намекает, что все происходит несколько иначе, однако об этом нигде не пишут явно. Главная проблема состоит в том, что шаг 3 не следует за шагом 2. Они происходят одновременно. В обычных сервисах удаление endpoint'ов происходит настолько быстро, что вероятность столкнуться с проблемами крайне низка. Однако с Ingress'ами все иначе: обычно они реагируют гораздо медленнее, поэтому проблема становится очевидной. Pod может получить SIGTERM задолго до того, как изменения в endpoint'ах попадут в Ingress.

В итоге, graceful shutdown — вовсе не то, что требуется от pod'а. Он будет получать новые подключения и должен продолжать обрабатывать их, иначе клиенты начнут получать 500-е ошибки и вся чудесная история о беспростойных развертываниях и масштабировании начнет разваливаться.

Вот что происходит на самом деле:

  1. Контроллер репликации решает удалить pod.
  2. Endpoint pod'а удаляется из сервиса или балансировщика нагрузки. В случае Ingress'ов это может занимать некоторое время, и новый трафик будет продолжать поступать в pod.
  3. Вызывается хук pre-stop, или pod получает сигнал SIGTERM.
  4. В значительной степени pod должен игнорировать это, продолжать работать и обслуживать новые подключения. Если это возможно, он должен намекнуть клиентам, что было бы неплохо переключиться в другое место. Например, в случае HTTP он может посылать Connection: close в заголовках ответов.
  5. Pod завершает работу только тогда, когда истекает период «изящного» ожидания и он убивается SIGKILL.
  6. Убедитесь, что этот период дольше, чем время, которое требуется на перепрограммирование балансировщика нагрузки.

Если это сторонний код и вы не можете изменить его поведение, тогда лучшее, что можно сделать — добавить хук pre-stop, который просто будет спать (sleep) в течение «изящного» периода, так что pod будет продолжать работать так, словно ничего не произошло.

№3. Как простой вебхук стал причиной сбоя кластера


Оригинал: Jetstack.

Jetstack предлагает своим клиентам multi-tenant-платформы на Kubernetes. Иногда возникают особые требования, которые мы не можем удовлетворить с помощью стандартной конфигурации Kubernetes. Чтобы их реализовать, с недавнего времени мы начали использовать Open Policy Agent (подробнее о проекте мы писали в этом обзоре — прим. перев.) в качестве контроллера доступа для внедрения особых политик.

В этом материале описывается сбой, вызванный неправильной настройкой этой интеграции.

Инцидент


Мы занимались обновлением мастера для dev-кластера, в котором различные команды тестировали свои приложения в течение рабочего дня. Это был региональный кластер в зоне europe-west1 на Google Kubernetes Engine (GKE).

Команды были предупреждены, что идет обновление, при этом простоя API не ожидалось. Ранее в тот день мы уже провели аналогичное обновление другой pre-production-среды.

К обновлению приступили с помощью своего GKE Terraform-пайплайна. Обновление мастера не завершилось до истечения таймаута Terraform'а, который мы установили на 20 минут. Это был первый тревожный звоночек о том, что что-то пошло не так, хотя в консоли GKE кластер по-прежнему значился как «upgrading» («обновляющийся»).

Повторный запуск пайплайна привел к следующей ошибке

google_container_cluster.cluster: Error waiting for updating GKE master version:
All cluster resources were brought up, but the cluster API is reporting that:
component "kube-apiserver" from endpoint "gke-..." is unhealthy

На этот раз связь с API-сервером стала периодически прерываться и команды не смогли деплоить свои приложения.

Пока мы пытались понять, что происходит, все узлы начали уничтожаться и воссоздаваться в бесконечном цикле. Это привело к неизбирательному отказу в обслуживании для всех наших клиентов.

Устанавливаем первопричину сбоя


С помощью поддержки Google мы смогли определить последовательность событий, которые привели к сбою:

  1. GKE завершил обновление на одном экземпляре мастера и начал принимать на него весь трафик на API-сервер по мере обновления следующих мастеров.
  2. Во время обновления второго экземпляра мастера API-сервер не смог выполнить PostStartHook для регистрации CA.
  3. В процессе выполнения этого хука API-сервер попытался обновить ConfigMap с названием extension-apiserver-authentication в kube-system. Сделать это не удалось, поскольку бэкенд для настроенного нами проверяющего вебхука Open Policy Agent (OPA) не отвечал.
  4. Чтобы мастер прошел health check (проверку работоспособности), эта операция должна завершиться успешно. Поскольку этого не произошло, второй мастер вошел в аварийный цикл и остановил обновление.

Итогом стали периодические сбои API, из-за которых kubelet'ы не смогли сообщить о работоспособности узла. В свою очередь, это привело к тому, что механизм автоматического восстановления узлов GKE (node auto-repair) начал перезапускать узлы. Эта особенность подробно описана в документации:

Статус unhealthy может означать: В течение заданного времени (приблизительно 10 минут) узел вообще не выдает какой-либо статус.

Решение


Когда мы выяснили, что ресурс ValidatingAdmissionWebhook является причиной проблемы с прерывистым доступом к серверу API — удалили его и восстановили работу кластера.

С тех пор настроили ValidatingAdmissionWebhook для OPA на мониторинг только тех пространств имен, где применима политика и к которым имеют доступ команды разработчиков. Мы также ограничили вебхук ресурсами Ingress и Service — единственными, с которыми работает наша политика.

С тех пор, как мы впервые развернули OPA, документация была обновлена, чтобы отразить это изменение.

Мы также добавили liveness-тест, чтобы гарантировать перезапуск OPA в случае, если он становится недоступным (и внесли соответствующие поправки в документацию).

Мы также рассматривали отключение механизма автоматического восстановления узлов GKE, но все же решили отказаться от этой идеи.

Итоги


Если бы мы подключили оповещения о времени отклика сервера API, то изначально смогли бы заметить его глобальное возрастание для всех запросов CREATE и UPDATE после развертывания вебхука для OPA.

Произошедшее подчеркивает важность настройки тестов для всех рабочих нагрузок. Оглядываясь назад, можно сказать, что развертывание OPA было настолько обманчиво простым, что мы даже не стали связываться с Helm-чартом (хотя следовало бы). Чарт производит ряд корректировок за пределами базовых настроек, описанных в руководстве, включая настройку livenessProbe для контейнеров с admission-контроллером.

Мы не первыми столкнулись с этой проблемой: upstream issue остается открытым. Функциональность в этом вопросе явно можно улучшить (и мы будем за этим следить).

P.S. от переводчика


Читайте также в нашем блоге:

Флант
698,77
Специалисты по DevOps и Kubernetes
Поддержать автора
Поделиться публикацией

Комментарии 12

    +4
    непонятно, как
    Pod'ы съедали все ресурсы CPU на узле.
    приводил к
    Далее возникал kernel panic
      0
      Авторы так и не раскрыли эту тайну до конца (на момент написания постмортема):

      Мы продолжаем работать с ребятами из Google над поиском и устранением причины сбоев в ядре ОС на узлах.
      +2
      Тоже поймали такую штуку как во второй истории, и так же фиксили.
      Сервис на Django если вдруг кому пригодится.
        0
        Pod завершает работу только тогда, когда истекает период «изящного» ожидания и он убивается SIGKILL.


        А что будет с активными запросами? Ingress-контроллер их перепошлет незаметно для «внешнего мира»? Или предполагается, что за это время их уже не будет?
        +1
        У нас был кейс, аналогичный п.1. ТОлько там был aws и ClusterAutoscaler. ClusterAutoscaler навешивает taint на ноду перед тем, как её удалить (scale down cluster). И из-за сбоя, некоторое количество нод оказалось с тэйнтом. Поды съезжались на ноды без тэйнтов и благополучно их вешали, после чего ноды убивались, заказывались новые и итерация повторялась.
          +1

          Хм, а почему поды загружали имеющиеся ноды по максимуму, а не cluster autoscaler создавал бы для них новые ноды? Автоскейлинг группа воркеров была уже на максимуме?

          +1
            0
            Ссылка на этот репозиторий указана во введении к статье от переводчика (второй абзац).
              0
              Извините сударь, что то не заметил.
            0

            "Изящное" выключение — очень интересный перевод. Я бы до такого не догадался. Звучит круто. Но по мне — это скорее "безопасное" выключение (аналогично "безопасному" извлечению USB-устройств). Все-таки русская терминология до сих пор хромает ((((

              +2
              Согласен: перевод здесь не совсем привычный, но остановились на таком варианте конкретно в этом материале для сохранения авторского стиля/настроения*. Этот стиль начинается с заголовка «Grace is overrated» и картинки, которую мы не оставили в тексте:



              А вот — лучшая иллюстрация красоты словесных оборотов в оригинале:

              We’ve got pod disruption budgets coming out of our ears, our statefulsets are very stately, and rolling node replacements run without a hitch.

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое