Foundationপ্রথম নীতি থেকে
LEVEL 12লেসন ৮/২২কঠিন৫০ মিনিট

cgroups v2 — কে কতটুকু ব্যবহার করতে পারবে, তার নির্ভুল নিয়ন্ত্রণ

cgroups v2: Precise Resource Control

namespaces-deep-dive লেসন দেখিয়েছিল namespace শুধু 'দৃশ্যমানতা' নিয়ন্ত্রণ করে, 'ব্যবহার' না — এই লেসন সেই বাকি অর্ধেক সম্পূর্ণ করে। cgroups v2-এর unified hierarchy, cpu.max/cpu.weight, memory.max/memory.high, io.max — প্রতিটার নির্ভুল mechanics, আর একটা সত্যিকার চালানো Python simulation দিয়ে cpu.max-এর quota/period থ্রটলিং হাতে-কলমে verify করা।

এই লেসন শেষে আপনি পারবেন

  • cgroups v1-এর separate-hierarchy সমস্যা কী ছিল আর v2-এর unified hierarchy কীভাবে সেটা স্থাপত্যগতভাবে সমাধান করে তা ব্যাখ্যা করতে পারবেন।
  • cpu.max (hard quota) আর cpu.weight (relative/proportional share)-এর মধ্যে নির্ভুল পার্থক্য বলতে পারবেন, আর কোন পরিস্থিতিতে কোনটা ব্যবহার করা উচিত যুক্তি দিয়ে বলতে পারবেন।
  • memory.max (hard limit, OOM kill) আর memory.high (soft limit, reclaim pressure/throttling)-এর মধ্যে পার্থক্য, আর cgroup-aware OOM killer ঠিক কীভাবে victim বেছে নেয় তা ব্যাখ্যা করতে পারবেন।
  • io.max দিয়ে একটা block device-এ read/write bandwidth ও IOPS কীভাবে সীমিত করা হয় তা বাস্তব উদাহরণ দিয়ে দেখাতে পারবেন।
  • cpu.max-এর quota/period থ্রটলিং মেকানিজম হাতে-কলমে গণনা করে, আর একটা চালিত simulation দিয়ে যাচাই করে, ঠিক কখন ও কতক্ষণ একটা cgroup থ্রটল হবে তা নির্ভুলভাবে বলতে পারবেন।

আগে যা বোঝা থাকা দরকার

আগে এটা বুঝি

virtualization-cloud/linux-namespaces-deep-dive লেসনের শেষ misconception-এ একটা দাবি ইচ্ছাকৃতভাবে অসম্পূর্ণ রাখা হয়েছিল: namespace একটা process-কে সম্পূর্ণভাবে বিচ্ছিন্ন করতে পারে — নিজস্ব PID তালিকা, নিজস্ব mount tree, নিজস্ব network stack — অথচ সেই একই process তখনও host-এর পুরো CPU খেয়ে ফেলতে পারে, পুরো memory দখল করে প্রতিবেশী প্রতিটা container-কে OOM-এ ঠেলে দিতে পারে। কারণ সাতটা namespace-এর একটাও CPU সময়, memory পরিমাণ, disk bandwidth বা process সংখ্যা সীমাবদ্ধ করে না — namespace শুধু নিয়ন্ত্রণ করে একটা process কী দেখতে পায়, সে কতটুকু ব্যবহার করতে পারে তা না।

এই লেসন ঠিক সেই বাকি অর্ধেক নিয়ে — cgroups (control groups)। যদি namespace-কে একটা container-এর “চোখ” ভাবা হয় (সে কী দেখে), cgroup তার “রেশন কার্ড” (সে কতটুকু পায়)। দুইটা সম্পূর্ণ orthogonal mechanism, সম্পূর্ণ ভিন্ন kernel subsystem-এ প্রয়োগ হয় (namespace nsproxy-তে, cgroup membership css_set-এ — operating-systems/namespaces-and-cgroups লেসনের task_struct diagram মনে করুন), অথচ একসাথেই একটা “container”-এর পূর্ণ সংজ্ঞা তৈরি করে: namespace + cgroup = isolation + resource bound।

operating-systems/namespaces-and-cgroups লেসন cgroups-এর একটা survey দিয়েছিল — v1/v2-এর একটা এক-লাইনের পার্থক্য, একটা controller টেবিল, আর memory.max ভেঙে সত্যিকার OOM kill ঘটানোর একটা experiment। এই লেসন সেই ভূমি থেকে গভীরে যায়: v1-এর ঠিক কোন স্থাপত্যগত সমস্যাটা v2 সমাধান করেছে, cpu.max বনাম cpu.weight-এর নির্ভুল পার্থক্য ও ব্যবহারের সিদ্ধান্ত, memory.max বনাম memory.high-এর সূক্ষ্ম কিন্তু গুরুত্বপূর্ণ পার্থক্য, io.max-এর mechanics, আর সবশেষে একটা honest experiment — এই লেসন যে sandbox-এ লেখা হচ্ছে (macOS) সেখানে /sys/fs/cgroup আদৌ বিদ্যমান না, তাই সেই সীমাবদ্ধতা স্পষ্টভাবে দেখিয়ে, বদলে cpu.max-এর quota/period থ্রটলিং মেকানিজমের একটা সত্যিকার, চালিত, verified Python simulation দিয়ে দেখানো হবে।

মূল ধারণা

cgroup v1 বনাম v2 — কেন unified hierarchy একটা সত্যিকার স্থাপত্যগত উন্নতি

namespaces-and-cgroups লেসন এই পার্থক্যটা এক লাইনে বলেছিল। এখানে কেন এটা গুরুত্বপূর্ণ, তার পূর্ণ যুক্তি।

v1-এর সমস্যা — একই process, একাধিক অসামঞ্জস্যপূর্ণ গাছ

cgroups v1-এ (২০০৮) প্রতিটা controller (cpu, memory, io, pids, …) তার নিজস্ব, সম্পূর্ণ আলাদা hierarchy নিয়ে mount হতো — সাধারণত /sys/fs/cgroup/cpu/, /sys/fs/cgroup/memory/, /sys/fs/cgroup/blkio/, প্রতিটা একটা আলাদা filesystem mount, প্রতিটার নিজস্ব group-tree কাঠামো। একটা process তাত্ত্বিকভাবে cpu hierarchy-তে এক group-এ থাকতে পারত (ধরুন /cpu/web-tier/), অথচ একই সাথে memory hierarchy-তে সম্পূর্ণ ভিন্ন group-এ (ধরুন /memory/tenant-A/), যাদের গাছ-কাঠামোর কোনো মিলই নেই।

cgroup v1 — প্রতিটা controller-এর নিজস্ব, স্বতন্ত্র গাছ

/sys/fs/cgroup/cpu/
  ├── web-tier/       ← process X এখানে (CPU-র জন্য)
  └── batch-tier/

/sys/fs/cgroup/memory/
  ├── tenant-A/        ← একই process X এখানে (memory-র জন্য) — সম্পূর্ণ ভিন্ন নাম, ভিন্ন গঠন
  └── tenant-B/

/sys/fs/cgroup/blkio/
  └── default/         ← process X-এর জন্য এখানে কোনো নির্দিষ্ট group-ই বানানো হয়নি

এর ব্যবহারিক সমস্যা তিনটা:

১. অসামঞ্জস্য — একটা process “কোন group-এ আছে” প্রশ্নের একটা একক উত্তর নেই; প্রতিটা controller-ভেদে আলাদা উত্তর, যা debug করা ও reason করা কঠিন করে তোলে।

২. Delegation জটিল — একটা container runtime-কে (যেমন runc) নিরাপদে তার নিজের sub-tree পরিচালনা করতে “delegate” করতে হলে, প্রতিটা আলাদা hierarchy-তে আলাদাভাবে permission ও sub-tree সেটআপ করতে হতো — ছয়-সাতটা controller থাকলে ছয়-সাতবার একই কাজ, প্রতিটাতেই ভুল হওয়ার সুযোগ।

