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

Pod — container-এর দল, lifecycle আর probe

Kubernetes Pods — shared network, lifecycle, failures, probes and graceful termination

Taught in Bangla, this lesson explains why Kubernetes runs Pods rather than containers, shared networking, init and sidecar containers, phases, container states and restart policy, four real failures including CrashLoopBackOff, liveness, readiness and startup probes, graceful termination with preStop, SIGTERM and SIGKILL, and a debugging order using describe, logs, exec and kubectl debug.

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

  • Kubernetes-এর ছোট একক container না হয়ে Pod কেন, আর দুই container কীভাবে localhost-এ কথা বলে
  • Init container, sidecar আর নতুন native sidecar-এর পার্থক্য
  • Phase, container state আর restart policy আলাদা করে পড়া
  • চারটি আসল ব্যর্থতা দেখে কারণ বলা, আর CrashLoopBackOff-এর বিরতি হিসাব করা
  • Probe-এর সংখ্যা ঠিক করা, আর ভুল liveness probe থেকে production বাঁচানো
  • Pod মোছার সময় কী ঘটে, আর কীভাবে একটি অনুরোধও না হারিয়ে থামতে হয়

এই পর্বের transcript সম্পর্কে: নিচের সব kubectl আউটপুট একটি আসল run থেকে নেওয়া: এক node-এর local cluster, Kubernetes ১.৩৪. নাম, age, IP আর সময় আপনার মেশিনে আলাদা হবে। বাদ দেওয়া অংশে … আছে, আর describe-এর Events থেকে Age কলাম সরানো হয়েছে। Pod-এর YAML server-side dry-run পাস করেছে। এক node-এ যা দেখানো যায় না, যেমন phase Unknown, তা স্পষ্ট করে বলা আছে।

নিজে চালাতে চাইলে: সব পরীক্ষা নিজের মেশিনে kind, minikube বা Docker Desktop-এর Kubernetes-এ চলে, এতে cloud-এর বিল আসে না। Cloud-এর managed cluster-এ চালালে খরচ হতে পারে। শেষে নাম ধরে মুছুন: kubectl delete pod duo. অচেনা namespace-এ --all দিয়ে কিছু মুছবেন না।

১ · Pod কেন — container নয় কেন

পর্ব ৪২-এ দেখেছেন, Kubernetes একটি reconciliation loop: আপনি যা চান আর যা আছে, দুটো মিলিয়ে চলে। এই পর্বে প্রশ্ন একটাই: Kubernetes ঠিক কোন জিনিসটি চালায়? উত্তর container নয়, Pod.

মেসের একটি রুম ভাবুন। রুমে দুজন থাকেন, প্রত্যেকের আলাদা ট্রাঙ্ক আর আলাদা কাজ। কিন্তু দরজার নম্বর একটাই, ঠিকানাও একটাই, মাঝখানের টেবিলটাও দুজনের। চিঠি এলে রুম নম্বরে আসে। একজন আরেকজনকে ডাকতে রাস্তায় বেরোতে হয় না, পাশ ফিরে ডাকলেই হয়। Pod হলো সেই রুম, container হলেন রুমমেট, IP হলো দরজার নম্বর, localhost হলো পাশ ফিরে ডাকা, আর volume হলো টেবিল। আর রুম ছাড়লে দুজনকে একসাথেই ছাড়তে হয়।

কেন এই বাড়তি স্তর? কারণ কিছু কাজ সত্যিই জোড়ায় বাঁচে। একটি web server আর তার log পাঠানোর helper একই মেশিনে থাকতে হয়, একই ফাইল পড়তে হয়। এদের দুই node-এ বসানো চলে না। তাই Kubernetes-এর নিয়ম:

  • Pod হলো schedule-এর একক। kube-scheduler পুরো Pod-কে একটি node-এ বসায়। Pod-এর container কখনো দুই node-এ ভাগ হয় না।
  • Pod হলো network-এর একক। প্রতিটি Pod একটি IP পায়। ভেতরের container-গুলো সেই IP আর port-এর জায়গা ভাগ করে।
  • Pod হলো জীবনের একক। Container restart হতে পারে, কিন্তু Pod মোছা মানে ভেতরের সব container শেষ।

বেশিরভাগ Pod-এ container থাকে একটিই। তবু Pod-এর আবরণ থাকে, যাতে Kubernetes সব workload একভাবে সামলাতে পারে। একটি ভুল ধারণা এখনই সরিয়ে নিন: ওজন ভাগ করতে তিনটি web container এক Pod-এ রাখা হয় না। সেজন্য তিনটি আলাদা Pod লাগে। এক Pod-এ শুধু তারা থাকে, যারা একসাথে বাঁচে আর একসাথে মরে।

একটি Pod-এ দুই container

Shared network namespace and a shared emptyDir volume

Pod duo: দুই container, ভাগ করা network আর volume Pod: duo · IP 10.1.0.44 ভাগ করা network namespace একই IP, একই port-এর জায়গা, একই localhost ধরে রাখে: pause container container: web nginx শোনে port 80-এ নিজের image নিজের memory limit container: writer alpine ফাইল লেখে নিজের image নিজের memory limit localhost:80 ভাগ করা volume: shared (emptyDir) web দেখে /usr/share/nginx/html writer দেখে /data · একই ফাইল Container restart হলে IP আর emptyDir থাকে Pod মুছলে দুটোই শেষ
ছবি ৪৩.১ — একটি Pod-এ network আর volume ভাগ করা, কিন্তু image, process আর resource limit আলাদা।

ভাগ করা network কীভাবে হয়? Linux-এ container মানে namespace আর cgroup দিয়ে ঘেরা process, যা Operating Systems কোর্সের containers পর্বে দেখেছেন। Pod তৈরির সময় kubelet আগে একটি খুব ছোট pause container চালায়, যার কাজ শুধু network namespace ধরে রাখা। App container-গুলো সেই namespace-এ ঢোকে। তাই app restart হলেও Pod-এর IP বদলায় না। এই cluster-এ Docker দিয়ে দেখলাম: web container-এর network mode container:…, আর সেই container-এর image registry.k8s.io/pause:3.10. অন্য runtime-এ pause দেখার উপায় আলাদা, আর নতুন কিছু runtime sandbox অন্যভাবেও বানাতে পারে।

২ · Pod YAML আর দুই container-এর আলাপ

এবার রুমটি বানাই। নিচের YAML-এ দুই container: web ফাইল পরিবেশন করে, writer প্রতি ৫ সেকেন্ডে সেই ফাইল নতুন করে লেখে। ভাগ করা জিনিস একটি emptyDir volume.

apiVersion: v1
kind: Pod
metadata:
  name: duo
  labels:
    app: duo
spec:
  volumes:
    - name: shared
      emptyDir: {}
  containers:
    - name: web
      image: nginx:1.27-alpine
      imagePullPolicy: IfNotPresent
      ports:
        - containerPort: 80
      volumeMounts:
        - name: shared
          mountPath: /usr/share/nginx/html
      resources:
        requests: {cpu: 20m, memory: 32Mi}
        limits: {memory: 128Mi}
    - name: writer
      image: alpine:3.20
      imagePullPolicy: IfNotPresent
      command: ["sh", "-c"]
      args:
        - |
          n=0
          while true; do
            n=$((n + 1))
            echo "update $n from writer" > /data/index.html
            sleep 5
          done
      volumeMounts:
        - name: shared
          mountPath: /data
      resources:
        requests: {cpu: 10m, memory: 8Mi}
        limits: {memory: 32Mi}
Fieldমানে
apiVersion, kindকোন API-র কোন object. Pod আছে core group-এর v1-এ।
metadataনাম আর label. Label দিয়েই পরে Service Pod খুঁজে নেয় (পর্ব ৪৫)।
spec.volumesPod-স্তরের volume. Container শুধু volumeMounts দিয়ে নিজের পথে সেটি বসায়।
spec.containersContainer-এর তালিকা। প্রত্যেকের image আর resource আলাদা।
command, argsImage-এর ENTRYPOINT আর CMD-এর জায়গায় বসে।
resourcesrequests দেখে scheduler জায়গা খোঁজে, limits চলার সময়ের সীমা (পর্ব ৪৮)।

এবার kubectl দিয়ে চালিয়ে দুটো দাবি প্রমাণ করি: network এক, আর volume এক।

$ kubectl apply -f duo.yaml
pod/duo created
$ kubectl get pod duo
NAME   READY   STATUS    RESTARTS   AGE
duo    2/2     Running   0          1s

# writer has no web server, yet localhost:80 answers
$ kubectl exec duo -c writer -- wget -qO- http://localhost:80/
update 2 from writer

# both containers see one hostname and one IP
$ kubectl exec duo -c writer -- sh -c 'hostname; ip -4 addr show eth0 | grep inet'
duo
    inet 10.1.0.44/16 brd 10.1.255.255 scope global eth0
$ kubectl exec duo -c web -- sh -c 'hostname; ip -4 addr show eth0 | grep inet'
duo
    inet 10.1.0.44/16 brd 10.1.255.255 scope global eth0

# same file through a different path
$ kubectl exec duo -c web -- cat /usr/share/nginx/html/index.html
update 20 from writer
$ kubectl exec duo -c writer -- ls /data
index.html

# filesystems stay separate: nginx folder does not exist in writer
$ kubectl exec duo -c writer -- ls /usr/share/nginx
ls: /usr/share/nginx: No such file or directory
command terminated with exit code 1

writer-এ কোনো web server নেই, তবু localhost:80 উত্তর দিল, কারণ web একই network namespace-এ শুনছে। দুই container-এর hostname duo, IP 10.1.0.44. আর update 20 প্রমাণ করে একজনের লেখা ফাইল অন্যজন পড়ছে। শেষ লাইন দেখায় সীমাটা কোথায়: image-এর filesystem ভাগ হয়নি, ভাগ হয়েছে শুধু mount করা volume. 2/2 মানে দুটি container-এর দুটিই ready.

ভাগ করা port-এর জায়গার উল্টো দিকও আছে। একই Pod-এ দুটো nginx চালালে দুজনেই port 80 চাইবে:

$ kubectl get pod clash
NAME    READY   STATUS             RESTARTS   AGE
clash   1/2     CrashLoopBackOff   …
$ kubectl logs clash -c second | tail -3
nginx: [emerg] bind() to [::]:80 failed (98: Address in use)
…
nginx: [emerg] still could not bind()

first আগে port নিয়ে নিয়েছে, তাই second বারবার মরছে। মেসের রুমে দুজন একই খাটে শুতে চাইলে যা হয়। আলাদা Pod হলে এই সংঘর্ষ হতো না, কারণ প্রত্যেকের IP আলাদা।

৩ · Init container আর sidecar

এক Pod-এ একাধিক container রাখার দুটি পরিচিত নকশা আছে।

Init container

বিয়েবাড়ির ক্যাটারিংয়ে রান্নার আগে মসলা বাটা। App চালুর আগে একে একে চলে, শেষ হয়ে যায়। একটি ব্যর্থ হলে পরেরটি শুরু হয় না। প্রস্তুতি, ফাইল আনা, schema যাচাইয়ের কাজে লাগে।

spec.initContainers

Sidecar

মূল app-এর পাশে সারাক্ষণ চলা helper: log পাঠানো, TLS proxy, config টেনে আনা। Service mesh-ও এভাবেই proxy বসায়। মূল app-কে না ছুঁয়ে কাজ যোগ হয়।

