Диагностика и отладка - java dumps, debugging
Сервисы написанные на java и размещенные в Kubernetes можно отлаживать точно такими же методами, как и java приложения, работающие в другой среде. Другой вопрос, что большинство методов отладки подходит для тестовых стендов, а на промышленных средах для выявления проблем обычно ограничиваются изучением дашбордов и логов.
В данной статье мы последовательно рассмотрим различные средства диагностики и отладки, ситуации когда они могут быть полезны, а также нюансы, связанные с их применением в Kubernetes.
Подготовка образа
Для того, чтобы использовать различные утилиты из состава JDK они должны присутствовать в контейнере, поэтому если мы хотим использовать готовый образ для создания наших контейнеров, то для этих целей вполне подойдет openjdk:17-jdk-oracle. Для сборки образа с нашим Spring Boot приложением можно использовать jib, а как это сделать подробно описано в статье Как развернуть микросервис в Kubernetes
Создание постоянного тома для сохранения дампов
Одним из средств диагностики является снятие дампов памяти и потоков и, чтобы дампы сохранялись после перезагрузки контейнера их нужно хранить либо с использованием Persistent Volume, либо монтируя папку на ноде через hostPath. Последнее можем сделать в Deployment следующим образом:
volumeMounts:
- name: dump-volume
mountPath: /dump
volumes:
- name: dump-volume
hostPath:
path: /var/tmp
type: Directory
В приведенном примере мы монтируем папку /var/tmp на ноде и связываем с ней путь /dump, куда будем помещать и откуда скачивать локально снимаемые дампы
Наличие прав на проброс портов
Для подключения удаленных утилит диагностики и отладки пользователю необходимо иметь права на проброс портов работающего приложения через port-forward
Снятие дампа потоков работающего приложения скриптом jstack
Для снятия максимально полного дампа потоков и записи его в файл thread.dump в папку dump на ноде используем команду:
jstack -l -f 1 > dump/threads.dump
В данной команде, как и в последующих, цифра 1 это pid java-процесса. Если в Deployment основной контейнер — это наше Java приложение, то его pid будет равен 1.
Для скачивания дампа в локальную папку нужно выполнить:
kubectl -n myproject cp service-back-0.01-76788b47d7-4xgkp:/dump/threads.dump /work/jvm-threads.dump
Где -n myproject указывает на нэймспейс, в котором размещен под, service-back-0.01-76788b47d7-4xgkp — имя пода, dump/threads.dump – папка смонтированная на поде, /work – локальная папка
Thread dumps на работающем сервисе имеет смысл снимать, если мы видим на дашборде ресурсов стабильное значительное потребление ресурсов процессора. Данные о затрачиваемых java процессом и всей системой ресурсов процессора Spring Boot предоставляет в метриках process_cpu_usage и system_cpu_usage.
После получения дампа можно воспользоваться одним из множества онлайн анализаторов thread dumps, чтобы сгруппировать потоки по содержимому стека и статусу и посмотреть какую работу выполняют те, что в статусе Running, чтобы найти узкое место в работе приложения. Например, если сняв несколько дампов мы стабильно видим, что потоки выполняют парсинг json объектов, то нужно рассмотреть возможность оптимизации данного кода.
Даже если приложение не затрачивает много ресурсов процессора, то в дампах потоков мы можем увидеть какую работу выполняет приложение. Так если большинство потоков постоянно находятся в статусе RUNNING или WAITING, а в содержимом их стека мы видим ожидание данных от базы, брокера сообщений или других микросервисов, значит причину медленной работы нужно искать в транспортном взаимодействии с инфраструктурой.
Если в образ контейнера мы добавим команду top, то будем иметь также возможность просматривать потребление ресурсов процессора каждым из потоков java приложения. Это можно сделать, выполнив для процесса с pid 1:
top -H -p 1
В столбце CPU мы увидим утилизацию процессора потоками, а в столбце pid идентификатор потока, шестнадцатиричное значение которого отображается в поле nid дампа потоков
Снятие дампа памяти через jmap
Для снятия дампа памяти работающего java процесса мы можем воспользоваться следующей командой:
jmap -dump:live,file=/dump/live-dump.bin 1
Опция live указывает на то, что в дамп необходимо записать только живые объекты без мусора, сократив таким образом размер дампа.
Далее полученный файл можно скачать на локальный диск и проанализировать с помощью Eclipse Memory Analizer
Снятие thread dumps при падении пода
Часто дамп потоков бывает необходимо снять уже по факту падения пода. Для этого можно воспользоваться хуками Kubernetes. Хуки позволяют выполнить определенные команды на события postStart и preStop. Таким образом, по событию preStop мы можем вызвать jstack для снятия дампа:
spec:
containers:
- name: service-back
image: "lokrusta/lokrusta-repo:service-back-0.01-SNAPSHOT"
lifecycle:
preStop:
exec:
command:
- "/bin/sh"
- "-c"
- "jstack -l -e 1 > /dump/jvm-thread-$(date +'%Y%m%d%H%M').dmp"
- "sleep 2m"
Имя файла дампа в приведенном примере генерируется с отметкой текущего времени
Снятие heap dump при падении пода
Для получения дампа памяти при падении приложения в java предусмотрены опции:
-XX:+HeapDumpOnOutOfMemoryError и -XX:HeapDumpPath
Первая указывает на саму необходимость снятия дампа памяти при возникновении OutOfMemory, а вторая путь к файлу где надо разместить дамп:
spec:
containers:
- name: service-back
image: "lokrusta/lokrusta-repo:service-back-0.01-SNAPSHOT"
workingDir: /opt/app
command: [ 'java','-Dspring.config.location=file:/var/app/config/application.yml','-Dfile.encoding=UTF-8',, '-cp', '@/opt/app/jib-classpath-file', '@/opt/app/jib-main-class-file', '-Xms256m', '-Xmx512m','-XX:+HeapDumpOnOutOfMemoryError', '-XX:HeapDumpPath=/dump/dump.bin']
Нужно отметить, что речь идет не о безусловном снятии дампа, а только о случаях, когда java процесс завершился из за ошибки OutOfMemory. Более подробно об этом можно прочитать в статье Все ли мы знаем про OOMKilled
Подключение в debug-режиме к java процессу в Kubernetes
Мы также можем включить возможность удаленной отладки java приложения в контейнере Kunernetes. Для этого необходимо добавить в строку запуска, указываемую в Deployment файле приложения, строку agentlib:jdwp=transport=dt_socket,server=y,address=8001,suspend=n:
spec:
containers:
- name: service-back
image: "lokrusta/lokrusta-repo:service-back-0.01-SNAPSHOT"
workingDir: /opt/app
command: [ 'java','-Dspring.config.location=file:/var/app/config/application.yml','-Dfile.encoding=UTF-8','-agentlib:jdwp=transport=dt_socket,server=y,address=8001,suspend=n', '-cp', '@/opt/app/jib-classpath-file', '@/opt/app/jib-main-class-file', '-Xms256m', '-Xmx512m' ]
В данной строке указан отладочный порт 8001, который нам надо пробросить с пода на локальный узел, используя команду port-forward:
kubectl -n myproject port-forward service-back-0.01-cc6db6cc5-wvt8v 8001
Теперь в среде разработки можно включать удаленную отладку. Например, в Idea это можно сделать создав и запустив Remote Debug Configuration


