Привет!
Сегодня мы рассмотрим, как перезапустить полноценный ZooKeeper‑кластер в Kubernetes так, чтобы ни один из узлов не потерял кворум даже на микросекунду. Берём два проверенных инструмента — строгий PodDisruptionBudget с minAvailable: 100%
и StatefulSet с updateStrategy.RollingUpdate.partition
.
Зачем вообще это
ZooKeeper теряет кворум, если одновременно «отваливаются» больше ⌈N/2⌉ нод.
Классический
kubectl rollout restart sts/zk
бьёт по последнему поду, дожидается его готовности и переходит к предыдущему. На бумаге это безопасненько, но при drain-операциях или хаотичных эвикшенах можно внезапно уронить нод.Подцепив PDB с 100 % мы закручиваем гайки: eviction-контроллер не сможет снести под даже при drain-узла.
partition
даёт ручной триггер для каждого пода: пока мы явно не скажем «- 1», ничего не перезапустится.
Готовим строгий PDB
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: zk-pdb-strict
spec:
minAvailable: 100% # нулевая терпимость к эвикшенам
selector:
matchLabels:
app: zookeeper
Создаём:
kubectl apply -f zk-pdb-strict.yaml
kubectl get pdb zk-pdb-strict
Поле ALLOWED DISRUPTIONS
всегда «0», и это именно то, что нам нужно. Теперь ни плановый drain, ни kubectl delete pod
не разрушат кластер (контроллер просто скажет «eviction forbidden»).
Настраиваем StatefulSet
Напишем минимальный фрагмент StatefulSet
с RollingUpdate-стратегией и явно заданным partition
. Пусть у нас три реплики (replicas: 3
).
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: zk
spec:
serviceName: zk-hs
replicas: 3
updateStrategy:
type: RollingUpdate
rollingUpdate:
partition: 3 # freeze-точка — выше самого большого ординала
selector:
matchLabels:
app: zookeeper
template:
metadata:
labels:
app: zookeeper
spec:
containers:
- name: zk
image: bitnami/zookeeper:3.9.3
readinessProbe:
exec:
command: ["zkServer.sh", "status"]
livenessProbe:
exec:
command: ["zkServer.sh", "status"]
При изменении шаблона подов (новый тег образа, другие ресурсы) контроллер не заденет ни один экземпляр, потому что ординалы 0 – 2 меньше partition
.
Патчим шаблон подов
Например, хотим обновить образ до 3.9.4:
kubectl patch sts zk --type='json' \
-p='[{"op":"replace","path":"/spec/template/spec/containers/0/image","value":"bitnami/zookeeper:3.9.4"}]'
Смотрим, что ничего не происходит:
kubectl rollout status sts/zk # висит на Current revision
Это нормально — partition держит обновление.
Самописный one-liner для чистого rolling-update
#!/usr/bin/env bash
set -euo pipefail
STS="zk"
REPLICAS=$(kubectl get sts "$STS" -o=jsonpath='{.spec.replicas}')
for ((ORD=$((REPLICAS-1)); ORD>=0; ORD--)); do
echo "Понижаем partition до $ORD"
kubectl patch sts "$STS" -p \
"{\"spec\":{\"updateStrategy\":{\"rollingUpdate\":{\"partition\":$ORD}}}}"
echo " Ждём готовность pod/$STS-$ORD"
kubectl rollout status --watch --timeout=600s sts/"$STS" \
--revision=$(kubectl get sts "$STS" -o=jsonpath='{.status.updateRevision}')
done
echo " Все поды обновлены"
Что происходит:
partition
= 2 — контроллер убиваетzk-2
, поднимает новый с 3.9.4, ждёт Ready.Цикл ставит
partition
= 1 — очередь доходит доzk-1
.partition
= 0 — наконец обновляетсяzk-0
.
В любой момент только одна реплика недоступна, PDB не нарушается, кворум не покидает нас ни на минуту.
Проверяем, что кворум жив
После каждого шага можно проверять лидерство:
kubectl exec zk-0 -- zkCli.sh stat | grep -E 'Mode|Zxid'
kubectl exec zk-1 -- zkCli.sh stat | grep -E 'Mode|Zxid'
kubectl exec zk-2 -- zkCli.sh stat | grep -E 'Mode|Zxid'
Или сделать sanity-тест:
kubectl exec zk-0 -- zkCli.sh create /hello "$(date +%s)"
kubectl exec zk-2 -- zkCli.sh get /hello
Данные доступны сразу на всех серверах при условии двух готовых подов из трёх — именно то, что мы и сохраняем.
Возможные проблемы
Ситуация | Что пойдёт не так | Как лечить |
---|---|---|
Drain ноды |
| Временно снижаем |
CrashLoopReadiness | Readiness Probe ложится, под считается Unready, кворум падает | Перед обновлением убедитесь, что проба ruok стабильно отвечает |
Смена конфигурации ZK | Требуется reload, а не restart | Используйте |
Включён Istio | Sidecar мешает быстрому отсоединению от сети | Добавляем |
Автоматизируем в CI
Простейший GitLab-job:
update_zk:
stage: deploy
script:
- kubectl config use-context prod
- ./scripts/partition-rolling.sh
when: manual
Можно хранить желаемый тег образа в Helm-values, а скрипт брать replicas
и release
из helm status
. Главное — никогда не трогайте partition
и PDB в разных MR: держите их под одной ревизией, иначе рискуете схлопотать «Eviction is refused» в самый неудобный момент.
Что насчёт maxUnavailable
С Kubernetes 1.25 можно задать maxUnavailable
прямо в StatefulSet — это альтернатива нашему циклу. Но ZooKeeper-кластеру критичен порядок остановки (последний ординал первым), а maxUnavailable
не гарантирует этот порядок. Плюс нам нужна совместимость с версиями до 1.25, где поля ещё нет. Поэтому комбинация partition
+ скрипт пока быстрее и надёжнее.
Итог
Спасибо, что дочитали до конца — надеюсь, теперь раскатывать ZooKeeper без потери кворума для вас будет так же естественно, как kubectl get pods
. Делитесь своими кейсами, подходами и замечаниями в комментариях — всегда интересно увидеть, как это решают в других задачах.
Даже в зрелых командах CI/CD часто остаётся точкой боли: внешние сервисы ломают изоляцию, пайплайны зависимы от ручного вмешательства, а деплой — это всё ещё зона повышенного риска. Если вы хотите перевести доставку кода в предсказуемую, автономную и контролируемую систему — эти уроки помогут на практике разобраться, как это сделать.
28 июля, 20:00
GitlabCI + ArgoCD — сборка и доставка приложений, не покидая кластер
Как собрать и доставить код, не выходя за границы Kubernetes: связка GitlabCI + ArgoCD для полной автономии CI/CD.12 августа, 20:00
CI/CD: 90 минут от платформы до конвейера
От пустого проекта до рабочего пайплайна за 100 секунд, а дальше — по шагам: всё, что нужно знать, чтобы CI/CD действительно работал.
Чтобы открыть доступ ко всем открытым урокам, а заодно проверить своей уровень знаний Kubernetes, пройдите вступительное тестирование.