৩. Thread-granularity বিভ্রান্তি — v1-এ ভিন্ন controller-এ একই process-এর ভিন্ন thread-কে ভিন্ন group-এ রাখা সম্ভব ছিল, যা resource accounting-কে প্রায় অযৌক্তিক জটিল করে তুলত।

v2-এর সমাধান — একটাই গাছ, সব controller একসাথে সংযুক্ত

cgroups v2 (২০১৬, kernel 4.5+, systemd 230+ থেকে ডিফল্ট, আজ প্রায় সব major distro ও container runtime-এ default) এই পুরো সমস্যাটা একটা একক সিদ্ধান্তে সমাধান করে: একটা process ঠিক একটাই cgroup-এ থাকে — একটাই গাছ, একটাই path। যে controller-গুলো সেই group-এর জন্য enable করা আছে, তারা সবাই একই সাথে, একই group-membership-এর উপর প্রযোজ্য হয়।

cgroup v2 — একটাই unified hierarchy, সব controller একই গাছে

/sys/fs/cgroup/
  ├── cgroup.controllers        ← কোন controller kernel-এ available
  ├── cgroup.subtree_control    ← কোন controller children-এর জন্য enable করা
  ├── web-tier/
  │     ├── cgroup.procs        ← এই group-এর সদস্য PID-দের তালিকা
  │     ├── cpu.max / cpu.weight
  │     ├── memory.max / memory.high
  │     └── io.max
  └── batch-tier/
        ├── cgroup.procs
        ├── cpu.max / cpu.weight
        ├── memory.max / memory.high
        └── io.max

process X এখন একটাই প্রশ্নের একটাই উত্তর — সে web-tier/-এ আছে, ব্যস। CPU, memory, IO — সব সীমা সেই একই directory-র ফাইলে। Delegation এখন একটাই sub-tree-র মালিকানা কাউকে দেওয়ার প্রশ্ন, ছয়বার না। আর thread-level split v2-তে মূলত নিষিদ্ধ (একটা “no internal process constraint” নিয়ম আছে — নিচে) — যা accounting-কে অনেক বেশি predictable করে তোলে।

দুইটা নিয়ম যা unified hierarchy-কে predictable রাখে

“No internal process constraint” — কোনো non-root cgroup-এ যদি child cgroup থাকে, সেই cgroup নিজে সরাসরি কোনো process ধারণ করতে পারে না (root cgroup ব্যতিক্রম)। অর্থাৎ একটা group হয় “leaf” (process আছে, child group নেই) নাহয় “internal node” (child group আছে, নিজে process ধারণ করে না) — কখনো দুইটাই না। এটা resource-ভাগাভাগির হিসাবকে অস্পষ্টতামুক্ত রাখে: একটা internal node-এর পুরো বাজেট সবসময় শুধু তার children-এর মধ্যে ভাগ হয়, নিজে কোনো অংশ “চুরি” করে না।

“Top-down constraint” — একটা controller একটা child group-এ তখনই enable করা যায় যখন তা তার parent-এর cgroup.subtree_control-এ explicit ভাবে চালু করা থাকে:

echo "+cpu +memory +io +pids" | sudo tee /sys/fs/cgroup/cgroup.subtree_control
mkdir /sys/fs/cgroup/web-tier
# এখন web-tier/ নিজেই cpu.max, memory.max, io.max, pids.max ফাইল পাবে

এই উপর-থেকে-নিচে নিয়মটাই delegation নিরাপদ করে — একটা child group কখনো নিজে থেকে এমন কোনো controller চালু করতে পারে না যা তার parent অনুমতি দেয়নি।

চারটা মূল controller — এক নজরে, বিস্তারিত পরের অংশে

Controllerমূল ফাইলকী নিয়ন্ত্রণ করে
cpucpu.max, cpu.weightCPU সময় — hard quota (cap) বা relative/proportional share
memorymemory.max, memory.highMemory ব্যবহার — hard limit (OOM kill) বা soft limit (reclaim pressure)
ioio.maxপ্রতি block device read/write bandwidth ও IOPS
pidspids.maxসর্বোচ্চ process/thread সংখ্যা (fork bomb প্রতিরোধ — namespaces-and-cgroups লেসনে বিস্তারিত)

পরের সেকশনে প্রথম তিনটার (এই module-এ সবচেয়ে বেশি ব্যবহৃত, সবচেয়ে বেশি ভুল বোঝা) নির্ভুল mechanics।

ভেতরে কী ঘটছে

CPU controller — cpu.max (hard cap) বনাম cpu.weight (relative share)

এই দুইটা ফাইল প্রায়ই গুলিয়ে ফেলা হয়, অথচ তারা সম্পূর্ণ ভিন্ন প্রশ্নের উত্তর দেয়। cpu.max উত্তর দেয় “এই group সর্বোচ্চ কতটুকু CPU পেতে পারবে” (একটা hard ceiling, system-এ contention থাকুক বা না থাকুক)। cpu.weight উত্তর দেয় “যখন CPU নিয়ে প্রতিযোগিতা হচ্ছে, তখন এই group অন্যদের তুলনায় কতটা অগ্রাধিকার পাবে” (system ফাঁকা থাকলে কোনো প্রভাব নেই)।

cpu.max — quota/period, একটা নির্ভুল bandwidth contract

ফরম্যাট: দুইটা সংখ্যা, microsecond-এ, space দিয়ে আলাদা — "<quota> <period>"।

echo "50000 100000" > /sys/fs/cgroup/web-tier/cpu.max

অর্থ: প্রতি 100000 microsecond (১০০ms) period-এ, এই group সর্বোচ্চ 50000 microsecond (৫০ms) CPU সময় ব্যবহার করতে পারবে, তার বেশি না — মানে গড়ে ০.৫টা CPU core-এর সমতুল্য cap। এটা kernel-এর CFS bandwidth controller-এর প্রত্যক্ষ প্রয়োগ — linux-schedulers-জাতীয় লেসনে দেখা CFS scheduler-এরই একটা এক্সটেনশন।

Burst-then-throttle আচরণ ঠিক কীভাবে তৈরি হয়:

  • প্রতিটা নতুন period শুরু হলে group-টা একটা তাজা বাজেট পায় — ঠিক quota microsecond, প্রতিবার শূন্য থেকে।
  • Group যতক্ষণ CPU-তে সত্যিই চলে (idle/sleep সময় গোনা হয় না), সেই বাজেট থেকে খরচ হতে থাকে।
  • বাজেট শূন্যে পৌঁছালে — period এখনো শেষ না হলেও — group থ্রটল হয়ে যায়: kernel তাকে আর CPU দেয় না, period-এর বাকি অংশ সে সম্পূর্ণ অপেক্ষা করে।
  • পরের period শুরু হলে বাজেট আবার quota-তে reset হয় — আগের period-এ অব্যবহৃত বাজেট (থাকলে) carry over হয় না, হারিয়ে যায়।

এই থেকেই সেই পরিচিত “burst তারপর থামা” প্যাটার্ন বের হয়ে আসে — একটা CPU-bound workload যদি একটানা চলতে চায়, সে প্রতিটা period-এর প্রথম quota microsecond চলবে, তারপর বাকি period জুড়ে জমে থাকবে, তারপর আবার চলবে — গড়ে ঠিক quota/period অনুপাতের CPU পাবে, কিন্তু মসৃণভাবে না, ধাপে ধাপে। পরের experiment/build অংশে এই আচরণটাই একটা প্রকৃত, চালানো simulation দিয়ে সংখ্যায় দেখানো হবে।

echo "max 100000" > /sys/fs/cgroup/web-tier/cpu.max   # "max" মানে unlimited quota, cap নেই

cpu.weight — contention-এর সময় আপেক্ষিক ভাগ, cap না

ফরম্যাট: একটা একক পূর্ণসংখ্যা, ডিফল্ট 100, রেঞ্জ 1–10000:

