সরাসরি পর্বে যান
পর্ব ৪৮ · ৮১-এর মধ্যে · মডিউল ৮

Scheduling আর resource — request, limit, QoS আর autoscaling

Kubernetes Scheduling and Resources — Requests, Limits, QoS and Autoscaling

Taught in Bangla, this lesson explains requests and limits, CPU throttling and OOMKilled in real runs, reading Pending Pod events, scheduler filtering and scoring, QoS classes and eviction, LimitRange and ResourceQuota, node affinity, taints and topology spread, the HPA formula worked by hand, VPA, Cluster Autoscaler and Karpenter, and cost tuning from requests versus actual usage.

এই পর্বে যা শিখবেন

  • Request আর limit কোন কাজে লাগে, আর cgroup-এ সেগুলো কী হয়ে বসে
  • Pending Pod-এর event পড়ে কারণ বের করা
  • QoS class চেনা, আর memory-র চাপে কে আগে সরে তা আন্দাজ করা
  • LimitRange আর ResourceQuota দিয়ে একটি দলের সীমা বাঁধা
  • nodeSelector, affinity, taint আর topology spread দিয়ে Pod-এর জায়গা ঠিক করা
  • HPA-র হিসাব হাতে করা, আর VPA, Cluster Autoscaler ও Karpenter-এর পার্থক্য বলা
  • Request বনাম আসল ব্যবহার মিলিয়ে খরচ কমানোর হিসাব করা

এই পর্বের আউটপুট কোথা থেকে: kubectl আর helm-এর আউটপুট একটি স্থানীয় একক-node cluster-এর (Kubernetes ১.৩৪) আসল run থেকে নেওয়া। নাম, সময় আর IP আপনার মেশিনে আলাদা হবে। লম্বা লাইন ছোট করতে … ব্যবহার করা হয়েছে। একটি মাত্র node থাকায় যেসব জিনিসের ফল কয়েকটি node ছাড়া দেখা যায় না (taint, spread, preemption, node যোগ হওয়া), সেগুলো সিমুলেটর বা মডেলে দেখানো হয়েছে, আর সেখানে তা বলা আছে।

১ · Request আর limit — বুকিং আর সীমা

ঈদের আগে লঞ্চঘাটের কথা ভাবুন। কেবিনের টিকিট বিক্রেতা জানেন লঞ্চে কতগুলো কেবিন আছে। তিনি গুনে গুনে বুকিং দেন। কোন যাত্রী কেবিনে কতক্ষণ ঘুমাবেন বা আদৌ উঠবেন কি না, তা তিনি দেখেন না। বুকিং শেষ মানে টিকিট শেষ, কেবিন ফাঁকা পড়ে থাকলেও।

Kubernetes-এর scheduler ঠিক এই টিকিট বিক্রেতা। প্রতিটি Pod-এর জন্য সে একটি node খুঁজে দেয়। তার হিসাবের খাতায় থাকে শুধু request, অর্থাৎ Pod-টি কতটা জায়গা বুক করতে চায়। আর লঞ্চের নিয়ম, যেমন “কেবিনের বাইরে মালপত্র রাখা যাবে না”, সেটি হলো limit. Limit মানে এর বেশি নেওয়া যাবে না। সেই নিয়ম প্রয়োগ করে node-এর kubelet আর Linux kernel.

মূল ধারণা

Request কাজ করে Pod বসানোর আগে: কোন node-এ জায়গা আছে। Limit কাজ করে Pod চলার সময়: কতটা পর্যন্ত নেওয়া যাবে। প্রথমটি হিসাবের খাতা, দ্বিতীয়টি দরজার তালা।

প্রতিটি container-এর জন্য এই দুটি মান লেখা হয়:

apiVersion: v1
kind: Pod
metadata:
  name: api
spec:
  containers:
  - name: app
    image: nginx:1.27-alpine
    imagePullPolicy: IfNotPresent
    resources:
      requests:
        cpu: 250m
        memory: 128Mi
      limits:
        cpu: 500m
        memory: 256Mi

এককগুলো একবার বুঝে নিন। CPU মাপা হয় core-এ: 1 মানে একটি vCPU, আর 250m মানে ২৫০ millicore, অর্থাৎ এক core-এর চার ভাগের এক ভাগ। Memory-তে 128Mi মানে 128 MiB (220 বাইটের হিসাব), আর 128M মানে দশমিক হিসাবের 128 MB. দুটি আলাদা। ছোট হাতের 128m লিখলে memory-র ক্ষেত্রে মানে দাঁড়ায় ০.১২৮ বাইট, যা প্রায় নিশ্চিত ভুল।

Limit লিখে request না লিখলে Kubernetes request-কে limit-এর সমান ধরে নেয়। এই ছোট্ট নিয়মটি অনেক হিসাব বদলে দেয়, তাই মনে রাখা ভালো। আর node-এর পুরো ক্ষমতা Pod পায় না। Operating system আর kubelet-এর জন্য কিছুটা সরিয়ে রেখে যা থাকে, তাকে বলে allocatable. Scheduler এই সংখ্যা ধরেই হিসাব করে।

নিচের ছবিতে একটি কাল্পনিক node-এ তিনটি Pod আছে। মাপগুলো উদাহরণের জন্য ধরে নেওয়া, কোনো আসল মেশিনের নয়।

একটি node-এর হিসাব: request, limit আর আসল ব্যবহার

Node capacity with requests, limits and real usage

Node-এর সীমা, request, limit আর আসল ব্যবহার Node: allocatable 4 CPU, 8 GiB CPU Request: মোট 2.5, scheduler এটি দেখে ABCখালি 1.5 Limit: মোট 5, node-এর সীমা ছাড়িয়েছে ABC আসল ব্যবহার: মোট 0.9 0.9 Memory Request: মোট 5 GiB ABCখালি 3 Limit: মোট 9 GiB, এখানেও বেশি ABC আসল ব্যবহার: মোট 3 GiB 3 GiB লাল দাগ: node-এর সীমা। Request এর ভেতরে থাকে, limit-এর যোগফল বাইরে যেতে পারে (overcommit).
ছবি ৪৮.১ — Scheduler ওপরের সারি দেখে Pod বসায়। মাঝের সারির যোগফল node-এর সীমা ছাড়াতে পারে, কারণ সব Pod একসাথে সীমায় পৌঁছাবে না বলে ধরে নেওয়া হয়। নিচের সারি বলে, বুক করা জায়গার বড় অংশই খালি পড়ে আছে।

এই ছবিতেই পুরো পর্বের গল্প আছে। Limit-এর যোগফল node ছাড়িয়ে গেলে তাকে বলে overcommit: সবাই একসাথে সীমায় পৌঁছালে কেউ না কেউ ভুগবে, যা অংশ ৪-এ দেখব। আর request-এর চেয়ে ব্যবহার অনেক কম হলে টাকা নষ্ট হয়, যা অংশ ৯-এ হিসাব করব।

২ · CPU limit ধীর করে, memory limit মারে

দুটি resource দুইভাবে আচরণ করে, কারণ দুটির প্রকৃতি আলাদা। CPU সময় ফুরিয়ে গেলে অপেক্ষা করা যায়, পরের মুহূর্তে আবার পাওয়া যায়। Memory নেওয়া হয়ে গেলে জোর করে ফেরত নেওয়ার সহজ উপায় নেই। বিদ্যুতের লোডশেডিং আর পানির ট্যাংকের পার্থক্যের মতো: বিদ্যুৎ গেলে কাজ থেমে থাকে, আবার আসে। ট্যাংক উপচে পড়লে পানি আর ফেরে না।

Kubernetes এই দুটি সীমা নিজে প্রয়োগ করে না। Kubelet আর container runtime সেগুলো Linux cgroup-এ লিখে দেয়, প্রয়োগ করে kernel. Cgroup আর CPU scheduler-এর ভেতরের কাজ জানতে Operating Systems · Linux-এর scheduler পর্বটি পড়ুন। Docker-এ একই জিনিস দেখেছেন পর্ব ৩৯ · Container চালানো-তে।

একই কাজ, ৫ সেকেন্ড ধরে একটি ফাঁকা লুপ, দুটি Pod-এ চালানো হলো। একটির CPU limit 100m, অন্যটির কোনো CPU limit নেই:

$ kubectl logs cpu-limited
wall=5.00s cpu=0.53s
cpu.max: 10000 100000
cpu.weight: 11
nr_periods 52
nr_throttled 52
throttled_usec 4470241

$ kubectl logs cpu-unlimited
wall=5.00s cpu=5.23s
cpu.max: max 100000

Limit-ওয়ালা Pod ঘড়ির ৫ সেকেন্ডে CPU পেয়েছে মাত্র ০.৫৩ সেকেন্ড, প্রায় দশ ভাগের এক ভাগ। cpu.max: 10000 100000 মানে প্রতি ১০০ মিলিসেকেন্ডের সময়-খণ্ডে (period) ১০ মিলিসেকেন্ড চালানো যাবে: 100m ঠিক এই সংখ্যায় বদলেছে। ৫২টি period-এর ৫২টিতেই সে আটকে গেছে (nr_throttled 52), আর মোট প্রায় ৪.৪৭ সেকেন্ড (throttled_usec) অপেক্ষায় কেটেছে। এটিই CPU throttling. Pod মরেনি, শুধু ধীর হয়েছে। দ্বিতীয় Pod-এ max মানে কোনো ছাদ নেই। সে ৫.২৩ সেকেন্ড CPU পেয়েছে। বাড়তিটুকু সম্ভবত প্রোগ্রাম শুরু হওয়ার সময়ের হিসাব।

এবার memory. ৩২ MiB সীমার Pod-এ একটি Python প্রোগ্রাম ৪ MiB করে memory নিতে থাকল:

$ kubectl logs mem-hog | tail -3
allocated 16 MiB
allocated 20 MiB
allocated 24 MiB

$ kubectl get pod mem-hog -o jsonpath='{.status.phase} {.status.containerStatuses[0].state.terminated.reason} {.status.containerStatuses[0].state.terminated.exitCode}{"\n"}'
Failed OOMKilled 137

শেষ লাইন 24 MiB, তারপর কিছু নেই। Python নিজেও কিছু memory নেয়, তাই সীমায় পৌঁছানোর আগেই লেখা থেমে গেছে। Kernel-এর OOM killer process-টিকে SIGKILL দিয়েছে, তাই app কোনো error লেখার সুযোগ পায়নি। প্রমাণ পাওয়া যায় Kubernetes-এর status-এ: OOMKilled আর exit 137 (128 + 9). Deployment-এর Pod হলে kubelet container-টি আবার চালাবে, আর বারবার মরলে দেখবেন CrashLoopBackOff, যা পর্ব ৪৩ · Pod-এ আলোচিত।

আর request কী করে? Cgroup-এর ভেতরে তাকিয়ে দেখা যাক। requests.cpu: 50m, limits.cpu: 200m, limits.memory: 64Mi দেওয়া একটি Pod-এর ভেতরে এই সংখ্যাগুলো পাওয়া গেল:

$ kubectl exec qos-burstable -- sh -c 'cat /sys/fs/cgroup/cpu.max /sys/fs/cgroup/cpu.weight /sys/fs/cgroup/memory.max'
20000 100000
11
67108864