Удаленное подключение диагностических утилит
Для интеркативного снятия thread dump и heap dump, мониторинга ресурсов, сэмплирования и профилирования кода приложения в контейнере Kubernetes удобно использовать утилиту jVisualVM. Также бывает удобно подключиться к контейнеру с помощью утилиты jConsole, позволяющей в том числе просматривать Mbeans и jmx метрики.
Для удаленных подключений в параметры запуска java приложения в манифесте Deployment нужно добавить следующие настройки:
-Dcom.sun.management.jmxremote=true
-Dcom.sun.management.jmxremote.port=3002
-Dcom.sun.management.jmxremote.rmi.port=3003
-Djava.rmi.server.hostname=127.0.0.1
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false
spec:
containers:
- name: service-back
image: "lokrusta/lokrusta-repo:service-back-0.01-SNAPSHOT"
workingDir: /opt/app
command: [ 'java','-Dspring.config.location=file:/var/app/config/application.yml','-Dfile.encoding=UTF-8','-Dcom.sun.management.jmxremote=true', '-Dcom.sun.management.jmxremote.port=3002','-Dcom.sun.management.jmxremote.rmi.port=3003', '-Djava.rmi.server.hostname=127.0.0.1', '-Dcom.sun.management.jmxremote.authenticate=false', '-Dcom.sun.management.jmxremote.ssl=false', '-cp', '@/opt/app/jib-classpath-file', '@/opt/app/jib-main-class-file', '-Xms256m', '-Xmx512m' ]
Тогда пробросив порты 3002 и 3003:
kubectl port-forward service-back-0.01-7b5f5b687f-9bp2l 3002 3003 -n myproject
Мы сможем подключиться jVisualVM (и jConsole) к localhost по порту 3002:

Мониторинг работы приложения с помощью Java Flight Recorder
Для постоянного и более глубокого мониторинга работы сервисов можно использовать утилиту Java Flight Recorder, которая затрачивая всего 1-2% дополнительных ресурсов может накапливать в динамике информацию о различных параметрах работы java приложения и периодически сбрасывать ее в файлы. Зная примерное время возникновения проблемы можно открыть соответствующий jfr файл c целью выявления потенциальных проблем. Включить и настроить Java Flight Recording при запуске приложения можно следующими параметрами:
-XX:StartFlightRecording и -XX:FlightRecorderOptions
Поскольку тема использования JFR довольно обширна подробная информация о Java Flight Recording будет доступна в отдельной статье