echo 200 > /sys/fs/cgroup/web-tier/cpu.weight     # ডিফল্টের দ্বিগুণ অগ্রাধিকার
echo 50  > /sys/fs/cgroup/batch-tier/cpu.weight   # ডিফল্টের অর্ধেক

দুইটা sibling group যদি একসাথে CPU-র জন্য প্রতিযোগিতা করে, তারা তাদের weight-এর অনুপাতে CPU সময় ভাগ পায় — উপরের উদাহরণে web-tier আর batch-tier 200:50 = 4:1 অনুপাতে ভাগ পাবে, যখন উভয়েই একসাথে সক্রিয়ভাবে CPU চাইছে। কিন্তু গুরুত্বপূর্ণ: batch-tier যদি সাময়িকভাবে idle থাকে (কোনো কাজ নেই), web-tier তখন পুরো CPU ব্যবহার করতে পারে — কোনো hard cap নেই, weight শুধু বিতর্কের সময় কে কতটা পাবে তা নির্ধারণ করে, সিস্টেম ফাঁকা থাকলে কাউকে আটকায় না।

cpu.maxcpu.weight
কী গ্যারান্টি দেয়Hard ceiling — এর বেশি কখনো নাContention-এর সময় আপেক্ষিক অগ্রাধিকার
System ফাঁকা থাকলেতবুও cap-এর বেশি নাপুরো available CPU ব্যবহারযোগ্য
কখন ব্যবহার করা উচিতMulti-tenant billing (গ্রাহক ঠিক যতটুকু কিনেছে ততটুকুই, বেশি না), নিশ্চিত latency budget দরকার হলেBest-effort fair-sharing — সবাই কাজ পাক, কিন্তু কেউ idle CPU নষ্ট না করুক
Kernel mechanismCFS bandwidth controller (quota/period accounting)CFS-এর নিজস্ব weighted fair scheduling (nice-এর group-স্তরের সংস্করণ)

Memory controller — memory.max (hard) বনাম memory.high (soft/throttling)

memory.max — অতিক্রম করলে cgroup-স্কোপড OOM kill

echo 100M > /sys/fs/cgroup/web-tier/memory.max

Group-এর মোট memory ব্যবহার (anonymous + page cache, সহজভাবে) এই সীমা ছাড়িয়ে যাওয়ার মুহূর্তে, kernel প্রথমে reclaim চেষ্টা করে (page cache ফেলে দেওয়া, swap-এ পাঠানো — page-faults-and-demand-paging-জাতীয় লেসনের reclaim যন্ত্রের সরাসরি প্রয়োগ)। তাতেও জায়গা না হলে, kernel এই group-এর ভেতর থেকেই একটা process বেছে SIGKILL পাঠায়।

এইখানে গুরুত্বপূর্ণ ডিটেইল — কেন এটা এত মূল্যবান একটা containment গ্যারান্টি: system-ব্যাপী traditional OOM killer পুরো মেশিন memory-চাপে থাকলে যেকোনো process-কে টার্গেট করতে পারে, ভুক্তভোগী নির্বাচনের হিসাব পুরো সিস্টেম জুড়ে চলে — একটা host-এ ৫০টা container চললে, একটা container-এর memory leak পুরো মেশিনের OOM killer-কে ট্রিগার করতে পারে, আর ভুক্তভোগী হতে পারে সম্পূর্ণ অন্য কোনো container-এর process। কিন্তু cgroup-aware OOM (memory controller enable থাকলে) cgroup-স্কোপড — victim নির্বাচন সীমাবদ্ধ থাকে শুধু সেই cgroup-এর ভেতরের process-দের মধ্যে যেটা limit ছাড়িয়েছে। ক্ষতি একটা single-tenant-এর ভেতরেই আটকে থাকে, প্রতিবেশী container অস্পর্শিত থাকে।

system-ব্যাপী OOM (traditional):
  host memory চাপে → পুরো host-এর সব process থেকে victim বাছাই → যেকোনো container ভুক্তভোগী হতে পারে

cgroup-aware OOM (memory.max ছাড়ালে):
  শুধু সেই cgroup memory চাপে → শুধু সেই cgroup-এর ভেতরের process থেকে victim বাছাই → প্রতিবেশী container সম্পূর্ণ অস্পর্শিত

memory.high — soft limit, throttle আগে, kill পরে (যদি লাগে)

echo 80M > /sys/fs/cgroup/web-tier/memory.high
echo 100M > /sys/fs/cgroup/web-tier/memory.max

memory.high অতিক্রম করলে kernel সাথে সাথে kill করে না — বরং group-টাকে সক্রিয়ভাবে reclaim pressure-এর মুখে ফেলে দেয় (aggressive page reclaim প্রয়োগ করে) আর নতুন allocation-কে ইচ্ছাকৃতভাবে ধীর করে দেয় (throttle), যাতে group নিজে থেকেই কম memory ব্যবহারে “চাপে পড়ে” ফিরে আসে। যদি group তারপরও বাড়তেই থাকে আর memory.max-এ পৌঁছে যায়, তখনই hard kill ঘটে।

memory.maxmemory.high
অতিক্রম করলে কী ঘটেReclaim চেষ্টা, ব্যর্থ হলে SIGKILL (cgroup-স্কোপড OOM)Aggressive reclaim + throttle, kill না (যতক্ষণ না memory.max-এও পৌঁছায়)
আচরণের ধরনআকস্মিক, hardক্রমান্বয়ী, graceful degradation
ব্যবহারিক উদ্দেশ্যচূড়ান্ত, অলঙ্ঘনীয় সীমাএকটা early-warning/soft-cushion — group-কে “থামতে বলা”, মেরে ফেলার আগে
সুপারিশকৃত প্যাটার্নসবসময় সেট করা উচিত (নিরাপত্তা-জাল)memory.max-এর কিছুটা নিচে সেট করে দুই-স্তরের graceful degradation তৈরি করা

I/O controller — io.max, প্রতি-device bandwidth ও IOPS সীমা

io.max-এ প্রতিটা লাইন একটা নির্দিষ্ট block device-কে (মেজর:মাইনর নম্বর দিয়ে চিহ্নিত) টার্গেট করে, চারটা স্বাধীন সীমা নিয়ে:

echo "8:0 rbps=10485760 wbps=10485760 riops=1000 wiops=1000" \
    > /sys/fs/cgroup/web-tier/io.max

অর্থ: device 8:0-এ (সাধারণত প্রথম SCSI/SATA disk) — read bandwidth সর্বোচ্চ rbps=10485760 byte/sec (১০ MB/s), write bandwidth একই, read IOPS সর্বোচ্চ 1000/sec, write IOPS একই। এই চারটা field-ই ঐচ্ছিক ও স্বাধীন — শুধু bandwidth সীমা দিতে চাইলে শুধু rbps/wbps লিখলেই চলে।

cat /sys/fs/cgroup/web-tier/io.max
8:0 rbps=10485760 wbps=10485760 riops=1000 wiops=1000

Enforcement ঘটে block layer-এ — প্রতিটা I/O request submit হওয়ার সময় kernel চেক করে group-এর বর্তমান rate এই সীমা ছাড়িয়ে যাচ্ছে কি না, ছাড়ালে request-টা কিছু সময়ের জন্য queue-তে delay করা হয় (drop না, throttle) — অনেকটা cpu.max-এর মতোই একটা bandwidth-shaping আচরণ, কিন্তু disk I/O-র জন্য। এটা বিশেষভাবে গুরুত্বপূর্ণ multi-tenant storage-এ, যেখানে একটা “noisy neighbor” container-এর ভারী sequential write পুরো disk-এর latency বাড়িয়ে অন্য সব tenant-কে প্রভাবিত করতে পারত — io.max সেই সম্ভাবনা প্রতিটা group-এর নিজস্ব bandwidth budget-এর ভেতরে আটকে রাখে।

উদাহরণ

একটা container-এর CPU ও memory সীমা বসানোর সম্পূর্ণ trace

