Foundationপ্রথম নীতি থেকে
LEVEL 12লেসন ১২/২২অ্যাডভান্সড৫৫ মিনিট

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-এর কেন্দ্রীয় ডিজাইন-দর্শন।

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

  • Kubernetes-কে কেন একটা orchestrator হিসেবে দরকার হলো — container-networking লেসনের শেষে রাখা প্রশ্ন থেকে শুরু করে — আর Google-এর অভ্যন্তরীণ Borg সিস্টেমের সাথে এর সরাসরি বংশগত সম্পর্ক ব্যাখ্যা করতে পারবেন
  • Control plane (API server, etcd, scheduler, controller-manager) আর worker node (kubelet, kube-proxy, container runtime) — এই স্থাপত্যগত বিভাজন প্রতিটা component-এর দায়িত্বসহ নির্ভুলভাবে বর্ণনা করতে পারবেন
  • etcd কীভাবে distributed-systems/raft লেসনের ঠিক সেই vocabulary (leader, term, log replication, majority commit) ব্যবহার করে নিজের consistency বজায় রাখে তা ব্যাখ্যা করতে পারবেন, আর কেন API server-ই একমাত্র etcd-স্পর্শকারী component তার যুক্তি দিতে পারবেন
  • Scheduler-এর two-phase (filtering তারপর scoring) অ্যালগরিদম ধাপে ধাপে trace করতে পারবেন, আর কেন scheduler নিজে কখনো container চালু করে না — শুধু সিদ্ধান্ত রেকর্ড করে — তা ব্যাখ্যা করতে পারবেন
  • Controller-manager-এর reconciliation loop (desired state বনাম observed state) প্যাটার্নটা code দিয়ে বাস্তবায়ন করে দেখাতে পারবেন, আর এই একই প্যাটার্ন কেন সামনের declarative-infrastructure লেসনগুলোতে (Pods/Services, Terraform) বারবার ফিরে আসবে তা ব্যাখ্যা করতে পারবেন

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

আগে এটা বুঝি

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 apply থেকে চলমান container পর্যন্ত — কে কী করছে
  1. kubectl -> API serverনতুন Pod object POST করা হয় — schema validation + admission control এখানেই হয়
  2. API server -> etcd (Raft write)নতুন Pod entry etcd-এর Raft log-এ যায়; majority replicate হলে committed
  3. scheduler-এর watch trigger"no node assigned" Pod দেখে scheduler সাড়া দেয় — filtering তারপর scoring
  4. scheduler -> API server: Binding লেখাscheduler নিজে container চালায় না — শুধু node-assignment রেকর্ড করে (আবার একটা etcd Raft write, API server-এর মাধ্যমে)
  5. kubelet-এর watch trigger (সেই node-এ)assigned node-এর kubelet Binding দেখে, CRI দিয়ে container runtime-কে নির্দেশ দেয়
  6. container runtime: namespace/cgroup সেটআপcontainer-runtime-oci লেসনের সেই একই runc/containerd flow — নতুন কোনো mechanism না
  7. 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]                │
└────────────────────────────────┘          └────────────────────────────────┘
স্থাপত্যএকটা ন্যূনতম Kubernetes cluster — একটা control plane node, দুইটা worker node। প্রতিটা তীর ঠিক দেখায় কে কার সাথে সরাসরি কথা বলে।

উপরের ছবিতে একটা জিনিস ইচ্ছাকৃতভাবে জোর দেওয়া হয়েছে — API server-এর দিকে সব তীর ঢোকে, কিন্তু etcd-এর দিকে তীর যায় শুধু একটাই জায়গা থেকে। এটাই “single point of entry” design decision-এর দৃশ্যমান রূপ।

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

EXPERIMENT

Scheduler simulation — filtering তারপর scoring, বাস্তবে চালিয়ে

যেকোনো OS (Python 3, কোনো dependency ছাড়াই)· ১০ মিনিট
# 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 চালু করে না।

নিজে বানান

BUILD IT

ReconciliationLoop — desired বনাম observed state, node-failure-সহ

Python · ●●●○○
  1. ToyController ক্লাস বানান — desired_replicas (int) আর observed_pods (list) ধরে রাখবে
  2. reconcile() লিখুন — observed < desired হলে ঠিক যতটুকু দরকার ততটুকু create, observed > desired হলে ঠিক ততটুকু delete
  3. কয়েক iteration চালান — শুরুতে খালি observed, তারপর scale-up, তারপর একটা simulated node failure (কিছু observed pod হারিয়ে যাওয়া), তারপর আবার reconcile
  4. দেখুন 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() ফাংশনেই বন্দি।