limits.cpu: 200m হয়েছে ১০০ মিলিসেকেন্ডে ২০ মিলিসেকেন্ড, আর 64Mi হয়েছে 67108864 বাইট। Request 50m বসেছে cpu.weight-এ, যা একটি ভাগের ওজন। CPU-তে ভিড় হলে এই ওজন অনুপাতে সময় ভাগ হয়, ভিড় না থাকলে এর কোনো প্রভাব নেই। Limit-ওয়ালা Pod ভিড় না থাকলেও ছাদে আটকায়। Weight-এর সংখ্যাটি request থেকে একটি সূত্রে আসে, যা runtime-এর version-ভেদে বদলাতে পারে, তাই নির্দিষ্ট মানটি মুখস্থ করার দরকার নেই।

CPUMemory
ধরনঘুরে আসে (compressible): থামালে পরে আবার পাওয়া যায়আটকে থাকে (incompressible): সহজে ফেরত নেওয়া যায় না
Request-এর কাজScheduler-এর হিসাব, ভিড়ে ভাগের ওজনScheduler-এর হিসাব, eviction-এর ক্রম
Limit পার হলেThrottling: ধীর হয়, মরে নাOOMKilled: container মরে, exit 137
লক্ষণLatency বাড়ে, log-এ কিছু নেইRestart বাড়ে, শেষ log হঠাৎ থেমে যায়
Cgroup v2 ফাইলcpu.max, cpu.weightmemory.max

৩ · Pod কেন Pending — scheduler-এর filter আর score

একটি নতুন Pod তৈরি হলে তার nodeName ফাঁকা থাকে। Scheduler সেই Pod-কে সারি থেকে তুলে দুটি ধাপে কাজ করে। প্রথম ধাপ filter: কোন কোন node-এ Pod বসতেই পারে না, তাদের বাদ দেওয়া। দ্বিতীয় ধাপ score: বাকি node-গুলোকে নম্বর দিয়ে সেরাটি বেছে নেওয়া। শেষে Pod-এ node-এর নাম লিখে দেওয়া হয় (bind), আর সেই node-এর kubelet container চালায়।

চাকরির নিয়োগ পরীক্ষার মতো। প্রথমে যোগ্যতা যাচাই: বয়স বা সনদ না মিললে আবেদন বাতিল, নম্বরের প্রশ্নই আসে না। এটি filter. তারপর যাঁরা টিকলেন, তাঁদের নম্বর দিয়ে মেধাতালিকা। এটি score. কেউ যোগ্যতাতেই না টিকলে তালিকা ফাঁকা থাকে, আর Pod থাকে Pending.

একটি অসম্ভব Pod বানানো যাক, যে 1000 CPU আর 2 TiB memory চায়। কোনো সাধারণ node এটি দিতে পারবে না:

$ kubectl get pod too-big
NAME      READY   STATUS    RESTARTS   AGE
too-big   0/1     Pending   0          4s

$ kubectl get events --field-selector involvedObject.name=too-big \
    -o custom-columns=TYPE:.type,REASON:.reason,MSG:.message
TYPE      REASON             MSG
Warning   FailedScheduling   0/1 nodes are available: 1 Insufficient cpu, 1 Insufficient memory. no new claims to deallocate, preemption: 0/1 nodes are available: 1 Preemption is not helpful for scheduling.

Event-টি একটি ছোট রিপোর্ট। 0/1 nodes are available মানে একটি node-এর মধ্যে শূন্যটি যোগ্য। 1 Insufficient cpu মানে একটি node CPU-র জন্য বাদ গেছে। Event আসল ব্যবহার নিয়ে কিছু বলে না, কারণ scheduler শুধু request-এর হিসাব করেছে। শেষ অংশে scheduler আরও একটি চেষ্টার ফল জানাচ্ছে: অন্য Pod সরিয়েও জায়গা হবে না (Preemption is not helpful). অংশ ৬-এ preemption দেখব।

Pending দেখলে আগে kubectl describe pod-এর Events অংশ পড়ুন। সাধারণ কারণগুলো এক টেবিলে:

Event-এ যা লেখামানেপ্রথম পদক্ষেপ
Insufficient cpu / memoryকোনো node-এ যথেষ্ট খালি request নেইRequest কমানো, node বাড়ানো
didn't match Pod's node affinity/selectorLabel মিলছে নাNode-এর label আর Pod-এর selector মেলানো
didn't match pod anti-affinity rulesনিয়ম মানলে বসার জায়গা নেইReplica বা নিয়ম বদলানো
untolerated taintNode-এ taint আছে, Pod-এ toleration নেইTaint-এর উদ্দেশ্য বুঝে toleration যোগ
unbound immediate PersistentVolumeClaimsVolume তৈরি হয়নিPVC আর StorageClass দেখা (পর্ব ৪৭)

একটি প্রচলিত ভুল ধারণা: “Node-এ CPU প্রায় খালি, তবু Pod Pending কেন?” কারণ খালি বলতে এখানে বোঝায় বুক না হওয়া অংশ। ছবি ৪৮.১-এর মতো অন্য Pod-গুলো বেশি request দিয়ে রাখলে node ব্যবহারে ফাঁকা থেকেও হিসাবে ভরা থাকে।

৪ · QoS class আর eviction — চাপের সময় কে আগে যাবে

Overcommit করা node-এ সবাই একসাথে memory চাইলে কাউকে যেতে হবে। Kubernetes প্রতিটি Pod-কে request আর limit দেখে একটি QoS class দেয়। এটি আপনি হাতে লেখেন না, নিজে থেকেই হিসাব হয়:

Guaranteed

প্রতিটি container-এ CPU আর memory-র request ও limit আছে, আর দুটি সমান। সবচেয়ে সুরক্ষিত।

request = limit

Burstable

অন্তত একটি request বা limit আছে, কিন্তু Guaranteed-এর শর্ত পূরণ হয়নি। বেশিরভাগ Pod এই দলে।

request < limit

BestEffort

কোনো container-এ কোনো request বা limit নেই। হাতে যা খালি আছে তাই পায়, চাপে সবচেয়ে ঝুঁকিতে।

কিছুই লেখা নেই

তিনটি Pod বানিয়ে Kubernetes-কেই জিজ্ঞেস করা যাক:

$ kubectl get pod -o jsonpath='{range .items[*]}{.metadata.name}{"  "}{.status.qosClass}{"\n"}{end}'
qos-besteffort  BestEffort
qos-burstable  Burstable
qos-guaranteed  Guaranteed

$ kubectl exec qos-guaranteed -- cat /proc/self/oom_score_adj
-997
$ kubectl exec qos-besteffort -- cat /proc/self/oom_score_adj
1000

প্রথম Pod-এ কিছুই লেখা ছিল না, দ্বিতীয়টিতে 50m/200m, তৃতীয়টিতে request আর limit সমান। QoS class শুধু একটি লেবেল নয়, kernel পর্যন্ত পৌঁছায়। oom_score_adj হলো OOM killer-এর জন্য একটি ইঙ্গিত: সংখ্যা যত বড়, মারা পড়ার সম্ভাবনা তত বেশি। BestEffort পেয়েছে সর্বোচ্চ 1000, Guaranteed পেয়েছে -997. Burstable-এর মান এই দুইয়ের মাঝে, আর সেটি node-এর memory অনুপাতে হিসাব হয়।

Node-pressure eviction

Node-এ memory বা disk কমে এলে kernel-এর OOM killer-এর জন্য অপেক্ষা না করে kubelet নিজেই কিছু Pod সরিয়ে দেয়। একে বলে node-pressure eviction. Linux-এ kubelet-এর default hard সীমাগুলোর একটি হলো memory.available<100Mi, যদিও cluster-ভেদে এটি বদলানো থাকতে পারে। সীমা পার হলে node-এর condition হয় MemoryPressure=True, আর নতুন BestEffort Pod সেখানে বসা বন্ধ হয়।

Kubelet Pod বাছে তিনটি প্রশ্নে: ব্যবহার request ছাড়িয়েছে কি না, Pod-এর priority কত, আর request-এর তুলনায় কতটা বেশি ব্যবহার করছে। খেয়াল করুন, QoS class সরাসরি এই তালিকায় নেই। কিন্তু BestEffort-এর request শূন্য, তাই সামান্য ব্যবহারও “request ছাড়ানো”। ফলে সে স্বাভাবিকভাবেই সবার আগে সারিতে আসে।

এই আচরণ চালিয়ে দেখাতে একটি node-এর memory ইচ্ছা করে ফুরিয়ে দিতে হতো, যা ভাগ করে ব্যবহার করা একটি মেশিনে নিরাপদ নয়। তাই নিচের stepper একটি মডেল। সংখ্যাগুলো ধরে নেওয়া, MiB এককে:

Node-এর অবস্থা (মোট 4096 MiB, system 672 MiB)
memory.available
MemoryPressure
cache · BestEffort · req 0
batch · Burstable · req 256
api · Burstable · req 512
db · Guaranteed · req 1024
  1. স্বাভাবিক অবস্থা। চারটি Pod আর system মিলে 3696 MiB ব্যবহার, তাই খালি 4096 − 3696 = 400 MiB. এটি 100 MiB সীমার ওপরে। কোনো চাপ নেই।
  2. Batch job হঠাৎ বাড়ল। Batch 700 থেকে 1020 MiB হলো, খালি রইল মাত্র 80 MiB. সীমা 100 MiB পার হওয়ায় kubelet node-কে MemoryPressure=True করল, আর নতুন BestEffort Pod এখানে বসা বন্ধ।
  3. Kubelet সারি বানাল। Request ছাড়িয়েছে দুজন: cache (900 − 0 = 900 বেশি) আর batch (1020 − 256 = 764 বেশি)। Priority সমান ধরে নিলে যার বাড়তি বেশি সে আগে, তাই cache প্রথম। Api আর db request-এর ভেতরে, তাই তারা নিরাপদ দিকে।
  4. Cache সরানো হলো। Pod-এর status হলো Failed, কারণ Evicted. 900 MiB ফিরল, খালি হলো 80 + 900 = 980 MiB. সমস্যা মিটলেও condition সঙ্গে সঙ্গে নামে না, কিছুক্ষণ অপেক্ষা করে (default ৫ মিনিট), যাতে অবস্থা বারবার ওঠানামা না করে।
  5. চাপ কাটল। Condition False হলো। Cache যদি Deployment-এর Pod হয়, controller নতুন Pod বানায়, আর scheduler সেটিকে অন্য node-এ বসায়। খালি Pod হলে কেউ আবার বানাবে না। Batch বেশি memory নিচ্ছিল, কিন্তু মরল cache, যার কোনো দোষ ছিল না। Request ঠিকমতো না দেওয়ার দাম এটিই।
Container OOM killNode-pressure eviction
কে করেLinux kernelKubelet
কখনContainer তার memory limit ছুঁলে, বা node-এ memory একদম শেষ হলেNode-এর খালি memory বা disk সীমার নিচে নামলে
ফলContainer মরে, Pod থাকে, kubelet আবার চালায়পুরো Pod সরে যায়, নতুন Pod বানাতে হয়
চিহ্নOOMKilled, exit 137Status Failed, reason Evicted

৫ · LimitRange আর ResourceQuota — দলের জন্য বেড়া

একটি cluster-এ অনেক দল থাকলে কেউ ভুলে সব জায়গা নিয়ে নিতে পারে, বা কেউ request না লিখে BestEffort Pod চালাতে পারে। এর জন্য namespace ধরে দুটি বেড়া আছে। LimitRange একটি container-এর default আর সীমা ঠিক করে। ResourceQuota পুরো namespace-এর মোট সীমা ঠিক করে। মেসের উদাহরণে LimitRange হলো “প্রতি জন এক বালতি পানি”, আর ResourceQuota হলো “পুরো মেসের জন্য মাসে এত ইউনিট বিদ্যুৎ”।

apiVersion: v1
kind: LimitRange
metadata:
  name: team-defaults
