Это перевод статьи системного инженера Cloudflare Брэкстона Шейфера: автор рассказывает о том, как его команда сумела выявить причину медленной работы Atlantis и устранить её маленьким изменением в конфиге Kubernetes.
Каждый перезапуск Atlantis — нашего инструмента для работы с Terraform — оборачивался 30-минутным простоем. То есть никаких тебе plan, apply или изменений в инфраструктуре для любого репозитория, которым управлял Atlantis. Мы перезапускали его по 100 раз в месяц — то для смены учётных данных, то для новых проектов. В итоге — минус 50 часов работы инженеров каждый месяц, да ещё и каждый раз дёргали дежурного инженера.
Как оказалось, всё дело было в слишком «осторожной» настройке Kubernetes, заданной по умолчанию. Она и стала проблемой, когда том Atlantis разросся до миллионов файлов. Рассказываем, как нашли корень зла и исправили всё одной строчкой.
Таинственно медленные рестарты
Мы управляем десятками Terraform-проектов через Merge Request'ы в GitLab, а Atlantis помогает нам планировать и применять изменения. Он также следит, чтобы два MR'а одновременно не могли менять один и тот же проект.
Он запущен как StatefulSet с единственной репликой, а для хранения состояний на диске использует PersistentVolume (PV) Kubernetes. Когда нужно добавить (или убрать) Terraform-проект или обновить доступы, приходится перезапускать Atlantis, чтобы тот подхватил изменения. Вот этот перезапуск как раз и занимает полчаса.
Проблема стала очевидна, когда у нас закончились inodes на томе Atlantis, и нам пришлось его перезапустить, чтобы увеличить размер тома. Каждый файл и директория съедают один inode, а их общее число задаётся при создании диска. Наша Kubernetes-платформа использует Ceph, который не позволяет прокинуть флаги в mkfs, так что нам приходится мириться с дефолтными значениями. Единственный выход — увеличивать размер всего диска, а для этого нужен перезапуск пода.
Сначала была идея просто сделать алерты менее чувствительными, но это лишь замаскировало бы проблему и задержало бы нашу реакцию на реальные инциденты. Вместо этого мы решили разобраться, почему так происходит.
Плохое поведение
Когда нам нужно было обновить секреты, мы запускали скользящее обновление командой kubectl rollout restart statefulset atlantis. Старый под корректно завершался, а новый тут же поднимался. Но его статус выглядел так:
$ kubectl get pod atlantis-0 atlantis-0 0/1 Init:0/1 0 30m
Что за дела? Естественно, первая идея — проверить события для этого пода. Он ожидает запуска init-контейнера, так что, может быть, события пода прояснят ситуацию?
$ kubectl events --for=pod/atlantis-0 LAST SEEN TYPE REASON OBJECT MESSAGE 30m Normal Killing Pod/atlantis-0 Stopping container atlantis-server 30m Normal Scheduled Pod/atlantis-0 Successfully assigned atlantis/atlantis-0 to 36com1167.cfops.net 22s Normal Pulling Pod/atlantis-0 Pulling image "oci.example.com/git-sync/master:v4.1.0" 22s Normal Pulled Pod/atlantis-0 Successfully pulled image "oci.example.com/git-sync/master:v4.1.0" in 632ms (632ms including waiting). Image size: 58518579 bytes.
Вывод выглядит почти нормально… Но почему проходит так много времени между планированием пода и фактическим началом загрузки образа init-контейнера? К сожалению, это всё, что мы могли получить от Kubernetes. Но наверняка есть способ узнать, почему запуск пода занимает целую вечность.
Копаем глубже
В Kubernetes на каждом узле работает kubelet. Он отвечает за создание подов, монтирование persistent-томов и кучу других вещей. По опыту работы в нашей команде Kubernetes я знаю, что kubelet работает как systemd-сервис, то есть его логи должны быть доступны в Kibana. Так как под уже был запланирован, мы знаем имя хоста. А поскольку в логах kubelet'а указывается связанный объект, можно просто отфильтровать их по atlantis, тем самым сузив поиск до релевантных записей.
Мы заметили, что PV для Atlantis успешно монтировался почти сразу после того, как под попадал на узел. Тома с секретами тоже монтировались без проблем. Но при этом в логах был необъяснимый пробел. Мы видели что-то вроде этого:

