На первый-второй рассчитайсь: как контролировать количество и очередность запросов к Kubernetes API с FlowControl

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

В статье мы расскажем о реальном случае: как Kubernetes API в одном из кластеров «парализовало» множественными запросами. И поделимся, как избежать этой проблемы.

Как множественные запросы поломали 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:

---
apiVersion: flowcontrol.apiserver.k8s.io/v1beta1
kind: FlowSchema
metadata:
 name: cilium-pods
spec:
 distinguisherMethod:
   type: ByUser
 matchingPrecedence: 1000
 priorityLevelConfiguration:
   name: cilium-pods
 rules:
   - resourceRules:
       - apiGroups:
           - 'cilium.io'
         clusterScope: true
         namespaces:
           - '*'
         resources:
           - '*'
         verbs:
           - 'list'
     subjects:
       - group:
           name: system:serviceaccounts:d8-cni-cilium
         kind: Group
---
apiVersion: flowcontrol.apiserver.k8s.io/v1beta1
kind: PriorityLevelConfiguration
metadata:
 name: cilium-pods
spec:
 type: Limited
 limited:
   assuredConcurrencyShares: 5
   limitResponse:
     queuing:
       handSize: 4
       queueLengthLimit: 50
       queues: 16
     type: Queue

… и задеплоили их в кластер.

После перезапуска cilium-agent потребление памяти API-сервером значимо не изменилось, поэтому мы вернули параметры узлов к изначальным.

Ниже расшифровываем, что именно мы сделали и как это решение помогает устранить проблему с множественными запросами к Kubernetes API. Кстати, похожую ситуацию мы решили и описали для подов vector.

Как управлять запросами в Kubernetes API

Управление очередями запросов в 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:

~# kubectl get prioritylevelconfigurations.flowcontrol.apiserver.k8s.io
NAME                 TYPE      ASSUREDCONCURRENCYSHARES   QUEUES   HANDSIZE   QUEUELENGTHLIMIT   AGE
catch-all            Limited   5                          <none>   <none>     <none>             193d
d8-serviceaccounts   Limited   5                          32       8          50                 53d
deckhouse-pod        Limited   10                         128      6          50                 90d
exempt               Exempt    <none>                     <none>   <none>     <none>             193d
global-default       Limited   20                         128      6          50                 193d
leader-election      Limited   10                         16       4          50                 193d
node-high            Limited   40                         64       6          50                 183d
system               Limited   30                         64       6          50                 193d
workload-high        Limited   40                         128      6          50                 193d
workload-low         Limited   100                        128      6          50                 193d

Рассчитаем, сколько одновременных запросов допустимо выполнять каждому уровню. 

  1. Суммируем все AssuredConcurrencyShares – 260. 

  2. Рассчитаем лимит запросов для priority level workload-low: (400+200)/260*100 = 230 запросов в секунду. 

Попробуем изменить одно из значений и посмотрим, что поменяется. Например, поднять AssuredConcurrencyShares для deckhouse-pod с 10 до 100. Лимит запросов изменится до величины (400+200)/350*100 = 171 запрос в секунду.

Увеличивая AssuredConcurrencyShares, мы увеличиваем лимит запросов для конкретного уровня, но уменьшаем для всех других.

Если количество запросов, попадающих в priority level, больше разрешенного лимита, запросы выстраиваются в очередь, которая обеспечивает им равноценный доступ. Параметры очереди можно настраивать. Также можно сразу отбрасывать запросы, выходящие за пределы лимита для этого priority level.

Посмотрим на пример ниже:

---
apiVersion: flowcontrol.apiserver.k8s.io/v1beta1
kind: PriorityLevelConfiguration
metadata:
 name: cilium-pods
spec:
 type: Limited
 limited:
   assuredConcurrencyShares: 5
   limitResponse:
     queuing:
       handSize: 4
       queueLengthLimit: 50
       queues: 16
     type: Queue

Здесь настроен priority level с AssuredConcurrencyShares = 5, при отсутствии других кастомных priority level это дает 12 запросов в секунду. Очередь запросов настроена на 200 запросов (handSize * queueLengthLimit), а для более равномерного распределения запросов от разных агентов созданы 16 внутренних очередей.

