Kubernetes: моніторинг процесів з process-exporter

Автор |  22/09/2025
 

Дебажимо одну проблему з використанням пам’яті в Kubernetes Pods, і вирішили подивитись на пам’ять і кількість процесів на нодах.

Сама проблема полягає в тому, що зазвичай Kubernetes Pod з Livekit споживає близько 2 гігабайт пам’яті, але іноді бувають спайки до 10-11 гіг, через що под вбивається:

Що ми хочемо визначити: це один процес починає стільки пам’яті “їсти” – чи просто створюється багато процесів в контейнері?

Самий простий варіант тут – використати Prometheus Process Exporter, який запускається у вигляді DaemonSet, на кожній WorkerNode створює власний контейнер, і для всіх чи обраних процесів на EC2 збирає статистику з /proc.

Є непоганий (і працюючий) Helm-чарт kir4h/process-exporter, візьмемо його.

Запуск Process Exporter

Додаємо репозиторій, встановлюємо:

$ helm repo add kir4h https://kir4h.github.io/charts
$ helm install my-process-exporter kir4h/process-exporter

Або в нашому випадку – встановлюємо через Helm dependency – додаємо чарт до Chart.yaml чарту нашого стеку моніторинга:

...
- name: process-exporter
  version: ~1.0
  repository: https://kir4h.github.io/charts
  condition: process-exporter.enabled

Додаємо values для нього:

...
process-exporter:
  enabled: true
  tolerations:
  - effect: NoSchedule
    operator: Exists
  - key: CriticalAddonsOnly
    operator: Exists
    effect: NoSchedule
  - key: CriticalAddonsOnly

Деплоїмо, перевіряємо DaemonSet:

$ kk get ds
NAME                                             DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR            AGE
atlas-victoriametrics-process-exporter           9         9         9       9            9           <none>                   76m
...

І перевіряємо ServiceMonitor:

$ kk get serviceMonitor | grep process
atlas-victoriametrics-process-exporter                   3d3h

Для VictoriaMetrcis автоматично створюється VMServiceScrape:

$ kk get VMServiceScrape | grep process
atlas-victoriametrics-process-exporter                   3d3h   operational

Перевіряємо чи є метрики, наприклад по namedprocess_namegroup_memory_bytes:

Створення Name Groups

Зараз маємо дані по взагалі всім процесам – нам це не треба.

Конкретно в нашому випадку нас цікавить статистика по процесам нашого Backend API, процеси Python.

У нас їх три основних – сам Backend API, Celery Workers, та власне Livekit, і кожен сервіс запускається у власних Pods з окремих Deployments.

Знаходимо процеси  в подах, дивимось як саме вони запущені.

Backend API:

root@backend-api-deployment-5695989cb5-rjhv9:/app# ps aux
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root           1  0.0  0.2  40348 34712 ?        Ss   07:59   0:02 /usr/local/bin/python /usr/local/bin/gunicorn challenge_backend.run_api:app [...]
root           7  1.2  2.5 2075368 414564 ?      Sl   07:59   1:32 /usr/local/bin/python /usr/local/bin/gunicorn challenge_backend.run_api:app [...]
root           8  1.1  2.6 1999384 422228 ?      Sl   07:59   1:23 /usr/local/bin/python /usr/local/bin/gunicorn challenge_backend.run_api:app [...]
root           9  1.2  2.6 2002492 429192 ?      Sl   07:59   1:30 /usr/local/bin/python /usr/local/bin/gunicorn challenge_backend.run_api:app [...]
...

Celery workers:

root@backend-celery-workers-deployment-5bc64557c8-zbq2j:/app# ps aux
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root           1  0.2  1.4 544832 236720 ?       Ss   07:27   0:24 /usr/local/bin/python /usr/local/bin/celery -A celery_app.app worker [...]
...

Та Livekit:

root@backend-livekit-agent-deployment-7d9bf86564-qgjzb:/app# ps aux
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root           1  0.4  1.8 2112944 294772 ?      Ssl  07:06   0:46 python -m cortex.livekit_agent.main start
root          24  0.0  0.0  15788 12860 ?        S    07:06   0:00 /usr/local/bin/python -c from multiprocessing.resource_tracker import main;main(34)
root          25  0.0  0.6 342976 102852 ?       S    07:06   0:02 /usr/local/bin/python -c from multiprocessing.forkserver import main [...]
...

Додаємо конфіг для process-exporter – описуємо nameMatchers:

...
process-exporter:
  enabled: true
  tolerations:
    operator: Exists
    effect: NoSchedule
  - key: CriticalAddonsOnly
  config:
    # metrics will be broken down by thread name as well as group name
    threads: true
    # any process that otherwise isn't part of its own group becomes part of the first group found (if any) when walking the process tree upwards
    children: true
    # means that on each scrape the process names are re-evaluated
    recheck: false
    # remove_empty_groups drop empty groups if no processes found
    remove_empty_groups: true
    nameMatchers: 
      # gunicorn (python + uvicorn workers)
      - name: "gunicorn"
        exe:
          - /usr/local/bin/python
        cmdline:
          - ".*gunicorn.*"

      # celery worker
      - name: "celery-worker"
        exe:
          - /usr/local/bin/python
        cmdline:
          - ".*celery.*worker.*"

      # livekit agent
      - name: "livekit-agent"
        exe:
          - python
          - /usr/local/bin/python
        cmdline:
          - ".*cortex.livekit_agent.main.*"

      # livekit multiprocessing helpers
      - name: "livekit-multiproc"
        exe:
          - /usr/local/bin/python
        cmdline:
          - ".*multiprocessing.*"

Тут в exe – список самого executable (можна кілька), а в cmdline – аргументи, з якими процес запущено.

Тобто для Livekit у нас exe – “/usr/local/bin/python“, а cmdline – це “-c from multiprocessing.resource_tracker [...]” або “-c from multiprocessing.forkserver [...]“.

Деплоїмо, і тепер залишилось тільки три групи:

 

Але є нюанси.

Перше – статистика збирається з кожної ноди по всій групі процесів.

Тобто, якщо ми зробимо:

sum(namedprocess_namegroup_memory_bytes{memtype="resident", groupname="celery-worker"}) by (groupname, instance, pod)

То отримаємо суму всіх RSS всіх Celery-воркерів на ноді, де запущений відповідний process-exporter Pod:

А друга проблема – що Process Exporter не має лейбли з іменем WorkerNode, з якої зібрані метрики.

Тому тут тільки шукати вручну – по Pod IP (лейбла instance) можемо знайти його Node:

$ kk get pod -o wide | grep 10.0.45.166
atlas-victoriametrics-process-exporter-4zdzl                      1/1     Running     0              6m51s   10.0.45.166   ip-10-0-40-195.ec2.internal   <none>           <none>

А потім вже дивитись які поди на цій ноді:

$ kk describe node ip-10-0-40-195.ec2.internal | grep celery
  dev-backend-api-ns          backend-celery-workers-deployment-5bc64557c8-hqhz4                 200m (5%)     0 (0%)      1500Mi (10%)     0 (0%)         3h28m
  dev-backend-api-ns          backend-celery-workers-long-running-deployment-57d7cb9984-nlfs4    200m (5%)     0 (0%)      1500Mi (10%)     0 (0%)         3h12m
  prod-backend-api-ns         backend-celery-workers-deployment-5597dfd875-m7c2n                 500m (12%)    0 (0%)      1500Mi (10%)     0 (0%)         99m
  staging-backend-api-ns      backend-celery-workers-long-running-deployment-5bb44795b7-pcmj2    200m (5%)     0 (0%)      1500Mi (10%)     0 (0%)         103m

І на ноді глянемо процеси і їхній RSS:

[root@ip-10-0-40-195 ec2-user]# ps -eo rss,cmd | grep celery
232888 /usr/local/bin/python /usr/local/bin/celery -A celery_app.app worker --loglevel=info -Q default
241656 /usr/local/bin/python /usr/local/bin/celery -A celery_app.app worker --loglevel=info -Q default
...
239232 /usr/local/bin/python /usr/local/bin/celery -A celery_app.app worker --loglevel=info -Q default
252240 /usr/local/bin/python /usr/local/bin/celery -A celery_app.app worker --loglevel=info -Q default
 2416 grep --color=auto celery

На графіку у нас тут 4,604,280,832 байт:

Рахуємо самі:

[root@ip-10-0-40-195 ec2-user]# ps -eo rss,cmd | grep celery | grep -v grep | awk '{sum += $1} END {print sum*1024 " bytes"}'
4608430080 bytes

Повертаючись до питання того, що немає інформації по кожному процесу: ми можемо отримати середнє значення по кожному, бо у нас є метрика namedprocess_namegroup_num_procs:

Перевіряємо ще раз самі на ноді:

[root@ip-10-0-40-195 ec2-user]# ps aux | grep celery | grep -v grep | wc -l
20

І можемо зробити такий запит:

sum(namedprocess_namegroup_memory_bytes{memtype="resident", groupname="celery-worker", instance="10.0.45.166:9256"}) by (groupname, instance, pod)
/
sum(namedprocess_namegroup_num_procs{groupname="celery-worker", instance="10.0.45.166:9256"}) by (groupname, instance, pod)

Результат ~230 MB:

Як ми і бачили в ps -eo rss,cmd.

Name Group Template variables та інформація по кожному процесу

Або, якщо нам прям дуже хочеться бачити статистику по кожному процесу – ми можемо використати динамічні імена для groupname з {{.PID}} – тоді для кожного процесу буде формуватись окрема група, див. Using a config file: group name:

...
    nameMatchers: 
      # gunicorn (python + uvicorn workers)
      - name: "gunicorn-{{.Comm}}-{{.PID}}"
        exe:
          - python
          - /usr/bin/python
          - /usr/local/bin/python
        cmdline:
          - ".*gunicorn.*"

      # celery worker
      - name: "celery-worker-{{.Comm}}-{{.PID}}"
        exe:
          - python
          - /usr/bin/python
          - /usr/local/bin/python
        cmdline:
          - ".*celery.*worker.*"

      # livekit agent
      - name: "livekit-agent-{{.Comm}}-{{.PID}}"
        exe:
          - python
          - /usr/bin/python
          - /usr/local/bin/python
        cmdline:
          - ".*livekit_agent.*"

      # livekit multiprocessing helpers
      - name: "livekit-multiproc-{{.Comm}}-{{.PID}}"
        exe:
          - python
          - /usr/bin/python
          - /usr/local/bin/python
        cmdline:
          - ".*multiprocessing.*"

В результаті маємо такі групи:

Але цей варіант ОК тільки для якщо вам треба щось подебажити, і відключити, бо призведе до High cardinality issue.

Результат нашого дебагу

Власне, що нам потрібно було дізнатись – пам’ять “утікає” в якомусь одному процесі, чи просто створюється багато процесів в одному Pod?

Для цього в Grafana зробили графік із запитом:

sum(namedprocess_namegroup_memory_bytes{memtype="resident", groupname=~"livekit-multiproc-.*"}) by (groupname, instance)

До нього додали графіки з метриками самого Livekit – lk_agents_active_job_count та lk_agents_child_process_count, і окремо – графік з VictoriaLogs, де виводимо кількість API-запитів кожного юзера по полю token_email:

namespace: "prod-backend-api-ns" "GET /cortex/livekit-token" | unpack_json fields (token_email) | stats by (token_email) count()

І в результаті маємо таку картину:

Де і бачимо, що один і той же юзер починає робити пачку запитів для підключення до Livekit, через що в Livekit Pod створюється пачка процесів (по новій Livekit Job на кожен запит), і в результаті загальна кількість пам’яті в поді зашкалює, бо 40 процесів по ~380 MB це ~15 гігабайт пам’яті.

Але в кожному конкретному процесі пам’ять тримається на рівні 300-400 мегабайт.

Залишилось розібратись чому саме спавняться процеси, але то вже задачка девелоперам.