Задача 8. Держим сервис живым во время обслуживания нод

Три реплики, а сервис лёг при обновлении кластера. Разбираем, почему количество реплик не равно отказоустойчивости и как связаны PodDisruptionBudget, anti-affinity и topology spread.
Опубликовано:

Задача

Deployment на 3 реплики — казалось бы, отказоустойчиво. Во время планового обновления нод (администратор поочерёдно выполняет kubectl drain) сервис полностью недоступен около минуты.

Вопрос: почему три реплики не спасли и что нужно было настроить?

Что кажется очевидным

«Три реплики — значит, две всегда останутся. Наверное, drain выполняли слишком быстро».

Скорость здесь ни при чём. Проблем на самом деле две, и они независимы.

Как это работает на самом деле

Проблема первая: все реплики оказались на одной ноде

Планировщик по умолчанию не обязан разносить поды Deployment по разным нодам. Он ищет ноду, где под помещается, с учётом ресурсов и ограничений. Если на одной ноде есть место для всех трёх — вполне вероятно, что все три там и окажутся.

Проверить фактическое размещение:

BASH
kubectl get pods -l app=api -o wide
# NAME                  READY   STATUS    NODE
# api-abc-1             1/1     Running   node-3
# api-abc-2             1/1     Running   node-3
# api-abc-3             1/1     Running   node-3   ← все на одной ноде
Нажмите, чтобы развернуть и увидеть больше

drain node-3 — и сервиса нет. Три реплики дали защиту от падения процесса, но не от отказа ноды.

Проблема вторая: drain никто не сдерживал

kubectl drain вытесняет поды через Eviction API, и этот API уважает PodDisruptionBudget (PDB). Если PDB не задан, ограничений нет: Kubernetes выселит столько подов, сколько потребуется, и одновременно.

PDB — это заявление «сколько экземпляров должно оставаться доступными при добровольных нарушениях работы».

Важно: PDB защищает только от добровольных нарушений (drain, вытеснение при обновлении узлов, autoscaler). От внезапного отказа ноды или kubectl delete pod он не спасает — там нечего сдерживать.

Ответ

Три реплики — это защита от падения процесса, но не от отказа ноды. Не хватало двух вещей:

  1. Распределения подов по разным нодам/зонам — чтобы одна нода не уносила весь сервис;
  2. PodDisruptionBudget — чтобы drain не выселил все реплики разом.

Что с этим делать

1. PodDisruptionBudget

YAML
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: api-pdb
spec:
  minAvailable: 2 # или maxUnavailable: 1
  selector:
    matchLabels:
      app: api
Нажмите, чтобы развернуть и увидеть больше

Теперь drain заблокируется, если выселение оставит меньше двух доступных подов, — и будет ждать, пока замена поднимется в другом месте.

Грабли, о которых стоит знать:

2. Распределение по нодам и зонам

Современный способ — topologySpreadConstraints:

YAML
spec:
  topologySpreadConstraints:
    - maxSkew: 1
      topologyKey: kubernetes.io/hostname
      whenUnsatisfiable: DoNotSchedule
      labelSelector:
        matchLabels:
          app: api
    - maxSkew: 1
      topologyKey: topology.kubernetes.io/zone
      whenUnsatisfiable: ScheduleAnyway
      labelSelector:
        matchLabels:
          app: api
Нажмите, чтобы развернуть и увидеть больше

maxSkew: 1 означает: разница в числе подов между любыми двумя нодами (зонами) не больше одного.

Обратите внимание на разные значения whenUnsatisfiable:

Более старый эквивалент — podAntiAffinity. Он всё ещё работает, но topologySpreadConstraints выразительнее и не требует выбирать между «строго по одному на ноду» и «как получится».

3. Сложить вместе

Полный набор для по-настоящему отказоустойчивого сервиса:

Проверка до инцидента

Самый честный способ — прорепетировать:

BASH
# Кто где живёт
kubectl get pods -l app=api -o wide

# Проверить, что PDB реально считает
kubectl get pdb api-pdb
# NAME      MIN AVAILABLE   ALLOWED DISRUPTIONS   AGE
# api-pdb   2               1                     5m

# Учения: увести ноду под нагрузкой и смотреть на ошибки
kubectl drain node-3 --ignore-daemonsets --delete-emptydir-data
Нажмите, чтобы развернуть и увидеть больше

Колонка ALLOWED DISRUPTIONS — самая полезная: 0 означает, что drain сейчас заблокируется.


Дальше: Задача 9. Прожорливый etcd.

Начать поиск

Введите ключевые слова для поиска статей

↑↓
ESC
⌘K Горячая клавиша