spec:
  limits:
  - type: Container
    defaultRequest: {cpu: 50m, memory: 32Mi}
    default: {cpu: 100m, memory: 64Mi}
    max: {cpu: 500m, memory: 128Mi}
---
apiVersion: v1
kind: ResourceQuota
metadata:
  name: team-quota
spec:
  hard:
    requests.cpu: 300m
    requests.memory: 256Mi
    limits.memory: 512Mi
    pods: "8"

এই দুটি নিয়ম বসিয়ে একটি namespace-এ পরপর এই কাজগুলো করা হলো:

# ১. resources ছাড়া Pod: default বসে গেল
$ kubectl apply -f no-resources.yaml
pod/no-resources created
$ kubectl get pod no-resources -o jsonpath='{.spec.containers[0].resources}{"\n"}{.status.qosClass}{"\n"}'
{"limits":{"cpu":"100m","memory":"64Mi"},"requests":{"cpu":"50m","memory":"32Mi"}}
Burstable

# ২. একটি container limit 2 CPU চাইল
$ kubectl apply -f over-max.yaml
Error from server (Forbidden): … pods "over-max" is forbidden: maximum cpu usage per Container is 500m, but limit is 2

# ৩. একটি Pod quota-র চেয়ে বেশি চাইল
$ kubectl apply -f over-quota.yaml
Error from server (Forbidden): … pods "over-quota" is forbidden: exceeded quota: team-quota, requested: requests.cpu=400m, used: requests.cpu=50m, limited: requests.cpu=300m

# ৪. কতটা খরচ হয়েছে
$ kubectl describe quota team-quota
Name:            team-quota
Namespace:       …
Resource         Used  Hard
--------         ----  ----
limits.memory    64Mi  512Mi
pods             1     8
requests.cpu     50m   300m
requests.memory  32Mi  256Mi

প্রথম পরীক্ষায় Pod-এ কিছুই লেখা ছিল না, তবু API server default বসিয়ে দিয়েছে, আর সে BestEffort না হয়ে Burstable হয়েছে। দ্বিতীয় আর তৃতীয় পরীক্ষায় Pod তৈরিই হয়নি। এই আটকানো হয় admission control ধাপে, scheduler পর্যন্ত যাওয়ার আগেই। তাই Pod Pending হয় না, সরাসরি Forbidden আসে। Error-টি নিজেই হিসাব দেখায়: 50m ব্যবহৃত, 400m চাওয়া, সীমা 300m.

দুটি নিয়ম মনে রাখা দরকার। LimitRange শুধু নতুন Pod-এর ওপর খাটে, চলমান Pod বদলায় না। আর quota-তে requests.cpu বসালে প্রতিটি Pod-কে সেটি বলতে হয়। LimitRange ছাড়া শুধু quota থাকা অবস্থায় resources ছাড়া Pod বানালে আসল run-এ এই উত্তর এল:

Error from server (Forbidden): … pods "no-resources" is forbidden: failed quota: team-quota: must specify limits.memory for: app; requests.cpu for: app; requests.memory for: app

৬ · Pod কোথায় বসবে — selector, affinity, taint, spread আর priority

কখনো জায়গা থাকাই যথেষ্ট নয়। Database চাই SSD-ওয়ালা node-এ, দুটি replica একই node-এ থাকা চলবে না, দামি GPU node-এ সাধারণ Pod ঢোকা চলবে না। এর জন্য চারটি হাতিয়ার আছে। দুই রকম ভাবনা মনে রাখুন: কিছু নিয়ম Pod টানে (“আমি ওখানে যেতে চাই”), আর কিছু নিয়ম Pod ঠেলে (“এখানে ঢোকা নিষেধ”)।

হাতিয়ারকার ওপর লেখাকী বলেতুলনা
nodeSelectorPodএই label-ওয়ালা node চাই, না পেলে Pending“শুধু দোতলার কেবিন”
Node affinityPodএকই কাজ, তবে বাধ্যতামূলক বা পছন্দ দুই রকমে“দোতলা হলে ভালো”
Pod anti-affinityPodঅমুক Pod-এর পাশে বসব না“দুই ভাই এক বাসে নয়”
Taint + tolerationNode + PodNode বলে “ঢোকা নিষেধ”, Pod-এর হাতে অনুমতি থাকলে ঢুকতে পারে“সংরক্ষিত আসন”
Topology spreadPodReplica-গুলো zone বা node জুড়ে সমান করে ছড়ানো“সব ডিম এক ঝুড়িতে নয়”

সবগুলো একসাথে একটি Deployment-এ দেখতে এমন। এটি পড়ার জন্য লেখা উদাহরণ, বাস্তবে এত নিয়ম একসাথে কম লাগে:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  replicas: 3
  selector:
    matchLabels:
      app: api
  template:
    metadata:
      labels:
        app: api
    spec:
      nodeSelector:
        kubernetes.io/arch: amd64
      affinity:
        nodeAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 80
            preference:
              matchExpressions:
              - key: disktype
                operator: In
                values: ["ssd"]
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchLabels:
                app: api
            topologyKey: kubernetes.io/hostname
      topologySpreadConstraints:
      - maxSkew: 1
        topologyKey: topology.kubernetes.io/zone
        whenUnsatisfiable: ScheduleAnyway
        labelSelector:
          matchLabels:
            app: api
      tolerations:
      - key: gpu
        operator: Equal
        value: "true"
        effect: NoSchedule
      containers:
      - name: app
        image: nginx:1.27-alpine
        imagePullPolicy: IfNotPresent
        resources:
          requests:
            cpu: 100m
            memory: 64Mi
          limits:
            memory: 128Mi

লম্বা নামটি ভেঙে পড়ুন। requiredDuringScheduling মানে বসানোর সময় বাধ্যতামূলক, preferred মানে পছন্দ, যা score-এ নম্বর বাড়ায়। IgnoredDuringExecution মানে বসার পরে পরিস্থিতি বদলালেও Pod-কে সরানো হবে না। topologyKey বলে কোন node-label ধরে “এলাকা” গোনা হবে: hostname মানে প্রতিটি node এক এলাকা, zone মানে প্রতিটি availability zone এক এলাকা।

এই cluster-এ node একটি মাত্র। তবু তিনটি নিয়ম চালিয়ে আসল ফল দেখা যায়: wants-ssd চেয়েছে disktype: ssd label, যা node-এ নেই; spread-web-এর ২টি replica-র ওপর বাধ্যতামূলক anti-affinity; skew-web-এর ৩টি replica-র ওপর hostname ধরে maxSkew: 1; আর important Pod-টি এই অংশের শেষে priority-র জন্য।

$ kubectl get pods -o custom-columns=NAME:.metadata.name,STATUS:.status.phase,NODE:.spec.nodeName,PRI:.spec.priority
NAME                         STATUS    NODE             PRI
important                    Running   docker-desktop   100000
skew-web-6c68b9fcc6-jkklz    Running   docker-desktop   0
skew-web-6c68b9fcc6-v7s49    Running   docker-desktop   0
skew-web-6c68b9fcc6-zgdvr    Running   docker-desktop   0
spread-web-d94f67f44-6w29j   Pending   <none>           0
spread-web-d94f67f44-h7ldv   Running   docker-desktop   0
wants-ssd                    Pending   <none>           0

$ kubectl get events --field-selector reason=FailedScheduling -o custom-columns=OBJ:.involvedObject.name,MSG:.message
OBJ                          MSG
spread-web-d94f67f44-6w29j   0/1 nodes are available: 1 node(s) didn't match pod anti-affinity rules. …
wants-ssd                    0/1 nodes are available: 1 node(s) didn't match Pod's node affinity/selector. …

wants-ssd Pending, কারণ label মেলেনি। spread-web-এর প্রথম replica বসেছে, দ্বিতীয়টি বসতে পারেনি, কারণ node একটিই। মজার ফল skew-web: তিনটিই একই node-এ বসেছে। Topology spread-এর হিসাবে এলাকা (domain) এখানে একটি, তাই বাড়তি-কমতি (skew) শূন্য, নিয়ম ভাঙেনি। একক node-এ এই নিয়মের আসল প্রভাব দেখা সম্ভব নয়। সেজন্য নিচের দুটি সিমুলেটর।

Taint বসাতে হয় node-এ, যেমন kubectl taint nodes node-c gpu=true:NoSchedule. এই cluster-এর node অন্য কাজেও ব্যবহৃত হচ্ছিল, তাই এটি চালানো হয়নি, শুধু নমুনা। Taint-এর effect তিন রকম: NoSchedule নতুন Pod আটকায়, PreferNoSchedule এড়িয়ে চলার চেষ্টা করে, আর NoExecute চলমান Pod-কেও বের করে দেয়। আপনি না লিখলেও প্রতিটি Pod-এ কিছু toleration থাকে। আসল run-এ একটি সাধারণ Pod-এ পাওয়া গেল:

$ kubectl get pod important -o jsonpath='{range .spec.tolerations[*]}{.key}{" "}{.operator}{" "}{.effect}{" "}{.tolerationSeconds}{"\n"}{end}'
node.kubernetes.io/not-ready Exists NoExecute 300
node.kubernetes.io/unreachable Exists NoExecute 300

এই দুটি লাইন একটি পরিচিত প্রশ্নের উত্তর দেয়: node মরে গেলে Pod সরতে এত সময় লাগে কেন? Node খারাপ হলে তার গায়ে NoExecute taint বসে, আর Pod ৩০০ সেকেন্ড, অর্থাৎ ৫ মিনিট সেটি সহ্য করে। সাময়িক সমস্যায় Pod যাতে অকারণে না সরে, সেজন্য এই অপেক্ষা। দ্রুত সরাতে চাইলে Pod-এ tolerationSeconds কমানো যায়।

সিমুলেটর ১ — Scheduler-এর filter আর score

তিনটি node-এর একটি খেলনা cluster. node-c GPU-র জন্য taint করা। Scheduler-এর মতোই প্রতিটি Pod পরপর বসে, তাই আগের সিদ্ধান্ত পরের Pod-এর জায়গা কমায়। Score এখানে সহজ রূপে: বসানোর পরে CPU আর memory-র গড় খালি শতাংশ, যা default LeastAllocated কৌশলের ভাব। আসল scheduler-এ আরও plugin মিলে নম্বর দেয়।