Последние два сообщения повторялись снова и снова, и только спустя какое-то время под наконец стартовал как надо.
Получается, kubelet считает, что под готов к запуску, но по какой-то причине не стартует его, в итоге что-то отваливается по таймауту.
Недостающий фрагмент
Даже самые низкоуровневые логи пода не прояснили ситуацию. Куда копать дальше? Последнее сообщение перед зависанием касалось монтирования PV на узел. Обычно, если с монтированием PV возникают проблемы (например, он всё ещё примонтирован к другому узлу), это отражается в соответствующем событии. Но здесь происходило что-то иное, и единственное, что осталось проверить, — это сам PV. Я вбил его имя в Kibana, ведь оно уникальное и хорошо подходит для поиска… и тут же наткнулся на кое-что интересное:
[volume_linux.go:49] Setting volume ownership for /state/var/lib/kubelet/pods/83089f13-2d9b-46ed-a4d3-cba885f9f48a/volumes/kubernetes.io~csi/pvc-94b75052-8d70-4c67-993a-9238613f3b99/mount and fsGroup set. If the volume has a lot of files then setting volume ownership could be slow, see https://github.com/kubernetes/kubernetes/issues/69699
Перевод
Настраиваю владельца тома /state/var/lib/kubelet/pods/83089f13-2d9b-46ed-a4d3-cba885f9f48a/volumes/kubernetes.io~csi/pvc-94b75052-8d70-4c67-993a-9238613f3b99/mount и задаю fsGroup. Если на томе много файлов, установка владельца может занять много времени, см https://github.com/kubernetes/kubernetes/issues/69699
Помните, в самом начале я упоминал, что у нас закончились inode'ы? Другими словами, на этом PV у нас очень много файлов. При монтировании PV kubelet выполняет команду chgrp -R, рекурсивно меняя группу для каждого файла и директории в файловой системе тома. Неудивительно, что это занимало кучу времени — объем операций был огромен даже для быстрого флеш-накопителя!
В spec.securityContext пода был указан параметр fsGroup: 1. Он открывает доступ к файлам на томе для процессов, запущенных с GID 1. Так как Atlantis запускается не от рута, без этой опции он просто не смог бы читать и писать в PV. А Kubernetes применяет это правило просто: рекурсивно меняет владельца для всех файлов на PV при каждом монтировании.
Решение
Решение оказалось до героического… скучным. Начиная с версии 1.20, Kubernetes поддерживает в pod.spec.securityContext дополнительное поле fsGroupChangePolicy. По умолчанию оно стоит в режиме Always, что и приводило к наблюдаемому поведению. Но есть и другая опция — OnRootMismatch, которая меняет права, только если у корневой директории PV нет нужных разрешений.
Важно: не ставьте
fsGroupChangePolicy: OnRootMismatch, если не знаете, как именно файлы создаются на вашем PV.
Мы убедились, что ни один процесс не меняет группу ни для чего в PV, а потом установили это поле:
spec: template: spec: securityContext: fsGroupChangePolicy: OnRootMismatch
Теперь перезапуск Atlantis занимает около 30 секунд вместо 30 минут.
Настройки Kubernetes по умолчанию хорошо подходят для маленьких томов, но по мере их роста они становятся бутылочным горлышком. В нашем случае изменение всего одной строки в fsGroupChangePolicy сэкономило почти 50 часов рабочего времени инженеров в месяц. Раньше команды просто сидели и ждали, пока выкатятся инфраструктурные изменения, а дежурные инженеры тратили время на ложные алерты. Это примерно 600 часов в год, которые снова стали продуктивными. И всё благодаря фиксу, который мы дольше искали, чем развёртывали.
«Осторожные» дефолты в Kubernetes предназначены для небольших, типовых нагрузок. Но с ростом они незаметно превращаются в проблему. Если у вас есть кластеры с большими PV, проверьте, не съедает ли драгоценное время рекурсивная перетасовка разрешений по типу той, что была у нас. Проведите аудит настроек securityContext, уделив особое внимание fsGroup и fsGroupChangePolicy. Опция OnRootMismatch доступна с версии v1.20 Kubernetes.
Не каждый фикс — это подвиг или что-то заумное. Часто достаточно просто спросить себя: «А почему система ведёт себя именно так?»
Если вам интересно разбираться с проблемами инфраструктуры в больших масштабах — у нас есть вакансии. Присоединяйтесь к нашему сообществу или заходите в Discord, чтобы пообщаться на рабочие темы.
ma1uta
“чтоПппПето” - это галлюцинация нейронки?
aleksluov
Это кот внёс свой вклад)