Разработка на Java для Kubernetes

Все о разработке высоконагруженных сервисов на Java в распределенных средах на основе Kubernetes и Istio

Введение в разработку приложений под Kubernetes

 

Сам заголовок статьи может показаться странным. Ведь, разместить в Kubernetes можно любое приложение (ну, или почти любое), упаковав его в контейнер типа Docker-а. Поэтому сразу необходимо сделать два существенных уточнения:

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

— мы будем говорить о построении высоконагруженных, масштабируемых, отказоустойчивых, наблюдаемых приложений корпоративного уровня, использующих при своей работе не только возможности самого Kubernetes, но и другие сопутствующие технологии

Стоит сделать и еще одну оговорку. Большая часть пунктов статьи не относится именно к Kubernetes, а может быть применена и в других аналогичных распределенных средах или, вообще, самостоятельно

Как размещаются экземпляры сервисов в Kubernetes

 

Для сервиса, упакованного в контейнер (это необязательно должен быть Docker), выделяется один или несколько подов (под — это набор из одного или нескольких контейнеров Linux и наименьшая единица приложения Kubernetes). Вместе с контейнером сервиса на этом же поде могут размещаться другие контейнеры, как правило, выполняющие служебные функции. Отдельно для каждого контейнера задаются лимиты оперативной памяти и процессора. Поды располагаются на нодах (нода — это виртуальная или физическая машина в кластере Kubernetes).

Проверки живучести

 

Для проверки работоспособности пода Kubernetes использует тесты работоспособности, называемые livenessprobe и readinessprobe. Первый говорит о том, что под жив, второй о том, что под готов принимать запросы. В случае отрицательного livenessprobe, Kubernetes пересоздает под, так как считает его неработоспособным, в случае непрохождения readinessprobe на под не будут приходить запросы.

Приложение должно обеспечить корректное информирование Kubernetes о своем состоянии и это можно сделать несколькими способами, но мы остановимся на способе проверки через REST-запрос. Необходимо предусмотреть два URL-а, вызов которых методом GET должен возвращать HTTP код 200(OK), если соответствующая проверка пройдена. Далее, мы будем рассматривать разработку с использованием Spring Boot, который предоставляет эту возможность автоматически, с помощью spring-boot-starter-actuator:

				
					implementation "org.springframework.boot:spring-boot-starter-actuator:${springBootVersion}"

				
			

Для включения доступности конечных точек нужно в application.yaml включить следующие настройки:

				
					    
    management:
       server:
        port: 8081
       endpoint:
        prometheus:
          enabled: true
        health:
          probes:
            enabled: true
       endpoints:
        web:
          exposure:
            include: "health"
				
			

Конфигурационные файлы приложения

 

Большинство приложений выносят свои настройки в конфигурационные файлы. В случае Spring Boot основной конфигурационный файл это application.yaml. Этот файл может хранится как внутри приложения, так и в файловой системе. Второй подход более гибкий, поскольку позволяет менять настройки без пересборки приложения. Kubernetes позволяет решить этот вопрос с помощью манифестов ConfigMap.

				
					apiVersion: v1
kind: ConfigMap
metadata:
 name: service-back-cm-0.01
 namespace: myproject
 labels:
     version: "0.01"
     app: service-back
     group: service-back
data:
 application.yml: |-
     server:
        port: 8080
        shutdown: graceful
        http2:
             enabled: true
				
			

Пароли и другие секретные данные

 

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

				
					apiVersion: v1
kind: Secret
metadata:
 name: service-back-db-secret
 namespace: myproject
data:
 db_pass: cXdlcnR5MTIz
type: Opaque
				
			

Доступ к файловой системе

 

Для того, чтобы приложение могло использовать временную файловую систему пода или внешнюю файловую систему, в основном конфигурационном манифесте Deployment нужно смонтировать тома (фактически, папки) разного типа — временные (emptyDir), хранящиеся на ноде (hostPath) или постоянные. В эти тома можно загрузить конфигурационный файлы из ConfigMap и файлы из секретов (например, хранилища ключей)

				
					volumeMounts:
 - name: config
   mountPath: /var/app/config/application.yml
   subPath: application.yml
 - name: log
   mountPath: /opt/app/log
 
volumes:
 - name: config
   configMap:
		name: service-back-cm-0.01
 - name: log
   emptyDir: {}

				
			

Логирование

 