const NODES = [ // cpu in millicores, mem in MiB, used = sum of requests
  { name: "node-a", cpu: 4000, mem: 8192, usedCpu: 3000, usedMem: 3000, labels: { disk: "ssd" }, taints: [], apps: [] },
  { name: "node-b", cpu: 4000, mem: 8192, usedCpu: 1000, usedMem: 2000, labels: { disk: "hdd" }, taints: [], apps: [] },
  { name: "node-c", cpu: 8000, mem: 16384, usedCpu: 500, usedMem: 1000, labels: { disk: "ssd", gpu: "yes" }, taints: ["gpu=true:NoSchedule"], apps: [] },
];
const PODS = [
  { name: "api-1", app: "api", cpu: 500, mem: 512, antiAffinity: true },
  { name: "api-2", app: "api", cpu: 500, mem: 512, antiAffinity: true },
  { name: "api-3", app: "api", cpu: 500, mem: 512, antiAffinity: true },
  { name: "db", app: "db", cpu: 800, mem: 2048, nodeSelector: { disk: "ssd" } },
  { name: "train", app: "train", cpu: 2000, mem: 4096, nodeSelector: { gpu: "yes" }, tolerates: ["gpu=true:NoSchedule"] },
  { name: "big", app: "big", cpu: 6000, mem: 1024 },
];
function whyNot(n, p) {
  const t = n.taints.find(x => !(p.tolerates || []).includes(x));
  if (t) return `untolerated taint ${t}`;
  const s = Object.entries(p.nodeSelector || {}).find(([k, v]) => n.labels[k] !== v);
  if (s) return `selector ${s[0]}=${s[1]} not matched`;
  if (p.antiAffinity && n.apps.includes(p.app)) return "pod anti-affinity";
  if (n.usedCpu + p.cpu > n.cpu) return `Insufficient cpu (${n.cpu - n.usedCpu}m free)`;
  if (n.usedMem + p.mem > n.mem) return `Insufficient memory (${n.mem - n.usedMem}Mi free)`;
  return "";
}
const score = (n, p) => { // LeastAllocated: more free after placing = higher
  return Math.round(((n.cpu - n.usedCpu - p.cpu) / n.cpu + (n.mem - n.usedMem - p.mem) / n.mem) * 50);
};
for (const p of PODS) {
  const fit = NODES.filter(n => !whyNot(n, p));
  NODES.filter(n => whyNot(n, p)).forEach(n => console.log(`  ${p.name} x ${n.name}: ${whyNot(n, p)}`));
  if (!fit.length) { console.log(`${p.name}: Pending (0/${NODES.length} nodes available)`); continue; }
  const ranked = fit.map(n => [score(n, p), n]).sort((a, b) => b[0] - a[0]);
  const n = ranked[0][1];
  n.usedCpu += p.cpu; n.usedMem += p.mem; n.apps.push(p.app);
  console.log(`${p.name}: ${n.name}  (scores: ${ranked.map(r => r[1].name + "=" + r[0]).join(" ")})`);
}

মূল লাইন: api-1: node-b (scores: node-b=66 node-a=35). দুটি node যোগ্য, খালি বেশি বলে node-b জিতল। node-c filter-এই বাদ, কারণ taint. api-3: Pending: anti-affinity দুটি node বাদ দিল, taint তৃতীয়টি। db-র কাহিনি আরও শিক্ষণীয়: db x node-a: Insufficient cpu (500m free). Api-2 এখানে বসায় জায়গা কমে গেছে, আর SSD-ওয়ালা অন্য node-টি taint করা। ক্রমের কারণে Pending, যদিও শুরুতে জায়গা ছিল। train-এর selector আর toleration দুটোই আছে, তাই সে node-c পেল। big কোথাও আঁটে না। বদলে দেখুন: db-তে tolerates যোগ করুন, বা api-র antiAffinity সরান।

সিমুলেটর ২ — Spread কী বাঁচায়

চারটি node, কিন্তু zone তিনটি: a-তে দুটি node. ৬টি replica প্রথমে শুধু “সবচেয়ে কম Pod-ওয়ালা node” নিয়মে, তারপর zone ধরে maxSkew: 1 দিয়ে বসানো হলো। শেষে হিসাব: একটি zone হারালে কয়টি replica থাকে?

// Topology spread across zones: without and with maxSkew. Edit and run.
const NODES = [["a1", "a"], ["a2", "a"], ["b1", "b"], ["c1", "c"]];
const REPLICAS = 6, MAX_SKEW = 1, LOST_ZONE = "a";

function place(useSpread) {
  const pods = Object.fromEntries(NODES.map(([n]) => [n, 0]));
  const inZone = z => NODES.filter(([, y]) => y === z).reduce((s, [n]) => s + pods[n], 0);
  const zones = [...new Set(NODES.map(([, z]) => z))];
  for (let r = 0; r < REPLICAS; r++) {
    const low = Math.min(...zones.map(inZone));
    const ok = NODES.filter(([, z]) => !useSpread || inZone(z) + 1 - low <= MAX_SKEW);
    const pick = ok.reduce((b, c) => (pods[c[0]] < pods[b[0]] ? c : b)); // fewest Pods
    pods[pick[0]]++;
  }
  const line = zones.map(z => `zone-${z}=${inZone(z)}`).join(" ");
  const left = REPLICAS - inZone(LOST_ZONE);
  console.log(`${useSpread ? "with spread   " : "without spread"}  ${line}  lose zone-${LOST_ZONE}: ${left} of ${REPLICAS} left`);
}
place(false);
place(true);

আউটপুট: without spread zone-a=4 zone-b=1 zone-c=1 lose zone-a: 2 of 6 left, আর with spread zone-a=2 zone-b=2 zone-c=2 lose zone-a: 4 of 6 left. Node ধরে সমান ছড়ানো মানেই zone ধরে সমান নয়। maxSkew: 1 মানে দুই এলাকার Pod-সংখ্যার পার্থক্য ১-এর বেশি হবে না। এখানে একই খরচে zone হারানোর ক্ষতি ৪ replica থেকে ২-এ নেমেছে। এটি সিমুলেশন, একক node-এর cluster-এ চালানো নয়।

Priority আর preemption — জরুরি Pod-এর জন্য জায়গা ছাড়া

হাসপাতালে সিরিয়াল ধরে রোগী দেখা হয়, কিন্তু জরুরি রোগী এলে অন্যদের অপেক্ষা করতে হয়। PriorityClass একটি নাম আর একটি সংখ্যা। সংখ্যা যত বড়, Pod তত গুরুত্বপূর্ণ। Scheduling-এর সারিতেও এটি আগে আসে। আর জায়গা না পেলে scheduler কম priority-র Pod সরিয়ে জায়গা বানাতে পারে। একে বলে preemption.

apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: abcl-kh-payment
value: 100000
preemptionPolicy: PreemptLowerPriority
globalDefault: false
description: "Payment workloads."

একই ধরনের একটি class (abcl-kh-high, value 100000) বানিয়ে Pod-এ priorityClassName দিলে admission ধাপে সংখ্যাটি Pod-এ বসে যায়। আর একটি সাধারণ cluster-এ আগে থেকেই দুটি system class থাকে:

$ kubectl get pod important -o jsonpath='{.spec.priorityClassName} {.spec.priority} {.spec.preemptionPolicy}{"\n"}'
abcl-kh-high 100000 PreemptLowerPriority

$ kubectl get priorityclass
NAME                      VALUE        GLOBAL-DEFAULT   AGE     PREEMPTIONPOLICY
system-cluster-critical   2000000000   false            …       PreemptLowerPriority
system-node-critical      2000001000   false            …       PreemptLowerPriority

ওপরের placement আউটপুটের PRI ঘরেও এই ফল দেখা গেছে: important-এর 100000, বাকিদের 0. Class না দিলে priority 0. system-* class দুটি cluster চালানোর জরুরি Pod-এর জন্য সংরক্ষিত, সাধারণ app-এ এগুলো ব্যবহার করবেন না।

Preemption নিজে এখানে চালানো হয়নি। তার জন্য node-এর request ভরে ফেলতে হতো, যা একই cluster-এর অন্য কাজকে আটকে দিত। কাজের ধাপগুলো এই রকম: উচ্চ priority-র Pod কোনো node-এ আঁটল না; scheduler দেখে কোন node-এ কম priority-র Pod সরালে সে আঁটবে; সেই Pod-গুলোকে স্বাভাবিকভাবে থামতে সময় দেয়; জায়গা খালি হলে Pod বসে। সরানোর সময় scheduler PodDisruptionBudget মানার চেষ্টা করে, কিন্তু এটি নিশ্চিত প্রতিশ্রুতি নয়।

একটি দলের সব Pod-কে “জরুরি” বানালে কেউই আর জরুরি থাকে না, আর ভুল করে বেশি priority দিলে একটি Pod অন্য দলের কাজ সরিয়ে দিতে পারে। তাই ২–৩টি class-ই যথেষ্ট, আর ResourceQuota দিয়ে কোন namespace কোন class ব্যবহার করবে তা সীমিত করা যায়। শুধু সারিতে আগে যেতে চান, কাউকে সরাতে চান না? তাহলে preemptionPolicy: Never.

৭ · HPA — Pod-এর সংখ্যা নিজে থেকে বাড়ানো-কমানো

ঈদের আগে বাসের কাউন্টারে ভিড় বাড়লে বাড়তি কাউন্টার খোলা হয়, ভিড় কমলে বন্ধ। VM-এর autoscaling দেখেছেন পর্ব ১৭-তে। Kubernetes-এ একই কাজ Pod স্তরে করে Horizontal Pod Autoscaler বা HPA. এটি একটি controller: নির্দিষ্ট সময় পরপর মেট্রিক দেখে Deployment-এর replicas বদলায়। “Horizontal” মানে Pod-এর সংখ্যা বদলানো, আকার নয়।

desiredReplicas = ceil(currentReplicas × currentMetric ÷ targetMetric)

ceil মানে ওপরের দিকের পূর্ণসংখ্যায় তোলা, যেমন ceil(1.24) = 2. কয়েকটি হিসাব হাতে করি। লক্ষ্য 60%:

এখন Podএখন ব্যবহারঅনুপাতহিসাবফল
390%1.50ceil(3 × 1.50) = ceil(4.5)5 Pod
1030%0.50ceil(10 × 0.50) = 55 Pod
463%1.050.9 থেকে 1.1-এর ভেতরেবদলায় না

তৃতীয় সারিতে tolerance কাজ করেছে: অনুপাত ১ থেকে ১০%-এর বেশি দূরে না গেলে (default 0.1) HPA কিছু করে না, যাতে সামান্য ওঠানামায় Pod না বদলায়। মেট্রিকের ব্যবহার মাপা হয় request-এর শতাংশে। Request না লিখলে এই শতাংশের হিসাবই হয় না, HPA কাজ করে না। Request-এর মান তাই HPA-র আচরণও ঠিক করে।

আসল run — একটি ছোট লোডে Pod বাড়ল

HPA-র CPU মেট্রিক আসে Metrics API থেকে, যা দেয় metrics-server. একক-node cluster-এ এটি Helm দিয়ে বসানো হলো (Helm-এর বিস্তারিত পর্ব ৫০-এ):

$ helm install abcl-kh-metrics metrics-server \
    --repo https://kubernetes-sigs.github.io/metrics-server/ \
    -n … --set 'args={--kubelet-insecure-tls}' \
    --set resources.requests.cpu=50m --set resources.requests.memory=64Mi \
    --set resources.limits.memory=128Mi
NAME: abcl-kh-metrics
…
STATUS: deployed
REVISION: 1
…
$ kubectl top pods
NAME                                              CPU(cores)   MEMORY(bytes)
abcl-kh-metrics-metrics-server-7b67f97c78-kwqtg   4m           18Mi
web-675bfcb8b7-tsp9t                              1m           22Mi

--kubelet-insecure-tls শুধু এই স্থানীয় পরীক্ষার জন্য। এটি kubelet-এর certificate যাচাই বন্ধ করে, যা বাড়ির একক-node cluster-এ certificate না মেলার ঝামেলা এড়ায়। Production-এ এটি ব্যবহার করবেন না; সেখানে kubelet-এর certificate ঠিক করে নিন। আর metrics-server-এর APIService পুরো cluster-এর জিনিস, তাই পরীক্ষা শেষে release মুছে নিশ্চিত হওয়া হয়েছে যে সেটি চলে গেছে।

Target হলো web নামের একটি ছোট Python server: request 50m, limit 150m. HPA-র লক্ষ্য 50%, Pod ১ থেকে ৪টি। Scale-down-এর অপেক্ষা ইচ্ছা করে ৬০ সেকেন্ড রাখা হয়েছে, যাতে পরীক্ষা ছোট হয়:

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: web
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: web
  minReplicas: 1
  maxReplicas: 4
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 50
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 60

