Платформенная команда даёт кластер, но четыре вещи в Kubernetes не может сделать никто, кроме автора сервиса: правильные probes, честные ресурсы под JVM, корректное завершение и конфигурация. Ошибки в каждой из них выглядят как «кластер глючит» — а лечатся в репозитории сервиса.
Probes: три вопроса с разными последствиями
Kubernetes задаёт приложению три вопроса, и путать их опасно, потому что последствия у ответов разные:
- startup — «ты уже запустился?» Пока нет — остальные probes не задаются. Защищает медленно стартующие приложения (прогрев кэша, миграции) от преждевременного убийства.
- readiness — «тебе можно слать трафик?» Нет — под выводится из endpoints, но живёт. Это пауза, не казнь.
- liveness — «ты вообще жив?» Нет — под перезапускается. Это казнь, и назначать её надо за то, что лечится перезапуском: deadlock, зависший event loop — а не за временные трудности.
Spring Boot Actuator делает разделение из коробки:
management:
endpoint:
health:
probes:
enabled: true
group:
readiness:
include: readinessState, db
containers:
- name: app
startupProbe:
httpGet: { path: /actuator/health/liveness, port: 8081 }
failureThreshold: 30
periodSeconds: 2
readinessProbe:
httpGet: { path: /actuator/health/readiness, port: 8081 }
periodSeconds: 5
livenessProbe:
httpGet: { path: /actuator/health/liveness, port: 8081 }
periodSeconds: 10
failureThreshold: 3
Главная ошибка probes — внешние зависимости в liveness. Если liveness проверяет БД, то отказ базы перезапускает все поды сервиса по кругу: к инциденту с БД добавляется самострел из рестартов, прогревов и потерянных соединений. Правило: liveness — только внутреннее состояние процесса (дефолтный livenessState); зависимости — максимум в readiness, и то осознанно: сервис, у которого недоступна одна из трёх зависимостей, возможно, должен отвечать деградированно, а не выпадать из балансировки целиком.
Ресурсы и JVM: почему OOMKilled
resources:
requests: { cpu: "500m", memory: "768Mi" }
limits: { memory: "768Mi" }
Requests — сколько гарантированно резервируется при планировании пода на ноду. Limits — потолок: превышение по памяти карается мгновенным убийством процесса (OOMKilled, exit 137), превышение по CPU — троттлингом.
С JVM это взаимодействует нетривиально. Память процесса — это не только heap: metaspace, стеки потоков, direct buffers, код JIT — ещё сотни мегабайт. Современная JVM видит лимит контейнера и берёт от него долю под heap:
-XX:MaxRAMPercentage=60 -XX:+ExitOnOutOfMemoryError
60–75% лимита под heap — рабочий диапазон; остальное — той самой «не-heap» памяти. Диагностический признак: OOMKilled (контейнер убит, exit 137, в логах тишина) — процесс превысил limit, чаще всего из-за не-heap потребления; OutOfMemoryError в логах — кончился heap внутри JVM. Лечатся они по-разному: первый — пересмотром доли heap и лимита, второй — поиском утечки или ростом heap.
По CPU практичный паттерн: requests честные, limit по CPU не ставить (или ставить высоким) — CPU-троттлинг бьёт по GC-паузам и латентности хвоста, а изоляцию нагрузок даёт планирование по requests. По памяти limit обязателен и равен request — без сюрпризов от соседей по ноде.
Graceful shutdown: не терять запросы на деплое
При остановке пода (deploy, scale down) происходит: под помечен на удаление → выведен из endpoints → контейнеру послан SIGTERM → через terminationGracePeriodSeconds — SIGKILL. Две проблемы и два решения:
- Вывод из endpoints не мгновенный — несколько секунд после SIGTERM трафик ещё может приходить. Решение — preStop-пауза: контейнер ждёт, пока балансировка про него забудет, и только потом начинает останавливаться.
- Активные запросы надо дорабатывать. Решение — graceful shutdown в самом Spring.
lifecycle:
preStop:
exec:
command: ["sleep", "5"]
terminationGracePeriodSeconds: 30
server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 20s
Бюджет должен сходиться: preStop + время дорабатывания < terminationGracePeriodSeconds, иначе SIGKILL оборвёт транзакции на середине. Полный разбор — включая Kafka-листенеры, пулы и идемпотентность прерванных операций — в Graceful Shutdown Style Guide.
Конфигурация: ConfigMap, Secret и профили
Образ один на все среды; различия — снаружи, в ConfigMap (настройки) и Secret (пароли, токены). В Spring это попадает двумя путями:
envFrom:
- configMapRef: { name: order-service-config }
- secretRef: { name: order-service-secrets }
Переменные окружения — простой и достаточный дефолт (APP_CLICKHOUSE_URL → app.clickhouse.url). Второй путь — смонтировать ConfigMap файлом и подключить spring.config.import: optional:configtree:/etc/config/ — удобен, когда конфиг большой и структурный. Правила гигиены: секреты не в ConfigMap и тем более не в образ; профиль (SPRING_PROFILES_ACTIVE) задаёт среда, не Dockerfile; обязательные настройки валидируются на старте через @Validated @ConfigurationProperties — упасть при деплое лучше, чем удивить в рантайме (fail fast).
HPA: автомасштабирование с оговорками
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
spec:
scaleTargetRef: { apiVersion: apps/v1, kind: Deployment, name: order-service }
minReplicas: 3
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target: { type: Utilization, averageUtilization: 70 }
Оговорки для JVM-сервисов: новая реплика не помогает мгновенно — старт JVM, прогрев JIT и пулов занимают десятки секунд, и при резком пике HPA опаздывает по построению (буфер ёмкости в minReplicas всё ещё нужен). CPU-метрика для I/O-bound сервисов малоинформативна — масштабироваться по RPS или длине очереди честнее (custom metrics через адаптер Prometheus). И HPA конфликтует с наивным пониманием «нагрузка выросла → подкинем CPU limit»: масштабирование в Kubernetes горизонтальное по природе.
Чек-лист сервиса перед продом
- Три probes разведены по смыслу; liveness без внешних зависимостей.
MaxRAMPercentageсогласован с memory limit; limit = request по памяти.server.shutdown: graceful+ preStop; бюджет завершения сходится.- Секреты — в Secret, конфиг — в ConfigMap, образ один на все среды.
@Validatedна@ConfigurationProperties— кривой конфиг валит старт.- Логи в stdout в JSON (observability) — собирать их иначе в k8s неоткуда.
minReplicas >= 2и PodDisruptionBudget — деплой ноды не должен ронять сервис в ноль.
Что почитать дальше
- Graceful Shutdown Style Guide — полный разбор корректного завершения с правилами R-SHUT-*.
- Деплой и конфигурация — как эти манифесты доезжают до кластера.
- Эксплуатация и отладка — OOMKilled и CrashLoopBackOff на практике.
- Spring Actuator, Micrometer — health groups и метрики, на которых стоят probes и HPA.