В этом разделе мы покажем, на какие возможности и настройки Kubernetes стоит обратить внимание, чтобы обеспечить высокую доступность ваших приложений, запущенных в K8s.

Количество реплик

Вряд ли получится говорить о какой-либо доступности, если приложение не работает по меньшей мере в двух репликах. Почему при запуске приложения в одной реплике возникают проблемы? Многие сущности в Kubernetes (Node, Pod, ReplicaSet и др.) эфемерны, т. е. при определенных условиях они могут быть автоматически удалены/пересозданы. Соответственно, кластер Kubernetes и запущенные в нём приложения должны быть к этому готовы.

К примеру, при автомасштабировании узлов вниз, какие-то узлы вместе с запущенными на них Pod’ами будут удалены. Если в это время на удаляемом узле работает ваше приложение в одном экземпляре, то неизбежна полная — хотя обычно и непродолжительная — недоступность приложения. В целом, при работе в одной реплике любое нештатное завершение работы приложения будет означать простой. Таким образом, приложение должно быть запущено по меньшей мере в двух репликах.

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

Рекомендации актуальны, если не используется HorizontalPodAutoscaler. Лучший вариант для приложений, у которых будет больше нескольких реплик, — настроить HorizontalPodAutoscaler и забыть про указание количества реплик вручную. О HorizontalPodAutoscaler мы поговорим в следующих разделах.

Стратегия обновления

Стратегия обновления у Deployment’а по умолчанию такая, что почти до конца обновления только 75% Pod’ов старого+нового ReplicaSet’а будут в состоянии Ready. Таким образом, при обновлении приложения его вычислительная способность может падать до 75%, что может приводить к частичному отказу. Отвечает за это поведение параметр strategy.rollingUpdate.maxUnavailable. Поэтому убедитесь, что приложение не теряет в работоспособности при отказе 25% Pod’ов, либо уменьшите maxUnavailable. Округление maxUnavailable происходит вниз.

Также у стратегии обновления по умолчанию (RollingUpdate) есть нюанс: приложение некоторое время будет работать не только в несколько реплик, но и в двух разных версиях — разворачивающейся сейчас и развернутой до этого. Поэтому, если приложение не может даже непродолжительное время работать в нескольких репликах и нескольких разных версиях, то используйте strategy.type: Recreate. При Recreate новые реплики будут подниматься только после того, как удалятся старые. Очевидно, здесь у приложения будет небольшой простой.

Альтернативные стратегии деплоя (blue-green, canary и др.) часто могут быть гораздо лучшей альтернативой RollingUpdate, но здесь мы не будем их рассматривать, так как их реализация зависит от того, какое ПО вы используете для деплоя. Это выходит за рамки текущей статьи.

Подробнее можно почитать во внешней статье «Стратегии деплоя в Kubernetes: rolling, recreate, blue/green, canary, dark (A/B-тестирование)».

Равномерное распределение реплик по узлам

Очень важно разносить Pod’ы приложения по разным узлам, если приложение работает в нескольких репликах. Для этого рекомендуйте планировщику не запускать несколько Pod’ов одного Deployment’а на одном и том же узле:

affinity:
podAntiAffinity:
  preferredDuringSchedulingIgnoredDuringExecution:
  - podAffinityTerm:
      labelSelector:
        matchLabels:
          app: testapp
      topologyKey: kubernetes.io/hostname

Предпочитайте preferredDuringScheduling вместо requiredDuringScheduling, который может привести к невозможности запустить новые Pod’ы, если доступных узлов окажется меньше, чем новым Pod’ам требуется. Тем не менее, requiredDuringScheduling может быть полезен, когда количество узлов и реплик приложения точно известно и необходимо быть уверенным, что два Pod’а не смогут оказаться на одном и том же узле.

PodDisruptionBudget

Механизм PodDisruptionBudget (PDB) — это must have для работы приложения в production-среде. Он позволяет контролировать, какое количество Pod’ов приложения могут быть недоступны в момент времени. Читая рекомендации выше, можно было подумать, что мы уже ко всему подготовлены, если у приложения запущены несколько реплик и прописан podAntiAffinity, который не позволит Pod’ам schedule’иться на один и тот же узел.

Однако может случиться ситуация, при которой из эксплуатации одновременно выйдет не один узел. Например, вы решили поменять инстансы на более мощные. Могут быть и другие причины, но сейчас это не так важно. Важно, что несколько узлов выведены из эксплуатации в один момент времени. «Это же Kubernetes! — скажете вы. — Тут всё эфемерно. Ну, переедут Pod’ы на другие узлы — что такого?» Давайте разберёмся.

Предположим, приложение состоит из 3-х реплик. Нагрузка распределена равномерно по ним, а Pod’ы — по узлам. Оно выдержит, если упадет одна из реплик. Но вот при падении двух реплик из трёх начнётся деградация сервиса: один Pod просто не справится с нагрузкой, клиенты начнут получать 500-е ошибки. ОK, если мы подготовились и заранее прописали rate limit в контейнере c nginx (конечно, если у нас есть контейнер с nginx в Pod’е…), то ошибки будут 429-е. Но это все равно деградация сервиса.

Тут нам на помощь и приходит PodDisruptionBudget. Взглянем на его манифест:

apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
  name: app-pdb
spec:
  maxUnavailable: 1
  selector:
    matchLabels:
      app: app

Конфиг довольно простой, и большая часть полей в нем скорее всего знакома (по аналогии с другими манифестами). Самое интересное — это maxUnavailable. Данное поле позволяет указать, сколько Pod’ов (максимум) могут быть недоступны в момент времени. Указывать значение можно как в единицах Pod’ов, так и в процентах.

Предположим, что для приложения настроен PDB. Что теперь произойдет, когда два или более узлов, на которые выкачены Pod’ы приложения, начнут по какой-либо причине вытеснять (evict) Pod’ы? PDB позволит вытеснить лишь один Pod, а второй узел будет ждать, пока реплик не станет хотя бы две (из трёх). Только после этого еще одну из реплик можно вытеснить.

Есть также возможность определять и minAvailable. Например:

apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
  name: app-pdb
spec:
  minAvailable: 80%
  selector:
    matchLabels:
      app: app

Так можно гарантировать, что кластер будет следить за тем, чтобы 80% реплик всегда были доступны, а вытеснять с узлов (при такой необходимости) можно только оставшиеся 20%. Указывать это соотношение снова можно в процентах и единицах.

Есть и обратная сторона медали: у вас должно быть достаточное количество узлов, причем с учетом podAntiAffinity. Иначе может сложиться ситуация, что одну реплику вытеснили с узла, а вернуться она никуда не может. Результат: операция drain просто ждет вечность, а вы остаетесь с двумя репликами приложения… В describe Pod’а, который висит в Pending, можно, конечно, найти информацию о том, что нет свободных узлов, и исправить ситуацию, но лучше до этого не доводить.

Итоговая рекомендация: всегда настраивайте PDB для критичных компонентов вашей системы.