লোড দিল একটিমাত্র ছোট Pod (CPU limit 100m), যা ১৫০ সেকেন্ড ধরে wget চালিয়ে নিজে থেকেই থেমে গেছে। kubectl get hpa web ১৫ সেকেন্ড পরপর চালিয়ে TARGETS আর REPLICAS ঘর তুলে আনা হলো, যেখানে বদল হয়েছে:

# t = লোড Pod তৈরির পরের সেকেন্ড (TARGETS, REPLICAS)
t=15s    cpu: 62%/50%    1
t=75s    cpu: 233%/50%   2
t=135s   cpu: 188%/50%   4

$ kubectl describe hpa web
…
  resource cpu on pods  (as a percentage of request):  188% (94m) / 50%
Behavior:
  Scale Up:
    Stabilization Window: 0 seconds
    Select Policy: Max
    Policies:
      - Type: Pods     Value: 4    Period: 15 seconds
      - Type: Percent  Value: 100  Period: 15 seconds
  Scale Down:
    Stabilization Window: 60 seconds
…
  ScalingLimited  True    TooManyReplicas   the desired replica count is more than the maximum replica count

$ kubectl get events --field-selector involvedObject.kind=HorizontalPodAutoscaler \
    -o custom-columns=REASON:.reason,COUNT:.count,MSG:.message
REASON              COUNT   MSG
SuccessfulRescale   1       New size: 2; reason: cpu resource utilization (percentage of request) above target
SuccessfulRescale   1       New size: 4; reason: cpu resource utilization (percentage of request) above target
SuccessfulRescale   1       New size: 1; reason: All metrics below target

প্রতিটি সংখ্যা সূত্রে মেলে। প্রথম সারি: ceil(1 × 62 ÷ 50) = ceil(1.24) = 2. দ্বিতীয় সারি: ceil(2 × 233 ÷ 50) = ceil(9.32) = 10, কিন্তু maxReplicas 4, তাই New size: 4. তৃতীয় সারিতে ৪টি Pod-এর গড় 94m, যা 50m request-এর 188%. সূত্র 16 চায়, পাচ্ছে 4, তাই TooManyReplicas. লোড থামার পরে ব্যবহার 88% হয়ে 8%-এ নামল: ceil(4 × 8 ÷ 50) = ceil(0.64) = 1, আর প্রায় দুই মিনিটের মধ্যে Pod ১টিতে নামল (All metrics below target). এই cluster-এ HPA-র status প্রায় ৬০ সেকেন্ড পরপর বদলেছে, যদিও Kubernetes-এর default চক্র ১৫ সেকেন্ড। আপনার cluster-এ এটি আলাদা হতে পারে। মেট্রিক নিজেও কিছুটা পুরোনো থাকে, তাই HPA সবসময় একটু দেরিতে সাড়া দেয়।

Tolerance আর stabilization window

মেট্রিক ওঠানামা করে। প্রতিটি ওঠানামায় Pod বদলালে কেবল Pod বানানো আর মোছাই চলতে থাকবে, যাকে বলে flapping. দুটি বাধা এটি থামায়। Tolerance ছোট বিচ্যুতি উপেক্ষা করে। Stabilization window scale-down-এর সিদ্ধান্তে গত কিছুক্ষণের সবচেয়ে বড় সুপারিশটি নেয়, তাই এক মুহূর্তের কম মেট্রিকে Pod কমে না। Default-এ scale-down window ৩০০ সেকেন্ড আর scale-up window ০, অর্থাৎ বাড়তে দেরি নেই, কমতে সতর্কতা। Scale-up-এর default policy আসল আউটপুটেই দেখেছেন: ১৫ সেকেন্ডে ৪টি Pod বা ১০০%, যেটি বড়।

সিমুলেটর ৩ — HPA-র হিসাবের ধারা

সিমুলেটরে একটি কাল্পনিক app ১৫ সেকেন্ডের ঘড়িতে চলে। লোড উঠে আবার নেমে যায়। সরলতার জন্য নতুন Pod পরের ধাপেই প্রস্তুত ধরা হয়েছে। raw হলো সূত্র আর min/max দেওয়ার পরের মান, next হলো policy আর window মানার পরের আসল সিদ্ধান্ত।

// HPA loop with tolerance, scale-up policy and scale-down window.
const CFG = {
  request: 200, target: 60,  // millicores per Pod, percent of request
  min: 2, max: 10,
  tolerance: 0.10,           // ignore a ratio inside 1 +/- this
  downWindow: 4,             // intervals (15 s each) of memory for scale-down
};
// Total CPU the app needs in each 15 s interval, in millicores.
const LOAD = [230, 250, 900, 1800, 2000, 2000, 1400, 500, 300, 300, 300, 300, 300, 300];

let replicas = 2;
const history = [];
console.log(" t  load  pods  util  ratio  raw  next");
LOAD.forEach((load, i) => {
  const util = load / replicas / CFG.request * 100;
  const ratio = util / CFG.target;
  let raw = replicas;
  if (Math.abs(ratio - 1) > CFG.tolerance) raw = Math.ceil(replicas * ratio);
  raw = Math.min(CFG.max, Math.max(CFG.min, raw));
  history.push(raw);
  if (history.length > CFG.downWindow) history.shift();
  let next = raw;
  if (raw < replicas) next = Math.min(replicas, Math.max(...history)); // window keeps the highest
  if (raw > replicas) next = Math.min(raw, Math.max(replicas * 2, replicas + 4)); // 100% or 4 Pods
  const row = [String(i * 15).padStart(3), String(load).padStart(5), String(replicas).padStart(4),
    (util.toFixed(0) + "%").padStart(5), ratio.toFixed(2).padStart(6), String(raw).padStart(4), String(next).padStart(4)];
  console.log(row.join(" "));
  replicas = next;
});

কয়েকটি সারি পড়ুন। t=30-এ ratio 3.75, raw 8, কিন্তু next 6: scale-up policy দুটি Pod থেকে এক ধাপে সর্বোচ্চ max(2 × 2, 2 + 4) = 6 দেয়। t=105-এ raw 5, কিন্তু next 10: window-তে আগের 10 এখনো আছে। t=150-এ window-র সবচেয়ে বড় মান 5, তাই Pod নামে। শেষে t=180-এ util 50%, ratio 0.83, তবু raw 3, কারণ ceil(3 × 0.83) = 3. LOAD, tolerance বা downWindow বদলে দেখুন। downWindow: 1 দিলে Pod কত দ্রুত নামে? লোড আবার ফিরলে কী হবে?

একই সংখ্যাগুলো এবার ধাপে ধাপে:

লক্ষ্য 60%, request 200m, min 2, max 10
সময়
মোট লোড
এখন Pod
ব্যবহার
অনুপাত
raw
পরের Pod
  1. শান্ত অবস্থা। 250m ÷ 2 Pod ÷ 200m = 63%. অনুপাত 1.04, যা tolerance-এর ভেতরে, তাই কোনো বদল নেই।
  2. ভিড় শুরু। ব্যবহার 225%, অনুপাত 3.75, সূত্র বলে ceil(7.5) = 8. কিন্তু policy এক ধাপে 6-এর বেশি দেবে না। পরের Pod 6.
  3. আরও ভিড়। 6 Pod-এ ব্যবহার 150%. Ceil(6 × 2.50) = 15, যা max 10-এ আটকে গেল। পরের Pod 10.
  4. ছাদে পৌঁছানো। 10 Pod-এও ব্যবহার 100%, লক্ষ্যের ওপরে। সূত্র 17 চায়, কিন্তু max 10. আসল cluster-এ এখানে ScalingLimited দেখা যেত। সীমা বাড়ানো বা app দ্রুত করা ছাড়া পথ নেই।
  5. লোড নামল, Pod নামল না। ব্যবহার 25%, সূত্র বলে 5. কিন্তু window-র গত চারটি সুপারিশ 10, 10, 10, 5, আর সবচেয়ে বড়টি নেওয়া হয়। Pod রইল 10.
  6. Window পার। এখন window-তে আছে 5, 3, 3, 3. বড়টি 5, তাই Pod এক ধাপে 10 থেকে 5.
  7. স্থির হওয়া। 5 Pod-এ ব্যবহার 30%, সূত্র 3, window-র সব মান 3. Pod 3-এ নামল। শেষে ব্যবহার 50%, অনুপাত 0.83, ceil(3 × 0.83) = 3, তাই এখানেই থিতু।

৮ · VPA, Cluster Autoscaler আর Karpenter

HPA Pod-এর সংখ্যা বদলায়। কিন্তু Pod-এর আকার ভুল হলে, বা node-এই জায়গা না থাকলে? এর জন্য আরও দুই স্তরের autoscaler আছে।

নামকী বদলায়কিসে সাড়া দেয়খেয়াল রাখবেন
HPAPod-এর সংখ্যাCPU, memory বা custom মেট্রিকApp-কে অনেক কপিতে চলতে পারতে হবে
VPAPod-এর request আর limitআগের ব্যবহারের ইতিহাসআলাদা project; একই মেট্রিকে HPA-র সাথে নয়
Cluster AutoscalerNode group-এর সংখ্যাPending Pod আর কম ব্যবহৃত nodeআগে থেকে ঠিক করা node group লাগে
Karpenterসরাসরি VM কেনা ও ফেরত দেওয়াPending Pod-এর চাহিদাProvider-সমর্থন দেখে নিন

VPA (Vertical Pod Autoscaler) Kubernetes-এর মূল অংশ নয়, kubernetes/autoscaler project-এর আলাদা component, যা CRD আর controller হিসেবে বসাতে হয়। এই cluster-এ এটি বসানো ছিল না, তাই এখানে কোনো YAML বা আউটপুট দেখানো হয়নি। এটি ব্যবহারের ইতিহাস দেখে request সুপারিশ করে। শুধু সুপারিশ দেখার mode-ও (Off) আছে, যা শুরু করার সবচেয়ে নিরাপদ পথ। সাধারণ mode-এ request বদলাতে Pod নতুন করে বানাতে হতো। চলমান Pod-এর CPU আর memory বদলানোর সুবিধা (in-place resize) Kubernetes ১.৩৩-এ beta হয়, আর ১.৩৫-এ স্থিতিশীল (GA) হয়েছে। তাই এই পর্বের ১.৩৪ cluster-এ এটি beta. VPA-র InPlaceOrRecreate mode এই সুবিধা ব্যবহার করে, যা লেখার সময়ে beta. নিজের version দেখে নিন। একই CPU মেট্রিকে HPA আর VPA একসাথে চালাবেন না: একজন Pod বাড়াবে, আরেকজন আকার বদলাবে, আর দুজনের হিসাব একে অন্যকে নাড়াবে।

Cluster Autoscaler Pending Pod দেখে node group বাড়ায়, আর দীর্ঘদিন কম ব্যবহৃত node থেকে Pod সরিয়ে node ফেরত দেয়। লক্ষ্য করুন, সেও request দেখে, আসল ব্যবহার নয়। একক-node স্থানীয় cluster-এ node যোগ হওয়া সম্ভব নয়, তাই নিচেরটি একটি মডেল, আসল run নয়:

  1. নতুন Pod2000m চায়
  2. SchedulerPending
  3. Autoscalergroup 2 → 3
  4. নতুন VMboot, join
  5. Pod বসলnode-3