runc-জাতীয় একটা runtime cgroup limit বসাচ্ছে — subtree_control থেকে enforcement পর্যন্ত
  1. echo "+cpu +memory +io +pids" > .../cgroup.subtree_controlparent-এ controller enable — top-down constraint অনুযায়ী বাধ্যতামূলক প্রথম ধাপ
  2. mkdir /sys/fs/cgroup/container-abc123নতুন leaf group — এখনই cpu.max, memory.max, io.max, pids.max ফাইল স্বয়ংক্রিয়ভাবে উপস্থিত হয়
  3. echo "50000 100000" > cpu.max; echo 100M > memory.max; echo 80M > memory.highএই লেসনের কেন্দ্রীয় তিনটা সীমা বসানো — namespace setup থেকে সম্পূর্ণ orthogonal ধাপ
  4. echo <container_pid> > cgroup.procscontainer-এর মূল প্রক্রিয়াকে (এখনও exec হয়নি) এই group-এর সদস্য করা — child process স্বয়ংক্রিয়ভাবে উত্তরাধিকার পাবে
  5. execve(entrypoint)container চালু — এখন থেকে scheduler প্রতিটা time slice দেওয়ার আগে, আর page-fault handler প্রতিটা allocation-এর আগে, এই cgroup-এর সীমা চেক করবে
  6. runtime: cat cpu.stat, memory.current, memory.eventsচলমান monitoring — nr_throttled, throttled_usec, oom_kill ইত্যাদি সংখ্যায় সরাসরি পড়া যায়

লক্ষ করুন — এই পুরো trace-টা linux-namespaces-deep-dive লেসনের clone()/pivot_root()/execve() trace-এর সম্পূর্ণ সমান্তরাল, কিন্তু ভিন্ন subsystem-এ। namespace setup filesystem/network/PID-visibility নিয়ে কাজ করে; cgroup setup সম্পূর্ণ আলাদা, filesystem-based (/sys/fs/cgroup) ইন্টারফেসে resource-বাজেট নিয়ে কাজ করে। একটা বাস্তব runc create কলে দুইটাই ঘটে, প্রায় একই সময়ে, কিন্তু কার্যত সম্পূর্ণ স্বাধীন দুইটা ধাপ-শৃঙ্খল হিসেবে।

চলমান পরিসংখ্যান পড়া — cpu.stat ও memory.events

cgroup v2-এর প্রতিটা controller নিজের group-এ একটা read-only পরিসংখ্যান ফাইল রাখে, যা enforcement-এর ইতিহাস সরাসরি সংখ্যায় দেয়:

cat /sys/fs/cgroup/web-tier/cpu.stat
usage_usec 4218532
user_usec 3800211
system_usec 418321
nr_periods 42
nr_throttled 7
throttled_usec 312000

nr_periods মোট কতগুলো period পার হয়েছে, nr_throttled তার মধ্যে কতগুলোতে group থ্রটল হয়েছিল, আর throttled_usec মোট কত microsecond থ্রটল অবস্থায় কেটেছে — ঠিক এই তিনটা সংখ্যাই পরের build অংশের simulation স্বাধীনভাবে গণনা করে দেখাবে।

cat /sys/fs/cgroup/web-tier/memory.events
low 0
high 23
max 0
oom 0
oom_kill 0

high সংখ্যাটা কতবার group memory.high অতিক্রম করে reclaim-চাপে পড়েছে (kill হয়নি) তা দেখায়; oom_kill সংখ্যাটা কতবার memory.max অতিক্রম করে সত্যিকার SIGKILL ঘটেছে তা দেখায় — namespaces-and-cgroups লেসনের experiment-এ ঠিক এই ফাইলটাই oom_kill 1 দেখিয়েছিল।

ছবিতে

cgroup v1                              cgroup v2
────────────                           ────────────
/cpu/web-tier/     ← process X         /web-tier/
/memory/tenant-A/  ← process X (!)       ├── cgroup.procs   ← process X
/blkio/(কোনোটাই না)                      ├── cpu.max
                                          ├── memory.max
"process X কোন group-এ?"                 ├── memory.high
  → cpu hierarchy অনুযায়ী: web-tier      └── io.max
  → memory hierarchy অনুযায়ী: tenant-A
  → blkio অনুযায়ী: অনির্ধারিত            "process X কোন group-এ?"
  (তিনটা ভিন্ন উত্তর — অসামঞ্জস্যপূর্ণ)      → web-tier (একটাই উত্তর, সব controller একসাথে)
চিত্র ১cgroup v1 (একাধিক আলাদা hierarchy) বনাম v2 (একটা unified hierarchy) — একই দুইটা process, দুইটা ভিন্ন কাঠামোতে
সময় (us):   0        50000       100000      150000      200000
             │          │            │           │           │
period #0:   [████ RUN ████][░░░░ THROTTLED ░░░░]
                (৫০ms কাজ)      (৫০ms অপেক্ষা — বাজেট শেষ)
period #1:                       [████ RUN ████]
                                  (নতুন ৫০ms বাজেট, তাজা reset)

RUN  = কাজ করছে, বাজেট থেকে খরচ হচ্ছে
THROTTLED = বাজেট শূন্য, period শেষ না হওয়া পর্যন্ত CPU পাচ্ছে না
প্রতিটা period-এর শুরুতে বাজেট সবসময় নতুন — আগের অব্যবহৃত অংশ কখনো carry হয় না
চিত্র ২cpu.max quota/period থ্রটলিং — একটা একটানা CPU-bound কাজের টাইমলাইন (quota=50000us, period=100000us)

নিজে চালিয়ে দেখুন

EXPERIMENT

একটা honest পরীক্ষা — এই sandbox-এ /sys/fs/cgroup সত্যিই বিদ্যমান কি?

এই sandbox: macOS (Darwin) — /sys/fs/cgroup Linux kernel-নির্দিষ্ট একটা virtual filesystem, macOS-এ থাকেই না· ৩ মিনিট

সততার সাথে বলা দরকার: এই লেসনটাও সেই একই macOS sandbox-এ লেখা ও পরীক্ষা করা হচ্ছে যেখানে আগের লেসন লেখা হয়েছিল। /sys/fs/cgroup একটা Linux kernel virtual filesystem (cgroupfs) — POSIX-এর অংশ না, cross-platform library-র অংশ না, বরং সরাসরি Linux kernel-এর একটা নির্দিষ্ট mount। macOS-এর XNU kernel-এ এর কোনো সমতুল্য নেই। নিচের কোডটা এই সীমাবদ্ধতা সরাসরি, verifiably দেখায়:

import sys, os

print("platform:", sys.platform)
print("exists /sys/fs/cgroup:", os.path.exists("/sys/fs/cgroup"))

try:
    with open("/sys/fs/cgroup/cgroup.controllers") as f:
        print("cgroup.controllers:", f.read().strip())
except Exception as e:
    print("open failed:", type(e).__name__, str(e))

প্রকৃত, এই sandbox-এ চালিয়ে পাওয়া output:

platform: darwin
exists /sys/fs/cgroup: False
open failed: FileNotFoundError [Errno 2] No such file or directory: '/sys/fs/cgroup/cgroup.controllers'

os.path.exists() সরাসরি False ফেরত দিচ্ছে, আর ফাইল খোলার চেষ্টা FileNotFoundError দিচ্ছে — এটা কোনো permission সমস্যা না (PermissionError না), বরং প্রমাণ যে path-টাই এই kernel-এ কখনো অস্তিত্বে আসেনি। এটাই নিশ্চিত করে cgroups একটা Linux-নির্দিষ্ট kernel feature, কোনো portable filesystem convention না।

এটা কী প্রমাণ করে

cgroups একটা raw Linux kernel filesystem interface, cross-platform API না — namespaces-deep-dive লেসনের experiment ঠিক যেভাবে unshare()-এর অনুপস্থিতি দেখিয়েছিল, এখানেও একই ধরনের প্রমাণ।

নিজে বানান

