Pod, Controller, Service, ও Ingress — Kubernetes-এ একটা App বাঁচিয়ে রাখার চারটা স্তর
Pods, Controllers, Services, and Ingress: Four Layers of Keeping an App Alive
kubernetes-architecture লেসন কন্ট্রোল প্লেনের reconciliation loop-টা দেখিয়েছিল বিমূর্তভাবে — আজ সেই loop-টাকে চারটা নির্দিষ্ট, প্রতিদিন ব্যবহৃত object-এর ওপর বসিয়ে দেখব। Pod কেন scheduling-এর atomic unit, কেন একটা মৃত Pod কখনো 'সারানো' হয় না বরং নতুন একটা দিয়ে বদলানো হয়, আর সেই disposability যে সমস্যা তৈরি করে — কার IP-তে কল করব যদি IP-ই টেকে না — তার সমাধান হিসেবে Service আর Ingress। প্রতিটা স্তর আগেরটার তৈরি করা একটা নির্দিষ্ট গর্ত ভরাট করে।
আগে এটা বুঝি
virtualization-cloud/kubernetes-architecture লেসন কন্ট্রোল প্লেনের কেন্দ্রীয় ধারণাটা দাঁড় করিয়েছিল: Kubernetes imperative না, declarative — আপনি বলেন কী চান, একটা controller ক্রমাগত observe-diff-act চালিয়ে সেই অবস্থাটা বজায় রাখে। এটা একটা বিমূর্ত loop হিসেবে বোঝানো হয়েছিল। আজ সেই loop-টাকে হাতে ধরে, চারটা নির্দিষ্ট object-এর ওপর বসিয়ে দেখব — কারণ বিমূর্ত ধারণাটা বোঝা একরকম, প্রতিদিন kubectl apply করার সময় ঠিক কোন object কী ভূমিকা রাখছে সেটা জানা আরেক রকম।
প্রশ্নটা শুরু হয় সবচেয়ে নিচের স্তর থেকে: Kubernetes কি সরাসরি container schedule করে? উত্তর — না। Kubernetes container শব্দটাই সরাসরি schedule করে না; সে schedule করে Pod। কেন এই এক ধাপ বাড়তি বিমূর্ততা — সেটাই আজকের প্রথম প্রশ্ন, আর তার উত্তরটা সরাসরি virtualization-cloud/linux-namespaces-deep-dive লেসনের network namespace মেকানিজমের একটা concrete প্রয়োগ।
তারপর দ্বিতীয় প্রশ্ন: একটা Pod তো একটা সাধারণ process-গ্রুপ মাত্র — মরে যেতে পারে, node ব্যর্থ হতে পারে। কে নিশ্চিত করে “আমি ৩টা replica চাই” এই দাবিটা সবসময় সত্যি থাকে? উত্তর — Deployment controller, আর এখানেই kubernetes-architecture লেসনের reconciliation loop প্রথমবার একটা নাম আর মুখ পায়।
তৃতীয় প্রশ্নটা তার থেকেই জন্মায়, প্রায় অনিবার্যভাবে: যদি Pod সত্যিই disposable হয় — মরে যেতে পারে, নতুন একটা ভিন্ন IP নিয়ে তার জায়গা নিতে পারে — তাহলে বাকি সিস্টেম কীভাবে নির্ভরযোগ্যভাবে “সেই app”-এর সাথে কথা বলে? উত্তর — Service। আর শেষ প্রশ্ন: cluster-এর বাইরে থেকে, ইন্টারনেট থেকে, কীভাবে সঠিক Service-এ পৌঁছানো যায়, যখন একটাই external IP-তে অনেক আলাদা app চালাতে হবে? উত্তর — Ingress।
চারটা প্রশ্ন, চারটা object, একটা রৈখিক যুক্তি-শৃঙ্খল — প্রতিটা আগেরটার তৈরি করা একটা নির্দিষ্ট গর্ত ভরাট করে।
মূল ধারণা
Pod — কেন scheduling-এর একক container না, একটা group
সবচেয়ে প্রাথমিক প্রশ্ন দিয়ে শুরু করা যাক: Kubernetes-কে যদি সরাসরি container schedule করতে দেওয়া হতো — “এই node-এ এই container চালাও” — তাহলে কী সমস্যা হতো? বাস্তব অ্যাপ্লিকেশনে প্রায়ই একটা main process-এর পাশে সাহায্যকারী process দরকার হয়: একটা web server-এর পাশে তার লগ পড়ে forward করা একটা agent, বা একটা কনফিগ ফাইল ক্রমাগত sync রাখা একটা প্রসেস। যদি এই দুইটা স্বাধীনভাবে schedule হতো, তাহলে scheduler-এর কোনো গ্যারান্টি নেই তারা একই node-এ থাকবে — আর একই node-এ না থাকলে “একে অপরের পাশে দাঁড়িয়ে সাহায্য করা”র পুরো ধারণাটাই ভেঙে পড়ে।
Kubernetes-এর সমাধান: একসাথে সবসময় একই node-এ schedule হওয়া container-দের একটা নির্দিষ্ট group সংজ্ঞায়িত করা, আর সেই group-টাকেই scheduling-এর একক বানানো। এই group-এর নাম Pod। একটা Pod-এর ভেতরের container-গুলো:
- সবসময় একই node-এ schedule হয় — কখনো আলাদা node-এ ভাগ হয় না, এটা scheduler-এর একটা hard guarantee
- একটাই network namespace শেয়ার করে
- ইচ্ছা করলে একটাই storage volume শেয়ার করতে পারে
দ্বিতীয় বুলেটটাই এই লেসনের প্রথম গভীর দাবি, আর সরাসরি linux-namespaces-deep-dive লেসনের মেকানিজমের ওপর দাঁড়িয়ে। সেই লেসনে দেখানো হয়েছিল, clone(CLONE_NEWNET) একটা process-কে একটা নতুন network namespace দেয় — নিজস্ব loopback, নিজস্ব routing table, নিজস্ব port space। একটা সাধারণ Docker container চালালে (Pod ছাড়া, খালি docker run) প্রতিটা container তার নিজস্ব নতুন network namespace পায় — দুইটা container একই মেশিনে চললেও তারা একে অপরকে localhost-এ দেখতে পায় না, প্রতিটার নিজস্ব IP।
Pod এই default আচরণটাই ইচ্ছাকৃতভাবে বাতিল করে। Pod তৈরি হওয়ার সময় Kubernetes প্রথমে একটা বিশেষ, প্রায় কিছুই-না-করা container চালায় — কনভেনশনে যার নাম “pause” container — আর সেটাই নতুন network namespace-টা তৈরি করে ও ধরে রাখে। Pod-এর বাকি সব container namespace তৈরি করে না; বরং clone()/setns() দিয়ে সেই একই, আগে থেকে তৈরি namespace-এ join করে। ফলাফল: একটা Pod-এর ভেতরের সব container একটাই network stack শেয়ার করে —
- সবার একটাই Pod-IP
- একটা container
localhost:8080-এ কিছু চালালে, একই Pod-এর অন্য containerlocalhost:8080-এ সরাসরি তাতে পৌঁছায় — কোনো network hop ছাড়াই - port-এর জন্য conflict এড়াতে হয় — দুইটা container একই port bind করতে পারবে না, ঠিক যেমন একই মেশিনে দুইটা প্রসেস পারে না
Sidecar pattern — multi-container Pod-এর বাস্তব কারণ
তাত্ত্বিকভাবে জানার পর প্রশ্ন ওঠে — এই multi-container সুবিধাটা বাস্তবে কে ব্যবহার করে, কেন? সবচেয়ে ব্যাপক উত্তর: sidecar pattern। ধরুন একটা main application container লগ লেখে একটা shared volume-এ (উদাহরণ, /var/log/app/-এ), আর একটা দ্বিতীয়, ছোট container সেই একই volume থেকে লগ পড়ে একটা centralized logging system-এ পাঠায়। এই দুইটা container:
- একটাই Pod-এ থাকে, তাই একটাই storage volume mount করতে পারে (volume-টা Pod-level-এ সংজ্ঞায়িত, container-level-এ না)
- একই node-এ schedule হওয়ার গ্যারান্টি পায় — আলাদা হলে shared volume-এর ধারণাটাই অর্থহীন হয়ে যেত
- স্বাধীনভাবে develop, build, আর version করা যায় — logging agent-এর কোড main application-এর কোডবেসে মেশে না
এই ধরনের সহযোগী container-কে বলা হয় sidecar — মোটরসাইকেলের পাশের সাইডকারের মতো, মূল যানের সাথে বাঁধা কিন্তু আলাদা। অন্য সাধারণ sidecar উদাহরণ: একটা service mesh proxy (যেমন Envoy) যা main container-এর সব ইনকামিং/আউটগোয়িং network traffic intercept করে, অথবা একটা config-sync container যা periodically একটা remote source থেকে configuration টেনে shared volume-এ আপডেট রাখে।
ভেতরে কী ঘটছে
Deployment controller — reconciliation loop-টা প্রথমবার একটা মুখ পায়
kubernetes-architecture লেসনে reconciliation loop বিমূর্তভাবে দেখানো হয়েছিল: observe current state, diff against desired state, act to close the gap। এখন সেই loop-এর সবচেয়ে সাধারণ, সবচেয়ে ব্যবহৃত একটা concrete বাস্তবায়ন — Deployment।
একটা Deployment object মূলত দুইটা জিনিস ঘোষণা করে: (১) একটা Pod template — কোন image, কোন port, কোন env variable দিয়ে একটা Pod দেখতে কেমন হবে; (২) একটা সংখ্যা, replicas — এই template-এর কয়টা কপি সবসময় চলমান থাকা উচিত। এই দুইটা মিলিয়ে যা তৈরি হয় সেটা একটা desired state-এর ঘোষণা: “এই ছাঁচের ৩টা Pod সবসময় Running থাকুক।”
Deployment controller (কার্যত একটা নিয়ন্ত্রণ-লুপ চালু process, control plane-এ চলমান) ক্রমাগত এই ঘোষণাটার সাথে বাস্তব cluster state তুলনা করে:
loop forever:
desired = deployment.spec.replicas # ঘোষিত সংখ্যা, উদাহরণ ৩
observed = count(pods matching this Deployment's selector, phase=Running)
if observed < desired:
create (desired - observed) নতুন Pod, template থেকে
if observed > desired:
delete (observed - desired) টা বাড়তি Pod
# observed == desired হলে কিছুই করার নেই — এইটাই স্থিতিশীল অবস্থালক্ষ্য করুন — এই লুপের কোনো ধাপেই “মেরামত করা” নেই। কোনো ধাপ এমন না যে “এই Pod-টা কেন কাজ করছে না দেখো, ঠিক করো।” ধাপগুলো শুধু গণনা আর তৈরি/মুছে ফেলা। এইটাই এই section-এর কেন্দ্রীয়, ইচ্ছাকৃতভাবে জোর দেওয়া দাবি।
Pod immutability — কেন “restart” শব্দটা প্রায়ই ভুল প্রয়োগ হয়
VM-management-এর জগত থেকে আসা যে কারো স্বাভাবিক প্রতিক্রিয়া: “একটা instance ক্র্যাশ করলে সেটাকে reboot/restart করো।” Kubernetes এই মডেলটা সচেতনভাবে প্রত্যাখ্যান করে একটা ভিন্ন নীতির পক্ষে — Pod immutability। যখন একটা Pod সম্পূর্ণভাবে হারিয়ে যায় (node ব্যর্থ হয়, evict হয়, বা মুছে ফেলা হয়), Deployment controller সেই একই Pod-কে “সারায়” না — সে ঘোষণা-অনুযায়ী গণনা করে দেখে observed count কমে গেছে, আর একটা সম্পূর্ণ নতুন Pod object তৈরি করে, নতুন নাম, নতুন internal ID, আর — এই লেসনের পরবর্তী অংশের জন্য গুরুত্বপূর্ণ — নতুন IP address-সহ।
কেন এই নকশা? কারণ একটা Pod-কে “মেরামত করার চেষ্টা” (in-place অবস্থা ঠিক করা, disk cleanup করা, ইত্যাদি) একটা বিশাল state-machine তৈরি করে — কতরকম আংশিক-ব্যর্থ অবস্থা হতে পারে তার তালিকা কার্যত অসীম। “মৃত হলে মুছে ফেলো, নতুন বানাও” — এই নিয়মটা সেই পুরো জটিলতাকে একটা মাত্র, সবসময়-নির্ভরযোগ্য অপারেশনে নামিয়ে আনে: create। এটাই cattle-not-pets দর্শনের সবচেয়ে সরাসরি, machine-enforced রূপ — Pod-রা নাম-দেওয়া, যত্ন-নেওয়া pet না, প্রতিস্থাপনযোগ্য cattle।
Rolling update — একই লুপ, শুধু template বদলে যাচ্ছে
Rolling update নতুন কোনো মেকানিজম না — এটা এই একই reconciliation লুপের একটা নিয়ন্ত্রিত প্রয়োগ। যখন একটা Deployment-এর Pod template বদলানো হয় (নতুন image version, ধরুন), controller সাথে সাথে সব পুরনো Pod মুছে ফেলে না — তাহলে মাঝের মুহূর্তে replica সংখ্যা শূন্যে নেমে যেত, downtime। বরং সে ধাপে ধাপে এগোয়:
t0: [old, old, old] desired=3 (old template)
t1: [old, old, old, new] +1 নতুন template-এর Pod তৈরি
t2: [old, old, new] -1 পুরনো Pod মুছে ফেলা (এখনো ৩টাই আছে)
t3: [old, old, new, new] +1 আরেকটা নতুন
t4: [old, new, new] -1 আরেকটা পুরনো
... (চলতে থাকে যতক্ষণ না সব পুরনো বদলে যায়)
tN: [new, new, new] রোলআউট সম্পূর্ণপ্রতিটা মুহূর্তে desired replica count (৩) হয় বজায় থাকে, নয় সাময়িকভাবে সামান্য বেশি থাকে (maxSurge কনফিগারেশন অনুযায়ী) — কখনো কমে যায় না এমনভাবে যে সেবা ব্যাহত হয়। প্রতিটা ধাপই সেই একই “observed vs desired, create/delete” গণনা — শুধু desired-এর সংজ্ঞায় এখন দুইটা template একসাথে আছে সংক্রমণকালীন সময়ে। কোনো Pod-ই “আপডেট” হয় না ভেতরে-ভেতরে — পুরনো template-এর Pod মুছে যায়, নতুন template-এর Pod তৈরি হয়। Pod immutability এখানেও অক্ষুণ্ণ।
উদাহরণ
যে সমস্যাটা disposability তৈরি করে — আর তার সমাধান, একধাপ একধাপ
আগের section-এর একটা লাইন এখন ফিরে আসা দরকার, একটু জোরে: প্রতিটা নতুন Pod একটা নতুন IP নিয়ে জন্মায়। এখন কল্পনা করুন একটা দ্বিতীয় Pod (বলুন, একটা “checkout” service) প্রথম Pod-টার (বলুন, “inventory” service) সাথে সরাসরি IP দিয়ে কথা বলছে — 10.244.0.2:8080-এ request পাঠাচ্ছে। inventory Pod-টা ক্র্যাশ করলে, Deployment controller নতুন একটা তৈরি করবে ঠিকই — কিন্তু তার IP হয়তো 10.244.0.7। checkout Pod-টা এখনো পুরনো, মৃত IP-তে request পাঠাচ্ছে। কেউ তাকে জানায়নি।
এটাই সেই ফাঁকটা যা Service ভরাট করে। একটা Service ঠিক দুইটা জিনিস নিয়ে গঠিত:
- একটা স্থায়ী ভার্চুয়াল IP — ClusterIP — যেটা Service অবজেক্ট বেঁচে থাকা পর্যন্ত কখনো বদলায় না
- একটা label selector — যেমন
app: inventory— যেটা বলে দেয় কোন Pod-গুলো এই Service-এর পেছনে আছে বলে গণ্য হবে
checkout Pod এখন আর inventory Pod-এর সরাসরি IP জানে না — সে শুধু জানে Service-এর ClusterIP, ধরুন 10.96.0.10। সেই ClusterIP-তে request পাঠালে, সিস্টেম নিজে থেকেই request-টা যে Pod-গুলো এখন app: inventory label বহন করছে তাদের একটায় পাঠিয়ে দেয় — এই মুহূর্তে যে Pod-ই বেঁচে থাকুক না কেন।
- inventory Pod (old-ip) ক্র্যাশ করলnode failure — Pod object সম্পূর্ণ হারিয়ে গেল
- Deployment controller reconcile করলobserved=2 < desired=3 → নতুন Pod তৈরি, একই label, নতুন IP (new-ip)
- Service-এর selector অপরিবর্তিত`app: inventory` — কোনো মানুষ বা কোড এটা স্পর্শ করেনি
- kube-proxy endpoint list পুনরায় গণনা করলold-ip বাদ, new-ip যোগ — Service-এর ClusterIP নিজে অপরিবর্তিত
- checkout Pod একই ClusterIP-তে পরের request পাঠালকোনো কোড পরিবর্তন লাগেনি — নিয়মিতভাবে ClusterIP-তে পাঠানো request এখন new-ip-তে পৌঁছাচ্ছে
Service কোনো প্রসেস না — এটা একটা বাস্তবায়িত বিভ্রম
এখানে একটা সূক্ষ্ম কিন্তু গুরুত্বপূর্ণ নির্ভুলতা দরকার। “Service” শব্দটা শুনে সহজাত অনুমান হয় — কোথাও একটা প্রসেস চলছে, request-গুলো তার মধ্য দিয়ে যাচ্ছে, সে ফরওয়ার্ড করছে। এটা ভুল। একটা Service ডিফল্টভাবে ট্রাফিকের পথে বসে থাকা কোনো single running process না — এটা একটা বিমূর্ত নাম আর নিয়মের সেট, যা প্রতিটা node-এ চলা kube-proxy নামের একটা component বাস্তবায়ন করে kernel-স্তরের rule দিয়ে — সাধারণত iptables, অথবা বড় cluster-এ কর্মক্ষমতার জন্য IPVS।
kube-proxy Kubernetes API server-এর সাথে ক্রমাগত sync থেকে জানে কোন Service-এর পেছনে এই মুহূর্তে কোন Pod IP-গুলো আছে (ঠিক আগের LayerTrace-এর endpoint-recalculation ধাপ)। সেই তথ্য দিয়ে সে প্রতিটা node-এর kernel-এ DNAT (destination NAT) rule বসায়: “কেউ 10.96.0.10:80-এ প্যাকেট পাঠালে, সেটার destination address বদলে দাও এই তিনটার একটায় — 10.244.0.1:8080, 10.244.0.5:8080, 10.244.0.7:8080 — কোনো একটা বেছে নাও (round-robin বা random)।”
এর মানে request-টা কোনো Service-প্রসেসের মধ্য দিয়ে যায় না — kernel নিজেই, packet forwarding-এর সময়, destination বদলে দেয়। ClusterIP নিজে কখনো কোনো নেটওয়ার্ক ইন্টারফেসে bind হওয়া কোনো সত্যিকার IP না; এটা একটা virtual ঠিকানা যার জন্য শুধু rewrite rule আছে। এই কারণেই একটা Service কোনো “single point of failure” প্রসেসের মতো ক্র্যাশ করতে পারে না — প্রতিটা node নিজে নিজের rule বহন করে।
Ingress — Service-এর থেকে ভিন্ন স্তর, ভিন্ন সমস্যার সমাধান
Service সমাধান করে ভেতরের সমস্যা — cluster-এর একটা Pod কীভাবে আরেকটা Pod-এ নির্ভরযোগ্যভাবে পৌঁছায়। কিন্তু একটা সম্পূর্ণ ভিন্ন সমস্যা থেকে যায়: cluster-এর বাইরে থেকে, ইন্টারনেট থেকে, কীভাবে সঠিক জায়গায় পৌঁছানো যায়? আর যদি cluster-এ ১০টা আলাদা web application থাকে, প্রতিটার জন্য কি আলাদা একটা external IP আর আলাদা load balancer লাগবে? ব্যয়বহুল, আর অপ্রয়োজনীয়।
Ingress এই সমস্যাটা সমাধান করে একটা সম্পূর্ণ ভিন্ন স্তরে বসে — L7, অর্থাৎ HTTP-সচেতন। একটা Service মূলত L4-এর কাছাকাছি — সে শুধু IP:port দেখে, request-এর ভেতরের HTTP host header বা URL path পড়ে না। একটা Ingress resource ঠিক সেটাই পড়ে, আর ঘোষণা করে routing rule:
host: api.example.com
path /v1 → service: api-v1-svc (port 80)
path /v2 → service: api-v2-svc (port 80)
host: blog.example.com
path / → service: blog-svc (port 80)একটামাত্র external IP, একটামাত্র load balancer — তিনটা সম্পূর্ণ আলাদা Service-এ ট্রাফিক ভাগ হচ্ছে host আর path দেখে। কিন্তু এখানে একটা জরুরি নির্ভুলতা: Ingress resource নিজে শুধু একটা ঘোষণা — একটা YAML ফাইলে লেখা নিয়ম, ঠিক Deployment-এর মতো “আমি এইটা চাই” বলা। এই নিয়মটা বাস্তবায়ন করার জন্য একটা প্রকৃত, চলমান Ingress controller লাগে — nginx-ingress, বা অনুরূপ কোনো reverse proxy সফটওয়্যার (এই reverse proxy-এর নিজস্ব ভেতরের কাজ একটা পরের লেসনের বিষয়) — যেটা এই rule-গুলো পড়ে, নিজে একটা real HTTP server হিসেবে external traffic গ্রহণ করে, header/path পার্স করে, আর সঠিক Service-এর ClusterIP-তে ফরওয়ার্ড করে।
ছবিতে
একটা Pod-এর ভেতরে — namespace শেয়ারিং চোখে দেখা
Pod (একটা Pod-IP: 10.244.0.5)
┌─────────────────────────────────────────────────────────┐
│ একটাই network namespace — pause container তৈরি করেছে │
│ │
│ ┌───────────────┐ ┌───────────────┐ │
│ │ main app │ │ logging │ │
│ │ container │ │ sidecar │ │
│ │ :8080 bind │ │ │ │
│ └───────┬───────┘ └───────┬───────┘ │
│ │ localhost:8080 │ │
│ └───────────────────┘ (network hop ছাড়াই) │
│ │
│ ┌────────────────── shared volume ──────────────────┐ │
│ │ main app এখানে লগ লেখে → sidecar এখান থেকে পড়ে │ │
│ └───────────────────────────────────────────────────┘ │
│ │
│ pause container (namespace-এর মালিক) │
└─────────────────────────────────────────────────────────┘
বাইরের কেউ শুধু একটাই IP দেখে: 10.244.0.5 — ভেতরে কয়টা
container আছে, কোনটা কোন port বাঁধছে, তা সম্পূর্ণ অদৃশ্য।Cluster-level path — বাইরের request থেকে ভেতরের Pod পর্যন্ত
ইন্টারনেট
│ GET https://api.example.com/v1/orders
▼
┌─────────────────────┐ external LoadBalancer IP-তে শোনে
│ Ingress controller │ host+path পড়ে rule মেলায়
│ (আসল, চলমান process) │ host=api.example.com, path=/v1 → api-v1-svc
└──────────┬───────────┘
│ api-v1-svc-এর ClusterIP-তে পাঠায়
▼
┌─────────────────────┐ কোনো নিজস্ব process না —
│ Service (ClusterIP) │ kube-proxy-র বসানো iptables/IPVS rule
└──────────┬───────────┘
│ DNAT — বর্তমান matching Pod-দের একটায়
▼
┌───────┐ ┌───────┐ ┌───────┐
│ Pod A │ │ Pod B │ │ Pod C │ app: api-v1 label বহনকারী,
│(এখনকার)│ │(এখনকার)│ │(এখনকার)│ এই মুহূর্তে যারাই বেঁচে আছে
└───────┘ └───────┘ └───────┘নিজে চালিয়ে দেখুন
Selector resolution কতটা সস্তা — একটা linear-scan label-match বাস্তবে মেপে দেখা
# selector_perf.py -- how expensive is "recompute which Pods match this
# Service's selector" really? Measured as a plain Python linear scan over
# labels -- the same SHAPE of work (not the same optimized implementation)
# that a naive Service-endpoint recomputation would do.
import random
import time
random.seed(7)
def make_pod(i, matches):
labels = {"tier": "frontend", "app": "web" if matches else "other",
"pod-template-hash": f"h{i%5}"}
return {"name": f"pod-{i}", "ip": f"10.244.{i//256}.{i%256}", "labels": labels}
def resolve(pods, selector):
return [p["ip"] for p in pods
if all(p["labels"].get(k) == v for k, v in selector.items())]
selector = {"app": "web"}
for n in (100, 1_000, 10_000, 100_000):
pods = [make_pod(i, matches=(i % 3 == 0)) for i in range(n)]
start = time.perf_counter()
eps = resolve(pods, selector)
elapsed_ms = (time.perf_counter() - start) * 1000
print(f"pods={n:>7,} matched={len(eps):>6,} resolve time={elapsed_ms:7.3f} ms")প্রকৃত চালিয়ে পাওয়া output (এই মেশিনে; সংখ্যা হার্ডওয়্যারভেদে সামান্য বদলাবে, কিন্তু মাত্রার পার্থক্য টিকে থাকবে):
pods= 100 matched= 34 resolve time= 0.083 ms
pods= 1,000 matched= 334 resolve time= 0.800 ms
pods= 10,000 matched= 3,334 resolve time= 7.838 ms
pods=100,000 matched=33,334 resolve time= 85.928 msলক্ষ্য করার মতো: এমনকি ১ লক্ষ Pod-এর একটা (বাস্তবে অস্বাভাবিক বড়) cluster-এও, একটা naive, কোনো index/cache ছাড়া linear scan মাত্র ~৮৬ মিলিসেকেন্ডে শেষ হচ্ছে। বাস্তব cluster-এ Pod সংখ্যা এর চেয়ে অনেক কম হয়, আর kube-proxy প্রতিবার পুরো তালিকা recompute করার বদলে incremental EndpointSlice আপডেট ব্যবহার করে — তাই বাস্তব latency এর চেয়েও কম। মূল বিষয়টা টিকে থাকে: selector resolution structurally সস্তা একটা অপারেশন, তাই এটা প্রতিটা Pod-change-এ বারবার চালানো বাস্তবসম্মত — Service-কে “সবসময় সঠিক” রাখার জন্য কোনো ভারী, centralized bottleneck লাগে না।
একটা Service-এর endpoint তালিকা পুনর্গণনা করা — সব Pod-এর label-এর সাথে selector মিলিয়ে দেখা — এমনকি একটা naive, index-বিহীন linear scan হিসেবেও বাস্তবসম্মত cluster-scale-এ (হাজার হাজার Pod) সাব-১০০ms-এ শেষ হয়, তাই kube-proxy-র জন্য এটা প্রতিটা Pod change-এ পুনরায় চালানো একটা সস্তা অপারেশন — একবারের জন্য একটামাত্র চলমান 'router process' দরকার নেই এই কারণেও।
নিজে বানান
ServiceEndpointResolver — disposable Pod, স্থায়ী Service, হাতে-কলমে প্রমাণ
- একটা toy cluster state বানান — Pod-দের list, প্রতিটার নিজস্ব IP আর labels dict, আর একটা Service (selector + অপরিবর্তনীয় ClusterIP)
- resolve_service_endpoints() লিখুন — কোন Pod-এর labels selector-এর superset, শুধু তাদের IP ফেরত দিন
- একটা Pod মেরে ফেলুন (list থেকে সরিয়ে দিন) — Service না ছুঁয়ে দেখান endpoints নিজে থেকে বদলায়
- Deployment controller-এর reconcile অনুকরণ করুন — observed < desired দেখে নতুন IP-সহ একটা নতুন Pod তৈরি করুন
- assert দিয়ে যাচাই করুন — মৃত IP বাদ, নতুন IP যুক্ত, ClusterIP পুরো সময় অপরিবর্তিত
লক্ষ্য: এই লেসনের কেন্দ্রীয় দাবিটা — “Pod disposable, Service স্থায়ী, আর মাঝখানের সংযোগটা label selector নিজে থেকে বজায় রাখে” — কোনো বড় infrastructure ছাড়া, একটা ছোট, সত্যিই চালানো simulation দিয়ে হাতে-কলমে দেখানো।
# service_selector_sim.py -- ServiceEndpointResolver: proves a Kubernetes
# Service is nothing but a stable selector re-evaluated against whichever
# Pods currently exist, while Pods themselves are disposable and replaced
# (never repaired) by the Deployment controller's reconciliation loop.
import itertools
_ip_seq = itertools.count(1)
def new_pod(labels):
n = next(_ip_seq)
return {"name": f"web-{n}", "ip": f"10.244.0.{n}", "labels": dict(labels)}
def resolve_service_endpoints(pods, service):
sel = service["selector"]
return [p["ip"] for p in pods
if all(p["labels"].get(k) == v for k, v in sel.items())]
def show(tag, pods, service):
eps = resolve_service_endpoints(pods, service)
names_ips = [(p["name"], p["ip"]) for p in pods]
print(f"{tag}")
print(f" live pods : {names_ips}")
print(f" endpoints : {eps} (ClusterIP {service['cluster_ip']} unchanged)")
print()
LABELS = {"app": "web", "tier": "frontend"}
pods = [new_pod(LABELS) for _ in range(3)]
web_svc = {"name": "web-svc", "cluster_ip": "10.96.0.10", "selector": {"app": "web"}}
show("t0 -- Deployment reconciled desired=3, observed=3", pods, web_svc)
dead = pods.pop(1)
print(f"t1 -- {dead['name']} ({dead['ip']}) crashes: node killed the process, IP gone forever\n")
show("t2 -- Service re-resolves instantly: dead IP silently absent, no one 'told' it", pods, web_svc)
print("t3 -- Deployment controller reconcile: observed=2 != desired=3 -> schedules 1 NEW Pod")
print(" (this is NOT a restart of web-2 -- web-2 is gone; a fresh object is created)\n")
replacement = new_pod(LABELS)
pods.append(replacement)
show(f"t4 -- {replacement['name']} ({replacement['ip']}) Running, same labels, brand-new IP", pods, web_svc)
assert dead["ip"] not in resolve_service_endpoints(pods, web_svc)
assert replacement["ip"] in resolve_service_endpoints(pods, web_svc)
assert web_svc["cluster_ip"] == "10.96.0.10" # never touched, ever
print("OK: ClusterIP never changed; dead Pod's IP dropped; new Pod's IP appeared")
print(" -- zero Service/DNS reconfiguration needed at any step.")প্রকৃত চালিয়ে পাওয়া output (deterministic — হুবহু এটাই আসবে):
t0 -- Deployment reconciled desired=3, observed=3
live pods : [('web-1', '10.244.0.1'), ('web-2', '10.244.0.2'), ('web-3', '10.244.0.3')]
endpoints : ['10.244.0.1', '10.244.0.2', '10.244.0.3'] (ClusterIP 10.96.0.10 unchanged)
t1 -- web-2 (10.244.0.2) crashes: node killed the process, IP gone forever
t2 -- Service re-resolves instantly: dead IP silently absent, no one 'told' it
live pods : [('web-1', '10.244.0.1'), ('web-3', '10.244.0.3')]
endpoints : ['10.244.0.1', '10.244.0.3'] (ClusterIP 10.96.0.10 unchanged)
t3 -- Deployment controller reconcile: observed=2 != desired=3 -> schedules 1 NEW Pod
(this is NOT a restart of web-2 -- web-2 is gone; a fresh object is created)
t4 -- web-4 (10.244.0.4) Running, same labels, brand-new IP
live pods : [('web-1', '10.244.0.1'), ('web-3', '10.244.0.3'), ('web-4', '10.244.0.4')]
endpoints : ['10.244.0.1', '10.244.0.3', '10.244.0.4'] (ClusterIP 10.96.0.10 unchanged)
OK: ClusterIP never changed; dead Pod's IP dropped; new Pod's IP appeared
-- zero Service/DNS reconfiguration needed at any step.তিনটা মুহূর্ত মনে রাখার মতো: t2-এ কোনো explicit “Service আপডেট করো” কল নেই — resolve_service_endpoints শুধু বর্তমান pods list-এর ওপর আবার চলেছে, আর dead IP এমনিতেই বাদ পড়ে গেছে কারণ সেই Pod object-টাই আর তালিকায় নেই। t3-এ নতুন Pod-এর IP (10.244.0.4) সম্পূর্ণ ভিন্ন — এটা web-2-এর “পুনরুত্থান” না, একটা নতুন entity। আর পুরো trace জুড়ে web_svc['cluster_ip'] এবং web_svc['selector'] — অর্থাৎ Service object নিজে — একবারও বদলায়নি। এই একটামাত্র সিমুলেশন এই লেসনের চারটা ধারণার শৃঙ্খলটাই ধরে ফেলে: disposable Pod → reconciliation দিয়ে replace → stable selector দিয়ে Service নিজে থেকে সমন্বয়।
নিজে বাড়ান:
- দ্বিতীয় একটা Service যোগ করুন যার selector
{"tier": "frontend"}— দেখুন এটা একই Pod-দের একটা superset-এ মেলে; দুইটা Service একই Pod-দের ভাগাভাগি করতে পারে কি না যাচাই করুন - Rolling update অনুকরণ করুন — অর্ধেক Pod-এর label-এ
version: v2যোগ করুন, কিন্তু Service selector-এversionনা রেখে দেখান রোলআউটের পুরো সময় ট্রাফিক v1 আর v2 দুই দিকেই যেতে থাকে - একটা দ্বিতীয় Service বানান যার selector-এ
version: v2অতিরিক্ত আছে — এখন এটা শুধু নতুন Pod-দের ধরবে; canary-routing-এর basic mechanism এভাবেই দাঁড় করানো যায়
বাস্তব সিস্টেমে
বাস্তব জগতে — এই চারটা object প্রতিদিন যেভাবে দেখা যায়
kubectl get pods কখনোই সম্পূর্ণ স্থির থাকে না। যেকোনো production cluster-এ kubectl get pods বারবার চালালে নাম বদলাতে দেখা যাবে — web-7d9f8c6b5-x2kpl জাতীয় hash-suffixed নাম, যেগুলো প্রতিটা reconciliation-এ নতুন করে জন্মায়। এই আপাত-বিশৃঙ্খলা আসলে এই লেসনের কেন্দ্রীয় দাবিরই দৃশ্যমান প্রমাণ — Pod-রা identity-সচেতন, দীর্ঘস্থায়ী entity না।
Service-এর একাধিক type — সবগুলোই একই ভিত্তির ওপর। আজ শুধু ডিফল্ট ClusterIP টাইপ কভার হয়েছে (শুধু cluster-এর ভেতরে পৌঁছানো যায়)। NodePort প্রতিটা node-এর একটা নির্দিষ্ট port-এও একই rule expose করে; LoadBalancer cloud provider-এর (AWS ELB, GCP Load Balancer) একটা external load balancer auto-provision করে যেটা ট্রাফিক সেই ClusterIP-তেই পাঠায়। তিনটাই আসলে একই অন্তর্নিহিত kube-proxy rule-এর ওপর ভিন্ন ভিন্ন “সামনের দরজা”।
nginx-ingress, Traefik, HAProxy — Ingress controller বাছাই একটা বাস্তব সিদ্ধান্ত। Ingress resource format কার্যত একই থাকে, কিন্তু কোন controller সেটা বাস্তবায়ন করছে তার ওপর নির্ভর করে performance, TLS-handling, আর advanced routing feature (rate limiting, canary header-matching) ভিন্ন হয়। এই কারণেই বড় প্রতিষ্ঠান সচেতনভাবে একটা নির্দিষ্ট Ingress controller বেছে নেয় ও deploy করে — এটা Kubernetes-এর সাথে built-in আসে না, নিজে ইনস্টল করতে হয়।
Service mesh — Service abstraction-এর ওপর আরেক স্তর। Istio বা Linkerd-এর মতো service mesh প্রতিটা Pod-এ একটা sidecar proxy (ঠিক এই লেসনের sidecar pattern) বসিয়ে Pod-থেকে-Pod traffic-এর ওপর mTLS, retry, আর fine-grained traffic-splitting যোগ করে — কিন্তু এই পুরো কাঠামো এখনো Pod/Service-এর ভিত্তির ওপর দাঁড়িয়ে, সেটা প্রতিস্থাপন করে না।
যে ভুলগুলো সবাই করে
“Pod মানে container — শব্দ দুইটা কার্যত সমার্থক”
ভুল। একটা Pod-এ একের অধিক container থাকতে পারে (sidecar pattern), যদিও বেশিরভাগ Pod-এ একটাই থাকে। মূল পার্থক্যটা সংখ্যার না, role-এর — Pod হলো scheduling-এর একক (সবসময় একই node-এ, একই network namespace-এ), container হলো তার ভেতরের একটা প্রক্রিয়া-চালানোর একক। “একটা container চালাচ্ছি” বলার চেয়ে নির্ভুল হলো “একটা Pod চালাচ্ছি যাতে একটা container আছে।”
“একটা Pod ক্র্যাশ করলে Kubernetes সেটাকে restart করে, ঠিক যেমন একটা VM reboot হয়”
আংশিক সত্য, কিন্তু বিপজ্জনকভাবে অস্পষ্ট। যদি Pod নিজে বেঁচে থাকে আর শুধু তার ভেতরের একটা container ক্র্যাশ করে, kubelet সেই container-টা একই Pod-এর ভেতরে in-place restart করে — Pod identity আর IP অক্ষুণ্ণ। কিন্তু যদি পুরো Pod-ই হারিয়ে যায় (node ব্যর্থ হয়, evict হয়), কোনো “restart” ঘটে না — Deployment controller একটা সম্পূর্ণ নতুন Pod object তৈরি করে, নতুন IP-সহ। প্রথমটা VM reboot-এর কাছাকাছি; দ্বিতীয়টা সম্পূর্ণ ভিন্ন একটা মডেল — cattle-not-pets, মেরামত না, প্রতিস্থাপন।
“Service একটা চলমান প্রসেস বা proxy, যার মধ্য দিয়ে সব ট্রাফিক যায়”
ভুল, আর এটা এই লেসনের সবচেয়ে গুরুত্বপূর্ণ সংশোধন। একটা Service ডিফল্টভাবে কোনো single running process না যেটা request forward করে। এটা একটা বিমূর্ত নাম আর নিয়মের সেট, যা প্রতিটা node-এ চলা kube-proxy বাস্তবায়ন করে kernel-level iptables/IPVS DNAT rule দিয়ে। Request কোনো “Service প্রসেসের” মধ্য দিয়ে যায় না — kernel নিজেই packet forwarding-এর সময় destination বদলে দেয়। এই কারণেই একটা Service কোনো single-process bottleneck হিসেবে ক্র্যাশ করতে পারে না।
“Ingress আর Service একই জিনিস, শুধু নাম আলাদা — অথবা Ingress থাকলে Service-এর দরকার নেই”
দুইটাই ভুল, বিপরীত দিক থেকে। Ingress Service-কে প্রতিস্থাপন করে না — Ingress rule-গুলো Service-এর নাম উল্লেখ করেই route করে, Service ছাড়া Ingress-এর কিছু route করার নেই। আর দুইটা ভিন্ন স্তরে কাজ করে: Service হলো L4-এর কাছাকাছি, IP:port-ভিত্তিক, cluster-এর ভেতরের একটা স্থায়ী ঠিকানা; Ingress হলো L7, HTTP host/path পড়ে, cluster-এর বাইরে থেকে ঢোকার একটা নিয়ন্ত্রিত প্রবেশপথ, আর তার কাজ করার জন্য একটা প্রকৃত চলমান controller লাগে যা Kubernetes built-in-এ আসে না।
বুঝেছেন কি না দেখুন
1একটা Pod-এর ভেতরের সব container ঠিক কী দুইটা জিনিস শেয়ার করে (default-ভাবে, storage বাদ দিলে)?স্মরণ
localhost-এ দেখে, port bind করার সময় পরস্পরের সাথে conflict এড়াতে হয়। আর তারা সবসময় একই node-এ schedule হওয়ার গ্যারান্টি পায় — Kubernetes scheduler কখনো একটা Pod-এর container-দের আলাদা node-এ ভাগ করে না।2Kubernetes কেন সরাসরি container schedule না করে একধাপ বাড়তি বিমূর্ততা — Pod — যোগ করেছে? এই ডিজাইন সিদ্ধান্তটা কোন বাস্তব সমস্যা সমাধান করে?যুক্তি
3“একটা মৃত Pod restart হয় না, replace হয়” — এই দাবিটা কি সবসময় সত্যি? kubelet-এর in-place container restart-এর সাথে এই দাবিটা কীভাবে মেলে?যুক্তি
4একটা Service-এর selector app=payments, env=prod। চারটা Pod আছে: P1 label {app: payments, env: prod, version: v2}, P2 label {app: payments, env: staging}, P3 label {app: payments, env: prod}, P4 label {app: billing, env: prod}। কোন Pod-গুলো এই Service-এর endpoint হিসেবে গণ্য হবে?প্রয়োগ
version: v2 label কোনো সমস্যা না, কারণ selector সেটা চায়নি। P2 বাদ পড়ে কারণ env=staging, selector-এর env=prod-এর সাথে মেলে না। P4 বাদ পড়ে কারণ app=billing, selector-এর app=payments-এর সাথে মেলে না।5একটা Pod-এ দুইটা container — main app volume /data/logs-এ লিখছে, logging sidecar একই path থেকে পড়ার কথা কিন্তু কিছুই পড়তে পারছে না, কোনো error ছাড়াই। সবচেয়ে সম্ভাব্য কারণ কী পরীক্ষা করবেন প্রথমে?প্রয়োগ
volumeMounts দিয়ে একই shared volume-কে /data/logs-এ mount করছে, নাকি প্রতিটার নিজস্ব আলাদা, mount-না-করা path আছে? Multi-container Pod-এ শেয়ার্ড volume কোনো automatic জিনিস না — সেটা Pod spec-এ explicit ঘোষণা করতে হয়; শুধু একই Pod-এ থাকলেই filesystem শেয়ার হয় না, যেমন network namespace হয়।6আপনি একটা payments-api Deployment-এর v1-থেকে-v2 canary rollout ডিজাইন করছেন — নতুন v2 code-এ শুরুতে মাত্র ৫% ট্রাফিক পাঠাতে চান, বাকি ৯৫% v1-এ, আর ধীরে ধীরে অনুপাত বদলাতে চান। Pod, Deployment, Service, Label-এর কোন সমন্বয় দিয়ে এটা বাস্তবায়ন করবেন, আর কোথায় ঝুঁকি আছে?ডিজাইন
payments-api-v1 (উদাহরণ, ১৯টা replica) আর payments-api-v2 (১টা replica) — দুইটার Pod-ই একই সাধারণ label বহন করবে (app: payments-api) কিন্তু আলাদা version label। একটামাত্র Service, selector শুধু app: payments-api (version বাদ) — তাই দুই Deployment-এর সব Pod-ই এক Service-এর endpoint-এ পড়ে, আর ট্রাফিক replica-count-এর অনুপাতে (প্রায়) ভাগ হয় kube-proxy-র round-robin-এ। অনুপাত বদলাতে হলে দুই Deployment-এর replicas সংখ্যা বদলান, ভিন্ন weight পেতে। ঝুঁকি: এই পদ্ধতি শুধু replica-count-অনুপাতে ভাগ করে, নির্ভুল ৫%/৯৫% পার্সেন্টেজ-based split না — সঠিক শতাংশ-নিয়ন্ত্রণ আর header/cookie-based routing দরকার হলে একটা service-mesh বা advanced Ingress controller লাগবে, শুধু Service selector দিয়ে যথেষ্ট না।এরপর কী
আজ চারটা object দেখা হলো — Pod, Deployment controller, Service, Ingress — আর Service-এর কেন্দ্রীয় মেকানিজম বারবার এসেছে: একটা স্থায়ী ClusterIP, একটা label selector, আর সেই selector-এর ফলাফল হিসেবে বদলে যেতে থাকা IP-দের একটা তালিকা। কিন্তু একটা প্রশ্ন ইচ্ছাকৃতভাবে এড়ানো হয়েছে এতক্ষণ — কোড লেখার সময় কেউ কি সত্যিই 10.96.0.10 জাতীয় raw IP মনে রাখে বা hardcode করে? বাস্তবে না — application কোড শুধু একটা নাম জানে, payments-api বা payments-api.default.svc.cluster.local।
পরের লেসন, virtualization-cloud/service-discovery-dns-k8s, ঠিক এই ফাঁকটা পূরণ করবে — Kubernetes কীভাবে প্রতিটা Service-এর নামকে স্বয়ংক্রিয়ভাবে সেই স্থায়ী ClusterIP-তে resolve করে দেয়, একটা অন্তর্নির্মিত DNS সিস্টেম দিয়ে, যাতে কোনো application-কে কখনোই কোনো IP hardcode করতে না হয়।
আরও পড়ুন
- Pods — Kubernetes Documentation · Pod-এর প্রামাণ্য সংজ্ঞা — শেয়ার্ড network/storage namespace-সহ co-located container-এর group হিসেবে; multi-container Pod pattern-এর (sidecar-সহ) অফিসিয়াল আলোচনা
- Deployments — Kubernetes Documentation · Deployment controller-এর reconciliation semantics, rolling update strategy, আর ReplicaSet-এর সাথে সম্পর্কের প্রামাণ্য বিবরণ — এই লেসনের immutability দাবির মূল উৎস
- Service — Kubernetes Documentation · ClusterIP, label selector, আর kube-proxy-এর iptables/IPVS বাস্তবায়নের প্রামাণ্য বিবরণ — এই লেসনের 'Service কোনো প্রসেস না' দাবির ভিত্তি
- Ingress — Kubernetes Documentation · Ingress resource ও Ingress controller-এর মধ্যেকার সম্পর্ক, host/path routing rule-এর প্রামাণ্য স্পেসিফিকেশন