Базовая конфигурация Kubernetes для продакшен-нагрузок
Requests, пробы, disruption budgets, корректное завершение и безопасность подов: настройки, которые должны быть у продакшен-Deployment до первого инцидента.
Примеры обобщённые и иллюстративные. Они не взяты из среды какого-либо клиента: подстройте пороги и параметры под свою систему.
Зачем нужна базовая конфигурация
Kubernetes запускает почти что угодно почти без настроек, и в этом проблема: значения по умолчанию рассчитаны на то, чтобы начать, а не на то, чтобы не падать. Настройки ниже дёшевы в первый день и дорого обходятся, если их обнаруживают во время сбоя. Они относятся к Deployment без состояния; нагрузкам с состоянием нужно больше.
Requests и limits
Планировщик размещает поды по их requests, а Horizontal Pod Autoscaler считает утилизацию относительно них. Под без requests размещается вслепую и не может масштабироваться по CPU.
Память несжимаема: контейнер, превысивший лимит памяти, принудительно завершается. Задайте лимит памяти равным request или близким к нему, чтобы под никогда не планировался на память, которую не сможет удержать. С CPU иначе — контейнер выше лимита CPU троттлится, и это проявляется как задержка. Многие команды задают requests по CPU и не ставят лимит CPU для сервисов, чувствительных к задержке; что бы вы ни выбрали, измеряйте троттлинг.
Три пробы — три задачи
- Readiness решает, получает ли под трафик. Осторожно с общими зависимостями: если все поды одновременно не пройдут readiness, у сервиса не останется ни одного endpoint.
- Liveness решает, перезапускать ли контейнер. Ограничьте её самим процессом: если она проверяет базу данных, сбой базы перезапустит все поды сразу.
- Startup придерживает остальные пробы, пока медленное приложение запускается, вместо того чтобы раздувать задержку liveness.
Пережить drain узлов и отказ зоны
Обновления узлов и автомасштабирование постоянно вытесняют поды. PodDisruptionBudget ограничивает, сколько реплик добровольное прерывание может остановить одновременно, а topology spread constraints распределяют реплики по разным зонам, чтобы отказ одной зоны не унёс весь сервис.
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: api
spec:
maxUnavailable: 1
selector:
matchLabels: { app: api }
Корректное завершение
Когда под удаляется, его исключение из endpoints сервиса и остановка контейнеров (сначала хук preStop, затем SIGTERM) начинаются одновременно. Приложение, которое завершается сразу, теряет запросы, всё ещё направляемые к нему. Добавьте короткую паузу в preStop, чтобы балансировщики успели обновиться, обрабатывайте SIGTERM, завершая текущую работу, и держите terminationGracePeriodSeconds больше, чем пауза плюс время дренажа.
Контекст безопасности и образы
Запускайте от непривилегированного пользователя, отключите все Linux capabilities, запретите повышение привилегий и монтируйте корневую файловую систему только на чтение; Pod Security Standard restricted обеспечивает большую часть этого на уровне namespace. Ссылайтесь на образы по digest, а не по изменяемому тегу, чтобы работало ровно то, что тестировалось.
Всё вместе
Deployment с применённой базовой конфигурацией. Значения условные: подбирайте requests по измеренному потреблению, а не по этому примеру.
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 3
selector:
matchLabels: { app: api }
template:
metadata:
labels: { app: api }
spec:
terminationGracePeriodSeconds: 45
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels: { app: api }
securityContext:
runAsNonRoot: true
seccompProfile: { type: RuntimeDefault }
containers:
- name: api
image: registry.example.com/api@sha256:<digest>
resources:
requests: { cpu: 250m, memory: 256Mi }
limits: { memory: 256Mi }
readinessProbe:
httpGet: { path: /ready, port: 8080 }
periodSeconds: 5
livenessProbe:
httpGet: { path: /live, port: 8080 }
periodSeconds: 10
failureThreshold: 3
startupProbe:
httpGet: { path: /live, port: 8080 }
periodSeconds: 5
failureThreshold: 30
lifecycle:
preStop:
sleep: { seconds: 5 } # native sleep action, Kubernetes 1.30+
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: { drop: ["ALL"] }