У Kubecost і подібних рішень є дуже корисна сторінка, де відображається статистика по Kubernetes Pods – скільки CPU/Memory вони використовують, скільки реквестів, лімітів, і які для них рекомендовані значення.
Додатково, щоб мати уяву про ефективність роботи Karpenter, я хочу мати дашборду в Grafana, яка буде відображати статистку по всім WorkerNode Kubernetes кластеру – ресурси CPU/Memory та кількість подів на них.
Тобто мета створення дашборди:
- оцінювати ефективність Karpenter
- оцінювати навантаження на кожній Worker Node
- швидко побачити яка нода overcomitted (забагато requsted ресурсів подами)
- швидко побачити на яких нодах запущені поди конкретного сервісу (у нас всі сервіси розбиті по неймпсейсам, створимо окремий фільтр на це)
Заодно трохи розберемося с Tables панелями, бо я ними давно не користувався, і щось підзабув, як для них готувати дані.
З чим будемо працювати:
- AWS Elastic Kubertes Service (1.28)
- Karpenter для менеджменту ЕС2 (v0.33.1)
- VictoriaMetrics (v1.97.1)
- Grafana (10.1.2)
До речі, у Grafana є чудова demo-версія, де можна погратись з дашбордами.
Зміст
Планування
Зверху робимо таблицю, яка буде відображати загальну інформацію по WorkerNodes – CPU, Memory, Pods.
А під цією таблицею зробимо таблиці з інформацією по кожній Node та Pods на ній – і там вже буде інфа по CPU/Memory подів і їхнім реквестам.
Dashboard variables
Нам будуть потрібні фільтри по:
- data source
- іменам WorkerNodes
- іменам Namespaces
Можна додати по кластеру – але в мене він наразі один, тому скіпаємо.
Data-source variable
По дата-сорсу – описував детальніше в Експорт існуючої dashboard та Data Source UID not found, але якщо стисло, то ідея полягає в тому, щоб не прив’язуватись до конкретного UID дата-сорса, а мати його в змінній – тоді можна легко переносити дашборду між інстансами Grafana.
Створюємо дашборду, переходимо в Settings > Variables, створюємо змінну для дата-сорса:
Ставимо Show on dashboard == Nothing, бо вона буде використовуватись тільки в панелях.
WorkerNodes variable
Додаємо змінну, в якій будуть всі воркер-ноди.
У нас Карпентер, тому можемо взяти karpenter_nodes_total_pod_requests
, і з регуляркою .*"(.*)".*
вирізати тільки імена нод:
Включаємо Multi-Value та Include All option:
Namespaces variable
Тут можемо взяти метрику kube_pod_info
, в якій є лейбла namespace
.
Також включаємо Multi-value та Include All option:
Тут начебто все – можна починати робити таблички.
Nodes resources – CPU, Memory, Pods
Отже, перша таблиця буде відображати список всіх активних WorkerNodes та інформацію по ресурсам на ній.
Мені поки не актуальні дані по Persistent Volumes/AWS EBS, тому не додаю, але використовуючи загальну ідею це зробити досить просто. Аналогічно с нетворкінгом – поки не актуально, але також додається легко.
Створюємо нову панель, вибираємо тип Table:
В Panel options можемо використати змінну $node_name
:
Колонка Instance Name
Далі нам потрібно задати такий собі “об’єднуючий селектор”.
Ідея роботи з Таблицями з multi queries полягає в тому, що у вас є загальна лейбла для всіх запитів, і по значенню цієї лейбли таблиця буду групувати дані в строках.
Отже, робимо першу колонку – тут будуть імена WorkerNodes, і по ним жеж будуть групуватись дані з інших запитів:
sum(karpenter_nodes_total_pod_requests{instance_memory!="", node_name=~"$node_name"}) by (node_name)
Я тут використовую instance_memory!=""
, бо є окрема default WorkerNode для CriticalAddons, яка створюється зі звичайної AWS AutoSacaling Managed Node Group, а не з Karpenter NodeClaim (див. Terraform: створення EKS, частина 2 – EKS кластер, WorkerNodes та IAM).
В Options запиту переключаємо Format на Table та Type на Instant – це потрібно буде робити для всіх запитів в таблицях:
Переключаємо Data source на ${datasource_vm}
:
Тепер приберемо з таблиці Time та Value.
Справа в Options відкриваємо Add field override:
Вибираємо Field with name – Time, додаємо property Hide in table:
Аналогічно з Value – але трохи згодом, бо ім’я колонки зміниться, коли ми додамо інші запити.
Можемо відразу задати колір імен нод в цьому стовпчику.
Transformations
Аби зручно перейменовувати колонки в таблиці – додаємо Transformations > Orginize fileds:
І задаємо ім’я першої колонки:
Table cell color
Знов вибираємо Field with name > Cell options > Cell type > Colored text:
Тепер, коли маємо фіксоване ім’я колонки – повертаємось до Add field override і додаємо другий Override. Вибираємо Field with name – Node Name, і теж додаємо property Cell options > Cell type > Colored text:
Зараз колір береться з Tresholds. Аби перевизначити його – додаємо другий property – Standard options > Color scheme > Single color:
Data links
В мене є окрема дашборда з деталями по конкретній Worker Node, і було б зручно мати змогу перейти з цієї таблиці відразу на дашборду по ноді, тим більш обидві мають Dashboard variable $
node_name
.
В Override додаємо Data links, в URL використовуємо ${__data.fields["Node Name"]}
(всі варіанти можна отримати по Ctrl+Space в полі URL):
Колонка Instance type
Наступним хочеться бачити тип інстансу.
Для цього використовуємо ту ж метрику karpenter_nodes_total_pod_requests
, яка в лейблі instance_type
має власне тип інстансу.
Робимо запит:
sum(karpenter_nodes_total_pod_requests{instance_memory!="", node_name=~"$node_name"}) by (node_name, instance_type)
В by (node_name)
використовуємо наш “об’єднучий селектор” по імені ноди.
Не забуваємо про Format та Type нашого запиту.
Але тепер маємо запити у вигляді випадаючого списку замість того, щоб відобразити типи інстансів новою колонкою:
Переходимо до Transformations, додаємо Merge:
Ця трансформація об’єднає дані в таблиці по “селектору” – загальній лейблі node_name
, і тепер маємо такі колонки:
Далі, прибираємо з таблиці колонки з Values – знов йдемо до Add field override > Field with name і додаємо Hide in table.
Іноді (часто) таблиця не оновлюється відразу – тому зверху справа тиснемо Refresh dashboard.
І маємо дві колонки – з іменами та типами інстансів:
Інформація по CPU
Почнемо з CPU, далі додамо пам’ять та поди по кожній ноді.
Колонка Node CPU total
Далі додаємо кількість vCPU на ноді – тут все аналогічно до типу інстансу, тільки лейбла instance_cpu
:
sum(karpenter_nodes_total_pod_requests{instance_memory!=""}) by (node_name, instance_cpu)
Колонка Node CPU requested
Теж аналогічно, тільки в запиті робимо вибірку по лейблі resource_type="cpu"
– тоді метрика нам поверне дані по кожній WorkerNode і загальній кількості CPU, яка була reqeusted всіма подами на цій ноді.
Не забуваємо про Orginize fields – задаємо імена колонкам:
Колонка Node CPU requested %
Тепер трохи більш цікаво: хочеться відобразити % CPU requested від загальної кількості.
Спочатку давайте впевнимось, що маємо правильні дані в метриці karpenter_nodes_total_pod_requests
.
Виконуємо kubectl describe node ip-10-0-32-219.ec2.internal
:
Тут маємо інформацію по всім requests
всіх подів ноди + загальна інформація в Allocated resources.
В Allocated resources cpu == 1472 milicpu (або millicores), але це з урахуванням подів від DaemonSets – aws-node
(50), ebs-csi-node
(30), eks-pod-identity-agent
(0), kube-proxy
(20) і так далі. Загалом ці поди зареквестили 50+30+20+30+10+50 == 190 milicpu.
Метрика ж karpenter_nodes_total_pod_requests
від Karpenter відображає всі реквести окрім DaemonSets – тож в ній дані будуть трохи менші, але в цілому картина має бути приблизно такою ж – 1452m в Allocated resources мінус 190m від DaemonSets, тобто реальні ворклоади зареквестили 1262 milicpu, або 0.631 від загальної кількості milicpu – 2.000, бо це t3.medium.
Повертаємось до дашборди, додаємо такий запит:
sum( sum(karpenter_nodes_total_pod_requests{instance_memory!="", node_name=~"$node_name", resource_type="cpu"}) by (node_name) / sum(karpenter_nodes_allocatable{instance_memory!="", node_name=~"$node_name", resource_type="cpu"}) by (node_name) ) by (node_name) * 100
Тут з karpenter_nodes_total_pod_requests
беремо загальну кількість requests від подів окрім DaemonSets і ділимо на загальну кількість vCPU на ноді – karpenter_nodes_allocatable{resource_type="cpu"}
.
Отримуємо значення у 65% – в принципі, збігається з тим, що порахували вручну (0.631):
Тепер, додамо трохи краси – хочеться відобразити це значення шкалою.
Йдемо до Field override, вибираємо колонку Node CPU requested %, і спершу міняємо тип даних на проценти – Standard options > Unit > Percent 0-100:
Додаємо ще один property – міняємо тип на Gauge, і ще один property – Standard options > Max == 100:
Трохи підлаштуємо Tresholds – базовий буде червоний, тобто – якщо значення CPU Requested % низьке – то це погано, бо нода не використовується повністю. Трохи вище – жовтий, і максимум – зелений:
Інформація по Memory
Тут в принципі все аналогічно до того, як ми робили для CPU.
Node Memory total
Запит:
sum(karpenter_nodes_total_pod_requests{instance_memory!="", node_name=~"$node_name"}) by (node_name, instance_memory)
Дані в метриці у мегабайтах, тому додаємо Override > Standard options > Unit – megabytes:
Node Memory requested by Pods
Запит:
sum(karpenter_nodes_total_pod_requests{instance_memory!="", node_name=~"$node_name", resource_type="memory"}) by (node_name)
Тут у нас байти, тому знов додаємо Override:
Node Memory requested by Pods %
Запит:
sum( sum(karpenter_nodes_total_pod_requests{instance_memory!="", node_name=~"$node_name", resource_type="memory"}) by (node_name) / sum(karpenter_nodes_allocatable{instance_memory!="", node_name=~"$node_name", resource_type="memory"}) by (node_name) ) by (node_name) * 100
І аналогічно до CPU % – робимо Gauge:
Інформація по Pods
Тут все схоже – скільки подів нода може мати максимум, скільки на ній зараз, і скільки % від максимуму зайнято.
Pods allocatable
Скільки подів максимум можна запустити на ноді:
sum(karpenter_nodes_allocatable{instance_memory!="", node_name=~"$node_name", resource_type="pods"}) by (node_name)
Pods allocated
Скільки подів запущено на ноді зараз.
Тут запит трохи інший, бо метрика karpenter_pods_state
зараз має лейблу node
замість node_name
(можливо, пізніше пофіксять), тому використовуємо label_replace()
.
І вибираємо всі поди в статусі Running:
sum (label_replace(karpenter_pods_state{phase="Running", node=~"$node_name"}, "node_name", "$1", "node", "(.*)")) by (node_name)
Pods allocated %
Запит, теж з label_replace
:
sum( sum (label_replace(karpenter_pods_state{phase="Running", node=~"$node_name"}, "node_name", "$1", "node", "(.*)")) by (node_name) / sum(karpenter_nodes_allocatable{instance_memory!="", node_name=~"$node_name", resource_type="pods"}) by (node_name) ) by (node_name) * 100
І налаштовуємо шкалу, як робили для CPU та Memory:
Перевіряємо дані по використанню подів у AWS Console > EKS > Compute:
Нода ip-10-0-34-184.ec2.internal
має 17 максимум, 11 запущених:
І на нашому графіку маємо ті ж самі дані:
Правда, чому AWS рахує 11 від 17 як 85% – не знаю, бо:
>>> 11/17*100 64.70588235294117
Тут у нас дані правильні теж.
І тепер все разом має такий вигляд:
Можемо переходити до наступної задачі – інформація по CPU/Memory подами по кожній WorkerNode.
Pods info tables
Що нам тут може бути цікавим?
- cpu, memory usage – current та avgerage – as numbers
- cpu, memory requested as number
- cpu, memory used as % from Node’s total
- cpu, memory used as % from requested
Імена подів та Namespaces на WorkerNodes
Почнемо з того, що створимо таблицю, в якій будуть виводитись імена подів (це буде наш “об’єднуючий селектор” для інших запитів) та імена відповідних неймспейсів.
Перший запит – імена подів:
sum(kube_pod_info{node=~"$node_name", namespace=~"$namespace", pod!~".*cron.*"}) by (pod)
Другий – неймспейси:
sum(kube_pod_info{node=~"$node_name", namespace=~"$namespace", pod!~".*cron.*"}) by (pod, namespace)
Аналогічно до попередньої таблиці – перемикаємо Data source на ${datasource_vm}
, налаштовуємо Overrides і Transformations:
Тепер цікаве: хочеться мати окрему таблицю для кожної WorkerNode, яка вибрана в фільтрах.
Для цього в Panel Options включаємо опцію Repeat options і вибираємо нашу змінну $node_name
.
З якогось дива не можна виставити Max per row == 1 в горизонтальному відображенні, тому, аби все було красиво, зробимо окрему колонку з таблицями під CPU і окрему під Memory, а в Repeate direction розмістимо їх Vertical:
Що це дуже зручно: значення $node_name
у queries у кожній таблиці буде мати тільки ту WorkerNode, для якої відображається конкретно ця панель, а не всі ноди, які обрані у загальному фільтрі зверху. Тобто фільтр буде впливати тільки на кількість панелей, а не на запити по ресурсам подів всередині цих панелей.
Тепер у нас панелі виглядають так:
Pod CPU info
Перша колонка у нас буде відображати інформацію по CPU на нодах – ім’я поду, його неймспейс, скільки використовується зараз (в milicpu), скільки використовується в середньому (в milicpu), відсоток використання від загального vCPU на ноді, скільки под зареквестив, і скільки % від реквестів він використовує.
Pod CPU usage
Додаємо наступний запит – скільки под використовує ресурсів CPU:
sum(rate(container_cpu_usage_seconds_total{instance=~"$node_name", image!="", namespace=~"$namespace"}[5m])) by (pod) * 1000
Тут рахуємо per-second average rate для значення container_cpu_usage_seconds_total
протягом останніх 5 хвилин по кожному контейнеру в поді, множимо на 1000, щоб перевести це значення у millicores.
Перевіримо значення з kubectl top pod
:
$ kk -n kube-system top pod aws-node-q56z4 NAME CPU(cores) MEMORY(bytes) aws-node-q56z4 4m 61Mi
І в панелі 3.67 millicores:
Це ми отримали поточне значення – давайте додамо average. Запит той самий, тільки з avg()
замість sum()
:
Pod CPU use % from vCPU total
Додамо шкалу, яка буде відображати скільки % від загального CPU на ноді використовує кожен под:
( sum(rate(container_cpu_usage_seconds_total{instance=~"$node_name", image!="", namespace=~"$namespace"}[5m])) by (pod) / sum(karpenter_nodes_allocatable{instance_memory!="", node_name=~"$node_name", resource_type="cpu"}) ) * 100
Налаштовуємо Tresholds:
Налаштовуємо Overrides:
Pod CPU Requests
Далі – Pod CPU requests.
Спочатку скільки под requested в milicpu:
sum(rate(container_cpu_usage_seconds_total{instance=~"$node_name", image!="", namespace=~"$namespace"}[5m])) by (pod) * 1000
Перевіряємо з kubectl
:
$ kk -n prod-backend-api-ns describe pod backend-api-deployment-7d7969d69f-7r66t ... Requests: cpu: 512m memory: 800Mi ...
І в панелі:
Pod CPU Requests %
І відобразимо, скільки % від загальної кількості vCPU ноди використовується кожним подом:
І вся борда тепер має такий вигляд:
Pod Mem info
Тут все аналогічно, тільки інші запити окрім перших двох – для імен подів і їхніх неймспейсів.
Pod name:
sum(kube_pod_info{node=~"$node_name", namespace=~"$namespace", pod!~".*cron.*"}) by (pod)
Namespace name:
sum(kube_pod_info{node=~"$node_name", namespace=~"$namespace", pod!~".*cron.*"}) by (pod, namespace)
Memory use
Current:
sum(container_memory_working_set_bytes{instance=~"$node_name", namespace=~"$namespace", container!="POD", container!=""}) by (pod)
Average:
avg(container_memory_working_set_bytes{instance=~"$node_name", namespace=~"$namespace", container!="POD", container!=""}) by (pod)
Не забуваємо перевіряти результати, аби потім, коли дашборда буде використовуватись, не виявилось, що вона відображає некоректні дані.
Глянемо пам’ять поду з kubectl top pod
:
$ kk -n prod-backend-api-ns top pod backend-api-deployment-7d7969d69f-nslxk NAME CPU(cores) MEMORY(bytes) backend-api-deployment-7d7969d69f-nslxk 45m 574Mi
І порівняємо з даними в дашборді:
Окей, наче все вірно. Го далі.
Memory use %
Скільки % від загальної пам’яті на ноді використовує под:
( sum(container_memory_working_set_bytes{instance=~"$node_name", namespace=~"$namespace", container!="POD", container!=""}) by (pod) / sum(karpenter_nodes_allocatable{instance_memory!="", node_name=~"$node_name", resource_type="memory"}) ) * 100
На ноді t3.medium маємо 3.469 доступної пам’яті (мінус всякі резервації):
Под backend-api-deployment-7d7969d69f-nslxk
використовує 575, це буде:
>>> 575/3469*100 16.57
І в нашій панелі бачимо 17.4% – норм, плюс-мінус сходиться.
Memory requsted
Запит:
sum(container_memory_working_set_bytes{instance=~"$node_name", namespace=~"$namespace", container!="POD", container!=""}) by (pod)
Вже дивились з kubectl describe pod
:
$ kk -n prod-backend-api-ns describe pod backend-api-deployment-7d7969d69f-7r66t ... Requests: cpu: 512m memory: 800Mi ...
І в панелі:
Memory requsted %
Скільки % від requested пам’яті на ноді використовує под:
( sum(container_memory_working_set_bytes{instance=~"$node_name", namespace=~"$namespace", container!="POD", container!=""}) by (pod) / sum(kube_pod_container_resource_requests{job="kube-state-metrics", node=~"$node_name", namespace=~"$namespace", resource="memory"}) by (pod) ) * 100
І вся борда разом тепер виглядає так:
В принципі, на цьому все.
Можна ще погратись, наприклад – додати Stats панелі з інформацією по загальній кількості CPU/Mem/Pods в кластері типу такого:
Але в мене це є в іншій дашборді (див. Karpenter: моніторинг та Grafana dashboard для Kubernetes WorkerNodes).