BUILD IT

cpu.max Throttling Simulator — quota/period bandwidth control-এর একটা সত্যিকার, চালিত পুনর্নির্মাণ

Python · ●●●○○
  1. period ও quota (microsecond-এ) সংজ্ঞায়িত করুন — cpu.max ফাইলের ঠিক একই দুই সংখ্যা
  2. একটা burst তালিকা বানান — প্রতিটা (arrival_time, duration) — একটা CPU-bound workload যা নির্দিষ্ট সময়ে আসে আর নির্দিষ্ট পরিমাণ কাজ চায়
  3. simulate() ফাংশন লিখুন যা প্রতিটা period-এর জন্য পৃথক বাজেট (usage_by_period) ট্র্যাক করে — কোনো period-এর বাজেট আরেক period-এ carry হয় না
  4. যখন বর্তমান period-এর বাজেট শেষ হয়ে যায়, সময়টাকে সরাসরি পরের period-এর শুরুতে লাফ দিন (এটাই থ্রটলিং) আর সেই ব্যবধানটা throttle_events-এ রেকর্ড করুন
  5. RUN আর THROTTLED segment-গুলো, আর প্রতি-period ব্যবহার প্রিন্ট করুন — তারপর হাতে দুই-একটা সংখ্যা cross-check করুন
  6. nr_throttled ও throttled_usec (মোট) গণনা করুন — ঠিক যেভাবে আসল cpu.stat ফাইলে দেখা যায়

এই simulation kernel-এর CFS bandwidth controller-এর মূল যুক্তির একটা সরল, ধারণাগতভাবে সঠিক পুনর্নির্মাণ — বাস্তব kernel কোড C-তে লেখা, আর per-CPU runtime slice, lock-free accounting-এর মতো বাড়তি জটিলতা সামলায়, কিন্তু মূল “প্রতি period-এ তাজা বাজেট, শেষ হলে থ্রটল, আগেরটা carry হয় না” — এই যুক্তিটা ঠিক এইটাই।

"""
cpu.max quota/period throttling simulation — cgroup v2 CFS bandwidth
controller-এর মূল যুক্তির একটা সরল, ধারণাগতভাবে সঠিক পুনর্নির্মাণ।

cpu.max ফাইলের ফরম্যাট: "<quota> <period>" (দুইটাই microsecond-এ)।
প্রতিটা period-এর শুরুতে cgroup একটা তাজা quota বাজেট পায়। সেই
বাজেট period-এর মধ্যে CPU-তে চলার সময়ের বিপরীতে খরচ হয় (idle সময়
খরচ হয় না)। বাজেট শেষ হয়ে গেলে, period শেষ না হওয়া পর্যন্ত cgroup
থ্রটল হয়ে থাকে — ব্যবহৃত না হওয়া বাজেট পরের period-এ carry over
হয় না, প্রতিটা period শূন্য থেকে শুরু।
"""

PERIOD_US = 100_000   # 100ms — cgroup v2-এর ডিফল্ট period
QUOTA_US = 50_000      # 50ms প্রতি period => 0.5 CPU সমতুল্য cap
                        # cpu.max ফাইলে যা লেখা হতো: "50000 100000"

# (arrival_time_us, duration_us) — প্রতিটা burst একটা একটানা CPU-bound
# কাজ যা যত দ্রুত সম্ভব শুরু হতে চায় (arrival-এর পরে, বা আগের burst
# শেষ হওয়ার পরে, যেটা পরে ঘটে)
BURSTS = [
    (0, 80_000),
    (150_000, 30_000),
    (300_000, 120_000),
]


def simulate(period, quota, bursts):
    usage_by_period = {}
    run_events = []       # (start, end, period_index)
    throttle_events = []  # (start, end, duration)
    t = 0

    for arrival, duration in bursts:
        t = max(t, arrival)
        remaining = duration
        while remaining > 0:
            period_index = t // period
            period_start = period_index * period
            period_end = period_start + period
            used = usage_by_period.get(period_index, 0)
            avail = quota - used

            if avail <= 0:
                throttle_start = t
                t = period_end
                throttle_events.append((throttle_start, t, t - throttle_start))
                continue

            run_time = min(avail, remaining, period_end - t)
            run_events.append((t, t + run_time, period_index))
            usage_by_period[period_index] = used + run_time
            t += run_time
            remaining -= run_time

    return run_events, throttle_events, usage_by_period


if __name__ == "__main__":
    print(f'cpu.max = "{QUOTA_US} {PERIOD_US}"  (quota/period, microsecond)')
    print(f"=> প্রতি period-এ {QUOTA_US/1000:.0f}ms CPU বাজেট, cap = {QUOTA_US/PERIOD_US:.2f} CPU\n")

    run_events, throttle_events, usage_by_period = simulate(PERIOD_US, QUOTA_US, BURSTS)

    print("burst অনুরোধ (arrival_us, duration_us):")
    for a, d in BURSTS:
        print(f"  arrival={a:>7}  duration={d:>7}")
    print()

    print("চলমান segment (RUN):")
    for start, end, pidx in run_events:
        print(f"  t=[{start:>7}, {end:>7}]  duration={end-start:>6}us  period#{pidx}")
    print()

    print("থ্রটল হওয়া segment (THROTTLED):")
    total_throttled = 0
    for start, end, dur in throttle_events:
        print(f"  t=[{start:>7}, {end:>7}]  duration={dur:>6}us")
        total_throttled += dur
    print()

    print(f"মোট throttle সংখ্যা (nr_throttled): {len(throttle_events)}")
    print(f"মোট throttled সময় (throttled_usec): {total_throttled}us = {total_throttled/1000:.1f}ms")
    print()

    print("প্রতি period-এ ব্যবহৃত বাজেট (usage_usec / quota_usec):")
    for pidx in sorted(usage_by_period):
        used = usage_by_period[pidx]
        print(f"  period#{pidx} [{pidx*PERIOD_US:>7},{(pidx+1)*PERIOD_US:>7}): "
              f"{used:>6}/{QUOTA_US}us used"
              f"{'  <- সম্পূর্ণ শেষ, থ্রটল ঘটেছে' if used == QUOTA_US else ''}")

এই sandbox-এ সত্যিই চালিয়ে পাওয়া output (macOS-এ Python 3.9.6, সম্পূর্ণ portable কোড — কোনো Linux syscall বা /sys/fs/cgroup লাগেনি):

cpu.max = "50000 100000"  (quota/period, microsecond)
=> প্রতি period-এ 50ms CPU বাজেট, cap = 0.50 CPU

burst অনুরোধ (arrival_us, duration_us):
  arrival=      0  duration=  80000
  arrival= 150000  duration=  30000
  arrival= 300000  duration= 120000

চলমান segment (RUN):
  t=[      0,   50000]  duration= 50000us  period#0
  t=[ 100000,  130000]  duration= 30000us  period#1
  t=[ 150000,  170000]  duration= 20000us  period#1
  t=[ 200000,  210000]  duration= 10000us  period#2
  t=[ 300000,  350000]  duration= 50000us  period#3
  t=[ 400000,  450000]  duration= 50000us  period#4
  t=[ 500000,  520000]  duration= 20000us  period#5

থ্রটল হওয়া segment (THROTTLED):
  t=[  50000,  100000]  duration= 50000us
  t=[ 170000,  200000]  duration= 30000us
  t=[ 350000,  400000]  duration= 50000us
  t=[ 450000,  500000]  duration= 50000us

মোট throttle সংখ্যা (nr_throttled): 4
মোট throttled সময় (throttled_usec): 180000us = 180.0ms

প্রতি period-এ ব্যবহৃত বাজেট (usage_usec / quota_usec):
  period#0 [      0, 100000):  50000/50000us used  <- সম্পূর্ণ শেষ, থ্রটল ঘটেছে
  period#1 [ 100000, 200000):  50000/50000us used  <- সম্পূর্ণ শেষ, থ্রটল ঘটেছে
  period#2 [ 200000, 300000):  10000/50000us used
  period#3 [ 300000, 400000):  50000/50000us used  <- সম্পূর্ণ শেষ, থ্রটল ঘটেছে
  period#4 [ 400000, 500000):  50000/50000us used  <- সম্পূর্ণ শেষ, থ্রটল ঘটেছে
  period#5 [ 500000, 600000):  20000/50000us used

