Як обмежити ресурси контейнера (CPU, пам'ять)?
Без resource-лімітів один проблемний container може з'їсти увесь host CPU або пам'ять і завалити усе інше. Docker експонує Linux cgroup-ліміти через прості флаги. Ставити їх це базова прод-гігієна.
Теорія
TL;DR
- Docker використовує Linux cgroups для enforce лімітів.
- Пам'ять:
--memory=512m(hard cap; container OOM-kill, якщо перевищено). - CPU:
--cpus=1.5(throttle; container може використовувати до 1.5 ядер CPU-часу). - Reservation:
--memory-reservation=256m(soft мінімум). - Compose має
deploy.resources.limitsіdeploy.resources.reservations. - Дефолт = необмежено. Container без лімітів може використати увесь host.
Memory-ліміти
docker run -d --memory=512m --name api myapp
# Hard cap. Якщо api алокує більше 512MB, kernel OOM-kill.
docker run -d --memory=512m --memory-swap=1g myapp
# 512MB RAM + до 512MB swap = 1GB сумарно
docker run -d --memory=512m --memory-swap=512m myapp
# 512MB RAM, БЕЗ swap (постав memory-swap = memory)
docker run -d --memory=512m --memory-swap=-1 myapp
# 512MB RAM, необмежений swap (НЕБЕЗПЕЧНО, заповнює диск)Memory-одиниці: b (bytes), k (KiB), m (MiB), g (GiB). Дефолт байти.
OOM-поведінка: коли container досягає memory-ліміту, Linux OOM-killer завершує процес всередині. Exit code 137. docker inspect показує OOMKilled: true.
CPU-ліміти
docker run -d --cpus=0.5 myapp
# 0.5 ядер CPU-часу. Container може спайкнути до 100% одного ядра,
# але в середньому за час лишається на 50% одного ядра.
docker run -d --cpus=2 myapp
# 2 повних ядра.
docker run -d --cpu-shares=1024 myapp
# Відносна вага (дефолт 1024). Два container з shares 1024 і 512 ділять CPU 2:1.
# Має значення лише під контестом.
docker run -d --cpuset-cpus=0,1 myapp
# Прив'язати до конкретних CPU-ядер (NUMA-aware).CPU-поведінка: на відміну від пам'яті, досягнення CPU-ліміту не вбиває container, лише сповільнює. Процес отримує менше CPU-слайсів.
Compose-синтаксис
services:
api:
image: myapp
deploy:
resources:
limits:
cpus: "0.5"
memory: 512M
reservations:
cpus: "0.25"
memory: 256MУ Compose v3+ deploy.resources працює і для standalone Compose, і для Swarm. У legacy v2 Compose синтаксис був cpus: і mem_limit: нагорі, досі працює для backward compat.
Reservation vs limit
- Limit: максимум, що container може використати. Hard cap для пам'яті; throttle для CPU.
- Reservation: мінімум гарантований. Scheduler (Swarm, K8s) розміщує container на node, що може задовольнити reservation; під контестом зарезервовані ресурси йдуть container раніше за інших.
Для single-host Compose reservation важать менше (один host); для Swarm вони рухають placement-рішення.
Inspecting лімітів і usage
# Які ліміти у container
docker inspect api --format '{{.HostConfig.Memory}} {{.HostConfig.NanoCpus}}'
# Пам'ять у байтах, CPU у nanoCPU (1 CPU = 1e9)
# Live-використання
docker stats --no-stream
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM %
a3f9d2b8c1e4 api 45.2% 312MiB / 512MiB 60.94%Колонка MEM USAGE / LIMIT робить contention очевидним. Наближення до 100% означає, що OOM ймовірний.
Типові помилки
Без лімітів у проді
docker run -d nginx # без лімітів
docker run -d misbehaving-app # без лімітів
# misbehaving-app з'їдає увесь RAM; OS OOM-kill випадкові процеси;
# nginx може стати жертвою. Увесь host стає нестабільним.Фікс: постав sensible-ліміти на кожен довгоживучий container. Навіть щедрий --memory=4g набагато краще за unlimited.
Memory-ліміт занизький для runtime
docker run --memory=64m java-app
# JVM скоріш за все стартує, потім OOM-kill себе, намагаючись алокувати heap.Мовні runtime мають мінімальний overhead. JVM, Node, Python всі потребують 50-100MB просто на старт. Обери ліміт, що вміщує навантаження + margin.
Плутати cpu-shares з cpus
--cpus=0.5= абсолютний throttle (50% одного ядра, завжди).--cpu-shares=512= відносна вага (має значення лише під contention; без contention container може використати увесь доступний CPU).
Для передбачуваного performance бери --cpus. cpu-shares для prioritization серед container, коли host повністю зайнятий.
Забути, що JVM не бачить container-лімітів без допомоги
На старих JVM (до Java 10) JVM дивився на host-пам'ять, не cgroup-ліміт, і намагався використати host-RAM. Результат: container OOM-kill. Сучасні JVM (Java 10+) container-aware за замовчуванням. Для старих JVM бери -XX:MaxRAMPercentage=75.0 або -Xmx. Та сама caveat для деяких Node і Python tools.
Реальне застосування
- Прод: кожен довгоживучий container має memory + CPU ліміти. Розмір на основі спостереженого P99-використання + буфер.
- Multi-tenant node: строгі ліміти запобігають заморюванню одним tenant'ом інших.
- CI-runner:
--cpus=2 --memory=4gна build-container, щоб паралельні job не сповільнювали одне одного. - Локальний dev: іноді варто ставити скромні ліміти, щоб ловити регресії раніше (memory leak, що проявляється на 8GB, ніколи не проявиться, якщо на лептопі 32GB).
Питання для поглиблення
Q: Яка різниця між --memory і --memory-swap?
A: --memory лише RAM. --memory-swap сумарно RAM + swap. Постав --memory-swap = --memory, щоб повністю вимкнути swap (рекомендовано для передбачуваного performance).
Q: Чому мій container вийшов з 137, хоч я думав, що ще було пам'яті?
A: Можливості: (1) OOM-killer обрав головний процес твого container через OOM-score. (2) kill -9 ззовні. (3) Daemon-grace-період минув під час stop. Перевір docker inspect <name> на OOMKilled: true, щоб підтвердити OOM спеціально.
Q: Чи можу оновити ліміти без рестарту?
A: Так, через docker update: docker update --memory=1g --cpus=1 api. Зміна застосовується одразу до running container, рестарт не потрібен.
Q: Як ліміти працюють з --privileged?
A: Ліміти все одно діють. --privileged зриває capability-обмеження (дає container raw block I/O тощо), але НЕ прибирає cgroup-ліміти.
Q: (Senior) Як на практиці підбирати memory-ліміти?
A: Запусти навантаження реалістично (load-test, прод-трафік) без лімітів. Дивися peak пам'яті у docker stats. Постав ліміт на peak + 30-50% запасу. Повторюй після кожної значимої зміни коду. Для JVM/Python враховуй overhead runtime і будь-які кеші. Rule of thumb: занадто тісно вбиває застосунок на traffic-сплесках; занадто вільно дає memory-leak'ам жити непомітно; правильна цифра трохи вище worst-case спостереженого.
Приклади
Розмірований сервіс з моніторингом
$ docker run -d \
--name api \
--memory=512m \
--memory-reservation=256m \
--cpus=1 \
--restart=unless-stopped \
myapp:1.0
$ docker stats --no-stream api
CONTAINER CPU % MEM USAGE / LIMIT MEM %
api 12.3% 220MiB / 512MiB 43.0%Usage-tracking вбудовано. Alert (через Prometheus або подібне), коли MEM% > 80% стабільно.
Compose з reservation
services:
api:
image: myapp
deploy:
resources:
limits:
cpus: "1"
memory: 512M
reservations:
cpus: "0.5"
memory: 256M
db:
image: postgres:16
deploy:
resources:
limits:
memory: 1GDB отримує вищу memory-стелю, бо Postgres кешує working set. API CPU-тісніший.
Оновити running container
$ docker stats --no-stream api
MEM USAGE / LIMIT MEM %
450MiB / 512MiB 87.9%
# Близько до ліміту
$ docker update --memory=1g api
api
$ docker stats --no-stream api
MEM USAGE / LIMIT MEM %
450MiB / 1GiB 43.9%Рестарт не потрібен. Корисно для emergency-response без redeploy.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.
Коментарі
Ще немає коментарів