Приложение обязательно должно писать логи. Это единственный способ проанализировать конкретную возникшую ошибку или внештатную ситуацию. С точки зрения разработчика ничего особенного, кроме стандартного логирования в консоль, не требуется. Однако, если требуется сквозной поиск по нескольким взаимодействующим микросервисам, то нужно в лог выводить идентификатор сквозного взаимодействия (в любом виде). Далее, Kubernetes собирает логи со всех подов в файлы на ноде. Для сбора и отправки этих файлов в базу данных, просмотра и поиска в них используются такие инструменты как logstash, promtail, fluentd, grafana, kibana.

Метрики

 

Метрики, фактически, основной способ получить статистику и определить узкие места работающих сервисов продуктовой среде. Остальные способы — подключение в debug режиме, снятие дампов памяти и потоков, скорее всего, будут доступны только на тестовых средах. Кроме того, на основании метрик можно реализовывать алерты (сигналы о критическом состоянии приложения или его частей). Поэтому, отброс метрик приложением обязателен. Наиболее известная библиотека, используемая для отброса метрик, это Micrometer. Отбрасываемые приложением метрики умеет собирать Prometheus, а к нему может обращаться Grafana для построения дашбордов. Базовая библиотека метрик

				
					io.micrometer:micrometer-core
				
			

входит в состав

				
					org.springframework.boot:spring-boot-starter-actuator
				
			

но для экспорта метрик в Prometheus нужно еще подключить:

				
					implementation "io.micrometer:micrometer-registry-prometheus:${micrometerVersion}"
				
			

в состав которой также входит

				
					io.micrometer:micrometer-core
				
			

Пример дашборда

Трассировка

 

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

Пример трассировочного дашборда

Сборка образа и заливка в реестр образов

 

После того как реализован код приложения и написаны тесты, нужно упаковать приложение в контейнер и залить в какой-либо реестр образов, откуда его сможет получить Kubernetes. Разумеется, для java-приложения, базовый контейнер должен содержать jre, но неплохо бы позаботиться о добавлении в контейнер различного рода диагностических утилит. Например:

jstack – для снятия дампов потоков
jmap – для снятия дампов памяти
top – для просмотра процессов и ресурсов
curl – для отправки запросов из консоли

После подготовки и заливки в реестр базового контейнера (в простейшем случае, в качестве базового можно взять и существующий, например, openjdk:17.0.1-jdk-slim) можно применить плагин jib для создания контейнера с приложением на основе базового и заливки его в реестр:

				
					task prepareDockerImageBuild {
    doFirst {
        jib {
            from {
                image = 'openjdk:17.0.1-jdk-slim'
                auth {
                    username = findProperty('DOCKER_USER')
                    password = findProperty('DOCKER_PASSWORD')
                }
            }
            container {
                jvmFlags = ['-Dfile.encoding=UTF-8', '-Dspring.config.location=file:/var/app/config/application.yml']
                format = 'Docker'
                mainClass = project.ext.applicationMainClass
                appRoot = '/opt/app'
            }
            to {
                image = "lokrusta/lokrusta-repo:${project.name}-${applicationVersion}"
                auth {
                    username = findProperty('DOCKER_USER')
                    password = findProperty('DOCKER_PASSWORD')
                }
            }
        }
    }
}

				
			

Deployment — главный конфигурационный файл приложения

 

Главный конфигурационный файл приложения в Kubernetes обычно имеет тип Deployment. В нем в числе прочего указываем:

Метки

				
					
  template:
    metadata:
      labels:
        app: service-back
        version: "0.01"
				
			

Метки используются в конфигах других ресурсов, чтоб указать применяется ли данная конфигурация или ее часть к ресурсу помеченному соответствующей меткой

Образ

				
					
spec:
 containers:
  - name: service-back
    image: "lokrusta/lokrusta-repo:service-back-0.01-SNAPSHOT"
    workingDir: /opt/app
				
			

Образ это ссылка на контейнер с приложением в репозитории образов, который будет установлен в под

Ресурсы памяти и процессора

				
					
resources:
 limits:
  cpu: 1000m
  memory: 1Gi
 requests:
  cpu: 1000m
  memory: 1Gi
				
			

Настройки livenessprobe и readnessprobe

				
					
livenessProbe:
 httpGet:
	path: /actuator/health/liveness
	port: 8081
	initialDelaySeconds: 10
	timeoutSeconds: 5
	periodSeconds: 10
	successThreshold: 1
	failureThreshold: 6
