img-1

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

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

img-2

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

 

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

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

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

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

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

 

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

img-3

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

 

Для проверки работоспособности пода 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
				
			

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

img-4

Трассировка

 

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

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

img-5

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

 

После того как реализован код приложения и написаны тесты, нужно упаковать приложение в контейнер и залить в какой-либо реестр образов, откуда его сможет получить 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 репозитория, где в разных папках хранятся файлы со значениями плэйсхолдеров. Каждой папке соответствует свой стенд.

Поделиться

ОСТАВЬТЕ КОММЕНТАРИЙВаш электронный адрес не будет опубликован. Обязательные поля отмечены *Ваше имя

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