Одна строчка в Kubernetes сэкономила Cloudflare 600 часов в год
Представьте: каждый раз, когда вы перезапускаете критически важный сервис, вся команда сидит без дела 30 минут. Никаких изменений в инфраструктуре, никаких деплоев — просто ожидание. И так 100 раз в месяц. Именно с этим столкнулась команда Cloudflare, управляющая десятками Terraform-проектов через Atlantis. Причиной оказалась одна настройка Kubernetes, которая незаметно превратилась в бутылочное горлышко по мере роста данных.
Разбираемся, как инженеры Cloudflare нашли проблему и решили её буквально одной строчкой конфигурации.
Ключевые выводы:
— fsGroup в Kubernetes рекурсивно меняет владельца всех файлов на PersistentVolume при каждом монтировании — это безопасный дефолт, который становится проблемой на больших томах.
— Параметр fsGroupChangePolicy: OnRootMismatch (доступен с K8s 1.20) проверяет только корневую директорию вместо обхода всех файлов.
— Одна строчка в манифесте сократила время рестарта Atlantis с 30 минут до 30 секунд — экономия ~600 инженерных часов в год.
— Безопасные дефолты Kubernetes рассчитаны на небольшие рабочие нагрузки. При масштабировании их стоит пересматривать.
Проблема — 30 минут на рестарт
Atlantis — это инструмент для автоматизации Terraform через pull/merge request'ы. В Cloudflare он управляет десятками проектов и работает как singleton StatefulSet в Kubernetes. Для хранения состояния репозиториев Atlantis использует PersistentVolume (PV).
Перезапуски нужны регулярно: ротация секретов, onboarding новых проектов, обновления конфигурации. С учётом примерно 100 рестартов в месяц каждый 30-минутный простой складывался в 50+ часов заблокированного инженерного времени ежемесячно. Плюс каждый рестарт триггерил алерт и будил дежурного.
Ситуация обострилась, когда на PV закончились inode'ы. Inode — это запись о файле или директории в файловой системе, и их количество фиксируется при создании ФС. Единственный способ получить больше inode'ов в их конфигурации — увеличить размер тома, а для этого нужен рестарт пода.
Расследование
Первые зацепки
После kubectl rollout restart statefulset atlantis новый под появлялся практически мгновенно, но зависал на стадии инициализации:
События пода выглядели нормально — Kubernetes успешно распределил под на ноду, скачал образ init-контейнера за доли секунды. Но между назначением на ноду и началом скачивания образа был необъяснимый промежуток в 30 минут.
Копаем глубже — логи kubelet
Событий Kubernetes было недостаточно. Инженеры обратились к логам kubelet — компонента, который на каждой ноде координирует создание подов, монтирование томов и другие операции. Поскольку kubelet работает как systemd-сервис, его логи доступны через централизованную систему (в данном случае Kibana).
В логах kubelet было видно, что PersistentVolume монтируется сразу после назначения пода. Секреты тоже монтируются без проблем. Но затем — снова провал, и в логах появляется ошибка:
Kubelet считал, что под готов к запуску, но что-то блокировало монтирование тома и вызывало таймаут.
Разгадка — fsGroup
Последней зацепкой стало имя PV. Инженеры использовали его как поисковый запрос в Kibana — и сразу нашли ключевое сообщение:
Вот в чём было дело. В spec.securityContext пода было указано fsGroup: 1. Это нужно, чтобы процессы Atlantis (запущенные не от root) имели доступ к файлам на томе. Но способ, которым Kubernetes это обеспечивает, оказался проблемой: при каждом монтировании kubelet выполняет рекурсивный chgrp по всем файлам и директориям на PersistentVolume.
А файлов к тому моменту были миллионы — команда недавно столкнулась с нехваткой inode'ов, что прямо указывало на огромное количество файлов. Рекурсивный обход миллионов записей на каждом рестарте — вот что съедало 30 минут.
Решение — одна строчка
Начиная с версии 1.20, Kubernetes поддерживает поле fsGroupChangePolicy в pod.spec.securityContext. По умолчанию оно установлено в Always — рекурсивный обход при каждом монтировании. Альтернативное значение OnRootMismatch проверяет только корневую директорию тома: если у неё уже правильная группа, рекурсивный обход не запускается.
Фикс целиком:
Важно: устанавливать OnRootMismatch стоит только если вы уверены, что ничто не меняет группу файлов на PersistentVolume извне. В случае Cloudflare инженеры проверили, что никакие процессы не модифицируют права на файлы в томе, и только после этого применили настройку.
Почему это работает
Параметр fsGroup в Kubernetes решает конкретную задачу: контейнеры, запущенные от непривилегированного пользователя, должны иметь доступ к файлам на примонтированном томе. Kubernetes обеспечивает это, выполняя chgrp -R по всему содержимому PV при каждом монтировании.
Для небольших томов с сотнями или тысячами файлов это работает незаметно. Но по мере роста данных время обхода растёт линейно. На миллионах файлов — даже на быстрых NVMe-дисках — операция занимает десятки минут.
Политика OnRootMismatch меняет логику: kubelet проверяет только корневую директорию тома. Если её группа совпадает с fsGroup, рекурсивный обход пропускается. Поскольку Atlantis сам создаёт файлы с правильной группой (она наследуется от корневой директории), проверять миллионы файлов при каждом рестарте нет необходимости.
Результат
- Время рестарта: 30 минут → 30 секунд (ускорение в 60 раз)
- Экономия: ~50 часов заблокированного инженерного времени в месяц
- В годовом исчислении: ~600 часов продуктивной работы, возвращённых команде
- Ложные алерты: дежурный инженер больше не получает пейджер на каждый рестарт
- Время на фикс: одна строчка в YAML-манифесте
Как отметили в Cloudflare: диагностика заняла больше времени, чем само исправление.
Уроки для разработчиков
- Проверяйте дефолты при масштабировании. Безопасные настройки по умолчанию в Kubernetes рассчитаны на простые рабочие нагрузки. Когда данные растут, дефолты могут незаметно стать бутылочным горлышком.
- Ищите не только в событиях Kubernetes. Pod events показывают не всё. Логи kubelet, метрики нод, журналы CSI-драйверов — полезные источники информации при отладке проблем с монтированием.
- Считайте стоимость «мелких» проблем. 30 минут простоя кажутся терпимыми. 30 минут x 100 раз в месяц = 50 часов. Масштаб меняет приоритеты.
- Аудит securityContext. Поля fsGroup, runAsUser, runAsGroup и fsGroupChangePolicy напрямую влияют на время старта подов с PersistentVolume. Если у вас большие тома — проверьте эти настройки.
- Не всякий фикс должен быть сложным. Самые ценные исправления часто оказываются самыми простыми — но требуют глубокого понимания того, как система работает под капотом.
Частые вопросы
Что делает fsGroupChangePolicy в Kubernetes?
Параметр fsGroupChangePolicy определяет, когда Kubernetes рекурсивно обновляет группу-владельца файлов на PersistentVolume. Значение Always (по умолчанию) запускает рекурсивный chgrp при каждом монтировании. Значение OnRootMismatch выполняет обновление только если группа корневой директории не совпадает с fsGroup. Поддерживается с Kubernetes 1.20.
Безопасно ли использовать OnRootMismatch?
Если файлы на PersistentVolume создаются только процессами внутри пода (и наследуют правильную группу от корневой директории), OnRootMismatch безопасен. Но если внешние процессы или другие поды могут менять группу файлов на томе, лучше оставить значение Always и искать другие способы оптимизации.
Как узнать, влияет ли fsGroup на время старта моих подов?
Проверьте логи kubelet на ноде, где запускается под. Если в них есть сообщение "Setting volume ownership... If the volume has a lot of files then setting volume ownership could be slow", значит рекурсивный обход выполняется. Также можно сравнить время между событиями Scheduled и Started для пода — большой разрыв при наличии PV с fsGroup указывает на эту проблему.
Выводы
История Cloudflare — отличный пример того, как безопасный дефолт может превратиться в серьёзную проблему на масштабе. fsGroupChangePolicy: Always — разумная настройка для небольших томов, но на миллионах файлов она съедает минуты при каждом рестарте.
Главный урок: если что-то в вашем кластере работает медленнее, чем должно — не расширяйте окно алертов, а копайте глубже. Иногда ответ — это одна строчка YAML.
По материалам блога Cloudflare.