Skip to main content

Як обмежити ресурси контейнера (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-ліміти

bash
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-ліміти

bash
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-синтаксис

yaml
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

bash
# Які ліміти у 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 ймовірний.

Типові помилки

Без лімітів у проді

bash
docker run -d nginx # без лімітів docker run -d misbehaving-app # без лімітів # misbehaving-app з'їдає увесь RAM; OS OOM-kill випадкові процеси; # nginx може стати жертвою. Увесь host стає нестабільним.

Фікс: постав sensible-ліміти на кожен довгоживучий container. Навіть щедрий --memory=4g набагато краще за unlimited.

Memory-ліміт занизький для runtime

bash
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 спостереженого.

Приклади

Розмірований сервіс з моніторингом

bash
$ 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

yaml
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: 1G

DB отримує вищу memory-стелю, бо Postgres кешує working set. API CPU-тісніший.

Оновити running container

bash
$ 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.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Коментарі

Ще немає коментарів