В статье мы расскажем о реальном случае: как 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
Рассчитаем, сколько одновременных запросов допустимо выполнять каждому уровню.
Суммируем все
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.
Посмотрим на пример ниже:
---
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) инсталляцию».
chako8
Добрый день.
Спасибо за статью, узнал для себя много нового.
Подскажите, а как определяете проблемные запросы? От кого они пришли?
Пробовал включить Audit policy на все операции get, list, watch, система логирования не справилась с нагрузкой.
RomanenkoDenys Автор
Проблемные обычно list запросы. Есть метрики аписервера по которым можно вычислить каких запросов много. Ну и аудит да.
chako8
Если будет время, подскажите о каких метриках идет речь