কয়েকটা ফলাফল হাতে যাচাই করা যাক। প্রথম burst (arrival=0, duration=80000) period#0-এ শুরু হয়, পুরো 50000 বাজেট খরচ করে t=50000-এ থ্রটল হয় (বাকি 80000-50000=30000 কাজ জমে থাকে)। period#1 শুরু হলে (t=100000) বাজেট রিসেট হয়ে 50000 হয়, বাকি 30000 কাজ t=130000-এই শেষ হয়ে যায় — period#1-এর বাজেটের 20000 তখনও অব্যবহৃত থেকে যায়। দ্বিতীয় burst (arrival=150000, duration=30000) সেই একই period#1-এ শুরু হয়, বাকি 20000 বাজেট ব্যবহার করে t=170000-এ আবার থ্রটল হয় (period#1-এর বাজেট এবার সম্পূর্ণ শেষ), বাকি 10000 কাজ period#2-এর নতুন বাজেট দিয়ে t=210000-এ শেষ হয়। তৃতীয় burst (120000 microsecond, একটা একক period-এর quota-র চেয়ে অনেক বড়) তিনটা পুরো period (#3, #4) জুড়ে থ্রটল হয়ে, চতুর্থ period-এ (#5) গিয়ে শেষ হয় — output-এর সাথে সংখ্যায় সংখ্যায় মিলছে। মোট nr_throttled=4, মোট throttled_usec=180000 — এই সংখ্যাগুলোই একটা প্রকৃত cat cpu.stat-এ দেখা যেত, এই workload প্রকৃত Linux-এ web-tier group-এর ভেতরে চালানো হলে।

বাস্তব সিস্টেমে

বাস্তব সিস্টেমে — cpu.max/memory.max-এর পেছনে কারা লুকিয়ে

  • Kubernetes resource requests/limits — একটা Pod spec-এ resources.limits.cpu: "500m" লিখলে kubelet এটাকে সরাসরি cpu.max = "50000 100000"-এ অনুবাদ করে (500m মানে ৫০০ millicore = ০.৫ CPU)। resources.requests.cpu মূলত cpu.weight-এ ম্যাপ হয় (scheduling-এর প্রাথমিক hint হিসেবেও ব্যবহৃত হয়, কিন্তু node-এর ভেতরে actual enforcement cpu.weight-এর মাধ্যমে)। resources.limits.memory সরাসরি memory.max-এ যায় — এটাই ব্যাখ্যা করে কেন একটা Pod তার memory limit ছাড়ালে সরাসরি OOMKilled status পায়, kubectl describe pod-এ যা দেখা যায়।

  • Docker/--cpus, --memory — docker run --cpus="0.5" --memory="100m" ... একদম সরাসরি cpu.max/memory.max লেখে container-এর cgroup-এ, containerd/runc-এর মাধ্যমে। docker stats-এর CPU% কলামটা মূলত cpu.stat-এর usage_usec-এর উপর ভিত্তি করে গণনা করা।

  • systemd resource-control ডিরেক্টিভ — যেকোনো systemd unit file-এ CPUQuota=50%, MemoryMax=100M, IOReadBandwidthMax=/dev/sda 10M লিখলে systemd সরাসরি সেই unit-এর cgroup-এ যথাক্রমে cpu.max, memory.max, io.max লেখে — systemd নিজেই একটা cgroup v2 delegation-ভিত্তিক resource manager, container দুনিয়ার বাইরেও (প্রতিটা systemd service নিজস্ব cgroup-এ চলে, বছরের পর বছর ধরে, Docker আসারও আগে থেকে)।

  • Multi-tenant SaaS billing — যে প্ল্যাটফর্মগুলো “vCPU” বা “compute unit” বিক্রি করে (serverless platform, managed database, CI/CD runner), তাদের ভেতরে প্রায় সবসময় cpu.max-ভিত্তিক hard quota থাকে, ঠিক এই লেসনের “কেন hard cap দরকার” আলোচনার বাস্তব প্রয়োগ — গ্রাহক যা কিনেছে ঠিক তাই পাবে, বেশি না, কম না।

  • cgroup.subtree_control ও delegation — Kubernetes-এর kubelet আর container runtime-এর মধ্যে, বা systemd --user সেশনের ভেতরে, cgroup.subtree_control-এর মাধ্যমে delegation ঠিক এই লেসনের concept সেকশনে বর্ণিত top-down constraint মেনে ঘটে — একটা runtime কখনো তার parent-এর অনুমতির বাইরে গিয়ে নিজের জন্য বেশি controller চালু করতে পারে না।

যে ভুলগুলো সবাই করে

“cpu.max দিয়ে '০.৫ CPU' সীমা বসালে process ঠিক অর্ধেক গতিতে, মসৃণভাবে চলে — অনেকটা CPU clock speed অর্ধেক করে দেওয়ার মতো।”

ভুল, আর এই লেসনের build অংশের সিমুলেশন ঠিক এই ভুল-ধারণাটা সংখ্যায় ভাঙে। cpu.max = "50000 100000" মসৃণ half-speed তৈরি করে না — এটা তৈরি করে একটা burst-then-throttle প্যাটার্ন: প্রতিটা ১০০ms period-এর প্রথম ৫০ms পূর্ণ গতিতে চলা, তারপর বাকি ৫০ms সম্পূর্ণ বন্ধ থাকা। গড়ে হিসাব করলে “অর্ধেক CPU” ঠিকই আছে, কিন্তু মুহূর্ত-থেকে-মুহূর্তে আচরণ একদমই মসৃণ না — একটা latency-sensitive workload-এ (যেমন একটা web request-এর মাঝপথে quota ফুরিয়ে যাওয়া) এই আচরণ p99 latency-তে হঠাৎ, অদ্ভুত spike তৈরি করতে পারে, যদিও গড় CPU ব্যবহার ঠিক প্রতিশ্রুতি অনুযায়ীই থাকে। এটাই বাস্তব production সিস্টেমে “CPU throttling” নামে পরিচিত একটা প্রকৃত, সাধারণ সমস্যা — Kubernetes-এ container_cpu_cfs_throttled_periods_total মেট্রিক ঠিক এই ঘটনাটাই ট্র্যাক করে।

“memory.high একটা দুর্বল, ঐচ্ছিক সেটিং — যেহেতু এটা সরাসরি kill করে না, তাই এটা বাস্তবে খুব বেশি প্রভাব ফেলে না।”

উল্টো — memory.high অতিক্রম করলে kernel সক্রিয়ভাবে সেই group-কে reclaim pressure-এর মুখে ফেলে দেয় আর নতুন allocation ইচ্ছাকৃতভাবে ধীর করে দেয়, যা বাস্তবে একটা memory-hungry workload-এর throughput-কে উল্লেখযোগ্যভাবে কমিয়ে দিতে পারে — এটা “নরম” মানে “নিষ্ক্রিয়” না। প্রকৃতপক্ষে ভালোভাবে টিউন করা memory.high প্রায়ই memory.max কখনো স্পর্শ না করতেই group-কে স্থিতিশীল রাখে, কারণ throttling নিজেই workload-কে ধীর করে কম memory allocate করতে বাধ্য করে। এটাকে সম্পূর্ণ বাদ দিয়ে শুধু memory.max ব্যবহার করলে group সরাসরি, কোনো সতর্কীকরণ ছাড়াই hard limit-এ আঘাত করে kill হয় — memory.high আসলে সেই আকস্মিক আঘাত এড়ানোর প্রধান হাতিয়ার, একটা দুর্বল বিকল্প না।