প্রতিটি node 4000m, ঘরে বুক করা request
node-1
node-2
node-3
report Pod
  1. Pod এল। report Pod 2000m request করল। খালি আছে node-1-এ 500m, node-2-এ 1000m. মোট 1500m খালি, আর একসাথে 2000m কোথাও নেই।
  2. Filter-এ দুটিই বাদ। Scheduler লিখল 0/2 nodes are available: 2 Insufficient cpu. Pod Pending, condition Unschedulable. দুটি node-এ টুকরো টুকরো মিলিয়ে 1500m থাকলেও একটি Pod ভাগ হয়ে বসতে পারে না।
  3. Autoscaler হিসাব করল। সে Pending Pod দেখে এবং node group-এর নমুনা ধরে পরীক্ষা করে: নতুন 4000m node-এ Pod আঁটবে কি? হ্যাঁ। সে cloud API-তে group-এর আকার 2 থেকে 3 করল।
  4. নতুন VM উঠছে। VM boot হয়, kubelet cluster-এ যোগ দেয়, node NotReady থেকে Ready হয়। এই সময় মিনিটের হিসাবে, provider আর image-ভেদে আলাদা। Pod এতক্ষণ Pending. HPA যত দ্রুতই হোক, node না থাকলে নতুন Pod অপেক্ষা করবে।
  5. Pod বসল। Scheduler-এর পরের চেষ্টায় node-3 filter পার হলো। Pod Running, node-3-এ 2000m বুক করা।
  6. কাজ শেষ, node ফেরত। Report শেষ হলো, node-3 প্রায় ফাঁকা। Default-এ request-এর হিসাবে ৫০%-এর কম ব্যবহার আর প্রায় ১০ মিনিট অপেক্ষার পরে autoscaler node সরায়। Default মান version আর cloud-ভেদে দেখে নিন। বিল এখানে কমে।

Karpenter ২০২১ সালে AWS open source করে। এটি আগে থেকে ঠিক করা node group-এর বদলে Pending Pod-গুলোর মোট চাহিদা দেখে সরাসরি উপযুক্ত আকারের VM আনে, আর পরে Pod গুছিয়ে node কমানোর চেষ্টা করে (consolidation). Cluster Autoscaler পুরোনো আর অনেক cloud-এ চলে, তবে node-এর ধরন আগে থেকে ঠিক করতে হয়। Karpenter বেশি নমনীয়, তবে তার provider-সমর্থন আলাদা, আর ঘনঘন node বদল চলমান Pod-কে নাড়া দেয়। লেখার সময়ের অবস্থা provider-এর document-এ দেখে নিন।

আরও গভীরে: Google-এর Borg paper (Verma et al., EuroSys ২০১৫) দেখায়, request-এর চেয়ে আসল ব্যবহার কম বলেই overcommit করে বড় সাশ্রয় সম্ভব, আর Kubernetes-এর request ও limit-এর ভাবনা সেই অভিজ্ঞতা থেকেই আসে। একই দলের Autopilot paper (Rzadca et al., EuroSys ২০২০) ব্যবহারের ইতিহাস থেকে নিজে থেকে request ঠিক করার পদ্ধতি বর্ণনা করে, যা VPA-র ধারণার কাছাকাছি। Scheduler-এর plugin কাঠামো (filter, score, bind) Scheduling Framework নামে পরিচিত, তাই নিজের plugin যোগ করা যায়। HPA-র Pod গোনার আরও সূক্ষ্ম নিয়ম আছে: যে Pod এখনো ready নয় বা যার মেট্রিক নেই, তাকে হিসাবে সতর্কভাবে ধরা হয়।

৯ · Bin-packing আর খরচ — request ভুল হলে দাম কত

গুদামঘরে নানা মাপের বাক্স যত কম তাকে সম্ভব গোছানোর সমস্যাটিই bin-packing. Scheduler-এর কাজও তাই: নানা মাপের Pod যত কম node-এ সম্ভব বসানো গেলে বিল কমে। তবে default LeastAllocated কৌশল Pod ছড়িয়ে দেয়, যাতে এক node মরলে ক্ষতি কম হয়। উল্টো MostAllocated কৌশল Pod ঠাসে, ফলে কিছু node ফাঁকা হয়ে ফেরত দেওয়া যায়। প্রথমটি নিরাপত্তার দিকে, দ্বিতীয়টি খরচের দিকে। Cluster-ভেদে এটি scheduler-এর কনফিগারেশনে ঠিক হয়।

Node-এর বিল আসে পুরো VM-এর, আর VM বেছে নেওয়া হয় request-এর হিসাবে। তাই request-ই খরচের চাবি। একটি হিসাব দেখুন। ধরে নিই ৪ CPU-র node, ঘণ্টায় $০.২০, মাস ৭২০ ঘণ্টা, অর্থাৎ প্রতি node মাসে 0.20 × 720 = $144. ১০টি Pod আছে, প্রতিটি 1000m চায়, কিন্তু আসলে ব্যবহার করে 200m:

প্রতি Pod-এর requestমোট requestNodeমাসিক বিল
এখন1000m10000mceil(10000 ÷ 4000) = 3$432
মেপে ঠিক করার পরে260m (200m + ৩০%)2600mceil(2600 ÷ 4000) = 1, এক node বিকল হওয়ার জন্য আরও 1 = 2$288

একটি node কমল, মাসে $১৪৪ বাঁচল। শেষ সারিতে একটি বাড়তি node ইচ্ছা করে রাখা হয়েছে: শুধু গণিত মেনে ১টি node রাখলে সেটি নষ্ট হলেই সব বন্ধ।

Request খুব বেশিRequest খুব কম
Scheduler যা ভাবেNode ভরা, যদিও আসলে ফাঁকাNode খালি, যদিও আসলে ভরা
ফলটাকা নষ্ট, Pending Pod, অকারণে নতুন nodeEviction, CPU-র ভিড়ে ধীরগতি, OOM
HPA-র ওপর প্রভাবশতাংশ কম দেখায়, Pod দেরিতে বাড়েশতাংশ বেশি দেখায়, Pod অকারণে বাড়ে
চোখে পড়ে কখনমাসের শেষে বিলেগভীর রাতে incident-এ

সিমুলেটর ৪ — Request বনাম ব্যবহারের অপচয় রিপোর্ট

চারটি কাল্পনিক app. দুই সপ্তাহের p95 ব্যবহার (১০০টি মাপের ৯৫টি যার নিচে থাকে) ধরে নতুন request হিসাব করা হলো: p95-এর ওপর ৩০% headroom. দামগুলো ধরে নেওয়া গোল সংখ্যা, কোনো provider-এর আসল দাম নয়। আসল হিসাবে provider-এর pricing পাতা দেখুন।

// Requests vs usage waste report. Prices are ASSUMED round numbers.
const PRICE_CPU = 0.04;   // $ per vCPU-hour (assumed)
const PRICE_GIB = 0.005;  // $ per GiB-hour (assumed)
const HOURS = 720;        // 30-day month
const HEADROOM = 1.3;     // keep 30% above p95
const APPS = [ // cpu in millicores, mem in MiB, p95 = measured over two weeks
  { name: "web",    pods: 6, cpu: 1000, p95Cpu: 180, mem: 1024, p95Mem: 300 },
  { name: "api",    pods: 4, cpu: 500,  p95Cpu: 320, mem: 512,  p95Mem: 400 },
  { name: "worker", pods: 3, cpu: 250,  p95Cpu: 240, mem: 256,  p95Mem: 310 },
  { name: "report", pods: 2, cpu: 2000, p95Cpu: 150, mem: 4096, p95Mem: 900 },
];
const up = (x, step) => Math.ceil(x / step) * step;
const cost = (m, mi, pods) =>
  (m / 1000 * PRICE_CPU + mi / 1024 * PRICE_GIB) * pods * HOURS;

let nowTotal = 0, newTotal = 0;
console.log("app      cpu req>new  mem req>new  $/mo now>new  note");
for (const a of APPS) {
  const newCpu = up(a.p95Cpu * HEADROOM, 10), newMem = up(a.p95Mem * HEADROOM, 16);
  const now = cost(a.cpu, a.mem, a.pods), next = cost(newCpu, newMem, a.pods);
  nowTotal += now; newTotal += next;
  const note = a.p95Mem > a.mem ? "RISK: usage above memory request"
    : a.p95Cpu > 0.9 * a.cpu ? "RISK: CPU near request"
    : a.cpu > 2 * newCpu ? "WASTE" : "ok";
  console.log(a.name.padEnd(8), `${a.cpu}>${newCpu}`.padEnd(10), `${a.mem}>${newMem}`.padEnd(12),
    `${now.toFixed(0)}>${next.toFixed(0)}`.padEnd(13), note);
}
const saved = nowTotal - newTotal;
console.log(`\ntotal now $${nowTotal.toFixed(0)}/mo, after rightsizing $${newTotal.toFixed(0)}/mo`);
console.log(`saving $${saved.toFixed(0)}/mo (${(saved / nowTotal * 100).toFixed(0)}%)`);

শেষ দুই লাইন: total now $428/mo, after rightsizing $158/mo আর saving $270/mo (63%). কিন্তু আসল শিক্ষা worker-এর সারিতে: RISK: usage above memory request. তার ব্যবহার 310 MiB, request 256 MiB, তাই memory-র চাপে সে আগে সরবে। সেখানে ঠিক করার মানে খরচ বাড়ানো, $24 থেকে $32. Rightsizing মানে শুধু কমানো নয়, ঠিক মাপে আনা। web আর report সবচেয়ে বেশি নষ্ট করছিল: request-এর বড় অংশই খালি পড়ে ছিল। report মাসে একবার চলে এমন কাজ হলে p95 ভুল ছবি দিতে পারে, তাই সংখ্যা বসানোর আগে app-এর চরিত্র দেখুন। Memory-র জন্য p95-এর বদলে সর্বোচ্চ ব্যবহার দেখা নিরাপদ, কারণ memory ছাড়ালে Pod মরে।

পরীক্ষা ও ইন্টারভিউতে:

  • Request আর limit-এর পার্থক্য? Request দিয়ে scheduler জায়গা ঠিক করে, limit চলার সময়ের সর্বোচ্চ সীমা।
  • CPU আর memory limit পার হলে কী হয়? CPU-তে throttling, memory-তে OOMKilled, exit 137.
  • তিনটি QoS class-এর শর্ত? Guaranteed: সব request = limit. Burstable: কিছু আছে। BestEffort: কিছুই নেই।
  • Node-এ ব্যবহার কম, তবু Pod Pending কেন? Scheduler ব্যবহার নয়, বুক করা request দেখে।
  • HPA-র সূত্র আর কেন Pod সঙ্গে সঙ্গে কমে না? ceil(current × metric ÷ target); stabilization window, default ৩০০ সেকেন্ড।

১০ · AI-এর যুগে resource আর autoscaling

AI খুব দ্রুত Deployment আর HPA-র YAML লিখে দেয়। কিন্তু request-এর সংখ্যা সে আপনার app মেপে লেখে না, অনুমান করে। YAML apply হয়ে যায়, ভুল ধরা পড়ে পরে, বিলে বা incident-এ।

AI-এর ভুলফলযাচাই
Request না মেপে গোল সংখ্যা (1 CPU, 1Gi)টাকা নষ্ট বা evictionkubectl top আর metric-এর ইতিহাস
Request ছাড়া HPAUtilization হিসাব হয় নাkubectl describe hpa-এর condition
autoscaling/v2beta2-র মতো সরানো APIApply ব্যর্থkubectl api-resources, --dry-run=server
Production-এ --kubelet-insecure-tlsCertificate যাচাই বন্ধFlag-এর মানে পড়া, certificate ঠিক করা
Key ছাড়া tolerations: - operator: ExistsPod সব taint উপেক্ষা করেনির্দিষ্ট key আর effect লেখা
সব Pod-এ উচ্চ priorityঅন্যের Pod সরে যেতে পারেPriorityClass-এর নীতি দেখা
“Pending ঠিক করতে” নির্দেশ ছাড়াই kubectl delete pod --allচলমান কাজ বন্ধআগে event পড়া, namespace দেখা
image: app:latestকোন version চলছে জানা যায় নানির্দিষ্ট tag বা digest

