❯ Когда взаимодействуют разработчики и операторы
Предположим, вы написали приложение на Java и развернули его в Kubernetes в среде разработки. Рано или поздно это приложение уйдёт в продакшен, и вам придётся узнать, каково оно на деле. Затем начинают возникать новые неожиданные проблемы. Причин у таких проблем может быть множество: слишком много пользователей, утечки памяти, условия гонки, и на этапе разработки такие проблемы выявить сложно.
Разумеется, в таких случаях неисправности требуется исправлять, и первым делом нужно запустить анализ коренных причин отказов, который вывел бы нас к источнику проблем. На ноутбуке это сделать просто: когда приложение заблокировано, можно выводить дампы потоков, тепловые карты и пытаться понять, откуда возникает блокировка.
В Kubernetes всё устроено немного иначе. Вам наверняка рекомендовали добавить отдельную конечную точку для проверки работоспособности, поэтому k8s уже успеет перезапустить под до вашего вмешательства. После этого под выглядит свежим, и в нём потеряна вся информация, которая вам требовалась. В этом посте будет показан очень простой способ, позволяющий снять информацию в такой ситуации.
❯ Перехватчики жизненного цикла в Kubernetes
Основная возможность Kubernetes, которой мы будем здесь пользоваться, называется Перехватчики жизненного цикла в контейнерах. Эта возможность позволяет выполнять некоторые команды в два определённых момента в рамках жизненного цикла пода:
- Перехватчик
PostStart:
сразу после запуска - Перехватчик
PreStop:
непосредственно перед остановом
Разумеется, в данном случае наиболее важен перехватчик
PreStop
. Он срабатывает всякий раз, когда Kubernetes направляет контейнеру команду останова. Это может произойти, если под удаляется, либо, если проверка работоспособности перестаёт откликаться, либо, если вы сами вручную или автоматически останавливаете контейнер (например, новые развёрнутые инстансы).Чтобы сконфигурировать перехватчик, нужно просто добавить к нашему ресурсу такую конфигурацию:
apiVersion: v1
kind: Pod
metadata:
name: mypod
spec:
containers:
- image: myapp:latest
lifecycle:
preStop:
exec:
command:
- /bin/sh
- /scripts/pre-stop.sh
...
terminationGracePeriodSeconds: 30
В данном примере Kubernetes выполнит скрипт
pre-stop.sh
прямо перед остановом контейнера. Другой важный параметр здесь — terminationGracePeriodSeconds
, означающий, сколько времени k8s будет дожидаться остановки пода: это обычный запущенный процесс, но в то же время и перехватчик останова. Вот почему скрипт preStop
должен выполняться достаточно быстро.❯ Приложение для тестирования
Чтобы продемонстрировать весь процесс, я разработал для примера приложение на Java, основанное на Quarkus. Это приложение позволит нам протестировать некоторые сценарии отказов. В данном случае идея такова: предоставлять REST API, который позволял бы переводить приложение в неисправный режим. Здесь как раз тот случай, когда ситуацию проще объяснить в коде, чем на словах – и вот этот код:
given()
.when().get("/q/health/live")
.then()
.statusCode(200);
given()
.when().put("/shoot")
.then()
.statusCode(200)
.body(is("Application should now be irresponsive"));
given()
.when().get("/q/health/live")
.then()
.statusCode(503);
❯ Снятие информации, нужной для устранения неполадок
Чтобы снять некоторую полезную информацию, можно попытаться запустить следующий скрипт. Разумеется, можно добавить и другие команды, чтобы узнать дополнительные подробности. Этот скрипт выдаст вам:
- Список открытых соединений
- Состояние процесса java
- Результат проверки liveness-пробы
- Использование памяти в JVM
- Дамп потоков
Этот скрипт можно было бы сохранить непосредственно в образе, но он не слишком поможет, если вы хотите внести только небольшие изменения. Вместо этого давайте сохраним его в карте настроек и смонтируем
ConfigMap
как отдельный том.apiVersion: v1
kind: ConfigMap
metadata:
name: pre-stop-scripts
data:
pre-stop.sh: |
#!/bin/bash
set +e
NOW=`date +"%Y-%m-%d_%H-%M-%S"`
LOGFILE=/troubleshoot/${HOSTNAME}_${NOW}.txt
{
echo == Open Connections =======
netstat -an
echo
echo == Process Status info ===================
cat /proc/1/status
echo
echo == Health endpoint =========================
curl -m 3 localhost:8080/q/health/live
if [ $? -gt 0 ]; then
echo "Health endpoint resulted in ERROR"
fi
echo
echo == JVM Memory usage ======================
jcmd 1 VM.native_memory
echo
echo == Thread dump ===========================
jcmd 1 Thread.print
} >> $LOGFILE 2>&1
Этот скрипт ожидает, что у него будет доступ с правом записи в каталог
/troubleshoot
, чтобы сохранить результат. Для этого потребуется предоставить дополнительный том.❯ Развёртывание приложения с перехватчиком PreStop
Вот полный ресурс развертывания, который мы создадим:
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: java-k8s-playground
name: java-k8s-playground
spec:
replicas: 1
selector:
matchLabels:
app: java-k8s-playground
template:
metadata:
labels:
app: java-k8s-playground
spec:
containers:
- name: java-k8s-playground
image: dmetzler/java-k8s-playground
# Health probes (1)
livenessProbe:
failureThreshold: 3
httpGet:
path: /q/health/live
port: 8080
scheme: HTTP
initialDelaySeconds: 5
periodSeconds: 5
successThreshold: 1
timeoutSeconds: 10
readinessProbe:
failureThreshold: 15
httpGet:
path: /q/health/ready
port: 8080
scheme: HTTP
initialDelaySeconds: 5
periodSeconds: 5
successThreshold: 1
timeoutSeconds: 3
# We ask to run the troubleshoot script when stopping (2)
lifecycle:
preStop:
exec:
command:
- /bin/sh
- /scripts/pre-stop.sh
# Volumes to get the script and store the result (3)
volumeMounts:
# The config map that contains the script
- mountPath: /scripts
name: scripts
# Where to store the information
- mountPath: /troubleshoot
name: troubleshoot
volumes:
- emptyDir: {}
name: troubleshoot
- configMap:
defaultMode: 493
name: pre-stop-scripts
name: scripts
terminationGracePeriodSeconds: 30
Сначала конфигурируем проверки работоспособности, так, что, когда приложение оказывается неработоспособно, Kubernetes автоматически перезапускает под.
Именно здесь мы сообщаем Kubernetes, что нужно запустить наш скрипт при остановке пода.
Мы предоставляем два пода. Один – чтобы монтировать скрипт, помогающий устранять неполадки, а другой – для сохранения результата. Здесь воспользуемся каталогом
emptyDir
. Это может оказаться не так просто, если планировщик перебросил под на другой узел (например, после удаления). Возможно, мы предпочтём использовать том, отличающийся большей персистентностью. По нашему опыту, контейнер будет просто перезапущен, так что такой проблемы не возникает.❯ Стрельба на поражение
Теперь давайте проведём наш эксперимент. Для начала развёртываем два наших ресурса:
$ kubectl create -f configmap.yaml
configmap "pre-stop-scripts" created
$ kubectl create -f deployment.yaml
deployment "java-k8s-playground" created
$ kubectl get deployment java-k8s-playground
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
java-k8s-playground 1 1 1 1 5s
Наше приложение заработало, и теперь нам нужно просто при помощи
cURL
постучать в конечную точку /shoot
, чтобы переключиться в неисправное состояние.$ kubectl exec -it java-k8s-playground-65d6b9b4f4-xrjgn -- bash
bash-4.4$ cd /troubleshoot/
bash-4.4$ ls
java-k8s-playground-65d6b9b4f4-xrjgn_2021-03-27_18-06-48.txt
bash-4.4$
bash-4.4$ head -n 5 java-k8s-playground-65d6b9b4f4-xrjgn_2021-03-27_18-06-48.txt
== Open Connections =======
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp6 0 0 :::8080 :::* LISTEN
tcp6 0 0 172.16.247.87:8080 172.16.246.1:42238 TIME_WAIT
Вот и всё, теперь в файле записана вся нужная нам информация. Полный вывод нашего скрипта находится здесь.
❯ Воспользуемся сообщением для завершения Kubernetes
Операция по добавлению тома, в котором будет храниться результат нашего исследования, может показаться несколько сложной, если проводить её в сценариях с развёртыванием в продакшене. В Kubernetes имеется и более лёгкое, но функционально ограниченное решение, позволяющее сохранить некоторую информацию о завершении. Речь идёт о сообщении для завершения (termination message).
В определении пода можно указать
spec.terminationMessagePath
(по умолчанию /dev/termination-log
), где можно записать некоторую информацию о том, почему под был остановлен. В документации указано, что:Вывод лога ограничен 2048 байтами или 80 строками, в зависимости от того, какой вариант меньше.
В нашем случае вывод скрипта обычно побольше (около 30 Кб), поэтому мы не можем перенаправить всё содержимое в этот файл. Нет, на самом деле было бы целесообразно сохранять только вывод health-пробы. Поэтому можно было бы видоизменить наш скрипт вот так:
...
echo == Health endpoint =========================
curl -m 3 localhost:8080/q/health/live > /dev/termination-log
if [ $? -gt 0 ]; then
echo "Health endpoint resulted in ERROR"
fi
cat /dev/termination-log
echo
...
Повторив тот же самый разрушающий эксперимент, посмотрим, что у нас теперь указано в описании пода:
$ oc describe pod java-k8s-playground-65d6b9b4f4-xrjgn
Name: java-k8s-playground-65d6b9b4f4-xrjgn
Namespace: XXXXX
Node: ip-XXXXXX.ec2.internal/XXXXXX
Start Time: Sat, 27 Mar 2021 18:41:27 +0100
....
Containers:
java-k8s-playground:
...
State: Running
Started: Sat, 27 Mar 2021 19:06:49 +0100
Last State: Terminated
Reason: Error
Message:
{
"status": "DOWN",
"checks": [
{
"name": "App is down",
"status": "DOWN"
}
]
}
Exit Code: 143
Started: Sat, 27 Mar 2021 18:42:29 +0100
Finished: Sat, 27 Mar 2021 19:06:49 +0100
Ready: True
Restart Count: 2
$
Kubernetes смог получить результат пробы и сохранить его в разделе Last State (также доступно в
status.containerStatuses[0].lastState.terminated.message
в YAML-представлении). Разумеется, здесь мы можем добавить только ограниченный объём данных, так как он, вероятно, окажется в etcd
.Но так можно быстро получить несколько полезных сообщений о состояниях, и для этого не требуется заранее предусматривать и планировать скрипт для устранения неполадок. Например, в Nuxeo можно сохранить результат пробы
runningstatus
. Ещё один вариант – установить terminationMessagePolicy
в FallbackToLogsOnError
. В таком случае, если сообщение будет пустым, то Kubernetes возьмёт последнюю строку логов консоли в виде сообщения. Итак, чтобы быстро получить статус, предваряющий устранение неполадок при использовании Nuxeo в Kubernetes, нужно добавить в определении пода следующий код:apiVersion: v1
kind: Pod
metadata:
name: mypod
spec:
containers:
- image: nuxeo:LTS
lifecycle:
preStop:
exec:
command:
- /bin/bash
- -c
- "/usr/bin/curl -m 3 localhost:8080/nuxeo/runningstatus > /dev/termination-log"
terminationMessagePolicy: FallbackToLogsOnError
В таком случае результат действующей пробы статуса будет сохранён в последнем состоянии нашего контейнера. Например, очень распространённая ошибка, допускаемая при конфигурации учётных данных в S3, легко выявляется; для этого достаточно всего лишь просмотреть последнее сообщение о состоянии.
❯ Вывод
Когда мы имеем дело с приложениями в продакшене, и приложение отказывает, первая реакция – посмотреть в логи. Это, конечно, полезно, но иногда бывает сложно перерыть тысячи строк логов, особенно в Java, где стектрейсы иногда крайне многословны. Дампы потоков или тепловых карт обычно очень помогают, если решение не вполне очевидно, и по одним только логам не просматривается.
При помощи перехватчиков жизненного цикла в Kubernetes удобно снимать состояние вашего приложения на момент его перезапуска. На самом деле рекомендуется заранее предусматривать такое событие, так как по умолчанию k8s даёт 30 секунд на сбор всего, что вам нужно.
mirwide
Heap dump, для анализа проблем в джаве нужен хип дамп, ни каких тепловых карт.
PS. А еще yaml и все кодблоки поломаны в статье.