“cgroup v1-এর বদলে v2 ব্যবহার করা শুধু 'নতুন সংস্করণ ব্যবহার করা ভালো' গোছের একটা preference — কার্যকরী কোনো পার্থক্য নেই।”

ভুল। v1 আর v2-এর মধ্যে পার্থক্য শুধু নামকরণ বা সংস্করণ সংখ্যার না — এটা একটা প্রকৃত স্থাপত্যগত পরিবর্তন যার বাস্তব ফলাফল আছে। v1-এ প্রতিটা controller আলাদা hierarchy-তে থাকায় delegation জটিল ও ঝুঁকিপূর্ণ ছিল (কেন তা concept সেকশনে বিস্তারিত), আর একই process-এর জন্য “কোন group” প্রশ্নের একাধিক, অসামঞ্জস্যপূর্ণ উত্তর সম্ভব ছিল। v2-এর একটাই unified hierarchy নির্দিষ্ট নিরাপত্তা গ্যারান্টি (no internal process constraint, top-down constraint) kernel-স্তরে বিল্ট-ইন করে দেয়, যেগুলো v1-এ শুধু userspace tooling-এর সতর্কতার উপর নির্ভর করত। এই কারণেই systemd, Kubernetes (১.২৫+ ডিফল্টে), আর বেশিরভাগ আধুনিক container runtime সম্পূর্ণভাবে v2-তে স্থানান্তরিত হয়েছে — এটা শুধু “নতুন তাই ভালো” না, বরং একটা measurable নিরাপত্তা ও predictability উন্নতি।

“namespace আর cgroup একসাথেই সবসময় ব্যবহার করতে হয় — একটা ছাড়া আরেকটা অর্থহীন।”

সম্পূর্ণ ভুল, আর namespaces-deep-dive লেসনের শেষ misconception-এরই সরাসরি সম্প্রসারণ। দুইটা সম্পূর্ণ orthogonal mechanism, স্বাধীনভাবে প্রযোজ্য। systemd প্রতিটা service-কে namespace ছাড়াই cgroup-এ রাখে (host-এর পুরো filesystem/network/PID তালিকা দেখা যায়, শুধু CPU/memory সীমিত) — এটা একটা সম্পূর্ণ বৈধ, বাস্তবে ব্যবহৃত প্যাটার্ন, “resource control ছাড়া isolation” না, বরং “isolation ছাড়া resource control”। উল্টোদিকে, একটা সাধারণ unshare --pid --net --mount bash কমান্ড কোনো cgroup সীমা ছাড়াই সাতটা namespace তৈরি করে — সম্পূর্ণ দৃশ্যমানতা-বিচ্ছিন্ন কিন্তু resource-সীমাহীন একটা process। “Container” শব্দটা দুইটাকেই একসাথে প্রয়োগ করার একটা userspace convention মাত্র — kernel-এর কাছে এরা চিরকাল দুইটা আলাদা, স্বাধীনভাবে টগলযোগ্য mechanism।

বুঝেছেন কি না দেখুন

1

cpu.max ফাইলে দুইটা সংখ্যা লেখা হয় — তাদের একক কী, আর প্রতিটার অর্থ কী?

স্মরণ

দুইটাই microsecond-এ, ফরম্যাট "<quota> <period>"। period হলো সময়ের একটা পুনরাবৃত্ত জানালা (সাধারণত 100000 microsecond = ১০০ms)। quota হলো প্রতিটা period-এ group সর্বোচ্চ কতটুকু microsecond CPU সময় ব্যবহার করতে পারবে — যেমন "50000 100000" মানে প্রতি ১০০ms-এ সর্বোচ্চ ৫০ms CPU সময়, গড়ে ০.৫ CPU সমতুল্য cap।

2

একজন সহকর্মী একটা multi-tenant billing platform-এ প্রতিটা গ্রাহকের জন্য শুধু cpu.weight সেট করেছেন, cpu.max না — যুক্তি: “weight-ই তো fair-sharing দেয়, quota দরকার নেই।” এটা কেন ভুল সিদ্ধান্ত হতে পারে বিশেষভাবে billing-এর প্রেক্ষাপটে?

যুক্তি

cpu.weight শুধু contention-এর সময় আপেক্ষিক অগ্রাধিকার দেয় — system ফাঁকা থাকলে একটা গ্রাহক তার weight যাই হোক না কেন, পুরো available CPU ব্যবহার করতে পারবে, কোনো hard cap নেই। একটা billing প্ল্যাটফর্মে এর মানে: রাতের বেলা (কম load) একজন “০.৫ vCPU” প্ল্যানের গ্রাহক আসলে পুরো মেশিনের CPU পেতে পারে বিনামূল্যে, আর দিনের বেলা (বেশি load) সেই একই গ্রাহক তার প্রতিশ্রুত অংশও নাও পেতে পারে যদি অন্য সব group-এর weight বেশি হয় আর তারা সক্রিয় থাকে। এটা inconsistent performance তৈরি করে, আর billing-এর যুক্তির সাথে সাংঘর্ষিক (গ্রাহক যা কিনেছে সবসময় ঠিক ততটুকুই পাওয়া উচিত, বেশি না কম না)। সঠিক সিদ্ধান্ত: cpu.max ব্যবহার করা একটা hard, প্রতিশ্রুতি-সামঞ্জস্যপূর্ণ ceiling হিসেবে, আর প্রয়োজনে cpu.weight তার ভেতরে (একই tier-এর গ্রাহকদের মধ্যে) fair-sharing-এর জন্য বাড়তি স্তর হিসেবে।

3