readinessProbe:
 httpGet:
	path: /actuator/health/readiness
	port: 8081
	initialDelaySeconds: 10
	timeoutSeconds: 5
	periodSeconds: 5
	successThreshold: 1
	failureThreshold: 6
				
			

Монтируем тома и указываем их содержимое

				
					
volumeMounts:
 - name: config
   mountPath: /var/app/config/application.yml
   subPath: application.yml
 - name: log
   mountPath: /opt/app/log
 - name: truststore-secret
   mountPath: /etc/dbkey/truststore.jks
   subPath: truststore.jks
 - name: keystore-secret
   mountPath: /etc/dbkey/keystore.jks
   subPath: keystore.jks
				
			
				
					
volumes:
 - name: config
   configMap:
    name: service-back-cm-0.01
 - name: log
   emptyDir: {}
 - name: truststore-secret
   secret:
    items:
    - key: truststore.jks
      path: truststore.jks
   secretName: service-back-dbkey-files-secret
 - name: keystore-secret
   secret:
   items:
    - key: keystore.jks
      path: keystore.jks
   secretName: service-back-dbkey-files-secret	
				
			

Загружаем секреты

				
					
envFrom:
 - secretRef:
    name: service-back-secrets
				
			

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

Подключаем вспомогательные образы (Istio, fluentbit и др)

				
					
template:
 metadata:
	annotations:
	 sidecar.istio.io/inject: "true"
	 sidecar.istio.io/proxyCPU: "200m"
	 sidecar.istio.io/proxyMemoryLimit: "300M"
	 sidecar.istio.io/logLevel: "debug"
	 sidecar.istio.io/excludeOutboundPorts: "9092"
				
			

В данном примере вспомогательный образ Istio (вспомогательные образы принято называть Sidecar) подключается неявно, с помощью технологии webhook, при которой API Kubernetes
сигнализирует о записи какого-либо ресурса и при нотификации проверяется наличие определенных меток или аннотаций и при их наличии код ресурса модифицируется — в данном случае добавляется подключение контейнера Istio.

Сайдкары также можно подключать обычным способом, указывая дополнительные элементы в списке containers

Сетевой доступ

 

В предыдущих пунктах не оговаривалась функциональность приложения, этапы построения которого мы описали, но наверняка наше приложение будет кто-то вызывать. Наиболее простой способ открыть приложение для внешних (из-за пределов кластера) или внутренних (внутри кластера) вызовов, это использование манифеста Service. В случае внутренних вызовов используется Service типа ClusterIP (это тип по-умолчанию)

				
					
apiVersion: v1
kind: Service
metadata:
 name: service-back
 labels:
  app: service-back
  service: service-back
  namespace: myproject
spec:
 ports:
 - port: 8080
   name: http-8080
 - port: 8081
   name: http-8081
 selector:
  app: service-back
				
			

Для внешних вызовов используются типы NodePort и LoadBalancer. Но, обычно, в том числе и с точки зрения безопасности, используются более сложные механизмы внешнего вызова, с использованием манифеста Ingress или Istio Gateway и Virtual Service

Автоматизация установки в Kubernetes и шаблонизатор

 

Ну и наконец приложение надо каким-то образом установить в Kubernetes. Ручная установка производится накатом манифестов командой:

				
					kubectl apply -f <Имя_Файла_Манифеста>
				
			

Но, до этого нам надо еще создать и залить образ, создание которого описывалось ранее

В любом случае, заливка вручную подходит только для небольших тестовых проектов. В промышленной разработке используется 2 варианта:

1) Через Devops-конвейер, который запускает необходимые скрипты, с помощью джобов Jenkins

2) Локально из среды с помощью запуска, соответствующих задач сборщика (например, gradle)

Локальный вариант подходит для установки приложения разработчиками на dev стенды

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

Параметризацию можно производить с использованием шаблонизатора, например jinja:

				
					
app:
 db:
  url: {{ db_conn }}
				
			

Соответственно, скрипт установки должен подставлять вместо меток шаблонизатора (плэйсхолдеров) конкретные значения для данного стенда, а брать сами значения можно из Git репозитория, где в разных папках хранятся файлы со значениями плэйсхолдеров. Каждой папке соответствует свой стенд.

Поделиться

LEAVE A REPLYYour email address will not be published. Required fields are marked *Your Name

Copyright © 2025 Разработка на Java для Kubernetes