Внутри гиперкуба плохих вайбов: размерность пространства имён

Многие команды работают с кластерами Kubernetes побольше нашего. В них больше узлов, больше подов, больше ingress и так далее. По большинству размерностей нас кто-нибудь, да побеждает.
Но есть одна размерность, по которой, как мы подозреваем, мы почти на вершине: это пространства имён. Я думаю так, потому что мы постоянно сталкиваемся со странным поведением во всех процессах, которые их отслеживают. В частности, все процессы, выполняющие их listwatch, занимают на удивление много памяти и подвергают apiserver серьёзной нагрузке. Это стало одной из сложностей масштабирования, которую замечаешь, только достигая определённого порога. При увеличении оверхеда памяти эффективность снижается: каждый байт, который нам нужно использовать для управления — это байт, отнятый у пользовательских сервисов.
Проблема сильно усугубляется, когда daemonset должен выполнять listwatch пространств имён или сетевых политик (netpol), которые мы определяем для каждого пространства имён. Так как daemonset запускают под в каждом узле, каждый из этих подов выполняет listwatch одних и тех же ресурсов, из-за чего объём используемой памяти увеличивается при росте количества узлов.
Хуже того — эти вызовы listwatch серьёзно нагружали apiserver. Если одновременно перезапускалось множество подов daemonset, например, при развёртывании, то они могли перегрузить сервер запросами и вызвать реальный вылет.
Идём по следу памяти
Несколько месяцев назад самыми большими потребителями памяти часто были daemonset. В частности, Calico и Vector, обрабатывающие сетевое конфигурирование и сбор логов.
В тесном сотрудничестве с мейнтейнерами проекта мы проделали работу по снижению объёма используемой Calico памяти, чтобы обеспечить его более эффективное масштабирование. Этот процесс оптимизации обернулся для нас крупным выигрышем и дал нам более глубокое понимание того, как ведёт себя память при масштабировании пространств имён.


Для удобства этой работы мы настроили staging-кластер с несколькими сотнями тысяч пространств имён. Мы знали, что большую нагрузку на Calico вызывают сетевые политики (netpol) каждого пространства имён, поэтому воссоздали эти условия, чтобы валидировать вносимые нами изменения.
В процессе выполнения этих тестов мы заметили нечто странное. Vector (ещё один daemonset) тоже начал потреблять большие объёмы памяти.

Этот паттерн выглядел знакомо, поэтому мы поняли, что нам придётся разбираться с ещё одной проблемой. Было очевидно, что Vector не использует netpol, но немного изучив вопрос, мы обнаружили, что он выполняет listwatch пространств имён для каждого узла, чтобы обеспечить возможность ссылок на метки пространств имён каждого пода в источнике логов kubernetes.
Нужны ли нам эти метки?
У нас появилась мысль: что, если Vector вообще не понадобятся пространства имён? Возможно ли это вообще?
Оказалось, что да: они использовались в нашей конфигурации, но только для проверки, принадлежит ли под к пользовательскому пространству имён.
type: filter
inputs: [add_log_levels]
condition:
type: vrl
source: .kubernetes.namespace_labels.userNS == "true"
Мы поняли, что нам повезло и что можно описать это условие иным образом; сэкономленная память полностью оправдывала такой подход.
.kubernetes.namespace_name != null &&
(
starts_with(string!(.kubernetes.namespace_name), "user-")
)
Собираем фикс (и ломаем логи)
Казалось, что нам слишком уж повезло. Мы связались с мейнтейнерами Vector и спросили, не поломает ли что-нибудь отключение этого поведения, и примут ли они наш вклад, если мы всё это сделаем.

Дальше оставалось только всё попробовать. Поменять код оказалось очень просто. Мы добавили новую опцию конфигурации и направили её по соответствующим частям кодовой базы.
insert_namespace_fields: {
description: """
Specifies whether or not to enrich logs with namespace fields.
Setting to `false` prevents Vector from pulling in namespaces and thus namespace label fields will not
be available. This helps reduce load on the `kube-apiserver` and lowers daemonset memory usage in clusters
with many namespaces.
"""
required: false
type: bool: default: true
}
Спустя несколько часов активной работы rustc образ Docker наконец-то собрался и мы были готовы проверить свою теорию. Контейнер гладко запустился без ошибок в логах, что вселяло надежду.
Но потом мы столкнулись препятствием. Никаких логов не выводилось, и мы понятия не имели, почему.
К счастью на помощь пришёл наш друг Claude:

Я всё пересобрал (на всё ушло где-то 73 часа, потому что это Rust), сгенерировал новый образ, обновил staging и начал нервно наблюдать. На этот раз логи выводились нормально, и…

Что-то не сходится
Благодаря этому изменению мы сэкономили 50% памяти. Серьёзная победа! Мы были готовы закончить работу и отправить её в продакшен.
Но потом член нашей команды Hieu задал очень хороший вопрос.

Он был прав, что-то здесь нечисто.
Спустя несколько часов, побившись головой о стену, я так ничего и не нашёл. Всё равно оставался гибибайт неучтённой памяти. Вся моя теория о том, как это работает, начала трещать по швам.
Я даже спросил в канале, есть ли у кого-нибудь опыт работы с Valgrind:
Я: Кто-нибудь работал с valgrind? Кажется, настроить его довольно просто, но в результате он не взаимодействует с pyroscope. Придётся подключиться к контейнеру и выполнять ручную отладку в gdb.
Такого опыта ни у кого не оказалось.
В последней отчаянной попытке повторного профилирования я наконец-то нашёл ответ. Всё это время он был у меня прямо перед глазами.
На самом деле, у нас было два источника kubernetes_logs для пользовательских узлов. Я установил флаг только для одного из них. Когда я применил его к обоим, объём используемой памяти упал до уровня, который мы наблюдали в staging до добавления дополнительных пространств имён.
sources:
kubernetes_system_logs:
type: kubernetes_logs
glob_minimum_cooldown_ms: 1000
...
kubernetes_logs:
type: kubernetes_logs
glob_minimum_cooldown_ms: 1000
...
Выпускаем пул-реквест
Я создал целый пул-реквест, и спустя какое-то время его выпустили в релиз!


Примерно в то же самое время на дежурстве был наш коллега Марк. Он сотворил свою обычную магию — соединил всё вместе, протестировал развёртывание в staging и выпустил в продакшен.
Результаты говорят сами за себя.


В нашем самом большом кластере объём занимаемой памяти снизился на 1 ТиБ, а суммарная экономия с учётом всех остальных кластеров составила чуть больше 7 ТиБ.
7 ТиБ спустя
Отладка масштабной инфраструктуры редко происходит одним махом. Часто она становится результатом множества точечных вопросов, мелких изменений и маленьких побед.
В данном случае мы начали с не очень красивого графика используемой памяти, потом в подходящий момент коллега задал правильный вопрос, а затем оставалось только упорно искать первопричину. Применив эти простые изменения ко всей нашей инфраструктуре, мы освободили 7 ТиБ памяти, снизили риск при развёртываниях и уменьшили когнитивную сложность системы.
Огромное спасибо Hieu за прогресс в расследовании, Марку за беспроблемное развёртывание и мейнтейнерам Vector за открытость и готовность к изменениям.
Если вы в больших масштабах работаете с daemonset и наблюдаете необъяснимую нагрузку на память, то стоит задаться вопросом: а действительно ли вам нужны метки пространств имён?