নিজে বাড়ান:

  1. একটা দ্বিতীয় ToyController (আলাদা name, আলাদা desired count) যোগ করে দেখান দুইটা controller স্বাধীনভাবে reconcile করে, একে অপরকে না ছুঁয়ে — ঠিক যেমন Kubernetes-এ প্রতিটা Deployment-এর নিজের ReplicaSet controller instance থাকে।
  2. reconcile()-এ একটা max_actions_per_pass limit যোগ করুন (একবারে সর্বোচ্চ ২টা create/delete) — দেখুন বড় একটা gap বন্ধ হতে কয়টা pass লাগে, আর কেন বাস্তব controller-manager-এও rate-limiting থাকা যুক্তিসঙ্গত (একসাথে হাজার container চালু করার চাপ এড়াতে)।
  3. 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-এর মিলটা কাকতালীয় না:

BorgKubernetesভূমিকা
BorgmasterAPI server + scheduler + controller-managerকেন্দ্রীয় সিদ্ধান্ত-গ্রহণ
Borgletkubeletnode-স্তরের এজেন্ট, প্রকৃত task চালায়
Paxos-ভিত্তিক replicated storeetcd (Raft-ভিত্তিক)কেন্দ্রীয় state-এর fault-tolerant storage
Job/TaskDeployment/Poddesired-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-এর নাম আর এক-বাক্যের দায়িত্ব বলুন।স্মরণ
Control plane: API server (একমাত্র প্রবেশদ্বার, authorization/admission), etcd (একমাত্র সত্য উৎস, Raft-ভিত্তিক), scheduler (কোন Pod কোন node-এ যাবে সিদ্ধান্ত নেয়), controller-manager (reconciliation loop দিয়ে desired-observed gap বন্ধ করে)। Worker node: kubelet (node-এজেন্ট, CRI দিয়ে container চালায়), kube-proxy (Service-এর ভার্চুয়াল IP রুট করে), container runtime (runc/containerd — container-runtime-oci লেসনের সেই একই স্ট্যাক)।
2কেন Kubernetes ডিজাইন করে যে শুধু API server-ই সরাসরি etcd-তে read/write করবে, বাকি সবাই না? দুইটা concrete কারণ দিন।যুক্তি
প্রথমত, authorization/validation/admission-control একটা একক জায়গায় centralize করা যায় — প্রতিটা component-কে আলাদাভাবে এই যুক্তি implement করতে হয় না। দ্বিতীয়ত, API server-এর REST/watch API একটা স্থিতিশীল contract হিসেবে কাজ করে — etcd-এর internal storage বদলালেও (বা তাত্ত্বিকভাবে অন্য backend দিয়ে replace হলেও) scheduler/kubelet-এর কোনো কোড বদলাতে হয় না।
3“etcd-এর প্রতিটা write একটা Raft log entry” — এই দাবিটা distributed-systems/raft লেসনের vocabulary ব্যবহার করে ব্যাখ্যা করুন: leader, term, majority commit ঠিক কোথায় খাটে?যুক্তি
etcd-এর ৩-৫টা replica-র মধ্যে একজন নির্দিষ্ট মুহূর্তে leader, নিজের term-এ নির্বাচিত (raft লেসনের সেই একই randomized-timeout + majority-vote নির্বাচন)। API server যখন কোনো write পাঠায়, সেটা leader-এর log-এ entry হিসেবে append হয়, followers-দের AppendEntries পাঠানো হয়, আর majority replicate করলেই entry committed — তখনই write client-কে সফল বলে জানানো হয়। এটা কোনো নতুন প্রোটোকল না — raft লেসনে হাতে বানানো ঠিক সেই engine, শুধু এখন cluster state ধরে রাখছে।
4Experiment-এর scheduler simulation-এ node-d-এর raw free CPU/memory node-b-এর চেয়ে কম না, তবু node-b জিতেছে। কেন — scoring যুক্তিটা এক বাক্যে পুনর্গঠন করুন।প্রয়োগ
Scoring raw free capacity দেখে না, দেখে placement-এর পরে node কতটা balanced থাকবে। 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 ব্যাখ্যা করুন।ডিজাইন
যদি এটা একটা hard requirement হয় (কখনো লঙ্ঘন করা চলবে না), সেটা filtering-এ বসাতে হবে — যে node-এ আগে থেকেই একই Deployment-এর replica চলছে, সেটাকে feasible set থেকে সরাসরি বাদ দেওয়া। কিন্তু এতে ঝুঁকি আছে: যদি cluster ছোট হয় (node-সংখ্যা replica-সংখ্যার কাছাকাছি), filtering সব node বাদ দিয়ে দিতে পারে — Pod কখনো schedule-ই হবে না (unschedulable)। যদি এটা একটা soft preference হয় (যতটা সম্ভব এড়ানো, কিন্তু বাধ্য না), সেটা scoring-এ বসানো ভালো — একই-Deployment-replica-থাকা node-কে কম স্কোর দেওয়া, কিন্তু বাদ না দেওয়া। বাস্তব Kubernetes ঠিক এই দুই বিকল্পই দেয় — 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 বাস্তবায়ন