Руководство по поиску и устранению неисправностей K8s

ПримечаниеПримечание. Полная ментальная карта Устранение неполадок K8 доступна по адресу: «Устранение неполадок K8s ментальная карта.

Недавно наш кластер K8s столкнулся с некоторыми проблемами процесса Zombie. Поды не могут быть удалены или созданы, и даже не могут быть подключены к узлу по SSH. Мы нашли множество неработающих процессов во многих подах. Симптом в Pod выглядит так:

CPU:   0% usr   0% sys   0% nic  98% idle   0% io   0% irq   0% sirq
Load average: 0.02 0.39 0.46 4/7217 25257
  PID  PPID USER     STAT   VSZ %VSZ CPU %CPU COMMAND
25228 25085 root     R     1988   0%  14   0% top
   28     1 root     S    21.9g  66%  14   0% /usr/bin/java -Dspring.profiles.active=PRD
    1     0 root     S     786m   2%   9   0% ./tools/linux/env-tools
25085     0 root     S     1672   0%   5   0% sh
23278     1 root     Z        0   0%  10   0% [cat]
  157     1 root     Z        0   0%  14   0% [ssl_client]
  177     1 root     Z        0   0%  10   0% [ssl_client]
  196     1 root     Z        0   0%   0   0% [ssl_client]
...

Если вы подсчитаете ps -ef | grep defunct | wc -l, вы увидите, что количество процессов defunct огромно:

$ ps -ef | grep defunct | wc -l
6533

Итак, как решить проблему такого рода? Прежде всего, давайте разберемся, что такое процесс Zombie.

Что такое зомби-процесс

Короче говоря, процесс azombie (несуществующий) — это процесс, который завершил выполнение, но его родительский процесс еще не прочитал код завершения этого процесса. Таким образом, даже процесс завершен, но он остается в таблице процессов. Обычно зомби-процесс не использует много системных ресурсов, но все же занимает запись в таблице процессов, которая по-прежнему использует часть памяти. Это может быть опасно, так как они могут довольно быстро заполнить таблицу процессов.

Если вы хотите узнать больше о том, что такое зомби-процесс, ознакомьтесь с моей статьей: «DevOps в Linux — зомби-процесс».

Приостановить контейнер

При создании пода процесс kubelet сначала вызывает интерфейс CRI RuntimeService.RunPodSandbox, чтобы создать среду песочницы и настроить базовую операционную среду, например сеть. Как только Pod Sandbox установлен, kubelet может создавать в нем пользовательские контейнеры. Когда придет время удалить под, kubelet сначала удалит песочницу пода, а затем остановит все контейнеры внутри.

Контейнер pause — это контейнер, который существует в каждом модуле, он похож на шаблон или родительский контейнер, от которого все новые контейнеры в модуле наследуют пространства имен. Контейнер pause запускается, а затем «засыпает».

Контейнер pause выполняет две основные функции:

  • Он служит основой для совместного использования пространства имен Linux в модуле.
  • Он служит PID 1 для каждого модуля и собирает процессы-зомби (с включенным общим пространством имен PID).