spec.containers বা native sidecar

পুরোনো পথে sidecar ছিল containers তালিকার সাধারণ একটি container. সমস্যা বাধত শেষের দিকে। ধরুন একটি রিপোর্ট বানানোর কাজ ২ মিনিটে শেষ হলো, কিন্তু log পাঠানো sidecar চলছেই। Pod তাহলে কখনো শেষ হয় না। শুরুতেও ক্রম নিশ্চিত ছিল না।

সমাধান native sidecar: init container-এর তালিকায় লিখে restartPolicy: Always দেওয়া। এই container app-এর আগে চালু হয়, app চলার পুরো সময় থাকে, app শেষ হলে নিজেও থামে। Feature-টি ১.২৮-এ alpha হয়ে আসে, ১.২৯ থেকে beta হিসেবে default চালু, আর ১.৩৩-এ stable হয়েছে বলে Kubernetes-এর নথিতে লেখা। পুরোনো cluster-এ এই field কাজ নাও করতে পারে, তাই নিজের version দেখে নিন। আমাদের ১.৩৪ cluster-এ এটি চলল:

apiVersion: v1
kind: Pod
metadata:
  name: init-side
spec:
  restartPolicy: Never
  volumes:
    - name: logs
      emptyDir: {}
  initContainers:
    - name: check-config            # ordinary init: runs, then ends
      image: alpine:3.20
      imagePullPolicy: IfNotPresent
      command: ["sh", "-c", "echo checking config; sleep 3; echo config ok"]
      resources:
        requests: {cpu: 10m, memory: 8Mi}
        limits: {memory: 32Mi}
    - name: log-shipper             # native sidecar: keeps running
      image: alpine:3.20
      imagePullPolicy: IfNotPresent
      restartPolicy: Always
      command: ["sh", "-c"]
      args:
        - |
          touch /logs/app.log
          tail -F /logs/app.log &
          trap 'echo shipper stopping; exit 0' TERM
          wait
      volumeMounts:
        - name: logs
          mountPath: /logs
      resources:
        requests: {cpu: 10m, memory: 8Mi}
        limits: {memory: 32Mi}
  containers:
    - name: job
      image: alpine:3.20
      imagePullPolicy: IfNotPresent
      command: ["sh", "-c"]
      args:
        - |
          for i in 1 2 3; do
            echo "report part $i done" >> /logs/app.log
            sleep 2
          done
      volumeMounts:
        - name: logs
          mountPath: /logs
      resources:
        requests: {cpu: 10m, memory: 8Mi}
        limits: {memory: 32Mi}
$ kubectl get pod init-side         # repeated, one line per check
init-side   0/2   Init:0/2          0     3s
init-side   0/2   Init:1/2          0     4s
init-side   1/2   PodInitializing   0     6s
init-side   2/2   Running           0     7s
init-side   1/2   Completed         0     13s
init-side   0/2   Completed         0     20s

$ kubectl logs init-side -c log-shipper
report part 1 done
report part 2 done
report part 3 done
shipper stopping

# phase and exit times (trimmed)
Succeeded
job          exitCode 0   finishedAt 19:18:05
log-shipper  exitCode 0   finishedAt 19:18:06

Init:1/2 মানে দুটি init-এর একটি শেষ। Sidecar চালু হতেই init ধাপ এগোল, শেষ হওয়ার অপেক্ষা করল না। job ১৯:১৮:০৫-এ শেষ হলো, এক সেকেন্ড পরে sidecar SIGTERM পেয়ে থামল, আর Pod হলো Succeeded. পুরোনো নকশায় এই Pod শেষ হতো না। খেয়াল করুন READY 2/2: native sidecar-ও গোনা হয়।

৪ · Pod-এর জীবন — phase, container state আর restart policy

একটি Pod জন্মের পর কী কী পেরোয়? নিচের stepper একটি আসল Pod-এর সময় ধরে সাজানো, যার ভেতরে ৩ সেকেন্ডের init container আর readiness probe আছে। “পরে” চেপে এগোন।

  1. API serverPod জমা
  2. Schedulernode বাছাই
  3. kubeletsandbox
  4. Initprepare
  5. webচালু
  6. Readytraffic নিতে পারে
kubectl যা দেখায়
সময়
phase
STATUS
READY
নতুন condition
  1. জমা পড়ল। kubectl apply-এর পর API server Pod-টি etcd-তে রাখল। Pod-এর এখনো কোনো node নেই, তাই phase Pending.
  2. Node বাছাই। Scheduler জায়গা দেখে node ঠিক করল। Condition PodScheduled=True হলো। এই মুহূর্তকে আমরা +০ ধরছি। Container এখনো চালু হয়নি, তাই phase এখনো Pending.
  3. Sandbox. এক সেকেন্ডে kubelet pause container আর network সাজাল: PodReadyToStartContainers=True. Pod-এর IP এখন আছে।
  4. Init চলছে। prepare ৩ সেকেন্ড ঘুমাচ্ছে। STATUS কলাম দেখাচ্ছে Init:0/1, কিন্তু phase এখনো Pending: কোনো app container চালু হয়নি।
  5. App চালু। +৫ সেকেন্ডে Initialized=True. web চালু হলো, তাই phase Running. কিন্তু READY 0/1: readiness probe এখনো পাস করেনি। Running আর Ready এক কথা নয়।
  6. Ready. +৯ সেকেন্ডে প্রথম probe পাস করল (initialDelaySeconds: 4). ContainersReady আর Ready দুটোই True. এখন Service চাইলে এই Pod-এ traffic পাঠাতে পারে।

Phase বনাম container state

Phase হলো পুরো Pod-এর এক শব্দের সারাংশ, আর container state হলো প্রতিটি container-এর আলাদা অবস্থা। দুটো মিলিয়ে না পড়লে ভুল হয়। সবচেয়ে বড় ফাঁদ: kubectl get pods-এর STATUS কলাম phase নয়, বরং kubectl-এর বানানো একটি সারাংশ।

Phaseমানে
PendingPod গৃহীত, কিন্তু অন্তত একটি container এখনো চালু হয়নি: node পায়নি, image নামছে, init চলছে।
RunningNode ঠিক হয়েছে, container তৈরি হয়েছে, অন্তত একটি চলছে বা চালু বা restart হচ্ছে।
Succeededসব container exit 0 দিয়ে শেষ, আর restart হবে না।
Failedসব container শেষ, অন্তত একটি ব্যর্থ, আর restart হবে না।
UnknownPod-এর অবস্থা জানা যাচ্ছে না, সাধারণত node-এর সাথে যোগাযোগ বিচ্ছিন্ন। এক node-এর এই cluster-এ এটি দেখানো যায়নি।
Container stateমানেসাথে যা থাকে
Waitingএখনো চলছে না, কোনো কিছুর অপেক্ষায়reason, যেমন CrashLoopBackOff
RunningProcess চলছেstartedAt
Terminatedশেষ হয়েছেexitCode, reason, সময়

আসল run-এ STATUS আর phase পাশাপাশি রাখলে এই ফাঁক চোখে পড়ে:

Pod-এর ঘটনাSTATUS কলামআসল phase
Node নেই, CPU চাওয়া হয়েছে ৫০০টিPendingPending
Image নেইImagePullBackOffPending
বারবার crash, AlwaysCrashLoopBackOffRunning
exit 0, NeverCompletedSucceeded
exit 3, NeverErrorFailed

Monitoring-এ শুধু phase == Running দেখলে সতর্ক হোন। CrashLoopBackOff-এ পড়ে থাকা Pod-ও phase-এ Running. তাই alert বসান restart count আর Ready condition-এ।

Restart policy

Docker-এর restart policy পর্ব ৩৯-এ দেখেছেন। Pod-এ এটি spec.restartPolicy, পুরো Pod-এর জন্য একটি, default Always. Restart করে kubelet, একই node-এ, একই Pod-এর ভেতরে। Pod নতুন হয় না, IP-ও বদলায় না। তিনটি পরীক্ষার ফল (২৫ সেকেন্ড পরে):

# onfail-exit3: OnFailure, exit 3 · onfail-exit0: OnFailure, exit 0
# always-exit0: Always, exit 0
$ kubectl get pods onfail-exit3 onfail-exit0 always-exit0 \
    -o custom-columns=NAME:.metadata.name,PHASE:.status.phase,RESTARTS:.status.containerStatuses[0].restartCount
NAME           PHASE       RESTARTS
onfail-exit3   Running     2
onfail-exit0   Succeeded   0
always-exit0   Running     2

Always মানে সত্যিই সবসময়: always-exit0 সফলভাবে শেষ হয়েও restart হচ্ছে। এজন্যই একবার চলার কাজ Always দিয়ে চালালে CrashLoopBackOff আসে, যদিও কোনো ভুল নেই। OnFailure শুধু exit code শূন্য না হলে restart করে। Never কখনোই করে না। Deployment-এর Pod template-এ শুধু Always চলে, আর একবারের কাজের জন্য আছে Job.

৫ · চারটি আসল ব্যর্থতা

এবার ইচ্ছে করে Pod ভাঙি। ঈদের আগে বাসের কাউন্টার ভাবুন: কখনো যাত্রী বারবার ফিরে আসেন, কখনো গাড়িই আসেনি, কখনো ছাদে বেশি মাল তুলে গাড়ি থামানো হয়, কখনো টিকিটের কাগজই নেই। Pod-এর চার ব্যর্থতাও এমন আলাদা।

৫.১ CrashLoopBackOff — বারবার ফিরে আসা

নিচের container চালু হয়েই exit 1 দেয়, যেমন ডেটাবেসে যুক্ত হতে না পারা app. শিক্ষার জন্য এটি প্রতিবার শুরুর সময় একটি emptyDir-এ লিখে রাখে। Volume restart-এ টিকে থাকে, তাই পুরো ইতিহাস এক জায়গায় পাওয়া যায়।

# container command: date -u +%T >> /notes/starts; cat /notes/starts; exit 1
$ kubectl get pod crash2
NAME     READY   STATUS             RESTARTS      AGE
crash2   0/1     CrashLoopBackOff   2 (29s ago)   45s

$ kubectl logs crash2      # start times of every run
19:26:44
19:26:45
19:26:59
19:27:28
19:28:08
19:29:32
19:32:14
19:37:17

# waiting message from the same kind of Pod, in order
back-off 10s restarting failed container=app …
back-off 20s restarting failed container=app …
back-off 40s restarting failed container=app …
back-off 1m20s restarting failed container=app …
back-off 2m40s restarting failed container=app …
back-off 5m0s restarting failed container=app …
back-off 5m0s restarting failed container=app …
back-off 5m0s restarting failed container=app …

পরপর দুই শুরুর ফাঁক: ১, ১৪, ২৯, ৪০, ৮৪, ১৬২, ৩০৩ সেকেন্ড। এখান থেকে তিনটি কথা পাওয়া গেল। এক, প্রথম restart-এ কোনো অপেক্ষা নেই, মাত্র ১ সেকেন্ড। দুই, এরপর kubelet-এর বার্তায় বিরতি ১০ সেকেন্ড থেকে দ্বিগুণ হতে হতে ৫ মিনিটে থামল, আর 5m0s পরপর তিনবার এসে প্রমাণ করল এটিই ছাদ। তিন, মাপা ফাঁক বার্তার চেয়ে ০–৯ সেকেন্ড বেশি, কারণ বিরতি ফুরালেই container চালু হয় না, kubelet-এর পরের কাজের পালায় হয়। CrashLoopBackOff তাই “চিরকাল crash” বার্তা নয়, এর মানে “এখন বিরতিতে আছি”।

নিয়মটি exponential backoff: প্রতিবার অপেক্ষা দ্বিগুণ, সর্বোচ্চ ৫ মিনিট। লক্ষ্য হলো ভাঙা container যেন node-এর CPU আর log না খেয়ে ফেলে। Kubernetes-এর নথি অনুযায়ী container ১০ মিনিট টানা ভালো চললে backoff আবার শুরু থেকে গোনা হয়। লেখার সময়ের নথিতে (v1.37) এই সংখ্যা কমানোর দুটি পথ আছে: ReduceDefaultCrashLoopBackOffDecay (১.৩৩-এ alpha, বন্ধ থাকে) বিরতি ১ থেকে ৬০ সেকেন্ডে নামায়, আর KubeletCrashLoopBackOffMax (১.৩৫-এ beta) দিয়ে node-প্রতি ছাদ কমানো যায়। ১.৩৪-এর এই cluster-এর default-এ আমরা ১০ সেকেন্ড থেকে ৫ মিনিটই পেয়েছি। নিচের simulator-এ LIVES বদলে দেখুন:

// CrashLoopBackOff delays, simulated clock in seconds.
// Model of what we measured: 1st restart at once, then 10 s doubling.
const BASE = 10, CAP = 300;  // kubelet defaults in this run
const RESET = 600;           // a run this long clears the backoff
// How long each start lived before it crashed. Edit and run again.
const LIVES = [0, 0, 0, 0, 0, 0, 0, 0, 0, 700, 0, 0, 0];

const mmss = s => `${String(Math.floor(s / 60)).padStart(3)}m${String(s % 60).padStart(2, "0")}s`;
let clock = 0, wait = 0;
console.log("start  at        lived  next wait");
LIVES.forEach((life, i) => {
  const at = clock;
  clock += life;
  let note = "";
  if (life >= RESET) {       // ran long enough: forget the history
    wait = 0;
    note = "  (backoff reset)";
  }
  console.log(`#${String(i).padEnd(5)} ${mmss(at)}  ${String(life).padStart(4)}s  ${wait}s${note}`);
  clock += wait;
  wait = wait === 0 ? BASE : Math.min(wait * 2, CAP);
});
console.log(`total time in this history: ${mmss(clock)}`);

চালানোর ফল থেকে কয়েকটি লাইন: #2 0m10s, #6 5m10s, #7 10m10s. অর্থাৎ ৬ নম্বর restart আসে ৫ মিনিট ১০ সেকেন্ডে, আর এরপর প্রতি ৫ মিনিটে একবার। #9 ৭০০ সেকেন্ড বেঁচে ছিল, তাই next wait 0s আর (backoff reset). পরের crash-গুলো আবার ১০ সেকেন্ড থেকে শুরু করে। মডেলটি আমাদের মাপা ক্রম ০, ১০, ২০, ৪০, ৮০, ১৬০, ৩০০ মেনে চলে; আসল kubelet-এ প্রতি ধাপে আরও কয়েক সেকেন্ড যোগ হয়। সবশেষে total time in this history: 33m00s. রিসেটের নিয়মটি আমরা নথি থেকে নিয়েছি, এই run-এ মাপিনি।

৫.২ ImagePullBackOff — গাড়িই আসেনি

$ kubectl get pod badimage
NAME       READY   STATUS             RESTARTS   AGE
badimage   0/1     ImagePullBackOff   0          2m9s

$ kubectl describe pod badimage | sed -n '/^Events/,$p'     # Age column removed
Events:
  Type     Reason     From      Message
  Normal   Pulling    kubelet   Pulling image "nginx:1.27-no-such-tag"
  Warning  Failed     kubelet   Failed to pull image "nginx:1.27-no-such-tag": Error response from daemon:
           failed to resolve reference "docker.io/library/nginx:1.27-no-such-tag": … not found
  Warning  Failed     kubelet   Error: ErrImagePull
  Normal   BackOff    kubelet   Back-off pulling image "nginx:1.27-no-such-tag"
  Warning  Failed     kubelet   Error: ImagePullBackOff

প্রথমে ErrImagePull, তারপর বিরতি দিয়ে চেষ্টা, তাই ImagePullBackOff. Message-এর শেষ শব্দই কারণ বলে দেয়। not found মানে tag বা নাম ভুল। unauthorized বা denied সাধারণত private registry-র অনুমতির সমস্যা। Container একবারও চলেনি, তাই RESTARTS ০, phase Pending, আর kubectl logs-এ কিছু পাবেন না।

৫.৩ OOMKilled — exit 137

Memory limit ৬৪ MiB. Python প্রোগ্রামটি ৮ MiB করে memory নিতে থাকে। পর্ব ১১-এর cgroup সীমা এখানে কাজে নামে: সীমা ছাড়ালেই kernel-এর OOM killer process মারে।

$ kubectl get pod oom
NAME   READY   STATUS      RESTARTS   AGE
oom    0/1     OOMKilled   0          12s
$ kubectl logs oom | tail -2
allocated 48 MiB
allocated 56 MiB
$ kubectl describe pod oom
…
    State:          Terminated
      Reason:       OOMKilled
      Exit Code:    137
…

৫৬ MiB-র পরে আর লাইন নেই। Python-এর নিজেরও memory লাগে, তাই ৬৪-তে পৌঁছানোর আগেই শেষ। Exit code 137 মানে 128 + 9, অর্থাৎ SIGKILL. কিন্তু 137 মানেই OOM নয়: Reason Error আর 137 হলে অন্য কেউ SIGKILL পাঠিয়েছে, যা পরে দেখবেন। তাই সবসময় Reason পড়ুন। Limit আর request-এর হিসাব পর্ব ৪৮-এ।

৫.৪ CreateContainerConfigError — কাগজ নেই

# env DB_PASSWORD comes from secretKeyRef db-secret, which does not exist
$ kubectl get pod nosecret
NAME       READY   STATUS                       RESTARTS   AGE
nosecret   0/1     CreateContainerConfigError   0          6s
$ kubectl describe pod nosecret | sed -n '/^Events/,$p'     # Age column removed
…
  Warning  Failed     kubelet   Error: secret "db-secret" not found

# create the missing Secret and do nothing else
$ kubectl create secret generic db-secret --from-literal=password=not-a-real-password
secret/db-secret created
$ kubectl get pod nosecret        # a few seconds later
nosecret   1/1   Running   0     17s

Image আছে, node আছে, শুধু container বানানোর তথ্য অসম্পূর্ণ। সবচেয়ে ভালো খবর: Pod মোছার দরকার হয়নি। Secret তৈরি হতেই kubelet-এর পরের চেষ্টায় container চালু হলো। ConfigMap-ও এভাবে আটকে দিতে পারে (পর্ব ৪৭)।

STATUSPhaseContainer চলেছে?কোথায় দেখবেন
CrashLoopBackOffRunningহ্যাঁ, বারবারlogs --previous, exit code
ImagePullBackOffPendingনাdescribe-এর Events
OOMKilledনীতিভেদেহ্যাঁ, তারপর মৃত্যুReason, memory limit
CreateContainerConfigErrorPendingনাdescribe-এর Events

৬ · Probe — তিন প্রশ্ন, তিন শাস্তি

Process চলছে মানেই app ভালো আছে, তা নয়। একটি app deadlock-এ আটকে থাকতে পারে, process তবু বেঁচে। তাই kubelet নিয়ম করে container-কে প্রশ্ন করে। এই প্রশ্নই probe, পর্ব ১৮-এর load balancer health check-এর আত্মীয়।

একটি ব্যস্ত চায়ের দোকানের ম্যানেজার ভাবুন। সকালে দোকান খোলার সময় তিনি দেখেন, চুলা ধরেছে কি (startup). সারাদিন দেখেন, কর্মচারী জ্ঞান হারিয়ে পড়ে আছেন কি (liveness): থাকলে বদলি ডাকেন। আর দেখেন, এই মুহূর্তে হাত খালি আছে কি (readiness): না থাকলে নতুন খদ্দের অন্য টেবিলে পাঠান, কিন্তু কর্মচারীকে বিদায় করেন না।

Probeপ্রশ্নব্যর্থ হলেRestart?
Startupচালু হয়েছে কি?Budget ফুরালে container মারা হয়। সফল না হওয়া পর্যন্ত বাকি দুটি probe বন্ধ।হ্যাঁ
Livenessআটকে গেছে কি?Container মেরে restart policy অনুযায়ী চালানো হয়।হ্যাঁ
Readinessএখন traffic নিতে পারবে কি?Pod Ready=False, Service-এর ready তালিকা থেকে বাদ।না

Probe চার রকমে করা যায়: httpGet (200–399 হলে সফল), tcpSocket (port-এ যুক্ত হতে পারলে সফল), exec (command exit 0 দিলে সফল) আর grpc. সংখ্যাগুলোর default মুখস্থ রাখার মতো:

FieldDefaultমানে
initialDelaySeconds0Container চালুর পর প্রথম probe-এর আগে অপেক্ষা
periodSeconds10কত পরপর probe
timeoutSeconds1উত্তরের জন্য অপেক্ষা, এরপর ব্যর্থ ধরা হয়
failureThreshold3পরপর কতবার ব্যর্থ হলে ব্যবস্থা
successThreshold1পরপর কতবার সফল হলে ফেরা। Liveness আর startup-এ এটি ১-ই হতে হয়।
মূল সূত্র

ব্যবস্থা নিতে মোটামুটি সময় লাগে periodSeconds × failureThreshold. Default-এ ১০ × ৩ = ৩০ সেকেন্ড। Startup probe-এর ক্ষেত্রে এটিই চালু হওয়ার সর্বোচ্চ budget. Probe না দিলে kubelet process চলাকেই সফল ধরে।

Liveness ব্যর্থ হলো

nginx-এ /healthz নামে কোনো পথ নেই, তবু আমরা সেটিই probe করছি। এটি AI-এর লেখা YAML-এর খুব পরিচিত ভুল।

apiVersion: v1
kind: Pod
metadata:
  name: live-bad
spec:
  containers:
    - name: web
      image: nginx:1.27-alpine
      imagePullPolicy: IfNotPresent
      livenessProbe:
        httpGet:
          path: /healthz
          port: 80
        periodSeconds: 5
        failureThreshold: 3
      resources:
        requests: {cpu: 10m, memory: 16Mi}
        limits: {memory: 64Mi}
$ kubectl get pod live-bad
NAME       READY   STATUS    RESTARTS     AGE
live-bad   1/1     Running   1 (8s ago)   23s

$ kubectl describe pod live-bad      # Age column removed
…
    Last State:     Terminated
      Reason:       Completed
      Exit Code:    0
…
  Warning  Unhealthy  kubelet   Liveness probe failed: HTTP probe failed with statuscode: 404
  Normal   Killing    kubelet   Container web failed liveness probe, will be restarted

# a fresh run of the same Pod, about a minute later
$ kubectl get pod live-bad
NAME       READY   STATUS             RESTARTS     AGE
live-bad   0/1     CrashLoopBackOff   3 (8s ago)   69s

৫ × ৩ = ১৫ সেকেন্ডে প্রথম kill. nginx SIGTERM পেয়ে ভদ্রভাবে থেমেছে, তাই exit 0, Reason: Completed. Exit 0 দেখে ভাববেন না সব ঠিক: কারণটি Events-এ। শেষ লাইন দেখায় একই backoff এখানেও খাটে। পুরোপুরি সুস্থ nginx মিনিটখানেকের মধ্যে CrashLoopBackOff-এ পড়ল, শুধু ভুল পথের জন্য।

Readiness ব্যর্থ হলো

দুটি Pod, shop-a আর shop-b, একটি Service-এর পেছনে। প্রতিটি Pod নিজের hostname ফেরত দেয়, তাই traffic কোথায় গেল গোনা যায়। Readiness: cat /tmp/ready, period ৩, threshold ২। এবার shop-a-র ফাইল মুছে দিই। Service-এর ভেতরের কথা পর্ব ৪৫-এ, এখানে শুধু EndpointSlice-এর তালিকা দেখুন।

Service shop-এর পেছনে দুই Pod
shop-a READY
shop-a phase
shop-a RESTARTS
পরপর ব্যর্থ probe
EndpointSlice: shop-a
EndpointSlice: shop-b
৮ অনুরোধের ফল
  1. দুজনই ready. ৮টি অনুরোধ পাঠালে এই run-এ ৭টি গেল shop-a-তে, ১টি shop-b-তে। ভাগটি এলোমেলো, আসল কথা হলো দুজনই traffic পাচ্ছে।
  2. ফাইল মোছা হলো। kubectl exec shop-a -- rm /tmp/ready. পরের probe না আসা পর্যন্ত কিছুই বদলায় না।
  3. প্রথম ব্যর্থতা। Threshold ২, তাই একবার ব্যর্থ হলে এখনো ব্যবস্থা নেই। ক্ষণিকের ঝাঁকুনিতে Pod যেন বাদ না পড়ে, threshold সেজন্যই।
  4. দ্বিতীয় ব্যর্থতা। READY 0/1, আর EndpointSlice-এ ready=false. কিন্তু phase Running, RESTARTS ০: কেউ container-কে ছোঁয়নি।
  5. Traffic সরে গেল। ৮টি অনুরোধের ৮টিই shop-b-তে। shop-a-তে একটিও নয়। ব্যবহারকারী কিছু টের পেলেন না।
  6. ফিরে এলো। touch /tmp/ready-র পর পরের probe সফল। successThreshold ১, তাই সঙ্গে সঙ্গে ready=true. আবার ৮টির ৫টি shop-a-তে, কোনো restart ছাড়াই।
$ kubectl get pods -l app=shop
NAME     READY   STATUS    RESTARTS   AGE
shop-a   0/1     Running   0          14s
shop-b   1/1     Running   0          14s

$ kubectl get endpointslice -l kubernetes.io/service-name=shop
NAME         ADDRESSTYPE   PORTS   ENDPOINTS               AGE
shop-2frp8   IPv4          80      10.1.0.142,10.1.0.143   14s

# the column above is not the whole story: read each endpoint
$ kubectl get endpointslice -l kubernetes.io/service-name=shop -o jsonpath='…'
shop-a 10.1.0.142 ready=false serving=false
shop-b 10.1.0.143 ready=true serving=true

একটি সূক্ষ্ম ফাঁদ: ENDPOINTS কলামে এখনো দুটো IP দেখাচ্ছে। Pod তালিকা থেকে মুছে যায়নি, শুধু তার ready চিহ্ন false হয়েছে, আর kube-proxy সেটি দেখে traffic পাঠানো বন্ধ করেছে। আমাদের ৮/৮ ফলাফল তারই প্রমাণ।

Startup ব্যর্থ হলো

একটি app চালু হতে ৪০ সেকেন্ড নেয়। দুটি Pod-এ দুই রকম budget দিলাম: slow-bad-এ ৩ × ৫ = ১৫ সেকেন্ড, slow-ok-এ ৩ × ২০ = ৬০ সেকেন্ড।

$ kubectl get pods slow-bad slow-ok
NAME       READY   STATUS    RESTARTS     AGE
slow-bad   0/1     Running   1 (8s ago)   26s
slow-ok    0/1     Running   0            26s

$ kubectl describe pod slow-bad      # Age column removed
…
  Warning  Unhealthy  kubelet   Startup probe failed: dial tcp 10.1.0.153:8000: connect: connection refused
  Normal   Killing    kubelet   Container app failed startup probe, will be restarted

$ kubectl get pods slow-bad slow-ok     # 30 seconds later
NAME       READY   STATUS    RESTARTS     AGE
slow-bad   0/1     Running   3 (4s ago)   56s
slow-ok    1/1     Running   0            56s

slow-bad কখনো ৪০ সেকেন্ডে পৌঁছাতেই পারে না, ১৫ সেকেন্ডেই মারা পড়ে, আবার শূন্য থেকে শুরু করে। এটি অনন্ত চক্র। slow-ok ১৩ বার ব্যর্থ হয়ে (১৩ × ৩ = ৩৯ সেকেন্ড) তারপর সফল হলো, আর তখনই তার liveness probe জেগে উঠল। আগে মানুষ initialDelaySeconds: 300 দিয়ে এই সমস্যা ঢাকতেন। তাতে চালুর পর সত্যিকারের hang ধরতেও দেরি হতো। Startup probe দুটো আলাদা করে দেয়: চালুর সময় উদার, চালুর পরে কড়া।

Simulator: probe-এর হিসাব মেলান

নিচের প্রতিটি লাইনে . মানে probe সফল, x মানে ব্যর্থ। ok ফাংশন, period আর threshold বদলে দেখুন।

// Probe evaluator on a simulated clock (seconds). Edit the scenarios.
// ok(t) says whether the app would pass a probe at time t.
const SCENARIOS = [
  { name: "liveness, app hangs at 32 s", kind: "liveness",
    period: 5, threshold: 3, end: 60, ok: t => t < 32 },
  { name: "readiness, blip 20..26 s", kind: "readiness",
    period: 3, threshold: 2, end: 36, ok: t => t < 20 || t >= 27 },
  { name: "liveness only, app needs 40 s to boot", kind: "liveness",
    period: 5, threshold: 3, end: 60, ok: t => t >= 40 },
  { name: "startup budget 3 x 20, app needs 40 s", kind: "startup",
    period: 3, threshold: 20, end: 60, ok: t => t >= 40 },
];
for (const s of SCENARIOS) {
  let fails = 0, marks = "", events = [], ready = true;
  // kubelet adds a little jitter; here the first probe is at one period
  for (let t = s.period; t <= s.end; t += s.period) {
    const pass = s.ok(t);
    fails = pass ? 0 : fails + 1;
    marks += pass ? "." : "x";
    if (s.kind === "startup" && pass) { events.push(`started at ${t}s`); break; }
    if (s.kind === "readiness") {
      if (fails === s.threshold) { ready = false; events.push(`not ready at ${t}s`); }
      if (pass && !ready) { ready = true; events.push(`ready again at ${t}s`); }
    } else if (fails === s.threshold) {
      events.push(`container killed at ${t}s`);
      break;
    }
  }
  console.log(s.name);
  console.log(`  ${marks}  ${events.join(", ") || "no action"}`);
}

প্রথম লাইনে app ৩২ সেকেন্ডে আটকাল, কিন্তু container killed at 45s: ধরতে ১৩ সেকেন্ড লাগল। দ্বিতীয় লাইনে ছোট ঝাঁকুনিতে not ready at 24s, ready again at 27s, restart ছাড়াই। তৃতীয় লাইন হলো slow-bad-এর গল্প: ৪০ সেকেন্ড লাগে এমন app-কে killed at 15s, আর আমাদের আসল run-এও Started থেকে Finished ১৭ সেকেন্ড (১৫ + grace ২)। চতুর্থ লাইনে xxxxxxxxxxxxx. মানে ১৩ বার ব্যর্থ, তারপর started at 42s: আসল slow-ok-ও ঠিক ১৩ বার ব্যর্থ হয়েছিল। আসল kubelet প্রথম probe-এ সামান্য এলোমেলো দেরি যোগ করে, তাই সেকেন্ডের হিসাব একটু সরে।

ভুল liveness probe কীভাবে outage ঘটায়

Readiness ভুল হলে ক্ষতি তুলনায় কম। Liveness ভুল হলে Kubernetes নিজেই আপনার সুস্থ app মারতে থাকে। এই পরীক্ষায় app পুরোপুরি ঠিক আছে, শুধু /healthz উত্তর দিতে ২ সেকেন্ড নেয়, যেমন ভেতরে ডেটাবেসে একটি query করছে। আর probe-এর timeoutSeconds default ১।

$ kubectl get pod slow-health
NAME          READY   STATUS    RESTARTS     AGE
slow-health   1/1     Running   1 (4s ago)   24s

$ kubectl describe pod slow-health      # Age column removed
…
    Last State:     Terminated
      Reason:       Error
      Exit Code:    137
…
  Warning  Unhealthy  kubelet   Liveness probe failed: Get "http://10.1.0.123:8000/healthz":
           context deadline exceeded (Client.Timeout exceeded while awaiting headers)
  Normal   Killing    kubelet   Container app failed liveness probe, will be restarted

কোনো বাগ নেই, memory-র সমস্যাও নেই, তবু exit 137. এবার Reason Error, OOMKilled নয়: kubelet নিজে SIGKILL পাঠিয়েছে, কারণ এই Python process SIGTERM সামলায় না (পরের অংশে কারণ)।

এবার এটিকে production-এ ভাবুন। একটি কাল্পনিক অনলাইন বাজার অ্যাপের ১০টি Pod, সবার liveness probe ডেটাবেস ছুঁয়ে দেখে। ঈদের ভিড়ে ডেটাবেস ধীর হলো:

  1. সব Pod-এর probe একসাথে timeout.
  2. Kubernetes একসাথে সব container মারল। ডেটাবেসের সমস্যা ছিল, এখন app-ও নেই।
  3. নতুন container cache খালি নিয়ে চালু হলো, connection খুলল, ধীর ডেটাবেসে চাপ আরও বাড়ল।
  4. Probe আবার ব্যর্থ। Backoff বাড়ছে। ডেটাবেস সুস্থ হওয়ার পরেও Pod ৫ মিনিট পর্যন্ত বসে থাকতে পারে।

একটি ধীর dependency এভাবে পুরো সেবার পতনে বদলে যায়। আর probe-টিই সেই সেতু।

নিরাপদ নিয়ম
  • Liveness শুধু process-কে প্রশ্ন করবে। ডেটাবেস, cache, অন্য service ছোঁবে না। Restart করলে যা ঠিক হবে না, তা liveness-এর বিষয় নয়।
  • Liveness আর readiness আলাদা endpoint পাক। আলাদা প্রশ্ন, আলাদা উত্তর।
  • Liveness-কে readiness-এর চেয়ে ঢিলে রাখুন। বেশি threshold, বেশি timeout. আগে traffic কমবে, restart শেষ উপায়।
  • Liveness না দেওয়াও একটি সিদ্ধান্ত। App crash করলে এমনিতেই restart হয়। আটকে যাওয়ার সত্যিকারের ঝুঁকি না থাকলে probe না দেওয়াই ভালো হতে পারে।
  • Readiness-এ shared dependency সাবধানে। সব Pod একসাথে not ready হলে Service খালি হয়ে যায়। ভাবনার প্রশ্ন ৪ দেখুন।

এই ধরনের ঢেউয়ের সাধারণ চিত্র আছে System Design কোর্সের resilience patterns পর্বে।

৭ · Graceful termination — ভদ্রভাবে থামা

Pod মোছা, নতুন version আনা, node খালি করা: প্রতিদিন Pod মরে। ভালো মৃত্যু মানে চলমান অনুরোধ শেষ করে থামা। পর্ব ৩৯-এ docker stop-এর SIGTERM আর SIGKILL দেখেছেন। Kubernetes-এ তার সাথে দুটি জিনিস যোগ হয়: preStop hook আর Service থেকে সরে যাওয়া।

লঞ্চঘাটে শেষ লঞ্চ ছাড়ার সময় ভাবুন। আগে ঘোষণা হয় “আর টিকিট নয়” (Service থেকে সরা)। তারপর ঘাটের লোক কিছুক্ষণ অপেক্ষা করেন, যাতে পথে থাকা যাত্রীরা উঠতে পারেন (preStop). তারপর সারেং বাঁশি দেন (SIGTERM), যাত্রী নামানো শেষ হয়। ঘাটের নির্দিষ্ট সময় ফুরালে দড়ি কেটে দেওয়া হয় (SIGKILL).

apiVersion: v1
kind: Pod
metadata:
  name: term-good
spec:
  terminationGracePeriodSeconds: 30
  containers:
    - name: app
      image: alpine:3.20
      imagePullPolicy: IfNotPresent
      command: ["sh", "-c"]
      args:
        - |
          trap 'echo "SIGTERM received, finishing work"; sleep 3; echo "clean exit"; exit 0' TERM
          echo "serving"
          while true; do sleep 1; done
      lifecycle:
        preStop:
          exec:
            command: ["sh", "-c", "echo 'preStop started' > /proc/1/fd/1; sleep 5; echo 'preStop done' > /proc/1/fd/1"]
      resources:
        requests: {cpu: 10m, memory: 8Mi}
        limits: {memory: 32Mi}
$ kubectl logs -f term-good --timestamps &       # keep watching
$ kubectl delete pod term-good --wait=false
pod "term-good" deleted from abcl-kp namespace
$ kubectl get pod term-good
NAME        READY   STATUS        RESTARTS   AGE
term-good   1/1     Terminating   0          5s
$ kubectl get pod term-good -o jsonpath='{.metadata.deletionTimestamp} {.metadata.deletionGracePeriodSeconds}'
2026-10-30T19:22:28Z 30

# log lines (date removed from the timestamps)
19:21:56.202  serving
19:21:58.513  preStop started
19:22:03.514  preStop done
19:22:05.344  SIGTERM received, finishing work
19:22:08.345  clean exit

deletionTimestamp হলো মোছার সময় + ৩০ সেকেন্ড, অর্থাৎ অনুরোধ এসেছিল ১৯:২১:৫৮-এ। preStop পুরো ৫.০০ সেকেন্ড চলল, তারপরই SIGTERM. Handler ৩.০০ সেকেন্ডে কাজ শেষ করে exit 0 দিল। মোট প্রায় ১০ সেকেন্ড, ৩০ সেকেন্ডের সীমার অনেক ভেতরে। preStop শেষ আর SIGTERM-এর লেখার মাঝে ১.৮ সেকেন্ডের ফাঁক আছে, কারণ shell তার sleep 1 শেষ করে তবেই trap চালায়, আর stop-এর অনুরোধও পৌঁছাতে সামান্য সময় নেয়।

term-good, সীমা ৩০ সেকেন্ড
সময়
STATUS
Service থেকে
Container-এ
বাকি grace
  1. Delete. API server Pod-এ deletionTimestamp বসাল। Pod সঙ্গে সঙ্গে মোছে না, STATUS হয় Terminating. একই সময়ে EndpointSlice controller Pod-টিকে Service থেকে সরাতে শুরু করে। এই দুটো কাজ পাশাপাশি চলে, একটি আরেকটির জন্য অপেক্ষা করে না।
  2. preStop শুরু। kubelet খবর পেয়ে আগে preStop চালাল। এই সময়ে app স্বাভাবিকভাবেই চলছে, নতুন অনুরোধ এলেও নিতে পারে।
  3. preStop শেষ, SIGTERM. ৫ সেকেন্ড পরে hook শেষ। এবার kubelet container-এর PID 1-কে SIGTERM পাঠাল। preStop-এর ৫ সেকেন্ড grace budget থেকেই কেটে গেছে।
  4. App গুছিয়ে নিচ্ছে। Handler চালু: নতুন কাজ নয়, চলমান কাজ শেষ, connection বন্ধ। এই ধাপ graceful shutdown-এর আসল অংশ, আর এটি আপনার কোডে থাকতে হয়।
  5. পরিষ্কার exit. Exit 0, Pod মুছে গেল। বাকি ১৯.৭ সেকেন্ড লাগেনি, তাই SIGKILL-ও আসেনি।

SIGTERM না শুনলে

এবার term-deaf: Python-এর একটি লুপ, সরাসরি PID 1 হিসেবে চলছে, SIGTERM handler নেই, terminationGracePeriodSeconds: 5.

$ kubectl delete pod term-deaf --wait=false
$ kubectl get pod term-deaf     # repeated every second
term-deaf   1/1   Terminating   0     17s
term-deaf   1/1   Terminating   0     18s
term-deaf   1/1   Terminating   0     19s
term-deaf   0/1   Error         0     21s
Error from server (NotFound): pods "term-deaf" not found

# a second run of the same Pod, read just before it vanished
$ kubectl get pod term-deaf -o jsonpath='…'
137 Error

পুরো ৫ সেকেন্ড অপেক্ষা, তারপর 137. কারণটি Linux-এর একটি নিয়ম: PID 1 যে signal-এর handler বসায়নি, kernel সেটি তার কাছে পৌঁছে দেয় না। সাধারণ process SIGTERM-এ মরে, কিন্তু container-এর PID 1 অনেক সময় মরে না। তাই handler লিখুন, অথবা ছোট init process ব্যবহার করুন। sh -c "python app.py" লিখলে আরেকটি ফাঁদ আছে: shell PID 1, আর সে signal app-এ না-ও পাঠাতে পারে। সমাধান exec python app.py অথবা exec-রূপের CMD. বিস্তারিত পর্ব ৩৯-এ আর Operating Systems কোর্সের signals পর্বে।

Simulator: termination timeline

Grace-এর হিসাব একটি জায়গায় মানুষকে ঠকায়: সীমাটি preStop আর SIGTERM মিলিয়ে, আলাদা করে নয়। নথি অনুযায়ী hook সীমা পেরিয়ে গেলে kubelet একবার ২ সেকেন্ডের ছাড় দেয়। Simulator সেভাবেই হিসাব করে।

// Pod termination timeline, simulated seconds. Edit CASES.
// grace: terminationGracePeriodSeconds, hook: preStop duration,
// app: seconds the app needs after SIGTERM (Infinity = ignores it).
const CASES = [
  { name: "term-good", grace: 30, hook: 5, app: 3 },
  { name: "term-deaf", grace: 5, hook: 0, app: Infinity },
  { name: "hook eats the budget", grace: 30, hook: 25, app: 10 },
  { name: "hook too long", grace: 10, hook: 15, app: 3 },
];
const MIN_LEFT = 2;  // kubelet keeps at least this after the hook
for (const c of CASES) {
  const hookEnd = Math.min(c.hook, c.grace);
  const left = Math.max(c.grace - hookEnd, MIN_LEFT);
  const clean = c.app <= left;
  const end = hookEnd + (clean ? c.app : left);
  console.log(`${c.name}: grace ${c.grace}s`);
  const at = t => `t=${t}`.padEnd(6);
  console.log(`  ${at(0)}delete, deletionTimestamp set`);
  if (c.hook > 0) {
    const cut = c.hook > c.grace ? " (cut short)" : "";
    console.log(`  ${at(0)}preStop starts, ends t=${hookEnd}${cut}`);
  }
  console.log(`  ${at(hookEnd)}SIGTERM, ${left}s left`);
  console.log(`  ${at(end)}` + (clean ? "app exits cleanly, code 0" : "SIGKILL, exit code 137"));
}

চার ক্ষেত্রের ফল: term-good t=8-এ পরিষ্কার exit, আসল run-এ ১০.৩ সেকেন্ড লেগেছিল, কারণ সেখানে shell-এর sleep 1-এর দেরি আছে। term-deaf t=5-এ SIGKILL, যা আসল run-এর সাথে মেলে। তৃতীয়টি দেখায় বড় ফাঁদ: preStop ২৫ সেকেন্ড খেয়ে ফেলায় app পেল ৫ সেকেন্ড, তাই t=30 SIGKILL, যদিও সীমা শুনতে ৩০। চতুর্থটিতে hook সীমা পেরিয়ে গেল: (cut short), 2s left, t=12 SIGKILL. শেষ দুটি নথির নিয়মের মডেল, এই run-এ মাপা নয়। সূত্রটি মনে রাখুন: grace ≥ preStop + সবচেয়ে ধীর shutdown.

SIGTERM আর traffic-এর দৌড়

Stepper-এর প্রথম ধাপে দেখেছেন, Service থেকে সরা আর Pod থামা পাশাপাশি চলে। প্রতিটি node-এর kube-proxy, ingress, cloud load balancer নিজের সময়ে খবর পায়। তাই SIGTERM পেয়ে app দরজা বন্ধ করে দিলে কিছু অনুরোধ তখনো আসতে পারে, আর ব্যবহারকারী 502 দেখেন। এই ফাঁক ঢাকতে preStop-এ কয়েক সেকেন্ড ঘুমানো একটি পরিচিত কৌশল। নতুন version-এ shell ছাড়াই লেখা যায়, যা ১.৩৪-এর এই cluster-এ dry-run পাস করেছে; পুরোনো version-এ এই field নাও থাকতে পারে:

apiVersion: v1
kind: Pod
metadata:
  name: prestop-sleep
spec:
  containers:
    - name: app
      image: alpine:3.20
      imagePullPolicy: IfNotPresent
      command: ["sleep", "3600"]
      lifecycle:
        preStop:
          sleep:
            seconds: 5
      resources:
        requests: {cpu: 10m, memory: 8Mi}
        limits: {memory: 32Mi}

kubectl delete pod … --grace-period=0 --force ভদ্রতা বাদ দেয়। এই run-এ kubectl নিজেই সতর্ক করল: Immediate deletion does not wait for confirmation that the running resource has been terminated. The resource may continue to run on the cluster indefinitely. অর্থাৎ API থেকে Pod মুছলেও node-এ process চলতে পারে। শেষ উপায় হিসেবে রাখুন, প্রথম উপায় হিসেবে নয়।

৮ · Debug-এর চার হাতিয়ার

Pod ভাঙলে এলোমেলো কমান্ড নয়, একটি ক্রম মেনে চলুন। প্রথমে Kubernetes কী বলছে, তারপর app কী বলেছিল, তারপর ভেতরে ঢোকা।

ক্রমকমান্ডযা দেখায়কখন
১kubectl describe pod XEvents, state, last state, exit codeসবসময় প্রথমে
২kubectl logs X --previousআগের, মরে যাওয়া container-এর logCrash হলে
৩kubectl exec -it X -- shচলন্ত container-এর ভেতরে commandContainer চলছে, shell আছে
৪kubectl debug X --image=… --target=…Ephemeral container জুড়ে দেখাShell বা টুল নেই

Back-off চলাকালে সাধারণ kubectl logs শেষ চলা container-এর log দেখায়। কিন্তু নতুন container চালু হয়ে গেলে দেখায় তার log, যেখানে crash-এর কারণ নেই। --previous সবসময় আগের container-টি দেখায়, তাই এটিই নিরাপদ অভ্যাস। পুরোনো log kubelet সীমিত রাখে, তাই দীর্ঘমেয়াদের জন্য centralized logging লাগে।

$ kubectl get pod crash
NAME    READY   STATUS             RESTARTS      AGE
crash   0/1     CrashLoopBackOff   9 (16s ago)   21m

$ kubectl describe pod crash      # Age column removed
…
    State:          Waiting
      Reason:       CrashLoopBackOff
    Last State:     Terminated
      Reason:       Error
      Exit Code:    1
…
    Restart Count:  9
…
  Warning  BackOff   kubelet   Back-off restarting failed container app in pod crash

$ kubectl logs crash --previous
cannot connect to db

ছোট image-এ shell থাকে না। registry.k8s.io/pause image-এ তো কিছুই নেই, তাই এটিই ভালো পরীক্ষা। পর্ব ৩৮-এর ছোট image-এর এটিই দাম, আর এর সমাধানই ephemeral container.

$ kubectl exec nosh -- sh
OCI runtime exec failed: exec failed: unable to start container process: exec: "sh":
executable file not found in $PATH
command terminated with exit code 127

$ kubectl debug nosh --image=alpine:3.20 --target=app --container=dbg \
    --attach=false --profile=general -- sh -c 'ps; ls -l /proc/1/exe'
Targeting container "app". If you don't see processes from this container …
$ kubectl logs nosh -c dbg
PID   USER     TIME  COMMAND
    1 65535     0:00 /pause
   13 root      0:00 sh -c ps; ls -l /proc/1/exe
   19 root      0:00 ps
lrwxrwxrwx    1 65535    root    0 … /proc/1/exe -> /pause

$ kubectl get pod nosh
NAME   READY   STATUS    RESTARTS   AGE
nosh   1/1     Running   0          6s

exec ব্যর্থ, exit 127: sh নামের ফাইলই নেই। kubectl debug চলন্ত Pod-এ alpine image-এর একটি নতুন container জুড়ে দিল। --target তাকে app-এর process namespace-এ ঢোকায়, তাই সে PID 1 হিসেবে /pause-কে দেখল। Pod restart হয়নি, RESTARTS ০। Ephemeral container-এ probe, port বা resource রাখা যায় না, restart-ও হয় না, আর Pod-এর spec থেকে তাকে সরানো যায় না। Feature-টি ১.২৫-এ stable হয়েছে বলে নথিতে আছে। Production-এ এই অধিকার সবাইকে দেবেন না: RBAC নিয়ে পর্ব ৪৯-এ।

৯ · Production-এ bare Pod কেন চলে না

এতক্ষণ আমরা সরাসরি Pod বানিয়েছি, শেখার জন্য। Production-এ এমন bare Pod প্রায় কখনো নয়। কারণটি একটি কমান্ডেই দেখা যায়: bare সরাসরি বানানো, demo-… একটি Deployment-এর।

$ kubectl delete pod bare
pod "bare" deleted from abcl-kp namespace
$ kubectl delete pod demo-84d45bf8fb-9dg99
pod "demo-84d45bf8fb-9dg99" deleted from abcl-kp namespace
$ kubectl get pods       # 3 seconds later, other Pods trimmed
NAME                    READY   STATUS    RESTARTS   AGE
demo-84d45bf8fb-bb4xd   1/1     Running   0          4s

bare চিরতরে গেল, কেউ ফেরাল না। demo-র জায়গায় নতুন নামের একটি Pod হাজির। Pod নিজে থেকে নিজেকে বাঁচায় না: kubelet শুধু container restart করে, একই node-এ। Node মরলে bare Pod-ও শেষ। Pod চিরস্থায়ী নয়, আর এই কথা ধরে নিয়েই Kubernetes সাজানো।

ঘটনাBare PodController-এর Pod
Container crashkubelet restart করেkubelet restart করে
Pod মোছা বা evict হওয়াশেষনতুন Pod আসে
Node নষ্টশেষঅন্য node-এ নতুন Pod
নতুন versionনিজে মুছে নিজে বানানRolling update (পর্ব ৪৪)
Replica বাড়ানোপ্রতিটি হাতেএকটি সংখ্যা বদলানো

তাই সার্ভারের জন্য Deployment, ডেটাবেসের মতো কাজে StatefulSet (পর্ব ৪৭), প্রতি node-এ একটির জন্য DaemonSet, একবারের কাজে Job. Bare Pod থাকুক শেখা আর ছোট পরীক্ষার জন্য।

আরও গভীরে: Borg paper (Verma et al., EuroSys ২০১৫) আর “Borg, Omega, and Kubernetes” (Burns et al., ACM Queue ২০১৬) থেকে Pod-এর শিকড় বোঝা যায়: Borg-এর alloc ধারণা থেকেই এক মেশিনে একসাথে থাকা কাজের দল, আর Borg-এ host-এর port ভাগ করার ঝামেলা থেকে Pod-প্রতি IP-র সিদ্ধান্ত। kubelet CRI-এর মাধ্যমে runtime-কে প্রথমে একটি sandbox বানাতে বলে, তারপর তার ভেতরে container. status.conditions আর container-এর lastState পড়াই Pod debug-এর সবচেয়ে নির্ভরযোগ্য পথ, কারণ STATUS কলাম kubectl-এর নিজের হিসাব।

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

  • Pod আর container-এর পার্থক্য? Pod হলো schedule আর network-এর একক, ভেতরে এক বা একাধিক container একই IP ভাগ করে।
  • Liveness আর readiness-এর পার্থক্য? Liveness ব্যর্থ হলে restart, readiness ব্যর্থ হলে শুধু traffic বন্ধ।
  • Exit 137 মানে কী? 128 + 9, SIGKILL. Reason OOMKilled হলে memory, নইলে grace period ফুরানো বা অন্য কারণ।
  • CrashLoopBackOff-এ প্রথম কী দেখবেন? describe-এর exit code, তারপর logs --previous.
  • Pod মোছার ক্রম? Terminating, পাশাপাশি Service থেকে সরা, preStop, SIGTERM, সীমা ফুরালে SIGKILL.

১০ · AI-এর যুগে Pod YAML

AI কয়েক সেকেন্ডে Pod YAML লিখে দেয়, আর সেটি প্রায়ই apply-ও হয়ে যায়। মুশকিল হলো, এই পর্বের ভুলগুলোর বেশিরভাগ syntax-এ ধরা পড়ে না, ধরা পড়ে রাত দুটোয়।

AI-এর ভুলফলযাচাই
Liveness আর readiness একই endpoint-এ, যা ডেটাবেস ছোঁয়ডেটাবেস ধীর হলে সব Pod restartEndpoint-এর কোড পড়ুন
অস্তিত্বহীন /healthz পথসুস্থ app CrashLoopBackOff-এPod-এর ভেতর থেকে পথটি চালিয়ে দেখুন
ধীর app-এ initialDelaySeconds: 300Hang ধরতে দেরিStartup probe ব্যবহার করুন
image: …:latest, limit নেইঅজানা version, node-এ memory-র ঝুঁকিTag বা digest আর resource দেখুন
sh -c "app" দিয়ে চালানোSIGTERM app-এ পৌঁছায় না, ধীর shutdown, 137Delete করে সময় মাপুন
Bare Pod বা Deployment-এ restartPolicy: NeverPod ফেরে না, বা apply ব্যর্থ--dry-run=server
সমস্যায় kubectl delete pod --force বা --allআসল কারণ আর প্রমাণ হারায়, অন্যের Pod মোছেআগে describe, namespace দেখে নিন

AI-কে Pod লিখতে বললে শুধু “probe যোগ করুন” বলবেন না। প্রতিটি probe কেন, আর ব্যর্থ হলে কী হবে, সেটি জিজ্ঞেস করুন:

আমার একটি Python web app আছে: চালু হতে সর্বোচ্চ ৯০ সেকেন্ড লাগে, port 8000-এ শোনে, /ready পথ ডেটাবেস দেখে, /alive পথ শুধু process দেখে। Kubernetes ১.৩৪-এর জন্য একটি Deployment-এর container অংশ লিখে দাও। শর্ত: নির্দিষ্ট image tag, resources থাকবে, startup-liveness-readiness তিনটি probe, graceful shutdown. ১. প্রতিটি probe-এর period আর threshold কেন, হিসাব করে দেখাও। ২. ডেটাবেস ৩ মিনিট ধীর থাকলে Pod-গুলোর কী হবে, ধাপে ধাপে বলো। ৩. terminationGracePeriodSeconds কত, আর preStop লাগবে কি না, যুক্তি দাও। ৪. তুমি নিশ্চিত নও এমন field থাকলে আলাদা করে বলো। ৫. আমাকে কোন কমান্ডে যাচাই করতে হবে (--dry-run=server সহ), তা দাও। কোনো মোছার কমান্ড দিও না।

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

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

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

প্র ০১Docker-এর দুনিয়ায় container-ই ছিল একক। Kubernetes কেন Pod নামের বাড়তি স্তর আনল, আর এক Pod-এ এক IP-র সিদ্ধান্ত কেন এত বড়?

উত্তরের শিকড় Google-এর Borg-এ। ২০১৫ সালের Borg paper আর ২০১৬ সালের “Borg, Omega, and Kubernetes” লেখায় বলা হয়েছে, Borg-এ alloc নামে একটি ধারণা ছিল: এক মেশিনে একসাথে বাঁচা কাজের জন্য সংরক্ষিত জায়গা। অভিজ্ঞতা থেকে দেখা গিয়েছিল, helper process-গুলো মূল কাজের পাশে থাকা খুব স্বাভাবিক চাহিদা, যেমন log সংগ্রহ বা ফাইল নামানো। সব এক image-এ ঠেসে দিলে আলাদা দল আলাদা গতিতে আপডেট করতে পারে না, আর আলাদা resource হিসাবও রাখা যায় না।

দ্বিতীয় শিক্ষা network নিয়ে। Borg-এ এক মেশিনের সব কাজ host-এর IP ভাগ করত, তাই port-ও একটি schedule করার মতো সম্পদ হয়ে দাঁড়িয়েছিল। প্রোগ্রামকে আগে থেকে জানা port-এ না শুনে যে port দেওয়া হলো সেটিতে শুনতে হতো। Kubernetes উল্টো পথ নিল: প্রতিটি Pod নিজের IP পাবে। তাই nginx তার চিরচেনা port 80-এই শোনে, আর পুরো cluster-এ port-এর সংঘর্ষ নিয়ে ভাবতে হয় না। সংঘর্ষ থাকে শুধু Pod-এর ভেতরে, যা আমরা clash পরীক্ষায় দেখলাম।

দাম আছে। প্রতিটি Pod-কে routable IP দিতে হলে একটি CNI network লাগে, যার জটিলতা আসে Computer Networks কোর্সের cloud networking পর্বে। আর container আলাদা restart হলেও Pod-কে এক রাখতে pause container-এর মতো একটি অদ্ভুত কৌশল লাগে। এক-container-এর Pod-এও এই আবরণ থাকে, যা নতুনদের কাছে অকারণ মনে হয়।

মূল উপলব্ধি: ভালো abstraction সাধারণত আগের ব্যবস্থার ব্যথা থেকে জন্মায়। Pod কোনো বিমূর্ত সৌন্দর্য নয়, এটি বড় মাপের cluster চালানোর দীর্ঘ অভিজ্ঞতার সারাংশ: একসাথে বাঁচে এমন process-এর জন্য একটি একক, আর network-এর জন্য একটি সহজ নিয়ম।

প্র ০২Sidecar নকশা এত জনপ্রিয় হওয়ার পরেও native sidecar আসতে এত বছর লাগল কেন? এর আগে মানুষ কীভাবে চালাতেন?

Kubernetes-এর শুরুতে Pod-এর ভেতরের container-গুলো সমান ছিল। কে আগে চালু হবে, কে পরে থামবে, এমন কোনো ধারণা ছিল না। Service mesh আর log agent জনপ্রিয় হওয়ার পর এই ফাঁক ব্যথা দিতে শুরু করল। তিনটি সমস্যা বারবার ফিরত। এক, app আগে চালু হয়ে গেল, proxy তখনো তৈরি নয়, তাই প্রথম কয়েকটি network call ব্যর্থ। দুই, বন্ধের সময় proxy আগে থেমে গেল, app শেষ অনুরোধটি পাঠাতে পারল না। তিন, একবারের কাজে মূল container শেষ হলেও sidecar চলছে, তাই Job কখনো শেষ হয় না।

মানুষ নানা জোড়াতালি দিতেন। App-এর entrypoint-এ loop লিখে proxy-র জন্য অপেক্ষা করতেন। কাজ শেষে ফাইল লিখে sidecar-কে সংকেত দিতেন, বা Istio-র মতো mesh-এ proxy-র একটি বিশেষ endpoint ডেকে তাকে থামাতেন। প্রতিটি দল নিজের মতো কৌশল বানাত, আর প্রতিটি কৌশল app-কে platform-এর কথা জানাতে বাধ্য করত, যা sidecar-এর মূল উদ্দেশ্যের উল্টো।

সমাধান কেন দেরিতে এলো? কারণ Pod-এর lifecycle Kubernetes-এর সবচেয়ে কেন্দ্রীয় আচরণগুলোর একটি। আলাদা sidecars তালিকা বা container-এর ধরন যোগ করার নানা প্রস্তাব নিয়ে কয়েক বছর আলোচনা হয়েছে। শেষে বেছে নেওয়া হলো একটি কম নাটকীয় নকশা: init container-এর তালিকায় restartPolicy: Always. এতে ক্রম আগে থেকেই পাওয়া যায়, কারণ init container ক্রমেই চলে। বন্ধের সময় উল্টো ক্রমে থামে। আমাদের run-এ job শেষ হওয়ার এক সেকেন্ড পরে log-shipper থেমে Pod Succeeded হলো।

মূল উপলব্ধি: প্ল্যাটফর্মের কেন্দ্রীয় আচরণ বদলানো ধীর কাজ, কারণ লাখো workload পুরোনো আচরণের ওপর দাঁড়িয়ে। ভালো নকশা অনেক সময় নতুন ধারণা নয়, বরং পুরোনো অংশের ছোট একটি সম্প্রসারণ।

প্র ০৩CrashLoopBackOff-এ প্রতিবার অপেক্ষা দ্বিগুণ হয় কেন, ৫ মিনিটে থামে কেন, আর এই নকশার দুর্বলতা কী?

Exponential backoff মূলত একটি ন্যায্যতার হিসাব। ভাঙা container সঙ্গে সঙ্গে restart করলে সে প্রতি সেকেন্ডে node-এর CPU, disk আর log খাবে, আর একই node-এর সুস্থ Pod-গুলো ভুগবে। আবার খুব ধীরে চেষ্টা করলে ক্ষণিকের সমস্যায় সেবা অকারণে বসে থাকবে। দ্বিগুণ করা এই দুইয়ের মাঝখানের পথ: শুরুতে দ্রুত চেষ্টা, সমস্যা দীর্ঘ হলে হাত গুটিয়ে নেওয়া। আমাদের মাপা শুরুর সময়গুলোতে প্রথম restart প্রায় সঙ্গে সঙ্গে, তারপর ১০, ২০, ৪০ সেকেন্ডের ধাপ দেখা গেছে। পর্ব ১১-এর systemd start limit-এর সাথে তুলনা করুন: সেখানে সীমা পেরোলে systemd হাল ছেড়ে দেয়, আর Kubernetes কখনো হাল ছাড়ে না।

৫ মিনিটের ছাদ কেন? ছাদ না থাকলে দ্বিগুণ হতে হতে অপেক্ষা ঘণ্টা ছাড়াত, আর dependency ঠিক হওয়ার পরেও app বহুক্ষণ ঘুমাত। আমাদের run-এ সাত নম্বর restart-এর আগেই অপেক্ষা ৫ মিনিটে আটকে গেছে, যা বিরক্তিকর হলেও সহনীয়।

দুর্বলতাও এখান থেকেই। ধরুন ডেটাবেস ২০ মিনিট বন্ধ ছিল, সব Pod crash loop-এ। ডেটাবেস ফিরে এলেও একটি Pod ৫ মিনিট পর্যন্ত অপেক্ষা করতে পারে। অর্থাৎ recovery-তে অকারণ দেরি। উন্নয়নের সময়ও এটি কষ্ট দেয়: একটি typo ঠিক করে নতুন Pod না বানালে অপেক্ষা করতে হয়। এজন্য নতুন version-গুলোতে এই সংখ্যা কমানোর পরীক্ষামূলক কাজ চলছে, যদিও default বদলানো সাবধানের ব্যাপার, কারণ বড় cluster-এ হাজার Pod দ্রুত restart হলে API server আর node-এর ওপর চাপ বাড়ে। নিজের cluster-এর আচরণ জানতে তার version-এর release notes পড়ুন।

আরেকটি প্রকৌশল শিক্ষা: app নিজেই dependency-র জন্য retry করতে শিখুক। ডেটাবেস না পেলে process মরে না গিয়ে অপেক্ষা করুক আর readiness-এ না বলুক। তাহলে ডেটাবেস ফিরলে Pod কয়েক সেকেন্ডেই traffic নিতে পারে।

মূল উপলব্ধি: Backoff রক্ষা করে সিস্টেমকে, কিন্তু দাম দেয় recovery-র গতি। Crash-কে flow control হিসেবে ব্যবহার করবেন না; সাময়িক সমস্যা app-এর ভেতরে সামলান।

প্র ০৪Readiness probe কি ডেটাবেস দেখবে? দেখলে কী হয়, না দেখলে কী হয়?

এর একমাত্র সঠিক উত্তর নেই, আর ইন্টারভিউতে এই প্রশ্নের আসল পরীক্ষা trade-off বলতে পারা। দুটো চরম অবস্থা ভাবুন।

Readiness ডেটাবেস দেখলে: একটি Pod-এর নিজের connection pool নষ্ট হলে শুধু সেই Pod সরে যায়, বাকিরা traffic নেয়। এটি চমৎকার। কিন্তু ডেটাবেস নিজে ধীর হলে সব Pod একসাথে not ready হয়, Service-এর ready তালিকা খালি হয়। Client তখন connection-এর ভুল বা load balancer-এর 503 পায়, অথচ app হয়তো cache থেকে পণ্যের তালিকা দেখাতে পারত। Liveness-এর মতো সবাইকে মারা হয় না, তবু সব traffic বন্ধ।

Readiness শুধু process দেখলে: ডেটাবেস নষ্ট হলেও Pod-গুলো traffic নেয়, আর প্রতিটি অনুরোধে পরিষ্কার একটি ভুল বার্তা দিতে পারে, অথবা আংশিক সেবা। কিন্তু একটি Pod একা ডেটাবেসে যুক্ত হতে না পারলে সে ব্যর্থ অনুরোধ দিতেই থাকে।

বাস্তবের মাঝামাঝি পথগুলো: readiness দেখবে এই Pod-এর নিজের অবস্থা, যেমন cache গরম হয়েছে কি, config পড়া হয়েছে কি, thread pool খালি আছে কি। Shared dependency-র ব্যর্থতা সামলাবে app-এর ভেতরের timeout, circuit breaker আর degraded mode, যা System Design কোর্সে আছে। ডেটাবেসের check রাখলেও ফলাফল cache করা আর অল্প ব্যর্থতায় not ready না হওয়া ভালো। কারণ probe নিজেও চাপ: ১০০ Pod, period ৫ সেকেন্ড মানে ডেটাবেসে প্রতি সেকেন্ডে ২০টি বাড়তি query, ধীর সময়ে যা আরও ক্ষতি করে।

আরেকটি ব্যবহার হলো নিজেকে সরিয়ে নেওয়া। অতিরিক্ত চাপে Pod সাময়িকভাবে not ready হয়ে দম নিতে পারে। তবে সবাই একসাথে তা করলে বাকিদের চাপ আরও বাড়ে, তাই এটিও সাবধানে করতে হয়।

মূল উপলব্ধি: Probe-এর প্রশ্ন ঠিক করুন “এই Pod-কে সরালে সমস্যা কমবে কি?” দিয়ে। যে সমস্যা সব Pod-এর, তা একটি Pod সরিয়ে কমে না।

প্র ০৫SIGTERM পেলেই app থামালে কিছু অনুরোধ 502 পায় কেন? Kubernetes এই ক্রমটি নিজে কেন ঠিক করে দেয় না?

কারণ Kubernetes একটি distributed system, আর এখানে “Pod সরানো হয়েছে” কোনো এক মুহূর্তের ঘটনা নয়। Pod মোছার অনুরোধ এলে API server deletionTimestamp বসায়। এই একটি পরিবর্তন অনেকে আলাদাভাবে watch করে: kubelet, EndpointSlice controller, আর তারপর প্রতিটি node-এর kube-proxy, ingress controller, cloud load balancer, হয়তো service mesh. প্রত্যেকে নিজের সময়ে খবর পায়, নিজের সময়ে নিয়ম বদলায়। Cloud load balancer-এর ক্ষেত্রে এই দেরি কয়েক সেকেন্ড বা তার বেশি হতে পারে।

কেউ একজন “সবাই নিয়ম বদলেছে” নিশ্চিত করে না। করতে গেলে একটি নষ্ট node-ই সব Pod মোছা আটকে দিত। Kubernetes এখানে eventual consistency বেছে নিয়েছে: সবাই একসময় একমত হবে, কিন্তু কখন, তা নিশ্চিত নয়। পর্ব ৪২-এর reconciliation loop-এর স্বভাবই এই।

ফলে একটি দৌড় তৈরি হয়। kubelet দ্রুত হলে app SIGTERM পেয়ে listener বন্ধ করল, অথচ কোনো node তখনো নতুন অনুরোধ পাঠাচ্ছে। সেই অনুরোধ পায় connection refused, আর সামনের proxy দেখায় 502. ভুল আপনার কোডে নয়, ক্রমে।

সমাধান তিন স্তরে। এক, preStop-এ কয়েক সেকেন্ড অপেক্ষা, যাতে সবাই খবর পাওয়ার সময় পায়। এই সংখ্যা আপনার পথের দীর্ঘতম দেরি দেখে মাপতে হয়, অনুমানে নয়। দুই, app SIGTERM পেয়েও চলমান অনুরোধ শেষ করে, keep-alive connection গুছিয়ে থামে। তিন, grace period সেই দুইয়ের যোগফলের চেয়ে বড় রাখতে হবে: ৫ সেকেন্ড preStop আর সর্বোচ্চ ৩০ সেকেন্ডের অনুরোধ হলে ৩০ সেকেন্ডের default যথেষ্ট নয়। Cloud load balancer-এর জন্য provider-এর নিজস্ব readiness gate-ও থাকতে পারে, যা provider-ভেদে আলাদা।

পরীক্ষাই একমাত্র প্রমাণ। একটি load generator চালিয়ে rolling update করুন, আর ব্যর্থ অনুরোধ গুনুন। সংখ্যা শূন্য না হলে কোথাও ফাঁক আছে।

মূল উপলব্ধি: ভিন্ন ভিন্ন জায়গায় চলা কাজের মধ্যে “আগে” আর “পরে” বিনামূল্যে পাওয়া যায় না। নিশ্চিত ক্রম না পেলে অপেক্ষা আর সহনশীলতা দিয়ে নকশা করতে হয়।

অনুশীলন

হিসাব, ফলাফল অনুমান, নকশা সমালোচনা আর একটি কমান্ডের কাজ। Cluster না থাকলেও প্রথম তিনটি করা যায়।

  1. হিসাব করুন: (ক) একটি container চালু হয়েই crash করে, চলার সময় ০ ধরুন। আমাদের মাপা মডেলে প্রথম শুরু t = ০, প্রথম restart সঙ্গে সঙ্গে, তারপর অপেক্ষা ১০, ২০, ৪০, ৮০, ১৬০, ৩০০ সেকেন্ড। ৭ নম্বর restart কখন? (খ) একটি app চালু হতে সর্বোচ্চ ১২০ সেকেন্ড নেয়, startup probe-এর periodSeconds ১০। failureThreshold কমপক্ষে কত? (গ) Liveness period ১০, threshold ৩, timeout ১, app ঠিক t = ৩৫ সেকেন্ডে আটকে গেল, probe হয় t = ১০, ২০, ৩০…-এ। Kill কখন?

    (ক) Restart ১ t = ০, ২ t = ১০, ৩ t = ৩০, ৪ t = ৭০, ৫ t = ১৫০, ৬ t = ৩১০, ৭ t = ৬১০ সেকেন্ড। অর্থাৎ ১০ মিনিট ১০ সেকেন্ড। আসল kubelet-এ প্রতি ধাপে আরও কয়েক সেকেন্ড যোগ হয়।

    (খ) ১২০ ÷ ১০ = ১২। কমপক্ষে ১২, আর নিরাপত্তার জন্য একটু বেশি, যেমন ১৫ (budget ১৫০ সেকেন্ড)।

    (গ) ব্যর্থ probe t = ৪০, ৫০, ৬০. তৃতীয় ব্যর্থতায় t = ৬০-এ kill, অর্থাৎ আটকে যাওয়ার ২৫ সেকেন্ড পরে।

  2. ফলাফল অনুমান করুন: তিনটি Pod, প্রত্যেকের একটি container, যা sh -c "exit 3" চালায়। Policy যথাক্রমে Never, OnFailure, Always. ২৫ সেকেন্ড পরে প্রত্যেকের phase কী, আর RESTARTS শূন্য কি না? চতুর্থ Pod exit 0, policy OnFailure: তার phase আর STATUS কী?

    Never: phase Failed, STATUS Error, RESTARTS ০। OnFailure আর Always: phase Running, RESTARTS শূন্যের বেশি, STATUS সময়ভেদে Error বা CrashLoopBackOff. আমাদের run-এ OnFailure আর exit 3-এ ২৫ সেকেন্ডে RESTARTS ২, phase Running. চতুর্থটি: phase Succeeded, STATUS Completed, RESTARTS ০।

  3. নকশা সমালোচনা করুন: একটি দল production-এ এমন Pod চালাচ্ছে: bare Pod; image: shop:latest; কোনো resources নেই; livenessProbe /health-এ, যা ডেটাবেসে একটি query চালায়, periodSeconds: 2, failureThreshold: 1, timeoutSeconds: 1; readiness নেই; app চালু হতে ৬০ সেকেন্ড লাগে; terminationGracePeriodSeconds: 5, আর সবচেয়ে ধীর অনুরোধ ২০ সেকেন্ড। অন্তত ছয়টি সমস্যা বলুন, আর প্রতিটির সমাধান দিন।
    • Bare Pod: node নষ্ট হলে ফেরে না। Deployment ব্যবহার করুন।
    • latest: কোন version চলছে জানা নেই। নির্দিষ্ট tag বা digest দিন।
    • Resource নেই: Scheduler অন্ধ, node-এর memory-তে ঝুঁকি। Requests আর memory limit দিন।
    • Liveness ডেটাবেস ছোঁয়: ডেটাবেস ধীর হলে সবাই restart. শুধু process দেখুক।
    • Threshold ১, period ২: এক ঝাঁকুনিতেই kill. Period ১০, threshold ৩-এর মতো ঢিলে রাখুন।
    • Startup probe নেই: ৬০ সেকেন্ডের চালু ২ সেকেন্ডের liveness-এ কখনো শেষ হবে না, চিরকাল restart. Startup probe দিন, budget অন্তত ৬০ সেকেন্ড, ভালো হয় ৯০।
    • Readiness নেই: চালুর আগেই traffic. একটি readiness probe দিন।
    • Grace ৫: ২০ সেকেন্ডের অনুরোধ মাঝপথে কাটা পড়ে। Grace অন্তত ৩০ দিন, preStop থাকলে তাও যোগ করুন।
  4. কমান্ডের কাজ: (ঐচ্ছিক, নিজের local cluster-এ) ধরুন api-7f9 নামের Pod CrashLoopBackOff-এ। Image-এ shell নেই। কোনো কিছু না মুছে কারণ খুঁজতে কোন কমান্ড কোন ক্রমে চালাবেন, আর প্রতিটিতে কী খুঁজবেন?

    ১. kubectl describe pod api-7f9: Last State-এর Reason আর Exit Code, আর Events-এ probe ব্যর্থতা। OOMKilled হলে memory, liveness ব্যর্থতা হলে probe.

    ২. kubectl logs api-7f9 --previous: মরে যাওয়া container-এর শেষ লাইন।

    ৩. Container চললে kubectl debug api-7f9 --image=alpine:3.20 --target=<container> --profile=general: বাইরে থেকে টুল এনে দেখা। Container খুব দ্রুত মরলে kubectl debug … --copy-to=api-debug দিয়ে Pod-এর কপিতে দেখা যায়, যা পরে নাম ধরে মুছতে হবে।

    ৪. Pod-টি controller-এর হলে মূল কারণ ঠিক করে নতুন version deploy করুন। --force বা Pod মোছা এখানে প্রমাণই হারায়।

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

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

ক্রমএকটি Pod তৈরি থেকে traffic পাওয়া পর্যন্ত ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. API server Pod-টি জমা রাখে, phase Pending
  2. Scheduler একটি node ঠিক করে
  3. kubelet sandbox আর Pod-এর network তৈরি করে
  4. Init container-গুলো ক্রমে চলে শেষ হয়
  5. App container চালু হয়, phase Running
  6. Readiness probe সফল হয়, Pod Service-এর ready তালিকায় আসে

Kubernetes-এ kubectl get pods দেখাচ্ছে STATUS CrashLoopBackOff. ওই Pod-এর status.phase সাধারণত কী হয় (policy Always)?

  • Pending
  • Running
  • Failed
  • CrashLoopBackOff

একটি Pod-এর readiness probe পরপর ব্যর্থ হলো। কী ঘটবে?

  • Container restart হবে
  • Pod মুছে নতুন Pod আসবে
  • Pod চলতে থাকবে, কিন্তু Service আর তাকে traffic পাঠাবে না
  • Pod-এর phase Failed হবে

একটি container-এর Last State: Reason: OOMKilled, Exit Code: 137. এর সবচেয়ে সঠিক ব্যাখ্যা কোনটি?

  • Memory limit ছাড়ানোয় kernel process-কে SIGKILL পাঠিয়েছে
  • Process SIGTERM পেয়ে থেমেছে
  • Liveness probe ব্যর্থ হয়েছে
  • Image নামানো ব্যর্থ হয়েছে

একটি app চালু হতে সর্বোচ্চ ৯০ সেকেন্ড নেয়। চালুর পরে hang দ্রুত ধরতে হবে। Kubernetes-এ সবচেয়ে ভালো ব্যবস্থা কোনটি?

  • Liveness-এর failureThreshold 100 করা
  • Liveness-এ initialDelaySeconds: 90 দেওয়া
  • Liveness বাদ দিয়ে শুধু readiness রাখা
  • Startup probe period 5, failureThreshold 30, আর আলাদা কড়া liveness probe

terminationGracePeriodSeconds: 30, preStop চলে ২৫ সেকেন্ড, আর SIGTERM পেয়ে app-এর ১০ সেকেন্ড লাগে। কী ঘটবে?

  • App পুরো ১০ সেকেন্ড পাবে, কারণ preStop আলাদা হিসাব হয়
  • App ৫ সেকেন্ড পাবে, তারপর SIGKILL
  • preStop কেটে দেওয়া হবে, SIGTERM যাবে না
  • kubelet সীমা বাড়িয়ে ৬০ সেকেন্ড করবে

Kubernetes-এ একই Pod-এর দুই container সম্পর্কে কোনটি সত্য?

  • তাদের পুরো filesystem স্বয়ংক্রিয়ভাবে ভাগ করা
  • তারা আলাদা node-এ চলতে পারে
  • তারা একই IP ভাগ করে, localhost দিয়ে কথা বলতে পারে
  • তাদের image আর memory limit এক হতেই হবে

শব্দকোষ

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

মনে রাখুন

  1. Pod হলো schedule, network আর জীবনের একক। ভেতরের container-গুলো IP আর ইচ্ছে করলে volume ভাগ করে।
  2. STATUS phase নয়। Container state, exit code আর Reason পড়ুন।
  3. Liveness ব্যর্থ হলে restart, readiness ব্যর্থ হলে traffic বন্ধ, startup চালুর সময় ঢাকে। Liveness কখনো dependency ছোঁবে না।
  4. মোছার ক্রম: preStop → SIGTERM → সীমা ফুরালে SIGKILL. Service থেকে সরা পাশাপাশি চলে, আর পুরো সীমা terminationGracePeriodSeconds.
  5. Debug: describe → logs --previous → exec → kubectl debug. আর production-এ bare Pod নয়।

পরের পর্বে: Pod নশ্বর, তাহলে কে তাকে ফিরিয়ে আনে? Deployment আর ReplicaSet: সংখ্যা ধরে রাখা, rolling update আর rollback.

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

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