AI-কে আন্দাজ করতে না দিয়ে আপনার মাপা তথ্য দিন, আর তার যুক্তিও চান:

আমার Kubernetes ১.৩৪ cluster-এ একটি API Deployment আছে। গত দুই সপ্তাহের ব্যবহার: CPU p95 180m, CPU peak 400m, memory p95 300Mi, memory peak 380Mi. এখন request cpu 1, memory 1Gi; ৪টি replica. HPA লক্ষ্য 60% CPU. ১. নতুন request আর limit কত হওয়া উচিত? প্রতিটি সংখ্যার হিসাব দেখাও। ২. Request বদলালে একই HPA লক্ষ্যে replica-র সংখ্যা কীভাবে বদলাবে? সূত্রে দেখাও। ৩. autoscaling/v2 ছাড়া অন্য কোনো API version ব্যবহার করো না। YAML-এ latest tag দিও না, আর কোনো অজানা flag বসিও না। ৪. আগে kubectl apply --dry-run=server চালাতে বলো। কোন ঝুঁকি আছে, আর ফেরার পথ কী, সেটিও লেখো।

AI-এর YAML হাতে পেলে — যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।

প্র ০১CPU limit কি আদৌ দেওয়া উচিত? অনেক দল কেন শুধু request দিয়ে চালায়?

এটি Kubernetes জগতের একটি পুরোনো বিতর্ক, আর এক কথায় উত্তর নেই। আগে সমস্যাটি বুঝুন। CPU limit ভিড় না থাকলেও কাজ করে। উপরের আসল run-এ node প্রায় ফাঁকা থাকলেও 100m limit-এর Pod ৫ সেকেন্ডে ৪.৪৭ সেকেন্ড আটকে ছিল। অন্যদিকে request ভিড়ের সময় ন্যায্য ভাগ দেয়, ভিড় না থাকলে বাধা দেয় না।

Limit-এর বিরুদ্ধে যুক্তি: ১০০ মিলিসেকেন্ডের period-এ কোটা শেষ হলে বাকি সময় থেমে থাকতে হয়। অনেক thread-ওয়ালা app কয়েক মিলিসেকেন্ডেই পুরো কোটা শেষ করতে পারে, তারপর প্রায় পুরো period চুপ। গড় ব্যবহার কম দেখালেও latency-র লেজ (p99) লম্বা হয়। Linux kernel-এ একসময় CFS quota-র একটি ত্রুটিও অকারণ throttling বাড়াত, যা ২০১৯ সালের দিকে সংশোধন করা হয়। তাই অনেক দল latency-সংবেদনশীল service-এ CPU request রাখে, limit রাখে না।

Limit-এর পক্ষে যুক্তি: বহু-দলের cluster-এ একজনের ভুল লুপ অন্যদের বিরক্ত করতে পারে, যদিও request-এর ওজন অন্যদের ন্যূনতম ভাগ রক্ষা করে। আবার limit থাকলে পরীক্ষার সময় আর production-এর আচরণ কাছাকাছি হয়, কারণ app কখনো ফাঁকা node-এর বাড়তি CPU-তে অভ্যস্ত হয় না। Guaranteed QoS পেতেও CPU limit লাগে, আর ব্যাচের কাজে অনুমানযোগ্য খরচ চাইলে limit কাজে আসে।

Memory-র ক্ষেত্রে বিতর্ক কম। Memory ফেরত নেওয়া যায় না, তাই limit প্রায় সবাই রাখেন। অনেকে request আর limit সমানও রাখেন, যাতে node-এ overcommit না হয়।

মূল উপলব্ধি: নিয়ম মুখস্থ না করে আপনার nr_throttled আর latency মাপুন। CPU limit একটি নীতিগত সিদ্ধান্ত, আর তার দাম throttling দিয়ে শোধ করতে হয়।

প্র ০২Scheduler আসল ব্যবহার না দেখে request দেখে কেন? ব্যবহার দেখলে কি node আরও ভালো ভরত না?

প্রথম দেখায় ব্যবহার দেখাই বুদ্ধিমানের মনে হয়। কিন্তু ব্যবহারের সংখ্যাটি একটি মুহূর্তের ছবি। রাত ৩টায় ফাঁকা একটি API সকাল ১০টায় পাঁচ গুণ CPU চাইতে পারে। শুধু এখনকার ব্যবহার দেখে Pod বসালে দিনের ব্যস্ত সময়ে একই node-এ সবাই একসাথে জেগে উঠবে। Request হলো আগামীর প্রতিশ্রুতি: “এটুকু আমার জন্য রাখুন।” Scheduler সেই প্রতিশ্রুতি যোগ করে, তাই তার সিদ্ধান্ত অনুমানযোগ্য, আর একই অবস্থায় একই ফল দেয়।

দ্বিতীয় কারণ ব্যবস্থাগত। Borg paper-এর (২০১৫) অভিজ্ঞতায় দেখা গেছে, সুরক্ষিত বরাদ্দ আর বাড়তি ব্যবহারকে আলাদা করলে দুই রকম কাজই একসাথে চলতে পারে। Production-এর কাজ তার বুকিং পায়, আর অলস অংশে কম-অগ্রাধিকারের batch কাজ চলে। Kubernetes-এর Burstable আর BestEffort সেই ভাবনারই সরল রূপ।

দাম কী? মানুষ নিরাপদ থাকতে বাড়িয়ে লেখেন, আর cluster-এ বুক করা অংশের অনেকটাই খালি থাকে। এটি ঠিক করার পথ দুটি। একটি request-কে আসল ব্যবহারের কাছে আনা: VPA বা Google-এর Autopilot paper-এর (২০২০) মতো পদ্ধতি। আরেকটি হলো scheduler-কে ব্যবহারের তথ্য জানানো: কিছু community scheduler plugin ঠিক এটি করে, যাদের Kubernetes-এর মূল scheduler-এ default হিসেবে ধরা হয় না।

আরও একটি সূক্ষ্মতা আছে। Pod বসানোর পরে scheduler আর ফিরে দেখে না। সময়ের সাথে cluster এলোমেলো হলে আবার গোছাতে হয়, যা descheduler নামের আলাদা project করে।

মূল উপলব্ধি: Request একটি চুক্তি। চুক্তি ভুল হলে হিসাব ভুল হয়, কিন্তু চুক্তি না থাকলে কোনো নিশ্চয়তাই থাকে না।

প্র ০৩CPU শতাংশ দেখে HPA চালানো কি সব app-এ ঠিক? কখন queue length বা request rate ভালো মেট্রিক?

CPU-ভিত্তিক HPA সহজ, কারণ metrics-server থেকেই মেট্রিক আসে। কিন্তু CPU তখনই ভালো মেট্রিক, যখন কাজ বাড়লে CPU-ও একই অনুপাতে বাড়ে। অনেক app-এ তা হয় না।

একটি API যদি বেশিরভাগ সময় database-এর উত্তরের অপেক্ষায় থাকে, তবে লোড বাড়লেও CPU কম থাকে, আর ব্যবহারকারী ধীরগতি দেখেন। HPA কিছুই করবে না। আবার queue থেকে কাজ নেওয়া worker-এর ক্ষেত্রে আসল প্রশ্ন “কতগুলো কাজ জমে আছে”। Queue-তে ১০ লাখ কাজ থাকলেও worker-এর CPU স্থির থাকতে পারে, কারণ প্রতিটি worker তার সর্বোচ্চ গতিতে চলছে। এখানে queue-এর দৈর্ঘ্য সরাসরি কারণ, CPU শুধু পরোক্ষ লক্ষণ।

Memory-ও ভালো মেট্রিক নয়। JVM বা cache-এর মতো অনেক runtime memory নিলে ফেরত দেয় না, তাই Pod বাড়লেও গড় নামে না, আর HPA শুধু বাড়াতেই থাকে। ফলে লোড চলে যাওয়ার পরেও Pod আর নামে না, আর বিল উঁচুতেই থেকে যায়।

এজন্য HPA autoscaling/v2-তে custom আর external মেট্রিক নিতে পারে, যা আসে Prometheus adapter-এর মতো উপাদান থেকে। Queue-ভিত্তিক কাজে KEDA নামের CNCF project জনপ্রিয়, যা শূন্য Pod পর্যন্ত নামাতে পারে। সাধারণ ব্যবস্থায় HPA নিজে ১-এর নিচে নামে না।

একটি বিপদও আছে। Latency-র মতো মেট্রিক দিয়ে scale করলে সমস্যা database-এ হলে Pod বাড়িয়ে উল্টো database-এর ওপর চাপ বাড়তে পারে। মেট্রিকটি এমন হওয়া দরকার, যা Pod বাড়ালে সত্যিই কমে। Request rate প্রতি Pod সাধারণত এই শর্ত মানে, latency প্রায়ই মানে না।

মূল উপলব্ধি: HPA-র সূত্র ধরে নেয় মেট্রিক Pod-সংখ্যার ব্যস্তানুপাতিক। যে মেট্রিকে এই সম্পর্ক নেই, তাকে সূত্রে বসালে সূত্র নয়, সিদ্ধান্ত ভুল হবে।

প্র ০৪Guaranteed Pod কি সত্যিই “নিরাপদ”? কখন সেও মারা যেতে পারে?

নামটি ভরসা দেয়, কিন্তু Guaranteed মানে অমরত্ব নয়। এটি কিছু নির্দিষ্ট জিনিসের নিশ্চয়তা: request পরিমাণ resource তার জন্য বুক করা, আর চাপের সময় সে সবার শেষে সরবে।

প্রথম ঝুঁকি নিজের limit. Guaranteed Pod-এর request আর limit সমান, তাই একটু বেশি memory নিলেই OOMKilled. Burstable Pod যেখানে বাড়তি জায়গায় টিকে যেত, Guaranteed সেখানে আগেই মরতে পারে। অর্থাৎ eviction-এর ঝুঁকি কমে, কিন্তু নিজের সীমায় মরার ঝুঁকি বাড়ে। উপরের আউটপুটে oom_score_adj ছিল -997, কিন্তু সেটি node-এর সার্বিক memory সংকটের জন্য। Cgroup limit ছুঁলে মরার সিদ্ধান্ত সেই container-এর ভেতরেই হয়, তাই এই সংখ্যা তাকে বাঁচায় না।

দ্বিতীয়ত, preemption-এ QoS class দেখা হয় না, priority দেখা হয়। একটি Guaranteed Pod-এর priority কম হলে উচ্চ priority-র BestEffort Pod-ও তাকে সরাতে পারে। QoS আর priority দুটি আলাদা অক্ষ। প্রথমটি বলে চাপের সময় node কাকে আগে ছাড়বে, দ্বিতীয়টি বলে জায়গার লড়াইয়ে কার দাবি বড়। একটি ঠিক থাকলেই অন্যটি ঠিক হয়ে যায় না, তাই দুটিই আলাদা করে ভাবতে হয়।

তৃতীয়ত, node-এর জীবনের ঝুঁকি: VM বন্ধ, kernel panic, hardware নষ্ট, কিংবা spot VM ফেরত নেওয়া। Node drain বা আপগ্রেডের সময়েও Pod সরতে হয়। সেখানে PodDisruptionBudget সাহায্য করে, তবে এটি শুধু API-র মাধ্যমে করা স্বেচ্ছা-সরানোর জন্য। Kubelet-এর node-pressure eviction এটি মানে না, Kubernetes document তাই বলে।