একটা group-এ cpu.max = "30000 100000" সেট করা আছে। এই group-এ t=0-এ একটা একটানা 70000 microsecond-এর CPU-bound burst আসে (আগে কিছু চলছিল না)। ঠিক কখন প্রথম থ্রটল ঘটবে, আর কখন পুরো burst শেষ হবে (ধরে নিন period#0 = [0, 100000), period#1 = [100000, 200000))?

প্রয়োগ

period#0-এ বাজেট 30000 microsecond। burst t=0-এ শুরু হয়ে পুরো বাজেট খরচ করে t=30000-এ থ্রটল হবে (বাকি 70000-30000=40000 কাজ জমে থাকবে)। থ্রটল অবস্থা t=100000 পর্যন্ত চলবে (period#0-এর বাকি অংশ)। period#1 শুরু হলে (t=100000) নতুন 30000 বাজেট আসবে, কিন্তু বাকি কাজ (40000) তার চেয়ে বেশি — তাই t=130000-এ আবার থ্রটল হবে (বাকি 40000-30000=10000)। period#2 (t=200000 থেকে) নতুন 30000 বাজেট দিয়ে বাকি 10000 কাজ t=210000-এ শেষ হবে। সারমর্ম: দুইবার থ্রটল (t=30000-এ ও t=130000-এ), সম্পূর্ণ burst শেষ হয় t=210000-এ।

4

memory.max = 100M সেট করা একটা container-এর ভেতরে memory leak হওয়া একটা process শেষ পর্যন্ত SIGKILL পায়। একই হোস্টে চলা অন্য দশটা container-এর কোনো প্রভাব পড়ে কি? cgroup-aware OOM killer না থাকলে (শুধু traditional system-ব্যাপী OOM killer থাকলে) উত্তরটা কীভাবে বদলে যেত?

যুক্তি

memory.max অতিক্রম করার পর, cgroup-aware OOM killer victim নির্বাচন সীমাবদ্ধ রাখে শুধু সেই cgroup-এর ভেতরের process-দের মধ্যে — যে container leak করছে, ঠিক তার ভেতরের একটা process মারা যায়, অন্য দশটা container সম্পূর্ণ অস্পর্শিত থাকে, তাদের memory ব্যবহারে কোনো প্রভাব পড়ে না। কিন্তু cgroup-aware OOM ছাড়া, শুধু traditional system-ব্যাপী OOM killer থাকলে, memory leak-এর ফলে পুরো host memory-চাপে পড়ে যেত (কারণ কোনো per-group hard limit-ই নেই যা আগে থেকে থামাত), আর OOM killer পুরো সিস্টেমের প্রতিটা process থেকে victim বাছাই করত (badness/oom_score হিসাব অনুযায়ী) — ভুক্তভোগী হতে পারত সম্পূর্ণ অন্য কোনো container-এর গুরুত্বপূর্ণ process, এমনকি host-এর নিজস্ব কোনো system service-ও। এটাই cgroup-স্কোপড OOM-এর মূল্য — ক্ষতি single-tenant-এর ভেতরেই containment হয়ে থাকে।

5

একটা group-এ memory.high = 80M আর memory.max = 100M সেট করা আছে। group-এর ব্যবহার ধীরে ধীরে বেড়ে 85M-এ পৌঁছেছে, তারপর স্থির হয়ে গেছে (আর বাড়ছে না)। এই group কি kill হবে? memory.events-এ কোন কাউন্টার বাড়বে?

প্রয়োগ

না, kill হবে না — memory.max (100M) কখনো অতিক্রম হয়নি। কিন্তু memory.high (80M) অতিক্রম হয়েছে (85M > 80M), তাই kernel সেই group-কে reclaim pressure ও allocation throttling-এর মুখে ফেলেছে — group-টা এখন স্বাভাবিকের চেয়ে ধীরে চলছে, যতক্ষণ ব্যবহার 80M-এর উপরে থাকবে। memory.events-এ high কাউন্টার বাড়বে (প্রতিবার memory.high চেক-এ ছাড়িয়ে পাওয়া গেলে), কিন্তু oom/oom_kill কাউন্টার 0-ই থাকবে, কারণ সেটা শুধু memory.max ছাড়ানো হলে বাড়ে। এই পরিস্থিতিটাই memory.high-এর design-intent-এর সবচেয়ে ভালো উদাহরণ — group চাপে আছে, ধীর হয়ে গেছে, কিন্তু বেঁচে আছে।

6

আপনি একটা shared Kubernetes cluster ডিজাইন করছেন যেখানে দুই ধরনের workload চলবে: (ক) latency-sensitive API server, যার p99 response time-এর উপর একটা কড়া SLA আছে, আর (খ) best-effort ব্যাচ ডেটা-প্রসেসিং জব, যেগুলোর শুধু “শেষ পর্যন্ত শেষ হলেই হলো”। CPU ও memory controller-এর কোন সেটিংগুলো (এই লেসনের সবগুলো ফাইল থেকে বেছে) কোন workload-এর জন্য ব্যবহার করবেন, আর কেন?

ডিজাইন

API server-এর জন্য: cpu.weight বেশি মান (উচ্চ অগ্রাধিকার, যাতে batch job-এর সাথে প্রতিযোগিতায় সবসময় জিতে) কিন্তু সাধারণত cpu.max সেট না করা বা খুব উদার রাখা — কারণ এই লেসনের misconception অংশে দেখানো burst-then-throttle আচরণ ঠিক latency-sensitive workload-এর জন্য সবচেয়ে ক্ষতিকর (মাঝপথে quota ফুরিয়ে p99 spike তৈরি করতে পারে); বরং high cpu.weight দিয়ে নিশ্চিত করা ভালো যে contention-এর সময়ও সে যথেষ্ট CPU পাবে। Memory-তে memory.high একটা রক্ষণশীল মান-এ সেট করে early throttling-signal রাখা, memory.max একটা নিরাপত্তা-জাল হিসেবে অনেক উপরে।

batch job-এর জন্য উল্টো: নিচু cpu.weight (API server-এর সাথে প্রতিযোগিতায় হেরে যাক, কিন্তু system ফাঁকা থাকলে পুরো CPU ব্যবহার করুক — hard cap দরকার নেই কারণ latency কোনো উদ্বেগ না), আর memory-তে টাইট memory.max (batch job-এর একটা leak যেন কখনো node-ব্যাপী memory pressure তৈরি করে API server-কেও প্রভাবিত করতে না পারে — cgroup-aware OOM এখানে batch job-এর ভেতরেই containment নিশ্চিত করবে)। সংক্ষেপে: latency-critical workload-এ weight দিয়ে priority, hard cap এড়িয়ে; best-effort workload-এ hard memory cap দিয়ে containment, CPU cap এড়িয়ে যাতে idle cycle নষ্ট না হয়। এটাই precisely cpu.max/cpu.weight আর memory.max/memory.high-এর ভিন্ন ভিন্ন উদ্দেশ্যের বাস্তব, সম্মিলিত প্রয়োগ।

এরপর কী

namespace আর cgroups একসাথে এখন একটা process-এর “container” হয়ে ওঠার সম্পূর্ণ গল্প বলে — namespace নিয়ন্ত্রণ করে সে কী দেখতে পায় (PID তালিকা, mount tree, network stack, hostname, IPC id space, UID/GID, cgroup path), cgroup নিয়ন্ত্রণ করে সে কতটুকু ব্যবহার করতে পারে (CPU সময়, memory, disk bandwidth, process সংখ্যা)। রানটাইমে একটা process যখন এই দুইটা mechanism-ই বহন করে, সেটাই একটা “container” — আর কিছুই না।

কিন্তু একটা প্রশ্ন এখনও পুরোপুরি অনুত্তরিত। একটা container-কে শুধু isolate আর resource-bound করলেই চলে না — তার একটা filesystem-ও লাগে, একটা /bin, একটা /etc, একটা /usr। আর হাজার হাজার container যদি প্রতিটাই তাদের নিজস্ব সম্পূর্ণ root filesystem-এর একটা আলাদা কপি রাখত (ধরুন প্রতিটা Ubuntu-based image-এর জন্য কয়েকশো MB), সেটা ভয়াবহভাবে অপচয়ী হতো — ডিস্ক জায়গায়ও, আর একটা নতুন container শুরু করার সময়েও (পুরো filesystem কপি করা কখনো “instant” হতে পারে না)।

পরের লেসনে (virtualization-cloud/overlay-filesystems-image-layers) আমরা দেখব Docker-এর মতো টুল কীভাবে এই সমস্যা সমাধান করে — copy-on-write layer-এর মাধ্যমে হাজার হাজার container একই অপরিবর্তনীয় base layer (একই ubuntu:22.04 ইমেজ, একই ডিস্ক ব্লক) শেয়ার করে, অথচ প্রতিটার কাছে মনে হয় সে তার নিজস্ব, সম্পূর্ণ স্বাধীন filesystem-এ লিখছে। namespace দৃশ্যমানতার সীমানা আঁকে, cgroups ব্যবহারের সীমানা আঁকে, আর overlay filesystem আঁকে দক্ষতার সীমানা — এই তিনটা একসাথেই আজকের container ইকোসিস্টেমের সম্পূর্ণ ভিত্তি।

আরও পড়ুন

  • Control Group v2 (cgroup-v2.rst) — kernel admin guide · unified hierarchy, subtree_control, cpu.max/cpu.weight, memory.max/memory.high, io.max-এর প্রামাণ্য, নির্ভুল semantics — এই লেসনের প্রতিটা ফাইল-নাম ও সংখ্যার আচরণ এখান থেকেই
  • systemd.resource-control(5) · systemd unit-এ CPUQuota=, MemoryMax=, IOReadBandwidthMax=-এর মতো ডিরেক্টিভ কীভাবে সরাসরি cgroup v2 ফাইলে ম্যাপ হয়
  • Control Group APIs and Delegation (systemd.io) · কেন unified hierarchy delegation-কে (একটা container runtime-কে নিরাপদে নিজের sub-tree পরিচালনা করতে দেওয়া) v1-এর চেয়ে অনেক সহজ ও নির্ভুল করে তোলে, তার প্রামাণ্য আলোচনা
  • cgroups(7) · cgroup v1/v2-এর পার্থক্য, controller তালিকা, ও mount mechanics-এর প্রামাণ্য সংক্ষিপ্তসার