В статье мы расскажем о реальном случае: как Kubernetes API в одном из кластеров «парализовало» множественными запросами. И поделимся, как избежать этой проблемы. Путешествие в длительное изучение работы Kubernetes API и приоритезации запросов к нему началось буднично. Однажды вечером мы получили звонок от инженера техподдержки. Оказалось, что у одного из клиентов ничего не работает, прод практически упал, и с этой аварией нужно что-то делать. Подключившись к проблемному кластеру, мы обнаружили следующее: Kube API-серверы занимают всю память, падают, поднимаются, снова падают и так по кругу. Это приводит к тому, что API Kubernetes становится недоступен и полностью нефункционален. Так как это был production-кластер, мы временно решили проблему ресурсами: добавили процессоров и памяти на узлы control-plane. С первого раза добавленного не хватило, но на второй работа API стабилизировалась. Для начала мы оценили масштаб изменений. Изначально узлы control-plane были в конфигурации с 8 CPU и 16 Гбайт RAM, а после наших манипуляций размер узлов увеличился до 16 CPU и 64 Гбайт RAM. Графики потребления памяти в момент возникновения проблем показали следующее: Потребление памяти возрастало до 50 Гбайт. Некоторое время спустя мы выяснили, что в силу определенных условий поды cni-cilium запускали массовые LIST-запросы в API. А поскольку кластер большой и узлов в нем много (более 200), одновременные запросы увеличивали использование памяти. Мы согласовали с клиентом окно для тестов, перезапустили агенты Cilium и увидели следующую картину: Вырастает нагрузка на один из API-серверов. Он начинает потреблять память. Памяти не хватает. API-сервер падает. Запросы переключаются на другой сервер. Повторяется все то же самое. Мы решили, что будет разумно ограничить количество одновременных запросов к API со стороны cilium-agent. Даже если LIST-запросы выполнятся чуть медленнее, на работе Cilium это не скажется. Мы подготовили манифесты FlowSchema и PriorityLevelConfiguration: … и задеплоили их в кластер. Мы проверили, помогло ли наше решение: для этого перезапустили cilium-agent. Убедились, что на этот раз потребление памяти API-сервером значимо не изменилось. А значит, все работает так, как нужно, и мы можем вернуть параметры узлов к изначальным.   Ниже расшифровываем, что именно мы сделали и как это решение помогает устранить проблему с множественными запросами к Kubernetes API. Кстати, похожую ситуацию мы решили и описали для подов vector. Управление очередями запросов в Kubernetes API называется API Priority and Fairness (APF) и включено по умолчанию, начиная с Kubernetes 1.20. Сервер API тоже обладает функцией ограничения количества запросов, для этого предусмотрены два параметра: --max-requests-inflight (по умолчанию 400) и --max-mutating-requests-inflight (по умолчанию 200). Если APF включен, оба эти параметра суммируются — так определяется лимит параллельности API-сервера (total concurrency limit, максимальный лимит запросов). При этом есть некоторые особенности. Долгоживущие запросы к API (например, просмотр логов или выполнение команд в поде) не попадают под APF, также как и WATCH запросы. Еще существует специальный предопределенный priority level — exempt. Попадающие в него запросы обрабатываются немедленно. Без APF невозможно гарантировать, что запросы от агента Cilium не «задушат» пользовательские обращения к API. Также APF позволяет установить ограничения, чтобы важные запросы выполнились вне зависимости от нагруженности API-сервера. APF настраивается при помощи двух ресурсов: PriorityLevelConfiguration — определяет уровень приоритета запросов. FlowSchema — определяет, для каких запросов применяется PriorityLevelConfiguration. В каждом PiorityLevelConfiguration настраивается свой лимит параллельности. Общий лимит параллельности делится между всеми PriorityLevelConfiguration пропорционально их настройкам. Давайте посчитаем этот лимит на примере, ориентируемся на параметр AssuredConcurrencyShares: Рассчитаем, сколько одновременных запросов допустимо выполнять каждому уровню.  Суммируем все AssuredConcurrencyShares – 260.  Рассчитаем лимит запросов для priority level workload-low: (400+200)/260*100 = 230 запросов в секунду.  Попробуем изменить одно из значений и посмотрим, что поменяется. Например, поднять AssuredConcurrencyShares для deckhouse-pod с 10 до 100. Лимит запросов изменится до величины (400+200)/350*100 = 171 запрос в секунду. Увеличивая AssuredConcurrencyShares, мы увеличиваем лимит запросов для конкретного уровня, но уменьшаем для всех других. Если количество запросов, попадающих в priority level, больше разрешенного лимита, запросы выстраиваются в очередь, которая обеспечивает им равноценный доступ. Параметры очереди можно настраивать. Также можно сразу отбрасывать запросы, выходящие за пределы лимита для этого priority level. Посмотрим на пример ниже: Здесь настроен priority level с AssuredConcurrencyShares = 5, при отсутствии других кастомных priority level это дает 12 запросов в секунду. Очередь запросов настроена на 200 запросов (handSize * queueLengthLimit), а для более равномерного распределения запросов от разных агентов созданы 16 внутренних очередей. Особенности, которые стоит знать: Увеличение параметра queue снижает количество столкновений между потоками, но увеличивает использование памяти. Значение 1 отключает логику справедливого распределения запросов, но все равно позволяет ставить запросы в очередь. Увеличение queueLengthLimit позволяет выдерживать большие всплески трафика, не игнорируя ни одного запроса. Но запросы обрабатываются медленней и на них тратится больше памяти. Меняя handSize, вы можете регулировать вероятность столкновений между потоками и общий параллелизм, доступный для одного потока при перегрузке. Эти параметры подбираются экспериментально. С одной стороны, нам нужно добиться того, чтобы запросы в этом priority level работали не слишком медленно, с другой – чтобы при резком всплеске трафика API-сервер не перегружался. Теперь перейдем к ресурсу FlowSchema. Он определяет, какие запросы попадут в соответствующий priority level.  Основные параметры:  matchingPrecedence определяет порядок применения этой flowSchema. Чем ниже число, тем выше приоритет. Таким образом можно писать перекрывающиеся flowSchemas от более частных случаев к более общим. rules определяют правила выбора запросов, формат такой же, как используется в Kubernetes RBAC rules. distinguisherMethod определяет, по какому параметру запросы будут делиться на потоки при передаче в priority level — по пользователю или по пространству имен. Если параметр не указать, все запросы будут в одном потоке. Пример: В примере выше мы выбираем все запросы к apiGroup: cilium.io, включая запросы cluster-scope, из всех пространств имен ко всем ресурсам, тип запроса — list. Субъектом запроса выступают запросы от сервис-аккаунта d8-cni-cilium. API-сервер при ответе проставляет специальные заголовки: X-Kubernetes-PF-FlowSchema-UID и X-Kubernetes-PF-PriorityLevel-UID. По ним можно понять, куда попадает запрос. Например, выполним запрос к API от сервис-аккаунта агента Cilium: В выводе видно, что запрос относится к flowSchema d8-serviceaccounts и priorityLevelConfiguration d8-serviceaccounts. Полезные метрики: Apiserver_flowcontrol_rejected_requests_total — общее количество отброшенных запросов. Apiserver_current_inqueue_requests — текущее количество запросов в очереди. Apiserver_flowcontrol_request_execution_seconds — длительность выполнения запросов. Также полезно смотреть информацию из debug-эндпоинтов: Мы решили наш кейс, настроив управление очередями запросов. Но это был далеко не единственный случай в нашей практике. После нескольких случаев, когда мы столкнулись с необходимостью ограничивать запросы к API, мы реализовали настройку APF для компонентов нашей Kubernetes-платформы Deckhouse. Это сокращает нам и нашим клиентам число проблем с перегрузкой API в больших и нагруженных кластерах.  Поделитесь в комментариях, если тоже сталкивались с такими случаями в своей практике и знаете другие решения для них.  Читайте также в нашем блоге: «Как устроена разработка Kubernetes-платформы Deckhouse (обзор и видео доклада)»; «Мониторинг межсервисного взаимодействия Kubernetes с помощью протокола NetFlow»; «Наш опыт миграции PostgreSQL с AWS RDS на свою (self-hosted) инсталляцию».