Kubernetes для Java-разработчиков: от Docker-образа до продакшена

Практический курс, ориентированный на специфику запуска Java-приложений в Kubernetes. Вы научитесь контейнеризировать Spring Boot сервисы, управлять ресурсами JVM и настраивать мониторинг в кластере.

1. Основы контейнеризации Java-приложений и введение в архитектуру Kubernetes

Основы контейнеризации Java-приложений и введение в архитектуру Kubernetes

Добро пожаловать в курс Kubernetes для Java-разработчиков. Мы начинаем наше путешествие с фундаментальных понятий. Если вы привыкли деплоить приложения, копируя JAR-файлы на сервер или используя WAR-архивы в Tomcat, этот модуль изменит ваше представление о поставке программного обеспечения.

В этой статье мы разберем, как правильно упаковать Java-приложение в Docker-контейнер, почему «работает на моей машине» больше не аргумент, и как Kubernetes управляет тысячами таких контейнеров, создавая надежную систему.

От JAR-файла к Docker-образу

Традиционный способ развертывания Java-приложений часто страдает от проблемы несовместимости окружений. У вас стоит Java 17.0.1, на сервере — 17.0.2, а у коллеги — 11-я версия. Библиотеки операционной системы тоже отличаются. В итоге приложение падает с ошибками, которые невозможно воспроизвести локально.

Контейнеризация решает эту проблему, упаковывая приложение вместе со всем его окружением: JVM, библиотеками и зависимостями ОС.

Виртуальные машины против Контейнеров

Чтобы понять суть контейнеров, сравним их с виртуальными машинами (VM).

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

Главное отличие: контейнеры делят ядро хостовой операционной системы, но работают в изолированных пространствах пользователя. Это делает их запуск практически мгновенным по сравнению с загрузкой полноценной ОС в виртуальной машине.

Анатомия Dockerfile для Java

Сердцем контейнеризации является Dockerfile — инструкция по сборке образа. Для Java-разработчика важно не просто скопировать JAR, но и сделать образ эффективным.

Рассмотрим пример плохого Dockerfile:

Почему это плохо? Образ будет весить сотни мегабайт из-за лишних утилит Ubuntu, а слой с apt-get замедлит сборку и увеличит риск уязвимостей.

А теперь рассмотрим правильный подход с использованием Multi-stage build (многоэтапной сборки). Это стандарт индустрии.

Преимущества такого подхода:

  • Размер: Финальный образ содержит только JRE (Java Runtime Environment) и ваш JAR. В нем нет исходного кода, Maven и лишних инструментов.
  • Безопасность: Меньше файлов в образе — меньше поверхность атаки.
  • Кэширование: Docker кэширует слои. Если вы изменили код, но не pom.xml, Docker может переиспользовать слои с зависимостями.
  • Java и память в контейнерах

    Долгое время Java плохо работала в контейнерах. JVM видела всю память хост-машины, а не лимит, установленный Docker. Это приводило к тому, что OOM Killer (Out Of Memory Killer) убивал контейнер, когда Java пыталась взять больше памяти, чем ей разрешено.

    Начиная с Java 10 (и бэкпортировано в Java 8u191), JVM поддерживает флаг, который включен по умолчанию: -XX:+UseContainerSupport

    Это позволяет JVM корректно считывать ограничения cgroups (механизм ядра Linux для изоляции ресурсов) и автоматически настраивать размер Heap.

    Введение в Kubernetes (K8s)

    Docker отлично справляется с запуском одного контейнера. Но что делать, если у вас микросервисная архитектура из 50 сервисов? Как перезапустить контейнер, если он упал? Как масштабировать нагрузку? Как обновить приложение без простоя?

    Здесь на сцену выходит Kubernetes (K8s) — оркестратор контейнеров.

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

    Архитектура Kubernetes

    Кластер Kubernetes состоит из двух основных типов узлов (серверов):

  • Control Plane (Master Node) — управляющий слой, «мозг» кластера.
  • Worker Nodes — рабочие узлы, где фактически запускаются ваши приложения.
  • !Схема демонстрирует разделение обязанностей: Control Plane управляет состоянием, а Worker Nodes выполняют полезную нагрузку.

    #### Компоненты Control Plane

    * API Server: Единственная точка входа для управления кластером. Когда вы вводите команду kubectl, вы общаетесь именно с ним. Все остальные компоненты также общаются через API Server. * etcd: Высокопроизводительное хранилище «ключ-значение». Здесь хранится все состояние кластера. Это «база данных» Kubernetes. Если вы потеряете данные etcd, вы потеряете кластер. * Scheduler (Планировщик): Решает, на какой именно ноде запустить новый под (Pod). Он учитывает свободные ресурсы (CPU, RAM) и требования приложения. * Controller Manager: Следит за тем, чтобы текущее состояние кластера соответствовало желаемому. Например, если вы сказали «хочу 3 копии приложения», а одна упала, контроллер заметит это и прикажет запустить новую.

    #### Компоненты Worker Node

    * Kubelet: Агент, который работает на каждой ноде. Он получает инструкции от API Server и управляет запуском контейнеров на своей машине. * Kube-proxy: Отвечает за сетевые правила. Позволяет трафику попадать к вашим сервисам. * Container Runtime: Среда запуска контейнеров (например, containerd или Docker Engine). Именно она делает грязную работу по запуску процессов.

    Pod — атом Kubernetes

    Для Java-разработчика самым важным концептом является Pod.

    Pod (Под) — это минимальная единица развертывания в Kubernetes. Kubernetes не запускает контейнеры напрямую, он запускает Поды.

    Обычно в одном Поде живет один контейнер (ваше Spring Boot приложение). Но иногда там могут быть «сайдкары» (sidecar) — вспомогательные контейнеры, например, для сбора логов или проксирования трафика.

    Ключевые особенности Пода: * У всех контейнеров в Поде общий IP-адрес. * Они делят общие тома (storage). * Они могут общаться друг с другом через localhost.

    Декларативный подход

    Kubernetes исповедует декларативный подход. Вы не говорите системе: «Запусти сервер, потом скачай файл, потом запусти Java».

    Вы описываете желаемое состояние в YAML-файле: * «Я хочу, чтобы было запущено 3 экземпляра моего приложения версии 1.2».

    Kubernetes сам вычисляет разницу между текущим состоянием (0 экземпляров) и желаемым (3 экземпляра) и выполняет необходимые действия.

    Заключение

    Мы разобрали путь от Java-кода до архитектуры кластера. Контейнер — это надежная упаковка вашего приложения, а Kubernetes — это инфраструктура, которая гарантирует доставку и работу этой упаковки.

    В следующей статье мы перейдем от теории к практике и напишем наши первые манифесты для развертывания Java-приложения в локальном кластере Minikube.

    2. Создание манифестов для запуска: Pods, Deployments и Services на примере Spring Boot

    Создание манифестов для запуска: Pods, Deployments и Services на примере Spring Boot

    В предыдущей статье мы научились упаковывать Java-приложение в эффективный Docker-образ и познакомились с теоретической архитектурой Kubernetes. Теперь пришло время перейти от теории к практике. Мы заставим наш кластер (например, локальный Minikube) запустить ваше Spring Boot приложение, обеспечить его самовосстановление и открыть к нему доступ извне.

    В мире Kubernetes мы не запускаем контейнеры императивными командами вроде docker run. Вместо этого мы описываем желаемое состояние системы в виде YAML-файлов, которые называются манифестами. В этой статье мы разберем три кита, на которых держится запуск приложений: Pod, Deployment и Service.

    Анатомия манифеста Kubernetes

    Прежде чем мы напишем первую строчку кода, разберем общую структуру любого Kubernetes-ресурса. Каждый YAML-файл состоит из четырех обязательных полей:

  • apiVersion: Версия API Kubernetes, которую мы используем для этого ресурса (например, v1 или apps/v1).
  • kind: Тип ресурса, который мы хотим создать (Pod, Service, Deployment и т.д.).
  • metadata: Данные, помогающие идентифицировать объект (имя, пространство имен, метки).
  • spec: Самая важная часть. Здесь мы описываем спецификацию — то самое желаемое состояние объекта.
  • Pod: Атом Kubernetes

    Как мы помним, Pod (Под) — это минимальная развертываемая единица. Давайте посмотрим, как выглядит манифест для запуска одного экземпляра нашего Spring Boot приложения.

    Создайте файл pod.yaml:

    Разберем ключевые моменты: * labels: Метки (labels) — это система тегов «ключ-значение». Они критически важны. Именно по меткам Kubernetes понимает, какие Поды относятся к какому приложению. * image: Ссылка на ваш образ в Docker Registry. * containerPort: Это информационное поле. Оно говорит о том, что приложение внутри слушает порт 8080 (стандартный для Tomcat в Spring Boot).

    Чтобы применить этот манифест, используется команда: kubectl apply -f pod.yaml

    Почему Подов недостаточно?

    Запускать «голые» Поды в продакшене — плохая практика. Почему?

  • Они смертны. Если процесс Java упадет с ошибкой OutOfMemoryError или нода перезагрузится, Под умрет и не воскреснет сам по себе.
  • Нет масштабирования. Вы не можете сказать Поду: «Размножься в 3 экземпляра».
  • Нет обновлений. Чтобы обновить версию приложения, вам придется удалить старый Под и создать новый вручную.
  • Для решения этих проблем существует контроллер более высокого уровня — Deployment.

    Deployment: Управляющий вашим флотом

    Deployment (Развертывание) — это ресурс, который управляет Подами. Вы говорите Деплойменту: «Я хочу, чтобы у меня всегда работало 3 копии приложения версии 2.0». Деплоймент создает ReplicaSet, который, в свою очередь, следит за тем, чтобы количество Подов всегда соответствовало заданному.

    !Иерархия управления: Deployment управляет ReplicaSet, который гарантирует наличие нужного количества Pods.

    Давайте создадим deployment.yaml для нашего Spring Boot сервиса:

    Магия Selectors и Labels

    Обратите внимание на поле spec.selector.matchLabels и spec.template.metadata.labels. Они обязаны совпадать. Это механизм связывания: Deployment ищет в кластере Поды с меткой app: my-spring-boot-app и считает их «своими».

    Самовосстановление (Self-healing)

    Если один из трех Подов упадет (например, приложение выбросит необработанное исключение и завершится), Kubernetes заметит, что текущее состояние (2 Пода) отличается от желаемого (3 Пода). Он мгновенно запустит новый экземпляр, чтобы восстановить баланс.

    Настройка ресурсов для Java

    В примере выше мы добавили блок resources. Для Java-приложений это критически важно. * requests: Гарантированный минимум ресурсов, который получит контейнер. * limits: Жесткий потолок. Если приложение попытается потребить больше памяти, чем указано в limits, оно будет убито OOM Killer'ом.

    > Никогда не запускайте Java-приложения в Kubernetes без указания лимитов памяти. JVM по умолчанию может попытаться захватить всю доступную память на ноде, что приведет к нестабильности соседей.

    Service: Стабильный сетевой доступ

    Мы запустили 3 копии приложения. У каждого Пода есть свой внутренний IP-адрес. Но есть проблема: Поды эфемерны. При обновлении приложения старые Поды удаляются, и создаются новые с новыми IP-адресами.

    Как фронтенду или другому микросервису обращаться к нашему бэкенду, если адреса постоянно меняются?

    На помощь приходит Service (Сервис). Это абстракция, которая предоставляет единый стабильный IP-адрес и DNS-имя для группы Подов, а также распределяет нагрузку между ними.

    !Service действует как внутренний балансировщик нагрузки, распределяя запросы между динамическими Подами.

    Создадим service.yaml:

    Типы Сервисов

  • ClusterIP (по умолчанию): Сервис доступен только внутри кластера. Идеально для общения микросервисов между собой.
  • NodePort: Открывает порт на каждой ноде кластера (обычно в диапазоне 30000-32767). Позволяет получить доступ к приложению извне кластера. Удобно для разработки и Minikube.
  • LoadBalancer: Используется в облаках (AWS, Google Cloud, Azure). Создает реальный внешний балансировщик нагрузки с публичным IP.
  • В нашем примере мы использовали NodePort, чтобы иметь возможность «достучаться» до приложения с локальной машины разработчика.

    Конфигурация Spring Boot через переменные окружения

    Часто нам нужно передать настройки в приложение без пересборки образа (например, адрес базы данных или активный профиль Spring). Kubernetes позволяет прокидывать переменные окружения (Environment Variables) прямо в манифесте Deployment.

    Дополним наш deployment.yaml:

    Spring Boot автоматически подхватывает переменные окружения и мапит их на свойства конфигурации. Например, SPRING_PROFILES_ACTIVE переопределит настройку в application.properties.

    Собираем всё вместе

    Теперь у нас есть полный набор для запуска. Если вы используете Minikube, последовательность действий будет следующей:

  • Применяем манифесты:
  • Проверяем статус:
  • Вы должны увидеть 3 запущенных Пода и один Сервис.

  • Открываем приложение:
  • В Minikube есть удобная команда, которая сама найдет IP и порт: Эта команда откроет браузер с адресом вашего работающего Spring Boot приложения.

    Заключение

    Мы перешли от простого запуска контейнера к созданию отказоустойчивой архитектуры. Используя Deployment, мы гарантировали, что приложение будет перезапущено в случае сбоя. Используя Service, мы обеспечили стабильную точку входа для трафика.

    Однако, хранить конфигурацию (пароли от БД, API ключи) прямо в YAML-файле Deployment в открытом виде — это серьезная уязвимость безопасности. В следующей статье мы разберем ConfigMaps и Secrets — специальные ресурсы Kubernetes для безопасного управления конфигурацией Java-приложений.

    3. Конфигурация приложений через ConfigMaps и Secrets, работа с постоянным хранилищем

    Конфигурация приложений через ConfigMaps и Secrets, работа с постоянным хранилищем

    В предыдущих модулях мы научились запускать Spring Boot приложения в Kubernetes, используя Pods, Deployments и Services. Однако мы оставили открытым важный вопрос: где хранить настройки подключения к базе данных, API-ключи и другие параметры, которые меняются от окружения к окружению?

    Хардкодить настройки внутри JAR-файла или Docker-образа — это антипаттерн. Согласно методологии The Twelve-Factor App, конфигурация должна быть строго отделена от кода. В этой статье мы разберем, как Kubernetes решает эту задачу с помощью объектов ConfigMap и Secret, а также научимся сохранять данные, чтобы они не исчезали при перезапуске Подов.

    ConfigMap: Хранение неконфиденциальных данных

    ConfigMap — это API-объект Kubernetes, предназначенный для хранения несекретных данных в формате «ключ-значение». Это может быть содержимое файлов конфигурации (например, application.properties, logback.xml) или отдельные переменные окружения.

    Главная идея: вы меняете ConfigMap, перезапускаете Под, и приложение подхватывает новые настройки без пересборки Docker-образа.

    Создание ConfigMap

    Предположим, нашему приложению нужно знать URL внешнего сервиса и уровень логирования. Создадим файл app-config.yaml:

    Применим манифест: kubectl apply -f app-config.yaml

    Внедрение ConfigMap в Pod

    Существует два основных способа передать эти данные в Java-приложение:

  • Как переменные окружения (Environment Variables). Это самый простой способ для Spring Boot, так как фреймворк автоматически мапит переменные окружения на свойства (например, LOG_LEVEL превратится в свойство log.level или может быть использован в logback-spring.xml).
  • Как файлы в томе (Volume Mount). Kubernetes создает виртуальный файл внутри контейнера, содержащий значение из ConfigMap.
  • Рассмотрим пример внедрения через переменные окружения в deployment.yaml:

    Теперь внутри контейнера будут доступны переменные LOG_LEVEL и EXTERNAL_API_URL. Spring Boot умеет читать их автоматически.

    Secrets: Хранение чувствительных данных

    Хранить пароли от баз данных в ConfigMap небезопасно, так как они хранятся в открытом виде. Для этого существует объект Secret.

    Secret работает аналогично ConfigMap, но с важным отличием: данные в манифестах должны быть закодированы в формате Base64.

    > Важно понимать: Base64 — это кодирование, а не шифрование. Любой, у кого есть доступ к объекту Secret в кластере, может декодировать его. Однако Kubernetes умеет шифровать Secrets при хранении в базе данных etcd (Encryption at Rest), если это настроено администратором кластера.

    Создание Secret

    Допустим, у нас есть пароль к базе данных: super-secret-pass. Сначала закодируем его:

    Теперь создадим db-secret.yaml:

    Использование Secret в Java-приложении

    Мы можем внедрить секреты как переменные окружения, указывая конкретные ключи. Это безопаснее, чем envFrom, так как мы контролируем, что именно попадает в контейнер.

    Spring Boot автоматически подхватит SPRING_DATASOURCE_USERNAME и SPRING_DATASOURCE_PASSWORD для настройки подключения к БД.

    !Визуализация того, как данные из ConfigMap и Secret попадают в приложение в виде переменных окружения или файлов.

    Persistent Storage: Проблема эфемерности

    До сих пор мы говорили о приложениях, которые не хранят состояние (stateless). Но что, если мы хотим запустить в кластере базу данных (например, PostgreSQL) или нашему приложению нужно сохранять загруженные пользователями файлы?

    Файловая система контейнера эфемерна. Если Под перезапустится (из-за ошибки или обновления), все файлы, созданные внутри контейнера, исчезнут. Контейнер вернется к своему исходному состоянию из Docker-образа.

    Для решения этой проблемы Kubernetes использует абстракцию Volumes (Тома).

    PV и PVC: Контракт на хранение

    Работа с дисками в Kubernetes разделена на две части, чтобы отделить запрос разработчика от реализации инфраструктуры:

  • PersistentVolume (PV) — это кусок реального хранилища в кластере (NFS-диск, облачный диск AWS EBS, локальный диск на ноде). Обычно создается администратором.
  • PersistentVolumeClaim (PVC) — это заявка (запрос) разработчика на хранилище. Вы говорите: «Мне нужно 5 Гигабайт места с возможностью записи».
  • !Схема взаимодействия Pod, PersistentVolumeClaim и PersistentVolume.

    Пример использования PVC

    Давайте создадим заявку на диск размером 1 Гб. Создайте файл pvc.yaml:

    Теперь подключим этот том к нашему Поду в deployment.yaml:

    Теперь, когда ваше Java-приложение сохранит файл в директорию /app/uploads, он физически запишется на внешний диск. Даже если Под будет удален и создан заново на другой ноде, Kubernetes переподключит этот диск к новому Поду, и данные сохранятся.

    StorageClass и Динамическое выделение

    В современных облачных кластерах вам часто не нужно создавать PersistentVolume вручную. Существует механизм Dynamic Provisioning.

    Когда вы создаете PVC, Kubernetes смотрит на параметр StorageClass (или использует класс по умолчанию). Если провайдер облака поддерживает это, он автоматически создаст реальный диск (например, Google Persistent Disk) и создаст объект PV, привязав его к вашей заявке.

    Лучшие практики для Java-разработчика

  • Никогда не храните секреты в Git. Даже в закодированном Base64 виде. Используйте инструменты вроде Sealed Secrets, Vault или подставляйте значения в CI/CD пайплайне.
  • Используйте ConfigMaps для профилей Spring. Вы можете передать переменную SPRING_PROFILES_ACTIVE через ConfigMap, чтобы переключать поведение приложения (dev, test, prod).
  • Осторожнее с ReadWriteOnce. Если вы используете Deployment с replicas: 3 и PVC с режимом ReadWriteOnce, все три Пода не смогут одновременно писать в один и тот же диск (если они окажутся на разных нодах). Для общих файловых хранилищ нужен режим ReadWriteMany (например, NFS), но он поддерживается не всеми провайдерами.
  • Заключение

    Теперь ваше приложение стало по-настоящему готовым к жизни в кластере. Вы отделили конфигурацию от кода с помощью ConfigMaps, защитили пароли с помощью Secrets и обеспечили сохранность данных с помощью PersistentVolumes.

    В следующей части курса мы рассмотрим, как упростить управление всеми этими YAML-файлами с помощью пакетного менеджера Helm, который является стандартом де-факто для распространения сложных приложений в Kubernetes.

    4. Настройка Liveness и Readiness проб, управление памятью и CPU для JVM

    Настройка Liveness и Readiness проб, управление памятью и CPU для JVM

    В предыдущих статьях мы научились упаковывать Java-приложение в Docker, запускать его в Kubernetes и управлять конфигурацией. Казалось бы, всё готово к продакшену? Не совсем.

    Запустить приложение — это только половина дела. Вторая половина — гарантировать, что оно работает стабильно, эффективно использует ресурсы и умеет сообщать кластеру о своем самочувствии. В этой статье мы разберем две критически важные темы для любого Java-разработчика в Kubernetes: управление ресурсами (CPU/Memory) и настройку проб состояния (Probes).

    Управление ресурсами: Requests и Limits

    Kubernetes — это система управления ресурсами. Чтобы планировщик (Scheduler) мог эффективно размещать Поды на нодах, он должен знать, сколько «железа» нужно вашему приложению. В манифесте это описывается в секции resources.

    Существует два типа ограничений:

  • Requests (Запросы): Это гарантированный минимум. Kubernetes найдет ноду, где свободно именно столько ресурсов. Если свободных ресурсов нет, Под останется в статусе Pending.
  • Limits (Лимиты): Это жесткий потолок. Приложению не позволят потребить больше этого значения.
  • Особенности CPU

    CPU в Kubernetes измеряется в «ядрах» или «милли-ядрах» (m). 1000m = 1 ядро.

    * Если приложение пытается использовать больше CPU, чем указано в Limit, Kubernetes начнет его тротлить (искусственно замедлять), но не убьет. * Java-приложения многопоточны. JVM смотрит на доступное количество ядер для настройки пулов потоков (например, ForkJoinPool). В контейнере JVM (начиная с Java 10) автоматически считывает лимиты CPU и подстраивает Runtime.getRuntime().availableProcessors().

    Особенности Memory (RAM)

    С памятью всё строже. Память — ресурс несжимаемый.

    * Если приложение попытается потребить больше памяти, чем указано в Limit, ядро Linux вмешается и убьет процесс с ошибкой OOM Killed (Out Of Memory).

    !Иллюстрация механизма OOM Killer при превышении лимита памяти

    JVM в контейнере: Математика памяти

    Самая частая ошибка Java-разработчиков в Kubernetes — неправильный расчет памяти.

    Многие считают так: «Я выделил контейнеру 1 Гб памяти, значит, я могу поставить -Xmx1g». Это фатальная ошибка.

    Память процесса Java состоит из нескольких областей:

  • Heap (Куча): Здесь живут ваши объекты. Размер регулируется -Xmx.
  • Metaspace: Здесь живут метаданные классов. Растет по мере загрузки классов.
  • Thread Stacks: Память под стеки потоков (по умолчанию 1 Мб на поток).
  • Code Cache: Скомпилированный JIT-код.
  • GC Overhead: Память, нужная самому сборщику мусора.
  • Direct Buffers: Память вне кучи (NIO).
  • Если вы поставите -Xmx равным лимиту контейнера, то на остальные части (Metaspace, потоки) места не останется, и контейнер будет убит OOM Killer'ом, даже если в Heap еще есть место.

    Золотое правило настройки

    Всегда оставляйте запас (overhead) между -Xmx и лимитом контейнера. Обычно для микросервисов на Spring Boot требуется 25-30% запаса.

    Пример конфигурации для пода с лимитом 1 Гб:

    > Начиная с Java 10, можно использовать флаг -XX:MaxRAMPercentage=75.0 вместо жесткого указания -Xmx. Это заставит JVM автоматически вычислять размер кучи как процент от лимита контейнера. Это более гибкий подход.

    Probes: Как Kubernetes следит за здоровьем

    Kubernetes не умеет читать логи вашего приложения. Он не знает, завис ли ваш Tomcat или просто долго обрабатывает запрос. Чтобы дать кластеру «глаза», используются Probes (Пробы).

    Существует три основных типа проб:

    1. Liveness Probe (Проверка живучести)

    Вопрос: «Ты жив?» Действие при неудаче: Перезагрузить контейнер (Restart).

    Эта проба нужна для ситуаций, когда приложение зашло в тупик (deadlock), зависло в бесконечном цикле и не может восстановиться самостоятельно.

    Важно: Liveness проба должна быть очень легкой. Она не должна проверять подключение к базе данных. Если база данных упадет, а Liveness проба начнет падать у всех микросервисов, Kubernetes перезагрузит весь кластер, что только усугубит проблему (эффект лавины).

    2. Readiness Probe (Проверка готовности)

    Вопрос: «Ты готов принимать трафик?» Действие при неудаче: Исключить IP-адрес пода из балансировщика нагрузки (Service). Трафик перестанет поступать на этот под, но сам под не будет перезагружен.

    Эта проба используется: * При старте приложения (пока Spring поднимает контекст). * Если приложение временно перегружено. * Здесь можно и нужно проверять подключение к базе данных или брокеру сообщений.

    3. Startup Probe (Проверка запуска)

    Используется для приложений, которые грузятся очень долго (минуты). Она откладывает запуск Liveness пробы до тех пор, пока приложение не запустится первый раз.

    !Схема взаимодействия Startup, Liveness и Readiness проб

    Реализация в Spring Boot

    Spring Boot имеет встроенный модуль Actuator, который идеально интегрируется с Kubernetes.

    Добавьте зависимость в pom.xml:

    Включите пробы в application.properties:

    Теперь Spring Boot предоставляет два стандартных URL: * /actuator/health/liveness: Возвращает 200 OK, если внутреннее состояние приложения стабильно. * /actuator/health/readiness: Возвращает 200 OK, если приложение полностью инициализировано и готово к работе.

    Настройка Deployment манифеста

    Теперь добавим эти знания в наш YAML-файл:

    Graceful Shutdown (Плавная остановка)

    Когда Kubernetes решает остановить Под (например, при масштабировании вниз или обновлении версии), он посылает процессу сигнал SIGTERM.

    По умолчанию Java-приложение может оборвать текущие соединения. Чтобы Spring Boot дождался завершения обработки текущих запросов перед выключением, добавьте в конфигурацию:

    В Kubernetes также стоит добавить terminationGracePeriodSeconds в спецификацию Пода, чтобы дать приложению достаточно времени на завершение (значение должно быть больше, чем таймаут в Spring).

    Заключение

    Правильная настройка ресурсов и проб — это то, что отличает «домашний» проект от профессионального решения.

    * Используйте Requests для гарантии ресурсов и Limits для защиты соседей. * Помните, что JVM Heap != Container Memory. Оставляйте запас. * Настраивайте Liveness для перезапуска зависших приложений и Readiness для управления трафиком. * Используйте Spring Boot Actuator для автоматизации этих проверок.

    Теперь ваше приложение не только запущено, но и работает надежно. В следующей части мы рассмотрим, как упростить деплой всей этой конфигурации с помощью пакетного менеджера Helm.

    5. Логирование, мониторинг и упаковка приложений с использованием Helm

    Логирование, мониторинг и упаковка приложений с использованием Helm

    Мы прошли долгий путь. Ваше Java-приложение упаковано в Docker, работает в Kubernetes, имеет настроенные ресурсы и пробы живучести. Но как только приложение уходит в кластер, оно становится «черным ящиком». Вы больше не можете просто открыть консоль IDE и посмотреть, что происходит.

    В этой статье мы вернем вам контроль над ситуацией. Мы разберем, как правильно собирать логи, как настроить мониторинг метрик JVM и как перестать копировать гигабайты YAML-файлов, начав использовать Helm.

    Логирование: Читаем мысли приложения

    В традиционной разработке мы привыкли писать логи в файлы: app.log, error.log. В мире контейнеров это — антипаттерн. Контейнеры эфемерны: они появляются и исчезают. Если ваш Под перезапустится, файл внутри него исчезнет навсегда.

    Правило 1: Пишите в STDOUT/STDERR

    В Kubernetes (и Docker) стандартом является вывод логов в стандартные потоки вывода (System.out и System.err).

    !Визуализация пути логов от Java-приложения через стандартный вывод к системе сбора логов.

    Kubernetes перехватывает все, что приложение пишет в консоль, и сохраняет это. Вы можете просмотреть эти логи командой:

    Правило 2: Структурированное логирование (JSON)

    Когда у вас 100 микросервисов, искать ошибку grep-ом по текстовым файлам невозможно. Вам нужна централизованная система (например, ELK Stack: Elasticsearch, Logstash, Kibana).

    Чтобы такие системы могли эффективно индексировать ваши логи, они должны быть в формате JSON, а не просто текстом.

    Плохо (текстовый формат): 2023-10-05 12:00:00 INFO OrderService: Created order 123 for user Bob

    Хорошо (JSON формат): {"timestamp": "2023-10-05T12:00:00Z", "level": "INFO", "service": "OrderService", "event": "order_created", "orderId": 123, "user": "Bob"}

    Для Spring Boot это настраивается добавлением библиотеки logstash-logback-encoder и конфигурацией logback-spring.xml. Теперь вы сможете в Kibana сделать запрос service:OrderService AND user:Bob и мгновенно найти нужные записи.

    Мониторинг: Приборная панель вашего звездолета

    Логи нужны для расследования инцидентов («Почему упало?»). Мониторинг нужен для понимания текущего состояния («Здорово ли приложение прямо сейчас?»).

    Стандартом де-факто для мониторинга в Kubernetes является Prometheus.

    Pull-модель Prometheus

    В отличие от многих систем, где приложение отправляет метрики на сервер (Push), Prometheus сам приходит к вашему приложению и забирает их (Pull). Это называется scraping (скрапинг).

    !Схема Pull-модели: Prometheus опрашивает приложения через HTTP-endpoint.

    Интеграция Java и Prometheus

    Чтобы ваше Spring Boot приложение «заговорило» на языке Prometheus, нам снова поможет Spring Boot Actuator и библиотека Micrometer.

    Добавьте зависимость в pom.xml:

    И включите эндпоинт в application.properties:

    Теперь по адресу /actuator/prometheus ваше приложение будет отдавать метрики в формате, понятном Prometheus:

    Что мониторить Java-разработчику?

  • JVM Memory: Heap (куча) и Non-Heap. Утечки памяти видны именно здесь.
  • GC (Garbage Collection): Время пауз (Stop-the-world) и частота сборок.
  • Threads: Количество активных потоков. Резкий рост может говорить о зависании (deadlock) или перегрузке.
  • HTTP Request Duration: Как долго ваши контроллеры обрабатывают запросы.
  • Для визуализации этих данных обычно используется Grafana, которая строит красивые графики на основе данных из Prometheus.

    Helm: Пакетный менеджер для Kubernetes

    К этому моменту у нас накопилось много YAML-файлов: deployment.yaml, service.yaml, configmap.yaml, secret.yaml, ingress.yaml.

    Представьте, что вам нужно развернуть приложение в трех окружениях: Dev, Test и Prod. Они отличаются только количеством реплик, версией образа и URL базы данных. Копировать и править файлы вручную — это путь к ошибкам и «Аду YAML-файлов».

    Здесь на сцену выходит Helm.

    > Helm — это как maven или apt для Kubernetes. Он позволяет упаковать ваше приложение со всеми его манифестами в единый архив, называемый Chart (Чарт).

    Структура Helm Chart

    Helm использует шаблонизацию. Вместо жестко прописанных значений в YAML, мы используем переменные.

    Типичная структура Чарта:

    Магия шаблонов

    Посмотрим, как выглядит templates/deployment.yaml в Helm:

    Видите конструкции в двойных фигурных скобках {{ ... }}? Это места, куда Helm подставит значения.

    А вот файл values.yaml, где мы определяем эти значения:

    Установка и обновление

    Теперь, чтобы установить приложение, вам нужна всего одна команда:

    Helm возьмет шаблоны, подставит в них значения из values.yaml и отправит итоговые манифесты в Kubernetes.

    А если для продакшена нам нужно 5 реплик вместо одной? Мы не меняем код, мы просто переопределяем флаг при запуске:

    Или создаем отдельный файл values-prod.yaml:

    И применяем его:

    !Иллюстрация того, как Helm объединяет шаблоны и конфигурацию для генерации финальных манифестов.

    Преимущества Helm

  • Переиспользование: Вы можете написать один универсальный Чарт для всех своих Spring Boot микросервисов.
  • Управление версиями: Helm хранит историю релизов. Если обновление сломало прод, вы можете сделать helm rollback и мгновенно вернуться к предыдущей версии.
  • Зависимости: Ваш Чарт может зависеть от других Чартов. Например, ваше приложение может автоматически разворачивать PostgreSQL и Redis, если прописать их в зависимостях.
  • Заключение курса

    Поздравляю! Мы прошли путь от простого JAR-файла до полноценной облачной архитектуры.

  • Мы упаковали Java в Docker, сделав его переносимым.
  • Мы изучили Pod, Deployment и Service для запуска и масштабирования.
  • Мы настроили ConfigMap и Secret для безопасной конфигурации.
  • Мы защитили приложение лимитами ресурсов и Probes.
  • И, наконец, мы научились наблюдать за приложением через логи и метрики, и упаковали всё это в удобный Helm Chart.
  • Kubernetes — это сложная, но мощная система. Эти знания — фундамент, на котором строятся современные высоконагруженные системы. Теперь вы готовы не просто писать код, но и доставлять его пользователям надежно и эффективно.