চতুর্থত, node-এ disk ভরে গেলে (DiskPressure) ব্যবহার আর priority ধরে Pod সরে। Log বা emptyDir দিয়ে disk ভরিয়ে ফেলা Pod এখানে বিপদে পড়ে, যদিও CPU আর memory নিয়ে সে নির্দোষ।

মূল উপলব্ধি: QoS class কমায় ঝুঁকি, মুছে দেয় না। নির্ভরযোগ্যতা আসে একাধিক replica, spread আর PodDisruptionBudget থেকে, একটি Pod-কে অমর বানিয়ে নয়।

প্র ০৫Node ঠেসে ভরলে খরচ কমে, ছড়িয়ে রাখলে নিরাপত্তা বাড়ে। এই টানাপোড়েনের মীমাংসা কীভাবে হয়?

Bin-packing গণিতে NP-hard সমস্যা, অর্থাৎ বড় আকারে সর্বোত্তম উত্তর দ্রুত বের করার জানা পদ্ধতি নেই। তাই scheduler প্রতিটি Pod-এর জন্য একটি লোভী (greedy) সিদ্ধান্ত নেয়, আর যথেষ্ট ভালো ফলেই সন্তুষ্ট থাকে। অংশ ৬-এর সিমুলেটরেই দেখেছেন, Pod আসার ক্রম ফল বদলে দেয়।

আসল টানাপোড়েন গণিতের নয়, ব্যবসার। ৯০% ভরা node-এ বিল কম, কিন্তু একটি node মরলে তার Pod-গুলো বসার জায়গা পায় না। হঠাৎ ভিড় এলে HPA Pod বানায়, আর সেগুলো Pending থেকে নতুন node-এর অপেক্ষায় মিনিটের পর মিনিট কাটায়। আবার ৪০% ভরা cluster নিরাপদ, কিন্তু বিলের বড় অংশ খালি জায়গার।

বাস্তবে দলগুলো মাঝামাঝি পথ নেয়। এক: N+1 হিসাব, যাতে একটি node বা একটি zone হারালেও বাকিরা ভার নিতে পারে। দুই: খালি জায়গা ইচ্ছা করে রাখা। অনেকে কম priority-র “বসে থাকা” Pod চালান, যা জরুরি Pod এলে preempt হয়ে সঙ্গে সঙ্গে জায়গা ছাড়ে, আর ততক্ষণে নতুন node আসে। তিন: কাজ ভাগ করা। Stateless আর পুনরায় চালানো যায় এমন কাজ spot node-এ, আর জরুরি কাজ on-demand node-এ, যা taint আর affinity দিয়ে করা যায়। চার: Karpenter বা Cluster Autoscaler-এর consolidation, যা PodDisruptionBudget মেনে ধীরে ধীরে গোছায়।

মেপে দেখার মতো সংখ্যা হলো request-এর তুলনায় ব্যবহার (efficiency), node-এর request-ভরাট আর Pending থাকার সময়। প্রথম দুটি খরচের ছবি দেয়, তৃতীয়টি নিরাপত্তার। দুটি একসাথে না দেখলে এক দিকে উন্নতি আরেক দিকে ক্ষতি লুকিয়ে রাখে। পর্ব ৭৩ · FinOps-এ এই হিসাব আরও বিস্তারিত।

মূল উপলব্ধি: সেরা ভরাটের হার বলে কিছু নেই। ঠিক কতটা খালি রাখবেন, সেটি আপনার SLO আর বাজেটের মধ্যে একটি সচেতন চুক্তি।

অনুশীলন

হাতে হিসাব করুন, তারপর উত্তর মেলান। নিজের cluster-এ চেষ্টা করতে চাইলে শুধু স্থানীয় cluster ব্যবহার করুন; cloud cluster-এ খরচ হতে পারে।

  1. HPA-র হিসাব: লক্ষ্য 70%, min 2, max 12, tolerance 0.1. (ক) 6 Pod, ব্যবহার 105%. (খ) 8 Pod, ব্যবহার 66%. (গ) 10 Pod, ব্যবহার 21%. (ঘ) 9 Pod, ব্যবহার 140%. স্থিরতার window বাদ দিয়ে প্রতিটির ফল বলুন।

    (ক) অনুপাত 105 ÷ 70 = 1.50; ceil(6 × 1.50) = 9.

    (খ) অনুপাত 66 ÷ 70 ≈ 0.94, যা 0.9 থেকে 1.1-এর ভেতরে। 8-ই থাকবে।

    (গ) অনুপাত 0.30; ceil(10 × 0.30) = 3, যা min-এর ওপরে, তাই 3. Window থাকলে সঙ্গে সঙ্গে নামত না।

    (ঘ) অনুপাত 2.00; ceil(9 × 2.00) = 18, কিন্তু max 12, তাই 12, আর ScalingLimited সত্য হবে।

  2. ফল আন্দাজ করুন: একটি namespace-এ LimitRange আছে: defaultRequest cpu 50m, memory 32Mi; default cpu 100m, memory 64Mi; max cpu 500m. ResourceQuota: requests.cpu: 120m. পরপর তিনটি Pod apply করা হলো: (১) resources ছাড়া, (২) resources ছাড়া, (৩) resources ছাড়া। প্রতিটির ফল আর QoS class কী? প্রথম Pod-এর request আর limit কত হবে?

    প্রথম Pod-এ default বসবে: request 50m ও 32Mi, limit 100m ও 64Mi. QoS class Burstable, কারণ request আর limit সমান নয়। Quota-তে ব্যবহৃত 50m.

    দ্বিতীয় Pod-ও একই, ব্যবহৃত 100m.

    তৃতীয় Pod-এর জন্য 100 + 50 = 150m, যা 120m ছাড়ায়। তাই Forbidden: exceeded quota. Pod তৈরিই হবে না, তাই Pending-ও দেখা যাবে না।

  3. Manifest সমালোচনা: একটি Deployment: ২টি replica, image: shop-api:latest, কোনো resources নেই, tolerations: [{operator: Exists}], সাথে HPA লক্ষ্য 60% CPU, minReplicas: 1. কী কী সমস্যা, আর কীভাবে ঠিক করবেন?
    • Request নেই: HPA শতাংশ হিসাব করতে পারবে না। Pod হবে BestEffort, চাপে আগে সরবে। সমাধান: মেপে request, আর memory limit.
    • latest: rollback আর যাচাই কঠিন। নির্দিষ্ট tag বা digest দিন।
    • Key ছাড়া Exists: Pod সব taint উপেক্ষা করে, GPU বা মেরামতের অধীন node-এও ঢুকতে পারে। নির্দিষ্ট toleration লিখুন, দরকার না থাকলে মুছে দিন।
    • minReplicas: 1: ভিড় না থাকলে এক Pod, আর একটি node মরলে service বন্ধ। Min অন্তত 2 আর spread বা anti-affinity যোগ করুন।
    • spec.replicas হাতে লেখা: HPA-র সাথে deploy-এর সময় মান টানাটানি হতে পারে। HPA ব্যবহার করলে replica ঘর manifest থেকে বাদ দেওয়াই ভালো।
  4. নিজের চোখে দেখুন (ঐচ্ছিক): স্থানীয় cluster-এ nginx:1.27-alpine দিয়ে তিনটি Pod বানান: একটিতে request = limit, একটিতে request < limit, একটিতে কিছুই নয়। .status.qosClass দেখুন। তারপর memory: 2Ti request করে একটি Pod বানিয়ে event পড়ুন। শেষে পরিষ্কার করুন।

    প্রথম তিনটি হবে Guaranteed, Burstable, BestEffort. চতুর্থটি Pending, আর event-এ থাকবে Insufficient memory. শুধু memory চাওয়া হয়েছে বলে এখানে Insufficient cpu থাকবে না।

    # নিজের বানানো Pod-গুলোই নাম ধরে মুছুন
    kubectl delete pod qos-a qos-b qos-c too-big

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমCluster Autoscaler-এর ক্ষেত্রে ঘটনাগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. Pod তৈরি হলো, 2000m request করেছে
  2. Scheduler-এর filter-এ সব node বাদ, Pod Pending
  3. Autoscaler হিসাব করে node group-এর আকার বাড়াল
  4. নতুন VM চালু হলো, kubelet যোগ দিল, node Ready
  5. Scheduler Pod-টি নতুন node-এ বসাল

Kubernetes-এ একটি Pod কোন node-এ বসবে, তা ঠিক করার সময় scheduler কোনটি যোগ করে হিসাব করে?

  • Container-গুলোর limit
  • Container-গুলোর request
  • Container-গুলোর এই মুহূর্তের আসল ব্যবহার
  • Container image-এর আকার

একটি container-এর CPU limit 100m আর memory limit 32Mi. App দুটোই ছাড়াতে চাইলে সাধারণত কী হয়?

  • দুই ক্ষেত্রেই container মরে
  • CPU ছাড়ালে OOMKilled, memory ছাড়ালে ধীর
  • CPU-তে throttling হয়, memory-তে OOMKilled
  • দুই ক্ষেত্রেই শুধু event-এ সতর্কতা আসে

একটি Pod-এর একমাত্র container-এ requests: {cpu: 100m, memory: 64Mi} আর limits: {memory: 64Mi} আছে। তার QoS class কী?

  • Burstable
  • Guaranteed
  • BestEffort
  • কোনো class পাবে না

HPA-তে লক্ষ্য 50%, এখন 4 Pod, গড় ব্যবহার 90%, maxReplicas 10. Tolerance আর policy ছাড়া সূত্রে পরের Pod-সংখ্যা কত?

  • 7
  • 6
  • 10
  • 8

একটি node-এর memory কমে গেছে, আর kubelet Pod সরাতে চায়। সব Pod-এর priority সমান। কোন Pod সবচেয়ে আগে সরার সম্ভাবনা বেশি?

  • request = limit, আর ব্যবহার request-এর সমান এমন Pod
  • request ছাড়া একটি Pod, যে 900 MiB ব্যবহার করছে
  • request 512 MiB, ব্যবহার 400 MiB এমন Pod
  • যে Pod সবচেয়ে পুরোনো

GPU node-এ সাধারণ Pod ঢোকা বন্ধ করতে চান, আর শুধু GPU-র কাজ সেখানে যাক। কোন জোড়াটি এই কাজের সরাসরি উপায়?

  • LimitRange আর ResourceQuota
  • Pod anti-affinity আর topology spread
  • Node-এ taint, GPU-র Pod-এ toleration আর nodeSelector
  • HPA আর PriorityClass

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. Request ঠিক করে Pod কোথায় বসবে, limit ঠিক করে চলার সময় কতটা পাবে। Scheduler ব্যবহার দেখে না।
  2. CPU limit পার হলে throttling, memory limit পার হলে OOMKilled, exit 137.
  3. Guaranteed, Burstable, BestEffort নিজে থেকে হিসাব হয়। Request ছাড়ানো Pod memory-র চাপে আগে সরে।
  4. Pending দেখলে আগে event পড়ুন। Filter কারণ বলে দেয়: resource, label, taint বা anti-affinity.
  5. HPA: ceil(current × metric ÷ target), সাথে tolerance আর stabilization window. Request ঠিক না হলে HPA, node আর বিল, তিনটিই ভুল হয়।

পরের পর্বে: Pod কোথায় চলবে আর কতটা পাবে, তা জানলেন। এবার প্রশ্ন, কে কী করতে পারবে? পর্ব ৪৯-এ দেখব RBAC, NetworkPolicy আর Pod Security.

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান