❯ Когда взаимодействуют разработчики и операторы
Предположим, вы написали приложение на 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);
Приложение загружено на DockerHub, там оно называется dmetzler/java-k8s-playground
.
❯ Снятие информации, нужной для устранения неполадок
Чтобы снять некоторую полезную информацию, можно попытаться запустить следующий скрипт. Разумеется, можно добавить и другие команды, чтобы узнать дополнительные подробности. Этот скрипт выдаст вам:
- Список открытых соединений
- Состояние процесса 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 # Пробы работоспособности (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 # При останове запрашиваем здесь запуск скрипта для устранения неполадок (2) lifecycle: preStop: exec: command: - /bin/sh - /scripts/pre-stop.sh # Тома для получения скрипта и сохранения результата (3) volumeMounts: # Карта настроек, содержащая скрипт - mountPath: /scripts name: scripts # Где сохранять информацию - 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
, чтобы переключиться в неисправное состояние.
$ oc get pod -l app=java-k8s-playground NAME READY STATUS RESTARTS AGE java-k8s-playground-65d6b9b4f4-xrjgn 1/1 Running 1 23m $ kubectl exec -it java-k8s-playground-65d6b9b4f4-xrjgn -- bash bash-4.4$ curl -XPUT localhost:8080/shoot Application should now be irresponsive bash-4.4$ command terminated with exit code 137 $ # Здесь мы наконец=то теряем соединение, так как под перезапущен k8s The container has been restarted by Kubernetes. Let’s see what we have in the newly created container. $ 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 секунд на сбор всего, что вам нужно.
❯ Ссылки
ссылка на оригинал статьи https://habr.com/ru/company/timeweb/blog/713248/
Добавить комментарий