توی این درس یاد میگیری چرا کانتینر بدون محدودیت میتواند کل سرور را از پا بیندازد و چطور با --memory و --cpus سقف مصرف حافظه و پردازنده بگذاری. OOM killer را در عمل میبینی (کد خروج ۱۳۷ و OOMKilled=true)، با docker stats مصرف زنده را میخوانی، با --pids-limit جلوی تکثیر بینهایت پروسه را میگیری، با docker update محدودیت را در لحظه عوض میکنی و همه را در Compose هم مینویسی. در تمرین اصلی یک کانتینر را محدود میکنی و رفتارش زیر بار را میبینی.
مسئله: یک کانتینر، کل سرور
Section titled “مسئله: یک کانتینر، کل سرور”بهطور پیشفرض یک کانتینر میتواند تمام حافظه و CPU میزبان را بخورد. اگر اپی نشتی حافظه (memory leak) داشته باشد یا حلقهی بیپایان، فقط خودش نمیمیرد: کل سرور کند میشود و کانتینرهای دیگر (حتی دیتابیس) را هم میکشد.
تشبیه: سهم هر مستأجر از ساختمان
Section titled “تشبیه: سهم هر مستأجر از ساختمان”یک ساختمان مشترک را با مستأجرها شریک شدهای. اگر هر مستأجر بتواند هر قدر آب و برق بخواهد مصرف کند، یکی که شیر را باز بگذارد، کل ساختمان را بیآب میکند. سهمیه برای هر واحد (حافظه و CPU) یعنی مشکل یک واحد فقط در همان واحد میماند.
مثالهای عملی
Section titled “مثالهای عملی”مثال ۱: محدودیت را بگذار و ببین
Section titled “مثال ۱: محدودیت را بگذار و ببین”docker run -d --name lx-rl1 --memory 64m --cpus 0.5 python:3.12-alpine sleep 120 >/dev/nulldocker inspect lx-rl1 --format 'Memory={{.HostConfig.Memory}} بایت NanoCpus={{.HostConfig.NanoCpus}}'echo "داخل کانتینر (cgroup):"docker exec lx-rl1 sh -c 'echo " memory.max = $(cat /sys/fs/cgroup/memory.max)"; echo " cpu.max = $(cat /sys/fs/cgroup/cpu.max)"'Memory=67108864 بایت NanoCpus=500000000داخل کانتینر (cgroup): memory.max = 67108864 cpu.max = 50000 100000--memory 64m یعنی ۶۴ مگابایت (۶۷۱۰۸۸۶۴ بایت) و --cpus 0.5 یعنی نصف یک هسته. داکر اینها را در cgroup کانتینر مینویسد؛ داخل کانتینر هم همان فایلها را میبینی. در cpu.max عدد 50000 100000 یعنی «در هر پنجرهی ۱۰۰ میلیثانیهای، فقط ۵۰ میلیثانیه CPU».
مثال ۲: docker stats، مصرف زنده
Section titled “مثال ۲: docker stats، مصرف زنده”docker stats --no-stream --format 'table {{.Name}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.CPUPerc}}\t{{.PIDs}}' lx-rl1NAME MEM USAGE / LIMIT MEM % CPU % PIDSlx-rl1 1.82MiB / 64MiB 2.84% 0.00% 1ستونها: مصرف و سقف حافظه (1.7MiB / 64MiB)، درصد، درصد CPU و تعداد پروسهها. بدون --no-stream خروجی زنده است و مثل top هر ثانیه تازه میشود (با Ctrl+C خارج شو). برای چند کانتینر همزمان اسمها را پشت هم بنویس یا اسمی ندهی تا همه را نشان بدهد.
در docker stats ستون MEM USAGE / LIMIT چه نشان میدهد؟
اگر محدودیت نگذاشته باشی، LIMIT برابر کل حافظهی میزبان است.
مثال ۳: رد شدن از سقف حافظه، OOM killer
Section titled “مثال ۳: رد شدن از سقف حافظه، OOM killer”docker run --name lx-rl2 --memory 64m python:3.12-alpine python -c "x = b'a' * (200 * 1024 * 1024); print('تمام شد')" 2>&1 | tail -1docker inspect lx-rl2 --format 'ExitCode={{.State.ExitCode}} OOMKilled={{.State.OOMKilled}}'docker rm lx-rl2 >/dev/nullExitCode=137 OOMKilled=trueبرنامه خواست ۲۰۰ مگابایت بگیرد در حالی که سقف ۶۴ بود. هسته آن را کشت: هیچ پیام خطایی از خود برنامه نیست (پروسه بیصدا مرد)، کد خروج 137 (= ۱۲۸ + سیگنال ۹) و OOMKilled=true. اگر کانتینری ناگهانی با ۱۳۷ میمیرد، اول OOMKilled را ببین (همان چیزی که در درس عیبیابی ناگفته مانده بود).
مثال ۴: همین برنامه با سقف کافی
Section titled “مثال ۴: همین برنامه با سقف کافی”docker run --rm --memory 400m python:3.12-alpine python -c "x = b'a' * (200 * 1024 * 1024); print('تمام شد، ۲۰۰ مگابایت گرفتم')"تمام شد، ۲۰۰ مگابایت گرفتمبا سقف ۴۰۰ مگابایت همان برنامه کار میکند. پس پیدا کردن سقف مناسب یعنی اندازهگیری مصرف واقعی (با docker stats زیر بار معمولی و اوج) و گذاشتن حاشیهی معقول. سقف خیلی کم، اپ سالم را میکشد؛ سقف خیلی زیاد، محافظت نمیکند.
مثال ۵: سقف CPU زیر بار
Section titled “مثال ۵: سقف CPU زیر بار”دو حلقهی بیپایان (که میتوانند دو هسته را بخورند) داخل کانتینری با --cpus 0.5:
docker run -d --name lx-rl3 --cpus 0.5 alpine sh -c 'yes > /dev/null & yes > /dev/null & wait' >/dev/nullsleep 3cpu() { docker exec lx-rl3 awk '/^usage_usec/{print $2}' /sys/fs/cgroup/cpu.stat; }u1=$(cpu); sleep 5; u2=$(cpu)echo "CPU مصرفشده در ۵ ثانیه: $(( (u2 - u1) / 50000 ))٪ از یک هسته (سقف ۵۰٪)"echo "تعداد دفعاتی که throttle شد: $(docker exec lx-rl3 awk '/^nr_throttled/{print ($2 > 0 ? "بیشتر از صفر" : "صفر")}' /sys/fs/cgroup/cpu.stat)"docker rm -f lx-rl3 >/dev/nullCPU مصرفشده در ۵ ثانیه: 52٪ از یک هسته (سقف ۵۰٪)تعداد دفعاتی که throttle شد: بیشتر از صفربا اینکه دو پروسه میخواستند هستهها را پر کنند، کل کانتینر حدود ۵۰٪ یک هسته گرفت (این عدد را از شمارندهی cpu.stat گرفتم که دقیقتر از یک نمونهی docker stats است؛ نمونهی لحظهای ممکن است کمی بالا یا پایین باشد). ۱۰۰٪ در docker stats یعنی یک هستهی کامل (پس روی ۴ هسته تا ۴۰۰٪ ممکن است). برخلاف حافظه، اینجا هیچ پروسهای نمیمیرد؛ فقط کند میشود.
مثال ۶: محدودیت تعداد پروسه (--pids-limit)
Section titled “مثال ۶: محدودیت تعداد پروسه (--pids-limit)”docker run -d --name lx-rl4 --pids-limit 10 alpine sleep 300 >/dev/nulldocker exec lx-rl4 sh -c 'for i in $(seq 1 30); do sleep 100 & done; wait' 2>&1 | sort -u | head -2docker stats --no-stream --format 'تعداد پروسهها در کانتینر: {{.PIDs}}' lx-rl4docker rm -f lx-rl4 >/dev/nullsh: can't fork: Resource temporarily unavailableتعداد پروسهها در کانتینر: 9با سقف ۱۰ پروسه، ساختن ۳۰ پروسه نیمهکاره ماند: خطای «can’t fork: Resource temporarily unavailable» و تعداد پروسهها هرگز از ۱۰ بیشتر نشد. این سپر در برابر «fork bomb» (برنامهای که پروسهها را بینهایت تکثیر میکند) و نشتی پروسه است.
مثال ۷: تغییر محدودیت در لحظه با docker update
Section titled “مثال ۷: تغییر محدودیت در لحظه با docker update”docker run -d --name lx-rl5 --memory 64m alpine sleep 120 >/dev/nullecho "قبل: $(docker exec lx-rl5 cat /sys/fs/cgroup/memory.max)"docker update --memory 128m --memory-swap 128m lx-rl5 >/dev/nullecho "بعد: $(docker exec lx-rl5 cat /sys/fs/cgroup/memory.max)"docker rm -f lx-rl5 >/dev/nullقبل: 67108864بعد: 134217728بدون ریستارت کانتینر، سقف از ۶۴ به ۱۲۸ مگابایت رسید. برای اورژانس عالی است (کانتینر دارد OOM میخورد و میخواهی موقتاً سقف را بالا ببری). تغییر دائمی را در دستور اجرا یا Compose هم بنویس.
مثال ۸: اپ «حافظهی میزبان» را میبیند، نه سقف خودش
Section titled “مثال ۸: اپ «حافظهی میزبان» را میبیند، نه سقف خودش”docker run --rm --memory 64m python:3.12-alpine sh -c 'echo "MemTotal در /proc/meminfo: $(grep MemTotal /proc/meminfo | tr -s " " | cut -d" " -f2) KB"; echo "سقف واقعی (cgroup): $(( $(cat /sys/fs/cgroup/memory.max) / 1024 )) KB"'MemTotal در /proc/meminfo: 4010356 KBسقف واقعی (cgroup): 65536 KB/proc/meminfo داخل کانتینر هنوز حافظهی کل میزبان را نشان میدهد، ولی سقف واقعی کمتر است. برنامههایی که اندازهی کش یا heap خود را از /proc/meminfo حساب میکنند (بعضی نسخههای قدیمی Java، Node، کتابخانههای C) بیش از حد میگیرند و OOM میخورند. نسخههای جدید زبانها cgroup را میفهمند؛ همیشه با docker stats زیر بار بسنج.
مثال ۹: در Compose
Section titled “مثال ۹: در Compose”name: lxrl
services: worker: image: alpine command: sleep 120 mem_limit: 128m cpus: 0.5 pids_limit: 50cd rldocker compose up -d >/dev/null 2>&1docker inspect lxrl-worker-1 --format 'Memory={{.HostConfig.Memory}} NanoCpus={{.HostConfig.NanoCpus}} Pids={{.HostConfig.PidsLimit}}'docker compose down >/dev/null 2>&1Memory=134217728 NanoCpus=500000000 Pids=50کلیدهای mem_limit، cpus و pids_limit همان محدودیتها را در Compose میگذارند. (شکل قدیمیتر deploy.resources.limits هم در Compose نسخهی ۲ پشتیبانی میشود.) و restart: unless-stopped + سقف حافظه یعنی کانتینر نشتیدار بهجای از کار انداختن سرور، کشته و دوباره بالا میآید.
پشت پرده
Section titled “پشت پرده”محدودیتها را cgroupهای هستهی لینوکس اعمال میکنند (control groups: سلسلهمراتبی از گروه پروسهها با سهمیهی منابع). داکر برای هر کانتینر یک cgroup میسازد و مقدارها را در فایلهایی مثل memory.max و cpu.max مینویسد؛ هسته بقیه را انجام میدهد.
- حافظه: وقتی مصرف cgroup به
memory.maxبرسد، هسته اول سعی میکند حافظهی کش را آزاد کند؛ اگر نشد، OOM killer یکی از پروسههای همان cgroup را میکشد (کانتینری که پروسهی اصلیاش کشته شود، متوقف میشود). نه کانتینرهای دیگر را. - CPU:
--cpus 0.5با سهمیهی زمانی (cpu.max) اعمال میشود: در هر دورهی ۱۰۰ میلیثانیهای فقط ۵۰ میلیثانیه اجرا. اگر بیشتر بخواهد، پروسه تا دورهی بعد منتظر میماند (throttling).--cpu-sharesیک وزن نسبی است که فقط وقتی CPU کم باشد اثر میگذارد. - swap:
--memory-swapمجموع حافظه و swap است؛ برابر--memoryبگذاری یعنی swap ندارد. اگر آن را ندهی و swap روی میزبان فعال باشد، داکر بهطور پیشفرض به اندازهی--memoryهم swap اجازه میدهد (مجموع دو برابر)؛ در تمرین اصلی برنامه با سقف ۵۰ مگابایت تا ۹۰ مگابایت جلو رفت و بعد کشته شد، به همین دلیل. swap زیاد، کانتینر را بهجای مردن، بهشدت کند میکند. - روی Docker Desktop (مک و ویندوز) کانتینرها داخل یک ماشین مجازیاند که خودش سقف حافظه و CPU دارد (تنظیمات Docker Desktop)؛ در مثال ۸
MemTotalهمان سقف VM بود.
جدولهای مرجع
Section titled “جدولهای مرجع”| میخواهم… | گزینه |
|---|---|
| سقف حافظه | --memory 512m (g برای گیگابایت) |
| حافظه+swap | --memory-swap 512m (برابر memory: بدون swap) |
| سقف CPU | --cpus 1.5 |
| وزن نسبی CPU | --cpu-shares 512 (پیشفرض ۱۰۲۴) |
| انتخاب هستهها | --cpuset-cpus 0,1 |
| سقف تعداد پروسه | --pids-limit 100 |
| تغییر در لحظه | docker update --memory 1g ctr |
| مشاهده | docker stats، docker inspect |
| کلید Compose | معادل |
|---|---|
mem_limit |
--memory |
cpus |
--cpus |
pids_limit |
--pids-limit |
deploy.resources.limits.memory/cpus |
همان، به شکل Swarm/نسخهی جدید |
اشتباهات رایج
Section titled “اشتباهات رایج”۱) سقف حافظهی خیلی کم و کشته شدن اپ سالم
Section titled “۱) سقف حافظهی خیلی کم و کشته شدن اپ سالم”مثال ۳ و ۴. راهحل: مصرف واقعی را زیر بار بسنج و حاشیه بگذار (مثلاً ۱.۵ برابر اوج).
۲) فرمت اشتباه سقف
Section titled “۲) فرمت اشتباه سقف”docker run --rm --memory 64mega alpine true 2>&1 | head -1 | cut -c1-120invalid argument "64mega" for "-m, --memory" flag: invalid suffix: 'mega'واحد درست b، k، m، g است. راهحل: 64m.
۳) کمتر از حداقل مجاز
Section titled “۳) کمتر از حداقل مجاز”docker run --rm --memory 1m alpine true 2>&1 | head -1 | cut -c1-150docker: Error response from daemon: Minimum memory limit allowed is 6MBداکر برای حافظه حداقلی (حدود ۶ مگابایت) لازم دارد. راهحل: سقف واقعبینانه.
۴) تصور اینکه --cpus پروسه را میکشد
Section titled “۴) تصور اینکه --cpus پروسه را میکشد”CPU فقط throttle میشود؛ اپ کند میشود (latency بالا) ولی نمیمیرد. راهحل: مصرف CPU و تأخیر را در مانیتورینگ ببین.
۵) اعتماد به /proc/meminfo یا free در کانتینر
Section titled “۵) اعتماد به /proc/meminfo یا free در کانتینر”مثال ۸. راهحل: خواندن /sys/fs/cgroup/memory.max یا استفاده از نسخهای از runtime که cgroup را میفهمد.
یک کانتینر alpine sleep با سقف ۳۲ مگابایت بساز و با docker inspect مقدار Memory آن را (بر حسب بایت) بخوان.
دیدن جواب
docker run -d --name lx-e1 --memory 32m alpine sleep 60 >/dev/nulldocker inspect lx-e1 --format '{{.HostConfig.Memory}}'docker rm -f lx-e1 >/dev/null33554432تمرین اصلی: یک کانتینر را محدود کن و رفتارش زیر بار را ببین. اپ پایتونی را اجرا کن که هر ثانیه ۱۰ مگابایت حافظهی بیشتر میگیرد، با سقف ۵۰ مگابایت. نشان بده (۱) با docker stats مصرف بالا میرود (۲) در پایان کشته میشود (۳) OOMKilled=true است.
دیدن جواب
docker run -d --name lx-e2 --memory 50m python:3.12-alpine python -c "import timedata = []while True: data.append(b'x' * 10 * 1024 * 1024) print(len(data) * 10, 'MB', flush=True) time.sleep(1)" >/dev/nullsleep 2; echo "حدود ۲ ثانیه: $(docker stats --no-stream --format '{{.MemUsage}}' lx-e2 2>/dev/null || echo 'کانتینر مرده')"for i in $(seq 20); do [ "$(docker inspect lx-e2 --format '{{.State.Status}}')" = exited ] && break; sleep 1; donedocker inspect lx-e2 --format 'وضعیت={{.State.Status}} کد خروج={{.State.ExitCode}} OOMKilled={{.State.OOMKilled}}'echo "آخرین خروجی اپ قبل از مرگ: $(docker logs lx-e2 2>&1 | tail -1)"docker rm -f lx-e2 >/dev/nullحدود ۲ ثانیه: 34.38MiB / 50MiBوضعیت=exited کد خروج=137 OOMKilled=trueآخرین خروجی اپ قبل از مرگ: 90 MBعدد آخر (مثلاً ۹۰ مگابایت) از سقف ۵۰ مگابایت بیشتر است چون سقف فقط حافظه را شامل میشود و داکر بهطور پیشفرض به همان اندازه swap هم اجازه میدهد. برای سقف سختتر --memory-swap 50m هم بگذار تا مجموع حافظه و swap همان ۵۰ باشد.
یک کانتینر با --restart on-failure:3 و سقف حافظهی کم بساز که همیشه OOM میخورد. نشان بده کانتینر دقیقاً ۳ بار دوباره شروع میشود و بعد دست میکشد (RestartCount=3) و وضعیت نهایی exited با OOMKilled=true است.
دیدن جواب
docker run -d --name lx-e3 --memory 30m --restart on-failure:3 python:3.12-alpine python -c "x = b'a' * (100 * 1024 * 1024)" >/dev/nullfor i in $(seq 60); do [ "$(docker inspect lx-e3 --format '{{.State.Status}}')" = exited ] && [ "$(docker inspect lx-e3 --format '{{.RestartCount}}')" -ge 3 ] && break; sleep 1donedocker inspect lx-e3 --format 'وضعیت={{.State.Status}} RestartCount={{.RestartCount}} OOMKilled={{.State.OOMKilled}}'docker rm -f lx-e3 >/dev/nullوضعیت=exited RestartCount=3 OOMKilled=trueآزمونک
Section titled “آزمونک”اگر کانتینر از سقف --memory رد شود چه میشود؟
حافظه سقف سخت است.
اگر از سقف --cpus رد شود چه میشود؟
CPU سقف نرم است.
--cpus 0.5 یعنی…
در cgroup بهصورت 50000/100000 ثبت میشود.
برای دیدن مصرف زندهی کانتینرها؟
با --no-stream فقط یک عکس.
چرا free یا /proc/meminfo داخل کانتینر گمراهکننده است؟
سقف واقعی در /sys/fs/cgroup/memory.max است.
--pids-limit برای چه چیزی خوب است؟
تعداد پروسهها را محدود میکند.
جمعبندی
Section titled “جمعبندی”- بدون محدودیت، یک کانتینر میتواند همهی منابع میزبان را بخورد؛
--memory،--cpus،--pids-limitرا بگذار. - حافظه سقف سخت است: عبور = OOM killer، کد خروج
137،OOMKilled=true. CPU سقف نرم است: فقط throttle. - مصرف را با
docker statsبسنج؛ سقف را با حاشیه از اوج مصرف بگذار؛ باdocker updateدر لحظه عوض کن. /proc/meminfoدر کانتینر گمراهکننده است؛ سقف واقعی در/sys/fs/cgroup/memory.max.- در Compose:
mem_limit،cpus،pids_limit؛ همراهrestartتا کانتینر مشکلدار دوباره بالا بیاید. - روی Docker Desktop سقف VM هم عامل است.
| دستور | کاری که میکند |
|---|---|
docker run --memory 512m --cpus 1 IMG | سقف حافظه و CPU |
docker run --memory 512m --memory-swap 512m IMG | بدون swap |
docker run --pids-limit 100 IMG | سقف تعداد پروسه |
docker stats [--no-stream] | مصرف زنده/یکباره |
docker inspect C --format '{{.State.OOMKilled}}' | آیا OOM کشته؟ |
docker update --memory 1g C | تغییر سقف در لحظه |
cat /sys/fs/cgroup/memory.max | سقف واقعی حافظه داخل کانتینر |
mem_limit / cpus / pids_limit | معادل در Compose |