Дебажимо одну проблему з використанням пам’яті в 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 мегабайт.
Залишилось розібратись чому саме спавняться процеси, але то вже задачка девелоперам.