Рассказываем о проблеме, которая возникла в процессе эксплуатации кластера Kubernetes.
Проблеме когда, при запасе ресурсов программы работали так, словно процессор используется на 100% или есть нехватка оперативной памяти. А также о поиске источника проблемы, пути ее решения, выдвинутых гипотезах и проведенных экспериментах.
Что случилось
В какой-то момент мы заметили, что система стала странно себя вести, хотя нагрузка на процессор и оперативную память оставалась неизменной и не уходила в потолок.
Kubelet, базовая программа на нодах kubernetes, стала вести себя странно: процессор и память имели не 100% нагрузку, диск практически простаивал, никакого чтения и записи не было, но при создании подов kubelet медленно их поднимал, и это длилось в одном случае две минуты, а в другом пять.
Разница в разворачивании подов:
Events
Type Reason Age From Message
--- --- --- --- ---
Normal Scheduled 5S default-scheduler Successfully assigned some-ns/some-pod-688653457-snqkn to k8s-node-4
Normal Pulling 5s kubelet Pulling image "registry.site.ru/project/image master"
Normal Pulled 5s kubelet Successfully pulled image "registry.site.ru/project/image:master" in 155.9ms
Normal Created 342s kubelet Created container worker
Normal Started 4s kubelet Started container worker
Events
Type Reason Age From Message
--- --- --- --- ---
Normal Scheduled 5s default-scheduler Successfully assigned some-ns/some-pod-688653457-snqkn to k8s-node-4
Normal Pulling 5s kubelet Pulling image "registry.site.ru/project/image:master"
Normal Pulled 5s kubelet Successfully pulled image "registry.site.ru/project/image master" in 155.9ms
Normal Created 5s kubelet Created container worker
Normal Started 4S kubelet Started container worker
На изображении выше продемонстрировано, как было (вверху) и как должно быть (внизу). Также стоит отметить, что при передеплое старые поды находились в бесконечном терминейтинге, то есть не могли завершиться.
Как выявили
- Анализ всего
Изучая логи системы, kubelet и всего, что можно, мы поняли следующее: хотя CPU и MEM загружены не на 100%, но Load Average показывает уже значения за пределами приемлемого. Если посмотреть вывод команды top, то видно, что Load Average равен 10, при максимально положенных 8 (поскольку 8 соответствует количеству ядер CPU. Соответственно, при восьмиядерном CPU, 100% Load Average равен 8).
Вывод команды top:
top - 12:07:56 up 8 day 1:46, 1 user, load average: 10,89,12,33, 11,42
Tasks: 808 total, 1 running, 675 sleeping, 0 stopped, 0 zombie
%Cpu(s): 7,1 us, 2,8 sy, 0,0 ni, 67,4 id, 31,1 wa, 0,0 hi, 0,0 si,
МиБ Mem : 15960,3 total, 1651,7 free, 5803,9 used, 8447,7 buff/cache
МиБ Swap:0,0 total, 0,0 free, 0,0 used. 10163,3 avail Mem
Не менее странным делом стал wa со значением 31%, а wa, как говорится в мануале, это процент времени (циклов, секунд), в течение которого процессор простаивал, ожидая завершения операции ввода-вывода. Если смотреть на статистику по нагрузке на систему, но уже с Load Average, заметим, что у нас высокий Load Average, однако причина не ясна.
Если посмотрим на этот же график, но уже с отображением того, что использует процессор, то увидим следующее.
- Первые догадки
Тут уже видно, что большую часть времени процессор занят ожиданием ввода/вывода какой-то операции. Значит некий процесс или даже процессы влияют на iowait. При первом взгляде на процессы невозможно увидеть, что же с ними не так, но вот если их отфильтровать по состоянию, то уже будет видно несколько процессов, находящихся в состоянии D.
ps -aux --forest
root 10844 0.0 0.0 111700 8692? SI Jul14 1:26/usr/bin/containerd-shim-runc-v1-namespace k8s.io-id
0411fcb51221ad40f9cfa79369ed9002ff2a3974bd37461c705a3d8ffeba4830 -address /run/containerd/containerd.sock
root 10844 0.0 0.0 111700 8692 ? Sl Jul14 1:26 /usr/bin/containerd-shim-runc-vl
70 10865 0.0 0.1 193128 20896 ? Ss Jul14 0:07 \_ postgres:
70 11116 0.0 0.0 193252 5304 ? Ss Jul14 0:00 \_ postgres: checkpointer
70 11117 0.0 0.0 193224 4060 ? Ss Jul14 0:23 \_ postgres: background writer
70 11118 0.0 0.0 193192 7536 ? Ss Jul14 0:01 \_ postgres: walwriter
70 11119 0.0 0.0 193848 15768 ? Ss Jul14 0:18 \_ postgres: autovacuum launcher
70 11120 0.0 0.0 47648 3268 ? Ss Jul14 0:14 \_ postgres: stat collector
70 1663298 0.0 0.9 259752 160188 ? Ss Jul15 0:46 \_ postgres: archive 192.168.55.166(44910) SELECT
70 1701237 0.0 0.9 208108 155492 ? Ss Jul15 0:41 \_ postgres: archive 192.168.55.169(39990) UPDATE
70 1794331 0.0 1.0 258064 168340 ? Ss Jul15 0:05 \_ postgres: parallel worker for PID 1705
70 1794332 0.0 1.0 258064 168448 ? Ss Jul15 0:05 \_ postgres: parallel worker for PID 1705
70 1805219 0.0 0.0 0 0 ? Ss Jul15 0:00 \_ [postgres]
70 1036859 0.0 0.0 193388 3480 ? Ss 07:36 0:00 \_ postgres: logical replication launcher
Состояние D означает, что процесс находится в режиме сна и ожидает дискового ввода/вывода. Иначе говоря, у нас запущен под, в котором есть повисший процесс. Предположим, что-то случилось с диском. Однако система не висит, и остальные процессы работают нормально. Тогда что не так? Кто знает, что где сломалось…
Если посмотреть, что говорит kubelet, то увидим следующее:
Логи kebelet:
k8s-node-4 kubelets[763]:
pod "cbb-a0" found, but error fail to check mount point "/var/lib/kubelet/pods/cbb-a0/vol/k8s.io~nfs/pvc-5b": stat
/var/lib/kubelet/pods/cbb-a0/vol/k8s.io~nfs/pvc-5b:
operation not permitted occured during checking mounted volumes from disk
По сути, он говорит, что есть какой-то под, а также ошибка, которая не дает проверить точку монтирования.
- Суммирование информации
У нас есть под, у которого что-то смонтировано, и kubelet, который ругается на точку монтирования. Еще в этом поде выполняются процессы, которые висят в состоянии D, тем самым повышая iowait, из-за которого возрос Load Average.
Если вспомнить, что за процессы там выполнялись, то поймем, что это был postgres. Скорее всего, во время работы он что-то писал на смонтированную папку, и произошел сбой. А если посмотреть describe контейнера или пода, то там как раз увидим NFS storage, в который смонтирована папка с базой postgres.
Mounts:
/var/lib/postgresql/data/ from storage (rw)
/var/run/secrets/kubernetes.io/serviceaccount from default-token-phkbs (ro)
Conditions:
Type Status
Initialized True
Ready True
ContainersReady True
PodScheduled True
Volumes:
storage:
Type: PersistentVolumeClaim (a reference to a PersistentVolumeClaim in the same namespace)
ClaimName: storage-postgres-0
Mounts:
/var/lib/postgresql/data/ from storage (rw)
Volumes: <none>
Volume Claims:
Name: storage
Как решили
- Идея
В какой-то момент возникла мысль: «А давайте убьем под - и дело с концом. Посмотрим, правда ли в нем проблема была». Что мы и сделали. После этого под завис в состоянии терменейтинг и не завершался.
- Эксперименты
Завершаем под принудительно и… ничего. Load Average опустился немного и снова возрос. А что с процессами?
Процессы, что остались:
root 10844 0.0 0.0 111700 8692 ? Sl Jul14 1:28 /usr/bin/containerd
70 10865 0.0 0.0 0 0 ? Ss Jul14 0:07 \_ [postgres]
70 1701237 0.0 0.0 0 0 ? Ds Jul15 0:41 \_ [postgres]
70 1805219 0.0 0.0 0 0 ? Ss Jul14 0:00 \_ [postgres]
Те, что были в состоянии D, так и остались. Тут возникла новая идея: «Тогда давайте завершим процесс. Проверим, что у нас этот процесс есть внутри контейнера». Здесь уже нам открылись два пути: напрямую убить процесс на хостовой машине или завершить внутри контейнера.
Завершение работы контейнера:
ctrl -n k8s.io task Is | grep 10865
tages@k8s-node-4:-$ sudo ctr -n k8s.io task Is | grep 16865
0411fcb51221ad40f9cfa79369ed9002ff2a3974bd37461c705a3d8ffeba4830 10865 RUNNING
Ctrl -n k8s.io task kill -s SIGKILL 0411fcb51221ad40f9cfa79369ed9002ff2a3974bd37461c705a3d8ffeba4830
kill -9 10865
Применили команды.
Ничего не произошло:
tages<ak8s-node-4:ps -efl | awk ‘{if ($2 == “D”) print $0}’
1 D root 1182329 2 0 80 0 - 0 - 08:36 ? 00:00:00 [10.0.30.100-man]
1 D 70 1701237 10865 0 80 0 - 0 - Jull5 ? 00:00:41 [postgres]
1 0 70 1805219 10865 0 80 0 - 0 - Jull5 ? 00:00:00 [postgres]
tages@k8s-node-4:~$ sudo ctr -n k8s.io task Is | grep 10865
0411fcb51221ad40f9cfa79369ed9002ff2a3974bd37461c705a3d8ffeba4830 10865 RUNNING
tages@k8s-node-4: $ sudo ctr -n k8s.io task kill -s SIGKILL 0411fcb51221ad40f9cfa79369ed9002ff2a3974bd37461c705a3d8ffeba4830
tages@k8s-node-4: $ ps -efl | awk '{if ($2 == ”0") print $0}'
1 D root 1183177 2 0 80 0 - 0 - 08:36 ? 00:00:00 [10.0.30.100-man]
1 D 70 1701237 10865 0 80 0 - 0 - Jull5 ? 00:00:41 [postgres]
1 D 70 1805219 10865 0 80 0 - 0 - Jull5 ? 00:00:00 [postgres]
tages@k8s-node-4:~$ sudo ctr -n k8s.io task Is | grep 10865
O411fcb51221ad40f9cfa79369ed9002ff2a3974bd37461c705a3d8ffeba4830 10865 RUNNING
root 10844 0.0 0.0 111700 8692 ? SI Jull4 1:28 /usr/bin/containerd
70 10865 0.0 0.0 0 0 ? Ss Jull4 0:07 \_ [postgres]
70 1701237 0.0 0.0 0 0 ? Ds Jull5 0:41 \_ [postgres]
70 1805219 0.0 0.0 0 0 ? Ds Jull5 0:00 \_ [postgres]
Все осталось, как и было. И тут возник вопрос: «Что не так, почему они не завершаются?».
- Эврика
На самом деле всё оказалось очень просто. По порядку: запускается под, монтируется база по NFS, запускается Postgres и начинает работать с базой. В какой-то момент работы базы, например, UPGRADE DELETE WRITE, наш сервер с NFS-HDD не успевает обработать поток данных (не забываем, что у NFS тоже есть свой iowait, а у диска iops происходит задержка в записи/чтении, и postgres повисает на выполнении команды). К слову, конкретно эта база оказалась слишком большой, и была запущена на NFS-HDD. В результате получили повисший процесс в состоянии D. Следом kubelet, так как он все контролирует и запускает, начинает много времени уделять на под с зависшим процессом, вследствие чего происходит медленный старт и остановка подов.
А убить процесс мы уже не можем, так как он работает в режиме непрерывности, ядро не позволит его убить, пока не завершится ввод/вывод данных на/с диска.n
Остался только один вариант решения текущей проблемы - полностью перезагрузить ноду, ведь процесс нам уже не завершить.
Итоги
Мы практически избавились от подобных проблем и нивелировали при возникновении в будущем, а также сделали выводы:
- При использовании NFS, в рамках Kubernetes, следует обязательно использовать NFSv3 с опцией soft. Так как NFSv4 является stateful-протоколом и может использовать только hard опцию, а значит может подвиснуть в случае потери соединения и заморозить I/O, что у нас и происходило. В то же время, NFSv3 является stateless-протоколом и может использовать опцию soft, так что в случае потери соединения он просто выдаст ошибку, после чего сразу перезапустится.
- Не использовать медленные диски на задачах, которые слишком часто обращаются к диску. В нашем случае мы перенесли ресурсоемкие проекты на NFS-SSD.
- Не использовать медленную сеть на больших объемах данных для NFS и контролировать поток данных.
- Корректно и с запасом выделять ресурсы на серверах для всех проектов, потому что один проект, который потребляет слишком много ресурсов, может навредить работе других. К примеру, даже если у вас быстрый диск, но выделено мало процессорного времени, то к процессору будет копиться очередь на обработку запросов к диску.
- Необходимо реализовать мониторинг метрик и оповещений. Чтобы узнавать о надвигающейся нагрузке заранее мы выделили часть метрик и создали оповещения на Load Average и iowait со значениями близкими к пороговым.
Бонус: рекомендуем для прочтения
Для углубления в тему Load Average советуем ознакомиться со следующими материалами:
- Load Average / Хабр
- CPU Load: когда начинать волноваться? / Хабр
- Как считается Load Average / Хабр
- Изучаем процессы в Linux / Хабр
- Load Average в Linux: разгадка тайны / Хабр
- UNIX Load Average Part 1: How It Works / Help System
- UNIX Load Average Part 2: Not Your Average Average / Help System
- UNIX Load Average: Reweighed / Help System
- The Instrumentation of Multics / Web Mit
- Процессы в Linux / OpenNET
- Управление процессами и прерываниями / Linux по-русски
- Разработка ядра Linux / Викичтение
Автор: Максим Абдулхаликов (DevOps-инженер TAGES)