image

Когда взаимодействуют разработчики и операторы


Предположим, вы написали приложение на 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 секунд на сбор всего, что вам нужно.

Ссылки




Комментарии (1)


  1. mirwide
    27.01.2023 18:07
    +2

    Heap dump, для анализа проблем в джаве нужен хип дамп, ни каких тепловых карт.
    PS. А еще yaml и все кодблоки поломаны в статье.