Особенности, которые стоит знать:

  • Увеличение параметра queue снижает количество столкновений между потоками, но увеличивает использование памяти. Значение 1 отключает логику справедливого распределения запросов, но все равно позволяет ставить запросы в очередь.

  • Увеличение queueLengthLimit позволяет выдерживать большие всплески трафика, не игнорируя ни одного запроса. Но запросы обрабатываются медленней и на них тратится больше памяти.

  • Меняя handSize, вы можете регулировать вероятность столкновений между потоками и общий параллелизм, доступный для одного потока при перегрузке.

Эти параметры подбираются экспериментально. С одной стороны, нам нужно добиться того, чтобы запросы в этом priority level работали не слишком медленно, с другой – чтобы при резком всплеске трафика API-сервер не перегружался.

Теперь перейдем к ресурсу FlowSchema. Он определяет, какие запросы попадут в соответствующий PriorityLevel. 

Основные параметры: 

  • matchingPrecedence определяет порядок применения этой flowSchema. Чем ниже число, тем выше приоритет. Таким образом можно писать перекрывающиеся flowSchemas от более частных случаев к более общим.

  • rules определяют правила выбора запросов, формат такой же, как используется в Kubernetes RBAC rules.

  • distinguisherMethod определяет, по какому параметру запросы будут делиться на потоки при передаче в priority level — по пользователю или по пространству имен. Если параметр не указать, все запросы будут в одном потоке.

Пример:

---
apiVersion: flowcontrol.apiserver.k8s.io/v1beta1
kind: FlowSchema
metadata:
 name: cilium-pods
spec:
 distinguisherMethod:
   type: ByUser
 matchingPrecedence: 1000
 priorityLevelConfiguration:
   name: cilium-pods
 rules:
   - resourceRules:
       - apiGroups:
           - 'cilium.io'
         clusterScope: true
         namespaces:
           - '*'
         resources:
           - '*'
         verbs:
           - 'list'
     subjects:
       - group:
           name: system:serviceaccounts:d8-cni-cilium
         kind: Group

В примере выше мы выбираем все запросы к apiGroup: cilium.io, включая cluster-scope запросы, из всех пространств имен ко всем ресурсам, тип запроса — list. Субъектом запроса выступают запросы от ServiceAccount'а d8-cni-cilium.

Как посмотреть, в какую FlowSchema и PriorityLevelConfiguration попал запрос?

API-сервер при ответе проставляет специальные заголовки — X-Kubernetes-PF-FlowSchema-UID X-Kubernetes-PF-PriorityLevel-UID. По ним можно понять, куда попадает запрос.

Например, выполним запрос к API от сервис-аккаунта агента Cilium:

TOKEN=$(kubectl -n d8-cni-cilium get secrets agent-token-45s7n -o json | jq -r .data.token | base64 -d)

curl https://127.0.0.1:6445/apis/cilium.io/v2/ciliumclusterwidenetworkpolicies?limit=500  -X GET --header "Authorization: Bearer $TOKEN" -k -I
HTTP/2 200
audit-id: 4f647505-8581-4a99-8e4c-f3f4322f79fe
cache-control: no-cache, private
content-type: application/json
x-kubernetes-pf-flowschema-uid: 7f0afa35-07c3-4601-b92c-dfe7e74780f8
x-kubernetes-pf-prioritylevel-uid: df8f409a-ebe7-4d54-9f21-1f2a6bee2e81
content-length: 173
date: Sun, 26 Mar 2023 17:45:02 GMT

kubectl get flowschemas -o custom-columns="uid:{metadata.uid},name:{metadata.name}" | grep 7f0afa35-07c3-4601-b92c-dfe7e74780f8
7f0afa35-07c3-4601-b92c-dfe7e74780f8   d8-serviceaccounts

kubectl get prioritylevelconfiguration -o custom-columns="uid:{metadata.uid},name:{metadata.name}" | grep df8f409a-ebe7-4d54-9f21-1f2a6bee2e81
df8f409a-ebe7-4d54-9f21-1f2a6bee2e81   d8-serviceaccounts

