Grafana: AWS EC2 resources та Kubernetes Pods requests

Автор |  19/04/2024
 

У 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).