Исходный код контейнера паузы выглядит так (https://github.com/kubernetes-csi/driver-registrar/blob/master/vendor/k8s.io/kubernetes/build/pause/pause.c):

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#define STRINGIFY(x) #x
#define VERSION_STRING(x) STRINGIFY(x)
#ifndef VERSION
#define VERSION HEAD
#endif
static void sigdown(int signo) {
  psignal(signo, "Shutting down, got signal");
  exit(0);
}
static void sigreap(int signo) {
  while (waitpid(-1, NULL, WNOHANG) > 0)
    ;
}
int main(int argc, char **argv) {
  int i;
  for (i = 1; i < argc; ++i) {
    if (!strcasecmp(argv[i], "-v")) {
      printf("pause.c %s\n", VERSION_STRING(VERSION));
      return 0;
    }
  }
  if (getpid() != 1)
    /* Not an error because pause sees use outside of infra containers. */
    fprintf(stderr, "Warning: pause should be the first process\n");
  if (sigaction(SIGINT, &(struct sigaction){.sa_handler = sigdown}, NULL) < 0)
    return 1;
  if (sigaction(SIGTERM, &(struct sigaction){.sa_handler = sigdown}, NULL) < 0)
    return 2;
  if (sigaction(SIGCHLD, &(struct sigaction){.sa_handler = sigreap,
                                             .sa_flags = SA_NOCLDSTOP},
                NULL) < 0)
    return 3;
  for (;;)
    pause();
  fprintf(stderr, "Error: infinite loop terminated\n");
  return 42;
}

Контейнер pause в основном создает отдельное пространство имен для пода. Пока у нас запущен контейнер pause и когда мы запускаем реальный контейнер приложения, он присоединится к следующим пространствам имен контейнера pause:

  • Пространство сетевых имен
  • Пространство имен IPC
  • Пространство имен PID

Такое совместное использование пространства имен имеет следующие преимущества:

  • Позволяет контейнерам взаимодействовать напрямую с помощью локального хоста.
  • Позволяет контейнерам совместно использовать свое пространство имен межпроцессного взаимодействия (IPC) с другими контейнерами, чтобы они могли напрямую взаимодействовать с другими контейнерами через общую память.
  • Позволяет контейнерам совместно использовать пространство имен своего идентификатора процесса (PID) с другими контейнерами.

Обработка процессов зомби

Сбор зомби выполняется контейнером pause только в том случае, если у вас включен общий доступ к пространству имен PID. В K8s v1.8 и выше он отключен по умолчанию, если только он не включен флагом kubelet. Если совместное использование пространства имен PID не включено, то каждый контейнер в модуле K8s будет иметь свой собственный PID 1, и каждый из них должен будет сам пожинать процессы-зомби.

Если совместное использование пространства имен PID включено, PID процесса /pause будет равен 1, поэтому он может вызывать системный вызов wait после завершения дочернего процесса.

Демо

Давайте создадим образ Docker, который будет генерировать процесс zombie.

Файл Docker:

FROM python:bullseye
COPY zombie.py /root/
RUN chmod +x /root/zombie.py
ENTRYPOINT ["python", "/root/zombie.py"]

zombie.py:

import os
import subprocess

pid = os.fork()
if pid == 0:  # child
    pid2 = os.fork()
    if pid2 != 0:  # parent
        print('The zombie pid will be: {}'.format(pid2))
else:  # parent
    os.waitpid(pid, 0)
    subprocess.check_call(('ps', 'xawuf'))

Теперь создадим образ:

$ docker build -t <registry>/zombie:v0.0.1 .
Sending build context to Docker daemon  4.096kB
Step 1/4 : FROM python:bullseye
 ---> 63490c269128
Step 2/4 : COPY zombie.py /root/
 ---> bd4184c3ad49
...
Successfully built 295088e5c98a

Развернуть с отключенным shareProcessNamespace:

zombie_pod.yml:

apiVersion: v1
kind: Pod
metadata:
  name: zombie
spec:
  #shareProcessNamespace: true
  containers:
  - name: zombie
    image: <registry>/zombie:v0.0.1
    imagePullPolicy: Always

Теперь разверните в кластере K8s:

$ kubectl create -f zombie_pod.yml
pod/zombie created

Проверить процесс зума

$ kubectl exec -it zombie -- top
top - 19:51:52 up 66 days,  5:11,  0 users,  load average: 0.55, 0.91, 0.58
Tasks:   4 total,   1 running,   2 sleeping,   0 stopped,   1 zombie
%Cpu(s):  0.0 us,  0.0 sy,  0.0 ni,100.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
MiB Mem :   7847.7 total,   4136.6 free,    759.7 used,   2951.4 buff/cache
MiB Swap:      0.0 total,      0.0 free,      0.0 used.   6765.3 avail Mem

PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
    1 root      20   0   17764  14036   5636 S   0.0   0.2   0:00.15 python
    7 root      20   0   17764  10992   2580 S   0.0   0.1   0:00.00 python
    8 root      20   0       0      0      0 Z   0.0   0.0   0:00.00 python
    9 root      20   0    8940   3788   3312 R   0.0   0.0   0:00.00 top
$ kubectl get po
NAME                     READY   STATUS             RESTARTS   AGE
zombie                   0/1     CrashLoopBackOff   1          4s

Обратите внимание, что PID 8 — это процесс-зомби, а pod находится в состоянии CrashLoopBackOff. Процесс-зомби (несуществующий) не был собран, так как процесс python PID 1 (его родитель) не играл эту роль. Давайте также проверим журналы:

$ kubectl logs zombie
The zombie pid will be: 9
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.1  17748 14336 ?        Ss   20:02   0:00 python /root/zombie.py
root         9  0.0  0.0      0     0 ?        Z    20:02   0:00 [python] <defunct>
root        10  0.0  0.0   8652  3400 ?        R    20:02   0:00 ps xawuf

Вы можете видеть, что есть процесс <defunct>.

Развертывание с включенным shareProcessNamespace:

zombie_pod.yml:

apiVersion: v1
kind: Pod
metadata:
  name: zombie
spec:
  shareProcessNamespace: true
  containers:
  - name: zombie
    image: <registry>/zombie:v0.0.1
    imagePullPolicy: Always
  imagePullSecrets:
  - name: regcredartifactory

Разверните снова и проверьте, есть ли зомби-процесс:

$ kubectl exec -it zombie -- top
top - 19:55:31 up 66 days,  5:14,  0 users,  load average: 0.19, 0.65, 0.55
Tasks:   5 total,   1 running,   3 sleeping,   0 stopped,   1 zombie
%Cpu(s): 25.0 us, 25.0 sy,  0.0 ni,  0.0 id,  0.0 wa,  0.0 hi,  0.0 si, 50.0 st
MiB Mem :   7847.7 total,   4137.0 free,    759.2 used,   2951.6 buff/cache
MiB Swap:      0.0 total,      0.0 free,      0.0 used.   6765.8 avail Mem

PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
    1 root      20   0     972      4      0 S   0.0   0.0   0:00.18 pause
    8 root      20   0   17764  13908   5504 S   0.0   0.2   0:00.12 python
   15 root      20   0   17764  11048   2632 S   0.0   0.1   0:00.00 python
   17 root      20   0    8940   3716   3240 R   0.0   0.0   0:00.01 top
$ kubectl get po
NAME                     READY   STATUS      RESTARTS   AGE
zombie                   0/1     Completed   1          3s

Обратите внимание, что модуль zombie теперь находится в статусе Completed. Давайте проверим журналы

$ kubectl logs zombie
The zombie pid will be: 150
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root       143  0.2  0.1  17764 14492 ?        Ss   19:46   0:00 python /root/zombie.py
root       151  0.0  0.0   8652  3336 ?        R    19:47   0:00  \_ ps xawuf
root         1  0.0  0.0    972     4 ?        Ss   18:51   0:00 /pause

Мы видим, что зомби-процесс был получен без указания процесса инициализации или добавления его в контейнер. Процесс инициализации теперь представляет собой процесс pause (на самом деле это контейнер).

Заключение

Поды совместно используют много ресурсов, поэтому имеет смысл использовать совместное пространство имен процессов. Однако некоторые контейнеры могут быть изолированы от других, поэтому важно понимать различия:

  • Процесс контейнера больше не имеет PID 1. Некоторые контейнеры отказываются запускаться без PID 1 (например, контейнеры, использующие systemd) или запускают такие команды, как kill -HUP 1, чтобы сигнализировать процессу контейнера. В модулях с общим пространством имен процессов kill -HUP 1 будет сигнализировать о песочнице модуля (/pause в приведенном выше примере).
  • Процессы видны другим контейнерам в модуле. Сюда входит вся информация, видимая в /proc, например, пароли, которые были переданы в качестве аргументов или переменных среды. Они защищены только обычными разрешениями Unix.
  • Файловые системы контейнеров видны другим контейнерам в поде по ссылке /proc/$pid/root. Это упрощает отладку, но также означает, что секреты файловой системы защищены только разрешениями файловой системы.

Ссылка: