Kubernetes Architecture — Control Plane আর Worker Node-এর স্থাপত্য
Kubernetes Architecture: Control Plane and Worker Nodes
শত শত container, বহু মেশিন জুড়ে — কে ঠিক করবে কোনটা কোথায় চলবে, কে ব্যর্থতা সামলাবে, কে ট্রাফিক রুট করবে? Kubernetes এই প্রশ্নের উত্তর — Google-এর Borg-এর সরাসরি উত্তরসূরি একটা orchestrator, যার control plane (API server, etcd, scheduler, controller-manager) আর worker node (kubelet, kube-proxy, container runtime) স্পষ্টভাবে আলাদা। etcd নিজের consistency পায় Raft থেকে — ঠিক সেই protocol যা আগের লেসনে হাতে বানানো হয়েছিল — আর controller-manager-এর reconciliation loop-ই Kubernetes-এর কেন্দ্রীয় ডিজাইন-দর্শন।
আগে এটা বুঝি
virtualization-cloud/container-networking লেসন একটা প্রশ্ন রেখে শেষ হয়েছিল যেটা এতদিন এড়ানো হয়েছিল: veth pair, bridge, আর NAT দিয়ে একটা মেশিনে দশটা container-কে একে অপরের সাথে আর বাইরের দুনিয়ার সাথে কথা বলানো গেছে — কিন্তু production-এ দশটা container একটা মেশিনে থাকে না। থাকে হাজার হাজার container, শত শত মেশিন জুড়ে ছড়ানো। আর তখন প্রশ্নগুলো বদলে যায়:
কোন container ঠিক কোন মেশিনে চলবে? কেউ crash করলে কে টের পাবে, কে নতুন একটা তুলবে? নতুন একটা মেশিন যোগ হলে কে জানবে সেটা ব্যবহারযোগ্য? আর এই হাজারটা সিদ্ধান্ত কে একটানা, ম্যানুয়াল হস্তক্ষেপ ছাড়াই নিতে থাকবে?
container-runtime-oci লেসন দেখিয়েছে একটা একক container ঠিক কীভাবে চালু হয় — runc, OCI spec, namespace/cgroup সেটআপ। container-networking দেখিয়েছে দুইটা container কীভাবে একটা মেশিনে বা মেশিন জুড়ে কথা বলে। কিন্তু কোনোটাই বলে না — এই হাজারটা container-এর মধ্যে কোনটা কোথায় বসবে, সেই সিদ্ধান্তটা কে নেয়। এই ফাঁকটাই আজকের লেসনের বিষয়: orchestration।
এই সমস্যাটা নতুন না। Google নিজের data center-এ দশকের পর দশক ধরে ঠিক এই সমস্যার মুখোমুখি হয়েছে — হাজার হাজার application, লক্ষ লক্ষ job, প্রতিদিন। তাদের অভ্যন্তরীণ সমাধানের নাম Borg — একটা cluster manager যা resource allocation, scheduling, আর fault-recovery একসাথে সামলায় (Verma et al., EuroSys 2015)। ২০১৪ সালে Google যখন Kubernetes open-source করে, এটা কাকতালীয় ছিল না যে অনেক মূল ডিজাইন-সিদ্ধান্ত — declarative configuration, label-based grouping, একটা কেন্দ্রীয় API-চালিত স্থাপত্য — সরাসরি Borg থেকে ধার করা। Kubernetes-এর অনেক মূল লেখক নিজেরাই আগে Borg টিমে কাজ করেছিলেন। এই লেসনে আমরা যা শিখব তার প্রায় পুরোটাই — আলাদা control plane, declarative desired-state, continuous reconciliation — Borg-এর দশকের অভিজ্ঞতা থেকে পাতিত একটা design।
আজকের কাজ: Kubernetes-এর architecture-কে দুইটা স্পষ্ট অর্ধে ভাঙা — control plane (সিদ্ধান্ত নেয়) আর worker node (সিদ্ধান্ত কার্যকর করে) — আর control plane-এর প্রতিটা অংশ ঠিক কী দায়িত্ব পালন করে তা নির্ভুলভাবে বোঝা। এই লেসনের একটা কেন্দ্রীয় payoff: etcd, যেটা পুরো cluster-এর state ধরে রাখে, নিজে নিরাপদ থাকে ঠিক সেই Raft protocol দিয়ে যা distributed-systems/raft লেসনে হাতে বানানো হয়েছিল — leader, term, log replication, majority commit, সবকিছু। এটা কোনো নতুন consensus algorithm না; এটা সেই একই ইঞ্জিন, একটা নতুন জায়গায় খাটানো।
মূল ধারণা
Control plane বনাম worker node — একটা বাক্যে
পুরো Kubernetes architecture-কে একটা বাক্যে ভাঙা যায়:
Control plane সিদ্ধান্ত নেয় — কী চলা উচিত, কোথায় চলা উচিত। Worker node সেই সিদ্ধান্ত কার্যকর করে — আসলে container চালায়।
এই বিভাজনটা স্থাপত্যগতভাবে কড়া — control plane সাধারণত আলাদা, dedicated “master” node-এ চলে (production cluster-এ প্রায়ই একাধিক, high-availability-এর জন্য), আর worker node আলাদা মেশিনে, যেগুলোর একমাত্র কাজ প্রকৃত application workload চালানো।
লক্ষ্য করুন শেষ সারিটা — worker node-এর container runtime আক্ষরিক অর্থেই সেই একই স্ট্যাক যা container-runtime-oci লেসনে দেখা হয়েছে। Kubernetes কোনো নতুন কম-স্তরের isolation mechanism আবিষ্কার করে না — namespace, cgroup, OCI runtime spec, সবকিছু অপরিবর্তিত। Kubernetes যা যোগ করে তা একটা সম্পূর্ণ নতুন স্তর — কোন container কোথায়, কেন, আর কীভাবে সেই সিদ্ধান্ত টিকিয়ে রাখা হবে তার orchestration।
etcd — একমাত্র সত্য উৎস, আর তার Raft-ভিত্তি
Control plane-এর প্রতিটা component একটা জিনিসের ওপর নির্ভর করে: cluster-এর সম্পূর্ণ, নির্ভুল বর্তমান অবস্থা। কোন Pod কোথায় চলছে, কোন Deployment-এর কয়টা replica চাওয়া হয়েছে, কোন Service কোন Pod-দের দিকে নির্দেশ করে — এই সবকিছুর একমাত্র, কর্তৃপক্ষীয় (authoritative) কপি থাকে etcd-তে। এটা কোনো cache না, কোনো secondary copy না — এটাই the single source of truth। API server নিজেও কোনো state নিজের মেমরিতে ধরে রাখে না দীর্ঘমেয়াদে; সবকিছু etcd-তে যায়।
কিন্তু etcd নিজেও একটা distributed system — সাধারণত ৩ বা ৫টা replica, যেকোনো একটা মেশিন হারিয়ে গেলেও cluster state যেন হারিয়ে না যায়। আর এখানেই এই লেসনের কেন্দ্রীয় সংযোগ:
etcd-এর প্রতিটা write একটা Raft log entry — যা আপনি
distributed-systems/raftলেসনে বানিয়েছিলেন, ঠিক সেই ইঞ্জিনই এখানে পুরো Kubernetes cluster-এর state ধরে রাখে।
কোনো নতুন প্রোটোকল নেই এখানে শেখার। etcd-এর তিন-পাঁচটা replica-র মধ্যে একজন leader — ঠিক raft লেসনের সেই leader, নিজের term-এ নির্বাচিত। যখন কোনো control plane component (API server-এর মাধ্যমে) একটা write পাঠায় — ধরুন “Pod X-এর status আপডেট করো” — সেটা etcd leader-এর কাছে যায়, leader তার নিজের log-এ entry হিসেবে append করে, followers-দের AppendEntries পাঠায় (হুবহু raft লেসনের সেই RPC), majority replicate করলেই entry committed — আর তখনই write সফল বলে ধরা হয়।
এর ব্যবহারিক ফল বিশাল: Kubernetes নিজে কোনো নতুন fault-tolerance যুক্তি বানায়নি — cluster-state-এর নিরাপত্তা সম্পূর্ণভাবে Raft-এর প্রমাণিত guarantee-র ওপর হাত ধোয়। একটা etcd replica মেশিন হারালে, বাকি majority-ই যথেষ্ট চালিয়ে যেতে; একটা leader crash করলে নতুন election হয় (raft লেসনের randomized timeout, distributed-systems/leader-election-এর vocabulary-তে), আর raft লেসনের Leader Completeness guarantee নিশ্চিত করে কোনো committed cluster-state হারায় না।
ভেতরে কী ঘটছে
API server — একমাত্র দরজা, আর কেন সেটা ইচ্ছাকৃত
এখানে একটা design decision আছে যেটা প্রথম শুনলে অদ্ভুত লাগতে পারে: scheduler, controller-manager, kubelet — control plane-এর বা worker node-এর কোনো component-ই কখনো সরাসরি etcd-তে read বা write করে না। সবাই — ব্যতিক্রম ছাড়া — cluster state ছোঁয় শুধুমাত্র API server-এর REST/watch API-এর মধ্য দিয়ে।
scheduler ─┐
controller-manager ─┼──► API server ──► etcd (একমাত্র সরাসরি সংযোগ)
kubelet ─┤ ▲
kubectl (ব্যবহারকারী) ─┘ │
সবার একমাত্র দরজাএই সিদ্ধান্তের পেছনে দুইটা concrete কারণ:
১. একটা একক জায়গায় authorization, validation, আর admission control। যদি প্রতিটা component সরাসরি etcd-তে লিখতে পারত, তাহলে প্রতিটাকে আলাদাভাবে নিশ্চিত করতে হতো যে লেখাটা বৈধ (schema মানছে কি না), অনুমোদিত (এই component-এর এই লেখার permission আছে কি না), আর নীতি মেনে চলছে (কোনো admission-control policy লঙ্ঘন করছে না)। API server একবার এই যুক্তিগুলো implement করে, আর সব traffic বাধ্যতামূলকভাবে তার মধ্য দিয়ে যায় — একটা centralized checkpoint, বহু জায়গায় ছড়ানো নীতি না।
২. একটা স্থিতিশীল interface, ভেতরের storage বদলালেও। API server-এর ওপরের দিকের REST/watch API একটা contract — scheduler বা kubelet এই contract-এর ওপর নির্ভর করে, etcd-এর নিজের internal detail-এর ওপর না। এর মানে etcd-এর স্টোরেজ ফরম্যাট বদলালে, বা (তাত্ত্বিকভাবে) সম্পূর্ণ ভিন্ন একটা backend দিয়ে বদলালেও, বাকি সব component-এর কোনো কোড বদলাতে হয় না — শুধু API server-এর ভেতরের implementation বদলাবে।
Scheduler — filtering, তারপর scoring, তারপর শুধু একটা রেকর্ড
যখনই একটা নতুন Pod তৈরি হয় যার কোনো node assign করা নেই, scheduler (তার watch-এর মাধ্যমে) সেটা টের পায় আর দুই-ধাপের একটা algorithm চালায়:
ধাপ ১ — Filtering: কোন node-গুলো আদৌ এই Pod চালাতে পারে? প্রতিটা node পরীক্ষা করা হয় resource request (CPU/memory যথেষ্ট free আছে কি না), taints/tolerations (node-এ কোনো taint থাকলে Pod-এর সেই taint সহ্য করার toleration আছে কি না), আর affinity/anti-affinity rule-এর বিরুদ্ধে। যে node-গুলো কোনো একটাতেও ব্যর্থ হয়, তারা বাদ — বাকিদের বলা হয় feasible set।
ধাপ ২ — Scoring: feasible node-গুলোর মধ্যে কোনটা সবচেয়ে ভালো? প্রতিটা feasible node-কে একটা স্কোর দেওয়া হয় — সাধারণ একটা মাপকাঠি হলো resource fragmentation কমানো: এই Pod বসানোর পর node-এর CPU% আর memory% ব্যবহার যত কাছাকাছি (balanced), স্কোর তত ভালো, কারণ একটা node-এর CPU পুরো ব্যবহার হয়ে গিয়ে memory অনেকটা ফাঁকা থাকা মানে সেই ফাঁকা memory আসলে অন্য কোনো Pod-এর জন্য কার্যত অব্যবহারযোগ্য (CPU-ই bottleneck)।
Controller-manager — reconciliation loop, Kubernetes-এর কেন্দ্রীয় দর্শন
Kubernetes-এ ব্যবহারকারী কখনো সরাসরি বলে না “একটা Pod চালাও”। তারা বলে “সবসময় ৩টা replica চলা উচিত” — এটা একটা desired state-এর ঘোষণা, একটা imperative কমান্ড না। এই ঘোষণা কার্যকর রাখার দায়িত্ব controller-manager-এর — যেটা আসলে একগুচ্ছ ছোট controller একসাথে চালায় (Deployment controller, ReplicaSet controller, Node controller, ইত্যাদি), প্রতিটা নিজের resource type-এর জন্য দায়ী।
প্রতিটা controller-ই একটা identical প্যাটার্ন follow করে, যেটা Kubernetes-এর সবচেয়ে গুরুত্বপূর্ণ ডিজাইন-দর্শন:
লুপ, অনন্তকাল ধরে:
desired = etcd-এ (API server-এর মাধ্যমে) ব্যবহারকারী যা ঘোষণা করেছে
observed = আসলে এই মুহূর্তে cluster-এ কী চলছে
gap = desired - observed
gap != 0 হলে -> ঠিক যতটুকু action লাগবে ততটুকুই action নাও
আবার শুরু থেকেএই প্যাটার্নের নাম declarative reconciliation। লক্ষ্য করুন এটা কোনো একবারের script না — এটা একটা অনন্ত লুপ, যেটা ক্রমাগত (প্রতি নতুন event-এ, আর নিয়মিত বিরতিতেও, নিরাপত্তার জন্য) নিজের কাজ পুনরায় করে। এই পুনরাবৃত্তিই Kubernetes-কে self-healing বানায় — কেউ যদি ম্যানুয়ালি একটা Pod মুছে দেয়, বা একটা node ক্র্যাশ করে দুইটা Pod হারিয়ে যায়, controller পরবর্তী পাসেই gap দেখে আবার সেগুলো তৈরি করবে, কোনো মানুষের হস্তক্ষেপ ছাড়াই।
kubelet — node-এর নিজস্ব এজেন্ট, CRI দিয়ে
Worker node-এ বসে থাকা kubelet-ই সেই কম্পোনেন্ট যেটা প্রকৃতপক্ষে scheduler-এর সিদ্ধান্ত আর controller-manager-এর reconciliation-কে বাস্তব container-এ রূপান্তরিত করে। kubelet নিজের node-এ assign হওয়া Pod-গুলোর জন্য API server-কে watch করে; নতুন কিছু দেখলে, সেটা CRI (Container Runtime Interface) — একটা standardized gRPC API — দিয়ে স্থানীয় container runtime-কে (আক্ষরিক অর্থেই সেই containerd/runc স্ট্যাক, container-runtime-oci লেসনের বিষয়) নির্দেশ দেয় container চালু করতে।
kubelet-এর লুপ (প্রতিটা worker node-এ, স্বাধীনভাবে):
watch API server: "আমার node-এ assign হওয়া Pod-গুলো কী?"
প্রতিটা Pod-এর জন্য: CRI দিয়ে container runtime-কে বলো — চালু/বন্ধ/আপডেট
নিয়মিত: node আর Pod-এর স্বাস্থ্য/status API server-কে জানাও (আরেকটা observed-state input)CRI-এর গুরুত্ব: এটা kubelet-কে কোনো নির্দিষ্ট container runtime-এর সাথে আটকে রাখে না। containerd, CRI-O, বা অন্য যেকোনো CRI-compliant runtime বসানো যায় — kubelet একই API-তে কথা বলে, ঠিক যেমন API server-এর REST/watch API বাকি সব component-কে etcd-এর internal detail থেকে আলাদা রাখে। একই abstraction-এর দর্শন, দুই জায়গায়।
উদাহরণ
একটা Pod তৈরির সম্পূর্ণ পথ — প্রতিটা component এক জায়গায়
kubectl apply -f pod.yaml চালানোর পর ঠিক কী ঘটে — এই একটা trace-এই আজকের প্রতিটা component-এর ভূমিকা এক জায়গায় দেখা যায়:
- kubectl -> API serverনতুন Pod object POST করা হয় — schema validation + admission control এখানেই হয়
- API server -> etcd (Raft write)নতুন Pod entry etcd-এর Raft log-এ যায়; majority replicate হলে committed
- scheduler-এর watch trigger"no node assigned" Pod দেখে scheduler সাড়া দেয় — filtering তারপর scoring
- scheduler -> API server: Binding লেখাscheduler নিজে container চালায় না — শুধু node-assignment রেকর্ড করে (আবার একটা etcd Raft write, API server-এর মাধ্যমে)
- kubelet-এর watch trigger (সেই node-এ)assigned node-এর kubelet Binding দেখে, CRI দিয়ে container runtime-কে নির্দেশ দেয়
- container runtime: namespace/cgroup সেটআপcontainer-runtime-oci লেসনের সেই একই runc/containerd flow — নতুন কোনো mechanism না
- kubelet -> API server: status আপডেটPod এখন Running — এই status-ও আবার etcd-তে যায়, controller-manager-এর পরবর্তী reconcile pass-এর observed-state input
এই trace-এ লক্ষ্য করার মতো দুইটা জিনিস: প্রথমত, প্রতিটা তীর যেটা “etcd” স্পর্শ করেছে তার আগে একটা “API server” আছে — কোথাও সরাসরি কোনো component etcd-কে ছোঁয়নি। দ্বিতীয়ত, শেষ ধাপটা (status আপডেট) প্রথম ধাপের (Pod তৈরি) মতোই একটা write — reconciliation loop-এর জন্য “observed state” আসলে ঠিক এভাবেই তৈরি হয়, প্রতিটা component নিজের observation-কে আবার API server-এর মধ্য দিয়ে ফিরিয়ে দিয়ে।
ছবিতে
Control plane আর worker node — পাশাপাশি
┌─────────────────────── CONTROL PLANE (dedicated master node) ───────────────────────┐
│ │
│ ┌───────────┐ ┌──────────────┐ ┌───────────┐ │
│ │ scheduler │◄────►│ │◄────►│controller- │ │
│ └───────────┘ │ │ │ manager │ │
│ │ API server │ └───────────┘ │
│ ┌───────────┐ │ (একমাত্র │ │
│ │ kubectl │◄────►│ দরজা) │ │
│ │ (ব্যবহারকারী) │ │ │ │
│ └───────────┘ └──────┬───────┘ │
│ │ একমাত্র সরাসরি সংযোগ │
│ ▼ │
│ ┌─────────────┐ │
│ │ etcd │ ◄── Raft leader + followers (distributed- │
│ │ (3-5 নোড, │ systems/raft লেসনের সেই একই protocol) │
│ │ Raft-ভিত্তিক)│ │
│ └─────────────┘ │
└──────────────────────────────────┬───────────────────────────────────────────────────┘
│ watch API (push, poll না)
┌─────────────────────┼─────────────────────┐
▼ ▼
┌──────── WORKER NODE 1 ────────┐ ┌──────── WORKER NODE 2 ────────┐
│ ┌─────────┐ ┌───────────┐ │ │ ┌─────────┐ ┌───────────┐ │
│ │ kubelet │──►│kube-proxy │ │ │ │ kubelet │──►│kube-proxy │ │
│ └────┬────┘ └───────────┘ │ │ └────┬────┘ └───────────┘ │
│ │ CRI │ │ │ CRI │
│ ▼ │ │ ▼ │
│ ┌─────────────────┐ │ │ ┌─────────────────┐ │
│ │ container runtime│ │ │ │ container runtime│ │
│ │ (runc/containerd │ │ │ │ (runc/containerd │ │
│ │ — অপরিবর্তিত) │ │ │ │ — অপরিবর্তিত) │ │
│ └─────────────────┘ │ │ └─────────────────┘ │
│ [Pod] [Pod] [Pod] │ │ [Pod] [Pod] │
└────────────────────────────────┘ └────────────────────────────────┘উপরের ছবিতে একটা জিনিস ইচ্ছাকৃতভাবে জোর দেওয়া হয়েছে — API server-এর দিকে সব তীর ঢোকে, কিন্তু etcd-এর দিকে তীর যায় শুধু একটাই জায়গা থেকে। এটাই “single point of entry” design decision-এর দৃশ্যমান রূপ।
নিজে চালিয়ে দেখুন
Scheduler simulation — filtering তারপর scoring, বাস্তবে চালিয়ে
# scheduler_sim.py -- toy two-phase Kubernetes scheduler: FILTERING then
# SCORING. The scheduler only ever DECIDES and RECORDS a binding -- it
# never itself starts a container (that's kubelet's job, on the node).
NODES = {
"node-a": {"cpu_total": 4000, "cpu_used": 3200, "mem_total": 8192, "mem_used": 5000,
"taints": []},
"node-b": {"cpu_total": 8000, "cpu_used": 2000, "mem_total": 16384, "mem_used": 4000,
"taints": []},
"node-c": {"cpu_total": 4000, "cpu_used": 500, "mem_total": 8192, "mem_used": 1000,
"taints": ["gpu-only"]},
"node-d": {"cpu_total": 8000, "cpu_used": 6000, "mem_total": 16384, "mem_used": 14000,
"taints": []},
}
POD = {"name": "web-7f9c-xk2p1", "cpu_request": 1000, "mem_request": 2048,
"tolerations": []} # ordinary pod, tolerates nothing special
def filter_nodes(pod, nodes):
"""Phase 1: which nodes CAN even run this pod?"""
feasible = []
rejected = {}
for name, n in nodes.items():
cpu_free = n["cpu_total"] - n["cpu_used"]
mem_free = n["mem_total"] - n["mem_used"]
reasons = []
if cpu_free < pod["cpu_request"]:
reasons.append(f"cpu_free={cpu_free}m < request={pod['cpu_request']}m")
if mem_free < pod["mem_request"]:
reasons.append(f"mem_free={mem_free}Mi < request={pod['mem_request']}Mi")
untolerated = [t for t in n["taints"] if t not in pod["tolerations"]]
if untolerated:
reasons.append(f"untolerated taints={untolerated}")
if reasons:
rejected[name] = reasons
else:
feasible.append(name)
return feasible, rejected
def score_node(name, pod, nodes):
"""Phase 2: among feasible nodes, score by post-placement resource
BALANCE -- how evenly CPU% and memory% would be used after placing
this pod (closer to balanced = less fragmentation = higher score)."""
n = nodes[name]
cpu_pct = (n["cpu_used"] + pod["cpu_request"]) / n["cpu_total"]
mem_pct = (n["mem_used"] + pod["mem_request"]) / n["mem_total"]
imbalance = abs(cpu_pct - mem_pct)
score = round((1 - imbalance) * 100)
return score, cpu_pct, mem_pct
print(f"scheduling pod: {POD['name']} (requests: {POD['cpu_request']}m CPU, {POD['mem_request']}Mi mem)\n")
feasible, rejected = filter_nodes(POD, NODES)
print("=== phase 1: FILTERING ===")
for name, reasons in rejected.items():
print(f" {name}: REJECTED -- {'; '.join(reasons)}")
for name in feasible:
print(f" {name}: feasible")
print(f" feasible set: {feasible}\n")
print("=== phase 2: SCORING (balanced-resource scoring) ===")
scores = {}
for name in feasible:
score, cpu_pct, mem_pct = score_node(name, POD, NODES)
scores[name] = score
print(f" {name}: post-placement cpu={cpu_pct:.1%} mem={mem_pct:.1%} "
f"imbalance={abs(cpu_pct - mem_pct):.1%} -> score={score}")
winner = max(scores, key=scores.get)
print(f"\n=== binding decision ===")
print(f" scheduler writes to API server: Binding({POD['name']} -> {winner})")
print(f" (scheduler does NOT start the container -- kubelet on {winner} will,")
print(f" after it observes this Binding via the API server's watch)")প্রকৃত চালিয়ে পাওয়া output (deterministic — হুবহু এটাই আসবে):
scheduling pod: web-7f9c-xk2p1 (requests: 1000m CPU, 2048Mi mem)
=== phase 1: FILTERING ===
node-a: REJECTED -- cpu_free=800m < request=1000m
node-c: REJECTED -- untolerated taints=['gpu-only']
node-b: feasible
node-d: feasible
feasible set: ['node-b', 'node-d']
=== phase 2: SCORING (balanced-resource scoring) ===
node-b: post-placement cpu=37.5% mem=36.9% imbalance=0.6% -> score=99
node-d: post-placement cpu=87.5% mem=97.9% imbalance=10.4% -> score=90
=== binding decision ===
scheduler writes to API server: Binding(web-7f9c-xk2p1 -> node-b)
(scheduler does NOT start the container -- kubelet on node-b will,
after it observes this Binding via the API server's watch)লক্ষ্য করুন filtering-এই node-a (অপর্যাপ্ত CPU) আর node-c (untolerated taint) বাদ পড়ে যায় — scoring পর্যন্ত পৌঁছায়ই না। বাকি দুইটার মধ্যে node-d-এর raw free capacity বেশি মনে হতে পারে, কিন্তু ওখানে Pod বসালে node প্রায় পুরো capacity-তে চলে যেত (CPU 87.5%, memory প্রায় ৯৮%) — একটা সংকীর্ণ, fragmented অবস্থা। node-b অনেক বেশি balanced থেকে যায়, তাই জেতে। এটাই “least resource fragmentation” স্কোরিং-এর মূল যুক্তি — শুধু “কার বেশি জায়গা আছে” না, “কার জায়গাটা placement-এর পরেও ব্যবহারযোগ্য থেকে যায়”।
Scheduler-এর দুই-ধাপের algorithm — কোন node feasible, তারপর কোনটা best-scored — বাস্তবে চালিয়ে দেখানো যায় শুধু arithmetic দিয়ে, আর শেষে scheduler শুধু একটা Binding রেকর্ড করে, container চালু করে না।
নিজে বানান
ReconciliationLoop — desired বনাম observed state, node-failure-সহ
- ToyController ক্লাস বানান — desired_replicas (int) আর observed_pods (list) ধরে রাখবে
- reconcile() লিখুন — observed < desired হলে ঠিক যতটুকু দরকার ততটুকু create, observed > desired হলে ঠিক ততটুকু delete
- কয়েক iteration চালান — শুরুতে খালি observed, তারপর scale-up, তারপর একটা simulated node failure (কিছু observed pod হারিয়ে যাওয়া), তারপর আবার reconcile
- দেখুন controller প্রতিবার ঠিক gap-টুকুই বন্ধ করে — না বেশি, না কম
লক্ষ্য: controller-manager-এর reconciliation loop প্যাটার্নটা শুধু বাক্যে না রেখে code-এ ঘটিয়ে দেখা — বিশেষভাবে, একটা node-failure-এর পর controller কীভাবে নিজে থেকেই, কোনো manual trigger ছাড়াই, ঠিক যা হারিয়েছে তা আবার তৈরি করে।
# reconcile.py -- toy simulation of the Kubernetes controller-manager's
# core pattern: a reconciliation loop that compares DESIRED state (what
# the user declared) against OBSERVED state (what's actually running) and
# computes the exact create/delete actions needed to converge. Modeled
# after the ReplicaSet/Deployment controller, stripped to its essence.
import itertools
pod_id_counter = itertools.count(1)
class ToyController:
"""Holds desired replica count + observed running toy-pods, and
reconciles the gap -- exactly the pattern every Kubernetes controller
(Deployment, ReplicaSet, Job, ...) repeats, each on its own resource."""
def __init__(self, name, desired_replicas):
self.name = name
self.desired_replicas = desired_replicas
self.observed_pods = [] # list of pod ids currently "running"
def reconcile(self):
"""One pass of the reconciliation loop. Returns (create, delete)."""
current = len(self.observed_pods)
desired = self.desired_replicas
actions_create, actions_delete = [], []
if current < desired:
need = desired - current
for _ in range(need):
new_id = f"pod-{next(pod_id_counter)}"
self.observed_pods.append(new_id)
actions_create.append(new_id)
elif current > desired:
excess = current - desired
for _ in range(excess):
removed = self.observed_pods.pop()
actions_delete.append(removed)
return actions_create, actions_delete
def kill(self, pod_id):
"""Simulate a node failure taking a pod down -- OBSERVED state
drifts away from DESIRED without anyone asking for it."""
if pod_id in self.observed_pods:
self.observed_pods.remove(pod_id)
def status(self):
return f"desired={self.desired_replicas} observed={sorted(self.observed_pods)}"
def report(step, ctrl, created, deleted):
print(f"--- iteration {step}: {ctrl.name} ---")
print(f" before reconcile desired={ctrl.desired_replicas}")
if created:
print(f" CREATE: {created}")
if deleted:
print(f" DELETE: {deleted}")
if not created and not deleted:
print(" no action -- observed already matches desired")
print(f" after reconcile -> {ctrl.status()}")
print()
ctrl = ToyController("web-deployment", desired_replicas=3)
# iteration 1: nothing observed yet -- controller must create 3
c, d = ctrl.reconcile()
report(1, ctrl, c, d)
# iteration 2: steady state -- observed already matches desired, no-op
c, d = ctrl.reconcile()
report(2, ctrl, c, d)
# iteration 3: user edits the Deployment, scales desired 3 -> 5
ctrl.desired_replicas = 5
c, d = ctrl.reconcile()
report(3, ctrl, c, d)
# iteration 4: simulated node failure -- two running pods vanish without
# the controller being told in advance; OBSERVED silently drifts from DESIRED
print("*** simulated node failure: 2 pods lost ***")
victims = ctrl.observed_pods[:2]
for v in victims:
ctrl.kill(v)
print(f" lost: {victims}")
print(f" observed now: {sorted(ctrl.observed_pods)} (desired still {ctrl.desired_replicas})")
print()
# iteration 5: reconcile notices the gap and recreates exactly what's missing
c, d = ctrl.reconcile()
report(5, ctrl, c, d)
# iteration 6: user scales down 5 -> 2
ctrl.desired_replicas = 2
c, d = ctrl.reconcile()
report(6, ctrl, c, d)
# iteration 7: steady state again
c, d = ctrl.reconcile()
report(7, ctrl, c, d)প্রকৃত চালিয়ে পাওয়া output (deterministic — হুবহু এটাই আসবে):
--- iteration 1: web-deployment ---
before reconcile desired=3
CREATE: ['pod-1', 'pod-2', 'pod-3']
after reconcile -> desired=3 observed=['pod-1', 'pod-2', 'pod-3']
--- iteration 2: web-deployment ---
before reconcile desired=3
no action -- observed already matches desired
after reconcile -> desired=3 observed=['pod-1', 'pod-2', 'pod-3']
--- iteration 3: web-deployment ---
before reconcile desired=5
CREATE: ['pod-4', 'pod-5']
after reconcile -> desired=5 observed=['pod-1', 'pod-2', 'pod-3', 'pod-4', 'pod-5']
*** simulated node failure: 2 pods lost ***
lost: ['pod-1', 'pod-2']
observed now: ['pod-3', 'pod-4', 'pod-5'] (desired still 5)
--- iteration 5: web-deployment ---
before reconcile desired=5
CREATE: ['pod-6', 'pod-7']
after reconcile -> desired=5 observed=['pod-3', 'pod-4', 'pod-5', 'pod-6', 'pod-7']
--- iteration 6: web-deployment ---
before reconcile desired=2
DELETE: ['pod-7', 'pod-6', 'pod-5']
after reconcile -> desired=2 observed=['pod-3', 'pod-4']
--- iteration 7: web-deployment ---
before reconcile desired=2
no action -- observed already matches desired
after reconcile -> desired=2 observed=['pod-3', 'pod-4']তিনটা মুহূর্ত মন দিয়ে পড়ুন: (১) iteration 2 আর 7-এ কোনো action নেই — reconciliation loop idempotent, একবার converge হয়ে গেলে বারবার চালালেও কিছু বদলায় না; (২) node-failure-এর পর controller-কে কেউ বলেনি “২টা pod হারিয়েছে, replace করো” — পরের reconcile pass নিজে থেকেই gap দেখে ঠিক ২টা নতুন pod (pod-6, pod-7) তৈরি করেছে, hardcoded কোনো “on-failure” handler ছাড়াই; (৩) scale-down-এ (iteration 6) ঠিক ততটুকুই delete হয়েছে যতটুকু দরকার, কোনো বাড়তি cleanup logic ছাড়াই — পুরো controller-এর behavior এই একটা reconcile() ফাংশনেই বন্দি।
নিজে বাড়ান:
- একটা দ্বিতীয়
ToyController(আলাদাname, আলাদা desired count) যোগ করে দেখান দুইটা controller স্বাধীনভাবে reconcile করে, একে অপরকে না ছুঁয়ে — ঠিক যেমন Kubernetes-এ প্রতিটা Deployment-এর নিজের ReplicaSet controller instance থাকে। reconcile()-এ একটাmax_actions_per_passlimit যোগ করুন (একবারে সর্বোচ্চ ২টা create/delete) — দেখুন বড় একটা gap বন্ধ হতে কয়টা pass লাগে, আর কেন বাস্তব controller-manager-এও rate-limiting থাকা যুক্তিসঙ্গত (একসাথে হাজার container চালু করার চাপ এড়াতে)।kill()-কে randomize করুন (প্রতি iteration-এ random সংখ্যক pod হারানোর সম্ভাবনা) আর ১০০ iteration চালিয়ে verify করুন observed কখনো desired-এর চেয়ে বেশি iteration ধরে কম থাকে না — এটাই “self-healing” দাবির একটা পরিমাণগত প্রমাণ।
বাস্তব সিস্টেমে
Borg-এর ছায়ায় — মিল, পার্থক্য, আর কেন এই লেসনটা গুরুত্বপূর্ণ
Borg paper (Verma et al., EuroSys 2015) নিজেই বর্ণনা করে কীভাবে Google-এর হাজার হাজার job আর লক্ষ লক্ষ task একটা কেন্দ্রীয় Borgmaster-এর মাধ্যমে নিয়ন্ত্রিত হয় — যেখানে scheduling সিদ্ধান্ত (কোন task কোন মেশিনে) কেন্দ্রীয়ভাবে নেওয়া হয়, কিন্তু প্রকৃত execution ছড়ানো থাকে প্রতিটা মেশিনের নিজস্ব Borglet-এ। এই কাঠামোর সাথে আজকের Kubernetes-architecture-এর মিলটা কাকতালীয় না:
| Borg | Kubernetes | ভূমিকা |
|---|---|---|
| Borgmaster | API server + scheduler + controller-manager | কেন্দ্রীয় সিদ্ধান্ত-গ্রহণ |
| Borglet | kubelet | node-স্তরের এজেন্ট, প্রকৃত task চালায় |
| Paxos-ভিত্তিক replicated store | etcd (Raft-ভিত্তিক) | কেন্দ্রীয় state-এর fault-tolerant storage |
| Job/Task | Deployment/Pod | desired-state ঘোষণা করা কাজের একক |
Borg paper স্পষ্টভাবে লেখে যে তাদের Paxos-ভিত্তিক replicated store Borgmaster-এর state-এর জন্য ব্যবহৃত হয় ঠিক এই কারণে — কেন্দ্রীয় সিদ্ধান্ত-গ্রহণকারী নিজে single point of failure হতে পারবে না। Kubernetes এই একই সমস্যার সমাধানে Paxos-এর বদলে Raft বেছে নেয় — distributed-systems/raft লেসনের ভাষায়, ঠিক এই কারণে যে Raft “একই power, কিন্তু বোঝার জন্য ডিজাইন করা” (Ongaro ও Ousterhout, 2014)। এই পছন্দটাই একটা concrete উদাহরণ কেন সেই লেসনের শেষ দাবিটা (“decomposability একটা বাস্তব ইঞ্জিনিয়ারিং সুবিধা”) শুধু তাত্ত্বিক না — production-এর সবচেয়ে ব্যাপকভাবে ব্যবহৃত orchestrator নিজেই সেই পছন্দ করেছে।
যে ভুলগুলো সবাই করে
“Scheduler, controller-manager, বা kubelet — এরা সরাসরি etcd-তে read/write করে, কারণ সবাই তো cluster state-ই ব্যবহার করছে।”
না — এই লেসনের সবচেয়ে গুরুত্বপূর্ণ স্থাপত্যগত নিয়মটাই এই ভুল ভাঙে। প্রতিটা component — user-এর kubectl থেকে শুরু করে scheduler, controller-manager, kubelet, সবাই — cluster state ছোঁয় শুধুমাত্র API server-এর REST/watch API দিয়ে। etcd-এর সাথে সরাসরি সংযোগ আছে শুধুমাত্র API server-এর, একমাত্র API server-এর।
“Scheduler একটা Pod-কে node-এ assign করার পর সেই Pod-এর container নিজেই চালু করে।”
Scheduler-এর কাজ decision নেওয়া আর সেই decision একটা Binding হিসেবে API server-এ (এবং তার মধ্য দিয়ে etcd-তে) রেকর্ড করা — এই পর্যন্তই। প্রকৃত container চালু করা সম্পূর্ণভাবে সেই node-এর kubelet-এর কাজ, যে Binding-টা নিজের watch-এ দেখেই CRI-এর মাধ্যমে container runtime-কে নির্দেশ দেয়।
“etcd আসলে redis বা যেকোনো সাধারণ key-value store-এর মতোই — Kubernetes এটাকে শুধু একটা cache হিসেবে ব্যবহার করে।”
etcd কোনো cache না — এটা Kubernetes cluster-এর একমাত্র, কর্তৃপক্ষীয় সত্য উৎস। এটা হারালে (বা majority হারালে) cluster নিজের বর্তমান অবস্থা সম্পর্কে কিছুই নিশ্চিতভাবে জানে না। আর সাধারণ key-value store-এর সাথে পার্থক্যটা কাঠামোগত — etcd নিজের প্রতিটা write-এর জন্য Raft consensus চালায় (leader, term, majority commit), যা এটাকে network partition বা মেশিন-হারানোর মধ্যেও নিরাপদে consistent রাখে — এই guarantee একটা সাধারণ single-node key-value store দেয় না।
“Kubernetes সম্পূর্ণ মৌলিক, নতুন একটা আবিষ্কার — এর আগে এই ধরনের কিছু ছিল না।”
Kubernetes-এর অনেক মূল ডিজাইন-সিদ্ধান্ত — কেন্দ্রীয় control plane, declarative desired-state, node-স্তরের local agent — সরাসরি Google-এর অভ্যন্তরীণ Borg সিস্টেম থেকে অনুপ্রাণিত (Verma et al., EuroSys 2015), যেটা দশকের পর দশক ধরে Google-এর প্রায় সব production workload চালিয়েছে Kubernetes প্রকাশের অনেক আগে থেকেই। Kubernetes-এর কোডবেসে সরাসরি কোনো Borg কোড নেই, কিন্তু architecture-এর যুক্তিটা ধার করা।
বুঝেছেন কি না দেখুন
1Control plane-এর চারটা component আর worker node-এর তিনটা component-এর নাম আর এক-বাক্যের দায়িত্ব বলুন।স্মরণ
runc/containerd — container-runtime-oci লেসনের সেই একই স্ট্যাক)।2কেন Kubernetes ডিজাইন করে যে শুধু API server-ই সরাসরি etcd-তে read/write করবে, বাকি সবাই না? দুইটা concrete কারণ দিন।যুক্তি
3“etcd-এর প্রতিটা write একটা Raft log entry” — এই দাবিটা distributed-systems/raft লেসনের vocabulary ব্যবহার করে ব্যাখ্যা করুন: leader, term, majority commit ঠিক কোথায় খাটে?যুক্তি
4Experiment-এর scheduler simulation-এ node-d-এর raw free CPU/memory node-b-এর চেয়ে কম না, তবু node-b জিতেছে। কেন — scoring যুক্তিটা এক বাক্যে পুনর্গঠন করুন।প্রয়োগ
node-d-এ Pod বসালে CPU 87.5% আর memory প্রায় ৯৮%-এ চলে যেত (বড় imbalance, ~১০.৪%) — এমন একটা fragmented অবস্থা যেখানে অবশিষ্ট সামান্য CPU বাকি memory-কে কার্যত অব্যবহারযোগ্য করে দেয়। node-b-তে দুটোই প্রায় সমান (~৩৭%) থেকে যায় — কম fragmentation, তাই বেশি স্কোর।5Build section-এর ToyController-এ desired_replicas=5 অবস্থায় হঠাৎ ৩টা pod হারিয়ে যায় (কোনো একটা node পুরো ক্র্যাশ করে)। পরের reconcile() কল ঠিক কী করবে, আর controller-কে এই ঘটনার কথা কে “বলে” দেয়?প্রয়োগ
reconcile() ঠিক ৩টা নতুন pod তৈরি করবে (desired=5, observed=2, gap=3) — কোনো বাড়তি বা কম না। controller-কে সরাসরি কেউ “node crashed” বলে না; পরবর্তী reconcile pass শুধু বর্তমান observed_pods-এর length পড়ে আর gap হিসাব করে — ব্যর্থতার কারণ জানার দরকারই নেই, শুধু বর্তমান observed-state যথেষ্ট। এটাই reconciliation loop-এর শক্তি: cause-specific handler ছাড়াই self-healing।6আপনাকে scheduler-এ একটা নতুন নিয়ম যোগ করতে বলা হয়েছে: “একই Deployment-এর দুইটা replica যেন কখনো একই node-এ না বসে” (একটা primitive anti-affinity)। এই যুক্তিটা filtering ধাপে বসাবেন, না scoring ধাপে — আর কেন? দুই পছন্দের trade-off ব্যাখ্যা করুন।ডিজাইন
requiredDuringScheduling (filtering-এর কড়া রূপ) বনাম preferredDuringScheduling (scoring-এর নরম রূপ) — সিদ্ধান্তটা availability বনাম strictness-এর একটা সচেতন trade-off।এরপর কী
আজ আমরা যা বুঝেছি তা পুরোপুরি control plane-এর যন্ত্র — API server কীভাবে একমাত্র দরজা, etcd কীভাবে Raft দিয়ে নিরাপদ, scheduler কীভাবে সিদ্ধান্ত নেয়, controller-manager কীভাবে সেই সিদ্ধান্ত টিকিয়ে রাখে। কিন্তু এতক্ষণ ইচ্ছাকৃতভাবে এড়ানো হয়েছে একটা প্রশ্ন: ব্যবহারকারী আসলে কী ঘোষণা করে? “Pod” শব্দটা বহুবার এসেছে, কিন্তু এর নির্ভুল সংজ্ঞা, আর Deployment/Service/Ingress-এর মতো উঁচু-স্তরের object-গুলো কীভাবে এই একই control plane machinery ব্যবহার করে — সেটা এখনও অস্পষ্ট।
পরের লেসন, virtualization-cloud/pods-controllers-services-ingress, ঠিক এই ফাঁকটা পূরণ করবে — আজকের বিমূর্ত control-plane যন্ত্রপাতি সেখানে concrete API object-এ রূপ নেবে। Pod (container-গুলোর একটা group, একটাই network namespace শেয়ার করে), Deployment (ঠিক আজকের reconciliation-loop প্যাটার্নের একটা concrete বাস্তবায়ন), Service (kube-proxy কীভাবে একটা স্থিতিশীল ভার্চুয়াল IP-কে বদলে-যাওয়া Pod IP-দের দিকে রুট করে), আর Ingress (cluster-এর বাইরে থেকে ভেতরে HTTP ট্রাফিক আনার স্তর) — প্রতিটাই আজকের control plane-এর ওপর দাঁড়িয়ে, কিন্তু প্রতিটার নিজস্ব নির্ভুল semantics আছে যা এখনও দেখা হয়নি।
আরও পড়ুন
- Kubernetes Components (Cluster Architecture) — Kubernetes Authors — Kubernetes Documentation · Official architecture reference — control plane ও node component-এর প্রামাণ্য সংজ্ঞা, এই লেসনের কাঠামোগত ভিত্তি
- Large-scale cluster management at Google with Borg — Abhishek Verma, Luis Pedrosa, Madhukar Korupolu, David Oppenheimer, Eric Tune, John Wilkes — EuroSys 2015 · Kubernetes-এর সরাসরি বংশগত পূর্বপুরুষ — Google-এর অভ্যন্তরীণ cluster manager, একই দলের বড় অংশ পরে Kubernetes ডিজাইন করেছিল
- kube-scheduler — Kubernetes Documentation — Kubernetes Authors — Kubernetes Documentation · Scheduler-এর filtering ও scoring দুই-ধাপের algorithm-এর প্রামাণ্য বর্ণনা
- etcd-io/raft — etcd project (CoreOS / CNCF) · etcd যে Raft engine-এর ওপর দাঁড়িয়ে — distributed-systems/raft লেসনে যা হাতে বানানো হয়েছিল, ঠিক সেই ইঞ্জিনের production বাস্তবায়ন