В выводе видно, что запрос относится к flowSchema d8-serviceaccounts и priorityLevelConfiguration d8-serviceaccounts.

Какие метрики можно посмотреть ?

Полезные метрики:

  • Apiserver_flowcontrol_rejected_requests_total — общее количество отброшенных запросов.

  • Apiserver_current_inqueue_requests — текущее количество запросов в очереди.

  • Apiserver_flowcontrol_request_execution_seconds — длительность выполнения запросов.

Также полезно смотреть информацию из debug-эндпоинтов:

kubectl get --raw /debug/api_priority_and_fairness/dump_priority_levels
PriorityLevelName,  ActiveQueues, IsIdle, IsQuiescing, WaitingRequests, ExecutingRequests
system,             0,            true,   false,       0,               0
workload-high,      0,            true,   false,       0,               0
catch-all,          0,            true,   false,       0,               0
exempt,             <none>,       <none>, <none>,      <none>,          <none>
d8-serviceaccounts, 0,            true,   false,       0,               0
deckhouse-pod,      0,            true,   false,       0,               0
node-high,          0,            true,   false,       0,               0
global-default,     0,            true,   false,       0,               0
leader-election,    0,            true,   false,       0,               0
workload-low,       0,            true,   false,       0,               0


kubectl get --raw /debug/api_priority_and_fairness/dump_queues
PriorityLevelName,  Index,  PendingRequests, ExecutingRequests, SeatsInUse, NextDispatchR,    InitialSeatsSum, MaxSeatsSum, TotalWorkSum
exempt,             <none>, <none>,          <none>,            <none>,     <none>,           <none>,          <none>,      <none>
d8-serviceaccounts, 0,      0,               0,                 0,          71194.55330547ss, 0,               0,           0.00000000ss
d8-serviceaccounts, 1,      0,               0,                 0,          71195.15951496ss, 0,               0,           0.00000000ss
...
global-default,     125,    0,               0,                 0,          0.00000000ss,     0,               0,           0.00000000ss
global-default,     126,    0,               0,                 0,          0.00000000ss,     0,               0,           0.00000000ss
global-default,     127,    0,               0,                 0,          0.00000000ss,     0,               0,           0.00000000ss

Заключение

Мы решили наш кейс, настроив управление очередями запросов. Но это был далеко не единственный случай в нашей практике. После нескольких случаев, когда мы столкнулись с необходимостью ограничивать запросы к API, мы реализовали настройку APF для компонентов нашей Kubernetes-платформы Deckhouse. Это сокращает нам и нашим клиентам число проблем с перегрузкой API в больших и нагруженных кластерах. 

Поделитесь в комментариях, если тоже сталкивались с такими случаями в своей практике и знаете другие решения для них. 

P.S.

Читайте также в нашем блоге:

  • «Как устроена разработка Kubernetes-платформы Deckhouse (обзор и видео доклада)»;

  • «Мониторинг межсервисного взаимодействия Kubernetes с помощью протокола NetFlow»;

  • «Наш опыт миграции PostgreSQL с AWS RDS на свою (self-hosted) инсталляцию».

 

Источник: https://habr.com/ru/companies/flant/articles/735718/


Интересные статьи

Интересные статьи

В облаке многие думают над стартом приложения, но не все задумываются о том, как оно завершается. В свое время мы наловили довольно много ошибок, связанных именно с остановкой подов. Например, увидели...
Kubernetes API развиваются и периодически обновляются. Когда готов улучшенный API на замену старому, старый удаляют. См. политику Kubernetes по удалению API.Скоро будет у...
Перевод статьи подготовлен в преддверии старта курса «DevOps практики и инструменты». Если вы это читаете, вероятно, вы что-то слышали о Kubernetes (а если нет, то как вы зде...
Коллеги, всем привет! На этой неделе состоялся релиз очередной версии нашего плагина DevOpsProdigy KubeGraf v1.4.0. Он разработан для Grafana и предназначен для мониторинга kuberne...
Современный подход к эксплуатации решает множество насущных проблем бизнеса. Контейнеры и оркестраторы позволяют легко масштабировать проекты любой сложности, упрощают релизы новых версий, де...