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

Service আর cluster DNS — ClusterIP, NodePort আর LoadBalancer

Kubernetes Services and Cluster DNS — ClusterIP, NodePort and LoadBalancer

Taught in Bangla, this lesson shows why Pod IPs are unreliable and how a Service gives a stable name and ClusterIP, selectors, EndpointSlice and readiness, port, targetPort and nodePort, CoreDNS names and the real cost of ndots:5, kube-proxy, NodePort and LoadBalancer, ExternalName and headless Services, and step-by-step debugging of unreachable Services, with real output and simulators.

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

  • Kubernetes-এর network model, আর Pod IP-তে ভরসা না করার কারণ
  • ClusterIP, NodePort, LoadBalancer, ExternalName আর headless-এর পার্থক্য
  • port, targetPort আর nodePort: কোনটি কোথায় খাটে
  • Selector, EndpointSlice আর readiness মিলিয়ে traffic-এর হিসাব
  • CoreDNS-এর নাম, search domain আর ndots-এর আসল খরচ
  • “Service-এ পৌঁছাতে পারছি না” সমস্যা ধাপে ধাপে ধরা

১ · Pod-এর ঠিকানা কেন ভরসার নয়

পর্ব ৪৩-এ আপনি Pod দেখেছেন, আর পর্ব ৪৪-এ দেখেছেন Deployment কীভাবে Pod বদলায়। প্রতিবার rolling update-এ পুরোনো Pod যায়, নতুন Pod আসে। এখন প্রশ্ন হলো, একটি frontend Pod যদি একটি API Pod-কে ডাকতে চায়, সে কোন ঠিকানায় ডাকবে?

একটি বড় হাসপাতাল ভাবুন। মেডিসিন বিভাগে ডাক্তাররা shift অনুযায়ী বদলান। কারও ব্যক্তিগত মোবাইল নম্বর রোগীকে দিলে পরের shift-এ সেই নম্বর কাজে আসবে না। তাই হাসপাতাল একটি স্থির রিসেপশন নম্বর দেয়। রিসেপশনের হাতে থাকে ডিউটি তালিকা: এই মুহূর্তে কে আছেন, আর কে রোগী দেখতে প্রস্তুত। Service হলো সেই রিসেপশন নম্বর, EndpointSlice হলো ডিউটি তালিকা, আর “মেডিসিন বিভাগের ডাক্তার” কথাটি হলো selector.

এই পর্বের transcript সম্পর্কে: নিচের সব kubectl আউটপুট একটি local single-node cluster (Kubernetes ১.৩৪)-এ আসল run থেকে নেওয়া। Pod-এর নাম, বয়স, IP আর সময় আপনার cluster-এ আলাদা হবে। কিছু লাইন ছেঁটে … দেওয়া হয়েছে, কিন্তু কোনো মান বানানো হয়নি। যেখানে একাধিক node বা cloud load balancer লাগে, সেখানে আলাদা করে বলা আছে। Transcript-এ --context flag বাদ রাখা হয়েছে।

নিজে চালাতে চাইলে: নিজের মেশিনের local cluster-এ (যেমন Docker Desktop-এর Kubernetes) কোনো বিল আসে না। একটি আলাদা namespace বানিয়ে নিচের YAML apply করুন। পরীক্ষার Pod-টি বানাতে পারেন এভাবে: kubectl run client --image=nginx:1.27-alpine --command -- sleep 3600. এই image-এ curl থাকে। কাজ শেষে নাম ধরে namespace মুছুন: kubectl delete ns <আপনার-namespace>. Cloud-এর cluster-এ LoadBalancer Service খরচ আনতে পারে, তাই সেখানে আগে দাম দেখে নিন।

আসল পরীক্ষা: Pod মরলে ঠিকানার কী হয়

একটি Deployment চালানো হলো, নাম api, তিনটি Pod. প্রতিটি Pod নিজের নাম ফেরত দেয়। পাশে আছে client নামের একটি Pod, যেখান থেকে আমরা curl চালাব। প্রথম Pod-টিকে মুছে দেখি।

$ kubectl get pods -l app=api -o custom-columns=NAME:.metadata.name,IP:.status.podIP
NAME                   IP
api-7d66f59445-7x6sx   10.1.0.10
api-7d66f59445-fb2fb   10.1.0.13
api-7d66f59445-th48x   10.1.0.11

$ kubectl exec client -- curl -s --max-time 3 http://10.1.0.10:8080/
api-7d66f59445-7x6sx

$ kubectl delete pod api-7d66f59445-7x6sx
pod "api-7d66f59445-7x6sx" deleted from abcl-ks namespace

$ kubectl get pods -l app=api -o custom-columns=NAME:.metadata.name,IP:.status.podIP
NAME                   IP
api-7d66f59445-9bm9z   10.1.0.16
api-7d66f59445-fb2fb   10.1.0.13
api-7d66f59445-th48x   10.1.0.11

$ kubectl exec client -- curl -sS --max-time 3 http://10.1.0.10:8080/
curl: (28) Connection timed out after 3002 milliseconds
command terminated with exit code 28

Deployment নিজের কাজ ঠিকই করেছে: Pod-এর সংখ্যা আবার তিন। কিন্তু নতুন Pod পেয়েছে নতুন নাম আর নতুন ঠিকানা 10.1.0.16. পুরোনো 10.1.0.10-তে হাতে লেখা কোনো config থাকলে এখন সেটি অকেজো। আরও খারাপ হলো, কিছুদিন পর সেই ঠিকানা অন্য কোনো Pod পেতে পারে, তখন আপনার অনুরোধ ভুল জায়গায় যাবে।

Kubernetes-এর network model

ঠিকানা বদলালেও Kubernetes-এ network-এর নিয়মটি খুব সরল। পুরো তত্ত্ব আছে Computer Networks · Cloud ও Container Networking পর্বে। এখানে দরকারি অংশটুকু:

  • প্রতিটি Pod নিজের একটি IP পায়, আর Pod-এর সব container সেই ঠিকানা ভাগ করে নেয়।
  • যেকোনো Pod অন্য যেকোনো Pod-এ NAT ছাড়াই পৌঁছাতে পারে, Pod দুটি আলাদা node-এ থাকলেও।
  • Node-এর নিজের agent (যেমন kubelet) সেই node-এর Pod-এ পৌঁছাতে পারে।

Kubernetes এই network নিজে বানায় না। Pod তৈরির সময় একটি CNI plugin ঠিকানা দেয় আর পথ সাজায়। Flannel, Calico আর Cilium এমন plugin-এর পরিচিত উদাহরণ।

“NAT ছাড়া” কথাটি আমরা যাচাই করলাম। client Pod-এর ঠিকানা 10.1.0.12. সে অংশ ২-এর Service-এর নাম ধরে একটি অনুরোধ পাঠাল, আর উত্তর দেওয়া Pod-এর nginx log-এ দেখা গেল:

10.1.0.12 - - [30/Oct/2026:19:15:05 +0000] "GET / HTTP/1.1" 200 21 "-" "curl/…

Pod যে ঠিকানা দেখল, সেটি client-এর নিজের ঠিকানা। অর্থাৎ উৎস-ঠিকানা মাঝপথে বদলায়নি। Service-এর পথেও এমন কেন থাকে, তা অংশ ৫-এ পরিষ্কার হবে।

২ · Service: স্থির নাম, স্থির ঠিকানা

Service একটি API object. এটি কোনো process নয়, আর কোনো Pod-ও নয়। এটি একটি ঘোষণা: “app=api label-ওয়ালা Pod-গুলোকে একটি নামে আর একটি ঠিকানায় পাওয়া যাবে।” নিচে আমাদের আসল Deployment আর Service. দুটোই একটি namespace-এ চালানো হয়েছে, আর kubectl apply --dry-run=server-ও পাস করেছে।

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
  namespace: abcl-ks
spec:
  replicas: 3
  selector:
    matchLabels:
      app: api
  template:
    metadata:
      labels:
        app: api
    spec:
      containers:
        - name: web
          image: nginx:1.27-alpine
          imagePullPolicy: IfNotPresent
          command: ["sh", "-c"]
          args:
            - |
              sed -i 's/listen  *80;/listen 8080;/' /etc/nginx/conf.d/default.conf
              echo "$HOSTNAME" > /usr/share/nginx/html/index.html
              echo ok > /usr/share/nginx/html/ready.html
              exec nginx -g 'daemon off;'
          ports:
            - name: http
              containerPort: 8080
          readinessProbe:
            httpGet:
              path: /ready.html
              port: http
            periodSeconds: 2
            failureThreshold: 1
          resources:
            requests:
              cpu: 10m
              memory: 16Mi
            limits:
              memory: 48Mi
apiVersion: v1
kind: Service
metadata:
  name: api
  namespace: abcl-ks
spec:
  type: ClusterIP
  selector:
    app: api
  ports:
    - name: http
      port: 80
      targetPort: http
$ kubectl get svc api
NAME   TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)   AGE
api    ClusterIP   10.105.249.110   <none>        80/TCP    2s

$ kubectl get endpointslices
NAME        ADDRESSTYPE   PORTS   ENDPOINTS                       AGE
api-k4nqn   IPv4          8080    10.1.0.10,10.1.0.13,10.1.0.11   2s

ClusterIP 10.105.249.110 হলো Service-এর স্থির ঠিকানা। Service মুছে না ফেলা পর্যন্ত এটি থাকে, পেছনের Pod যতই বদলাক। এই ঠিকানাটি একটি virtual IP: কোনো network card বা process এই ঠিকানায় বসে নেই।

তিনটি port: কে কোনটি

নতুনদের সবচেয়ে বেশি গুলিয়ে যায় এই তিনটি নাম। ধরুন একটি বাড়িতে তিনটি দরজা পরপর।

Fieldকার portআমাদের মানকে ব্যবহার করে
nodePortপ্রতিটি node-এর30845Cluster-এর বাইরের client
portService-এর (ClusterIP-এ)80Cluster-এর ভেতরের client
targetPortPod-এরhttp (মানে 8080)Service-এর শেষ গন্তব্য
containerPortContainer-এর ঘোষণা8080মূলত নথি; নাম দিলে targetPort তা ধরতে পারে

আমরা targetPort: http লিখেছি, সংখ্যা নয়। এর সুবিধা হলো, পরে app-এর port বদলালে শুধু Pod template বদলালেই চলে, Service একই থাকে। আসল run-এও Service-এর port 80, কিন্তু EndpointSlice-এর PORTS ঘরে 8080 দেখা যাচ্ছে।

Service-এর ধরনগুলো এক ছবিতে

NodePort আর LoadBalancer আলাদা কোনো জিনিস নয়। এগুলো ClusterIP-এর ওপরে একটি করে বাড়তি স্তর।

Service-এর তিন ধরন, স্তরে স্তরে

LoadBalancer wraps NodePort, NodePort wraps ClusterIP, selector picks Ready Pods

LoadBalancer, NodePort আর ClusterIP-এর স্তর, পেছনে তিনটি Pod বাইরের client LoadBalancer Cluster-এর বাইরে, provider বানায় Cluster NodePort :30845 প্রতিটি node-এ একই port ClusterIP 10.105.249.110:80 api.abcl-ks.svc.cluster.local ভেতরের Pod সরাসরি এখানেই আসে selector app=api EndpointSlice: শুধু ready ঠিকানায় traffic × Pod ১ :8080 Ready Pod ২ :8080 Not Ready Pod ৩ :8080 Ready port 80 → targetPort 8080
ছবি ৪৫.১ — প্রতিটি বাইরের স্তর ভেতরের স্তরটি ব্যবহার করে। ভেতরের client শুধু ClusterIP পর্যন্ত আসে।

৩ · Selector, EndpointSlice আর readiness

Service আর Pod-এর মধ্যে সরাসরি কোনো জোড়া নেই। জোড়া দেয় একটি controller. সে selector-এর সাথে মেলে এমন Pod খুঁজে বের করে, আর তাদের ঠিকানা EndpointSlice object-এ লিখে রাখে। প্রতিটি ঠিকানার পাশে থাকে অবস্থা: ready, serving, terminating.

Selector মেলানোর নিয়মটি হলো AND: Selector-এ যত label লেখা আছে, Pod-এ তার সবগুলো একই মানে থাকতে হবে। Pod-এ বাড়তি label থাকলে সমস্যা নেই। kubectl দিয়ে আমরা এখন দুটি জিনিস পরীক্ষা করব: Pod-এর সংখ্যা বদলালে কী হয়, আর একটি Pod অসুস্থ হলে কী হয়।

1kubectl get endpointslices2kubectl scale deploy/api --replicas=53# readiness pass করা পর্যন্ত অপেক্ষা4kubectl scale deploy/api --replicas=25# কয়েক সেকেন্ড পরে আবার দেখা
অবস্থা
চাওয়া Pod
তালিকায় ঠিকানা
  1. শুরু: তিনটি Pod, তালিকায় তিনটি ঠিকানা। EndpointSlice-এর নাম api-k4nqn.
  2. Scale 5: Deployment-এর চাহিদা এখন ৫। ঠিক পরমুহূর্তের আউটপুটে ঠিকানা এখনো ৩টি, কারণ নতুন Pod-এর তখনো IP-ই হয়নি, আর IP ছাড়া Pod তালিকায় ঢোকে না।
  3. কয়েক সেকেন্ড পরে: ঠিকানা ৫টি। + 2 more... মানে kubectl শুধু প্রথম তিনটি লিখেছে, বাকি দুটি সংক্ষেপ করেছে।
  4. Scale 2: প্রায় এক সেকেন্ড পরে তালিকায় ৪টি ঠিকানা (৩ + ১)। মুছে যাওয়া Pod একে একে বেরিয়ে যাচ্ছে, তাই সংখ্যাটি মাঝপথে।
  5. স্থির অবস্থা: দুটি ঠিকানা রইল। Service-এর ClusterIP বা নাম কোথাও বদলায়নি, শুধু পেছনের তালিকা বদলেছে।

একটি Pod অসুস্থ হলে: যে ফাঁদে অনেকেই পড়েন

আমাদের readiness probe /ready.html ফাইলটি চায়। একটি Pod-এর ভেতর থেকে ফাইলটি মুছে দিলে probe ব্যর্থ হবে, কিন্তু nginx চলতেই থাকবে। এবার দেখুন:

$ kubectl exec api-7d66f59445-fb2fb -- rm /usr/share/nginx/html/ready.html

$ kubectl get pods -l app=api
NAME                   READY   STATUS    RESTARTS   AGE
api-7d66f59445-fb2fb   0/1     Running   0          81s
api-7d66f59445-mn4r9   1/1     Running   0          7s
api-7d66f59445-th48x   1/1     Running   0          81s

$ kubectl get endpointslices -l kubernetes.io/service-name=api
NAME        ADDRESSTYPE   PORTS   ENDPOINTS                       AGE
api-k4nqn   IPv4          8080    10.1.0.13,10.1.0.11,10.1.0.40   81s

$ kubectl exec client -- sh -c 'for i in $(seq 1 30); do curl -s http://api/; done | sort | uniq -c'
     14 api-7d66f59445-mn4r9
     16 api-7d66f59445-th48x

খেয়াল করুন, ENDPOINTS কলামে অসুস্থ 10.1.0.13 এখনো আছে! অথচ ৩০টি curl-এর একটিও সেখানে যায়নি। কারণ এই কলাম শুধু ঠিকানা দেখায়, অবস্থা দেখায় না। অবস্থা দেখতে হলে object-এর ভেতরে তাকাতে হয়:

$ kubectl get endpointslices -l kubernetes.io/service-name=api -o yaml
…
  endpoints:
  - addresses:
    - 10.1.0.13
    conditions:
      ready: false
      serving: false
      terminating: false
    nodeName: docker-desktop
    targetRef:
      kind: Pod
      name: api-7d66f59445-fb2fb
…
  ports:
  - name: http
    port: 8080
    protocol: TCP

# Shorter: one column per field (a later run, other names and IPs)
$ kubectl get endpointslices -l kubernetes.io/service-name=api \
    -o custom-columns='NAME:.metadata.name,ADDRESS:.endpoints[*].addresses[0],READY:.endpoints[*].conditions.ready'
NAME        ADDRESS                            READY
api-4rcrm   10.1.0.140,10.1.0.138,10.1.0.139   true,false,true

$ kubectl get endpoints api
Warning: v1 Endpoints is deprecated in v1.33+; use discovery.k8s.io/v1 EndpointSlice
NAME   ENDPOINTS                         AGE
api    10.1.0.139:8080,10.1.0.140:8080   9s

মনে রাখার মতো: kubectl get endpointslices-এর ENDPOINTS কলামে ঠিকানা দেখলেই ধরে নেবেন না যে সেটি traffic পাচ্ছে। পুরোনো Endpoints object-এর কলামে শুধু ready ঠিকানা থাকে, কিন্তু আসল run-এই kubectl জানাচ্ছে যে এটি v1.33 থেকে deprecated. তাই conditions.ready পড়াটাই নিরাপদ অভ্যাস।

অসুস্থ Pod-টি কি সত্যিই মরে গেছে? না। আমরা সরাসরি curl http://10.1.0.13:8080/ চালালে সে উত্তর দিয়েছে api-7d66f59445-fb2fb. Readiness probe ব্যর্থ হলে Pod restart হয় না, শুধু Service-এর তালিকায় “not ready” হয়। ফাইলটি ফিরিয়ে দিতেই কয়েক সেকেন্ডের মধ্যে Pod আবার 1/1 হয়েছে।

নিচের simulator-এ selector, Pod-এর label, readiness আর targetPort বদলে দেখুন। app: "apii" লিখে একবার চালান, তারপর TARGET_PORT = "web" দিয়ে চালান।

// Service selection: selector + readiness = traffic. Edit and run.
const SELECTOR = { app: "api" };     // try { app: "apii" } or {}
const TARGET_PORT = "http";          // try 9090 or "web"
const PODS = [
  { name: "api-1", ip: "10.1.0.11", labels: { app: "api" }, ready: true, ports: { http: 8080 } },
  { name: "api-2", ip: "10.1.0.13", labels: { app: "api" }, ready: false, ports: { http: 8080 } },
  { name: "api-3", ip: "10.1.0.40", labels: { app: "api" }, ready: true, ports: { http: 8080 } },
  { name: "db-1", ip: "10.1.0.50", labels: { app: "db" }, ready: true, ports: { sql: 5432 } },
];
const keys = Object.keys(SELECTOR);
if (keys.length === 0) {
  console.log("No selector: Kubernetes manages no endpoints for this Service.");
} else {
  const members = PODS.filter(p => keys.every(k => p.labels[k] === SELECTOR[k]));
  for (const p of PODS) {
    const inSlice = members.includes(p);
    console.log(p.name.padEnd(6), p.ip.padEnd(10), inSlice ? "in slice" : "no match",
      inSlice ? "ready=" + p.ready : "");
  }
  const port = p => typeof TARGET_PORT === "number" ? TARGET_PORT : p.ports[TARGET_PORT];
  const ready = members.filter(p => p.ready);
  const shown = members.length ? port(members[0]) ?? "<unset>" : "<unset>";
  console.log("slice PORTS:", shown, "| addresses listed:", members.length);
  if (members.length === 0) console.log("Result: no endpoints, connection fails. Check the selector.");
  else if (ready.length === 0) console.log("Result: all listed, none ready. Check the probe.");
  else if (shown === "<unset>") console.log("Result: port name not found on the Pods.");
  else console.log("Traffic goes to:", ready.map(p => p.ip + ":" + port(p)).join(" "));
}

আউটপুট: api-2 10.1.0.13 in slice ready=false, তারপর slice PORTS: 8080 | addresses listed: 3, আর শেষে Traffic goes to: 10.1.0.11:8080 10.1.0.40:8080. তিনটি ঠিকানা তালিকায়, কিন্তু traffic দুটিতে। db-1 selector-এই মেলেনি। "apii" লিখলে আসে addresses listed: 0. "web" লিখলে ঠিকানা থাকে, কিন্তু slice PORTS: <unset> হয়; আসল cluster-এও অংশ ৮-এ ঠিক এটিই দেখবেন।

৪ · Cluster DNS: নাম থেকে ঠিকানা

ClusterIP স্থির হলেও সংখ্যা মনে রাখা বা config-এ হাতে লেখা ভালো নয়। Cluster-এর ভেতরে একটি DNS server চলে, সাধারণত CoreDNS. প্রতিটি Service-এর জন্য সে নিজে থেকেই একটি নামের উত্তর দেয়। এটিই Kubernetes-এর প্রধান service discovery. পর্ব ২৯-এ দেখা DNS-এর ধারণাই এখানে খাটে, তবে নামগুলো cluster-এর ভেতরের।

একটি Service-এর পুরো নাম (FQDN) হয় এমন:

api.abcl-ks.svc.cluster.local
service · namespace · svc · cluster domain

Pod-এর ভেতরের DNS সেটিং

$ kubectl exec client -- cat /etc/resolv.conf
nameserver 10.96.0.10
search abcl-ks.svc.cluster.local svc.cluster.local cluster.local
options ndots:5

$ kubectl exec client -- getent hosts api
10.105.249.110    api.abcl-ks.svc.cluster.local  api.abcl-ks.svc.cluster.local api

$ kubectl exec client -- getent hosts api.abcl-ks
10.105.249.110    api.abcl-ks.svc.cluster.local  api.abcl-ks.svc.cluster.local api.abcl-ks

$ kubectl exec client -- getent hosts kubernetes.default
10.96.0.1         kubernetes.default.svc.cluster.local  kubernetes.default.svc.cluster.local kubernetes.default

$ kubectl exec client -- nslookup api.abcl-ks
Server:		10.96.0.10
Address:	10.96.0.10:53

Non-authoritative answer:

** server can't find api.abcl-ks: NXDOMAIN

এই আউটপুটে তিনটি শিক্ষা আছে। এক, kubelet প্রতিটি Pod-এর /etc/resolv.conf বানিয়ে দেয়। এতে nameserver হলো CoreDNS-এর সামনের Service-এর ClusterIP, আর প্রথম search domain-এ Pod-এর নিজের namespace থাকে। তাই একই namespace-এ শুধু api লিখলেই চলে, অন্য namespace-এর জন্য api.abcl-ks লিখতে হয়।

দুই, getent আর curl যে পথে নাম খোঁজে, app-ও সেই পথে খোঁজে। শেষের nslookup api.abcl-ks NXDOMAIN দিলেও curl http://api.abcl-ks/ আসল run-এ 200 পেয়েছে। এই image-এর ছোট BusyBox nslookup dot-ওয়ালা নামে search list ব্যবহার করেনি। তাই DNS যাচাইয়ে getent hosts বেশি বিশ্বাসযোগ্য।

তিন, সব নাম একই ধরনের নয়। kubernetes.default-ও একই নিয়মে মিলেছে, কারণ default namespace-এ kubernetes নামের Service থাকে।

ndots:5 আসলে কী করে

নিয়মটি হলো: নামে ndots-এর চেয়ে কম dot থাকলে resolver আগে search domain জুড়ে চেষ্টা করে। শেষে dot থাকলে নামটি absolute, তখন search-এর প্রশ্নই আসে না। এই আচরণ চোখে দেখতে আমরা একটি ছোট Pod চালালাম, যা DNS server সেজে শুধু প্রশ্নগুলো লিখে রাখে আর সবকিছুতে NXDOMAIN দেয়। আরেকটি Pod-এর dnsConfig-এ এই server-এর ঠিকানা দেওয়া হলো, search আর ndots:5 একই রইল। তারপর সেই Pod থেকে getent hosts চালানো হলো। Log-এ যা এলো:

# getent hosts shop.example.com     (2 dots, 2 < 5: search first)
AAAA shop.example.com.abcl-ks.svc.cluster.local.
AAAA shop.example.com.svc.cluster.local.
AAAA shop.example.com.cluster.local.
AAAA shop.example.com.
A shop.example.com.abcl-ks.svc.cluster.local.
A shop.example.com.svc.cluster.local.
A shop.example.com.cluster.local.
A shop.example.com.

# getent hosts shop.example.com.    (trailing dot: absolute)
AAAA shop.example.com.
A shop.example.com.

# getent hosts a.b.c.d.e.example    (5 dots, not less than 5)
AAAA a.b.c.d.e.example.
A a.b.c.d.e.example.

একটি বাইরের নাম খুঁজতে ৮টি প্রশ্ন, আর শেষে একটি dot দিলে মাত্র ২টি! Log server সব কিছুতে NXDOMAIN দিয়েছে বলে এখানে সব ধাপ দেখা গেছে; আসল DNS-এ যে নাম মিলে যায়, সেখানেই খোঁজা থামে। এই Pod-এর image Alpine-ভিত্তিক, তাই resolver musl. glibc-ভিত্তিক image-এ ক্রম কিছুটা আলাদা হতে পারে, যেমন dot যথেষ্ট থাকলে absolute চেষ্টা ব্যর্থ হওয়ার পর সে search-ও চেষ্টা করে। নিচের simulator এই নিয়মটি হিসাব করে।

// Search-path expansion, musl-style. Edit NAME and NDOTS.
const NAME = "shop.example.com";     // try "api", "api.abcl-ks", "shop.example.com."
const NDOTS = 5;                     // try 1
const SEARCH = ["abcl-ks.svc.cluster.local", "svc.cluster.local", "cluster.local"];
const ZONE = {                       // names that really exist
  "api.abcl-ks.svc.cluster.local.": "10.105.249.110",
  "shop.example.com.": "203.0.113.10",
};
function candidates(name, ndots, search) {
  if (name.endsWith(".")) return [name];           // absolute name
  const dots = name.split(".").length - 1;
  if (dots >= ndots) return [name + "."];          // enough dots
  return [...search.map(s => name + "." + s + "."), name + "."];
}
let packets = 0, answer = "NXDOMAIN";
for (const q of candidates(NAME, NDOTS, SEARCH)) {
  packets += 2;                                    // one A + one AAAA
  const hit = ZONE[q];
  console.log((hit ? "HIT " : "miss"), q);
  if (hit) { answer = hit; break; }
}
const dots = NAME.split(".").length - 1;
console.log("dots:", dots, "| ndots:", NDOTS, "| packets (A+AAAA):", packets);
console.log("answer:", answer);

প্রথম চালানোয় তিনটি miss-এর পর HIT shop.example.com., আর dots: 2 | ndots: 5 | packets (A+AAAA): 8. এটি আসল log-এর ৮টি লাইনের সাথে মেলে। NDOTS = 1 দিলে একবারেই HIT, প্যাকেট ২টি। NAME = "api" দিলে প্রথম চেষ্টাতেই api.abcl-ks.svc.cluster.local. মেলে। ৮ নম্বরটি এসেছে এভাবে: ৩টি search + ১টি আসল নাম = ৪টি নাম, প্রতিটিতে A আর AAAA, মোট ৪ × ২ = ৮।

অভ্যাস: App বাইরের কোনো host-কে বারবার ডাকলে config-এ শেষের dot দেওয়া নাম (shop.example.com.) ব্যবহার করতে পারেন, তবে সব library বা TLS যাচাই এতে স্বচ্ছন্দ নয়, আগে পরীক্ষা করে নিন। আরেকটি পথ হলো Pod-এর dnsConfig-এ ndots কমানো। মনে রাখুন, এই cluster-এর CoreDNS config-এ TTL ৩০ সেকেন্ড; অন্য cluster-এ এটি আলাদা হতে পারে।

৫ · kube-proxy আর traffic ভাগ

ClusterIP-এ কেউ বসে নেই, তাহলে সংযোগ পৌঁছায় কীভাবে? উত্তর লুকিয়ে আছে প্রতিটি node-এর kernel-এ। kube-proxy প্রতিটি node-এ চলে, Service আর EndpointSlice দেখে, আর kernel-এ rule বসায়: “এই ClusterIP:port-এ যাওয়া packet-এর গন্তব্য এই Pod-গুলোর একটিতে বদলে যাবে।” নামে proxy হলেও প্রচলিত mode-এ packet তার ভেতর দিয়ে যায় না; সে শুধু নিয়ম লেখে।

পুরো পথটি এবার ধাপে ধাপে দেখি। মানগুলো আসল run-এর।

  1. Pod client10.1.0.12http://api/
  2. CoreDNS10.96.0.10নাম → ঠিকানা
  3. Node rule10.105.249.110:80DNAT
  4. Pod api10.1.0.13:8080উত্তর দেয়
Pod clientনাম খোঁজা শুরু
$ curl http://api/
# resolv.conf: nameserver 10.96.0.10, ndots:5
api: 0 dots, 0 < 5, so try the search list
DNS প্রশ্নUDP 53
A? api.abcl-ks.svc.cluster.local.
to 10.96.0.10:53 (itself a ClusterIP)
DNS উত্তরPod নয়, Service-এর ঠিকানা
Name:    api.abcl-ks.svc.cluster.local
Address: 10.105.249.110
Node-এর kernelনমুনা হিসাব
SYN  10.1.0.12:src → 10.105.249.110:80
rule: pick one Ready endpoint
DNAT → 10.1.0.13:8080
conntrack remembers this translation
Pod-এর nginx logআসল লাইন
10.1.0.12 - - […] "GET / HTTP/1.1" 200 21 …
api-7d66f59445-fb2fb
Pod clientকী দেখল
$ curl -sS -o /dev/null -w '%{remote_ip}:%{remote_port} %{http_code}\n' \
    http://api.abcl-ks.svc.cluster.local/
10.105.249.110:80 200
  1. নাম দিয়ে শুরু: App শুধু api জানে। Resolver দেখল নামে dot নেই, তাই search list ব্যবহার করবে।
  2. DNS প্রশ্ন: প্রথম search domain জুড়ে নাম হলো api.abcl-ks.svc.cluster.local. প্রশ্নটি গেল 10.96.0.10-এ, যা নিজেও একটি Service-এর ClusterIP.
  3. DNS উত্তর: CoreDNS দিল 10.105.249.110. লক্ষ করুন, এটি Pod-এর নয়, Service-এর ঠিকানা। Pod বদলালেও এই উত্তর বদলায় না।
  4. Node-এর rule: Client ClusterIP-এর port 80-তে সংযোগ শুরু করল। Packet Pod থেকে বেরোতেই node-এর rule একটি Ready Pod বেছে নিল, আর গন্তব্য বদলে দিল। একে বলে DNAT. conntrack এই বদল মনে রাখল।
  5. Pod-এ পৌঁছানো: Pod পেল port 8080-এ একটি সংযোগ, উৎস 10.1.0.12. শুধু গন্তব্য বদলেছে, উৎস নয়, তাই অংশ ১-এর log-এ client-এর আসল ঠিকানা দেখা গেছে।
  6. ফেরার পথ: Pod উত্তর দিল। Conntrack উৎসকে আবার ClusterIP বানিয়ে দিল। তাই curl মনে করল সে 10.105.249.110:80-এর সাথেই কথা বলেছে, আর দেখাল 200.

Traffic ভাগ: আসল গণনা

# Three Ready Pods, 30 separate connections
$ kubectl exec client -- sh -c 'for i in $(seq 1 30); do curl -s http://api/; done | sort | uniq -c'
      8 api-7d66f59445-9bm9z
     10 api-7d66f59445-fb2fb
     12 api-7d66f59445-th48x

# Same again
      8 api-7d66f59445-9bm9z
     11 api-7d66f59445-fb2fb
     11 api-7d66f59445-th48x

# One curl, six URLs: one TCP connection is reused
$ kubectl exec client -- curl -s http://api/ http://api/ http://api/ http://api/ http://api/ http://api/
api-7d66f59445-fb2fb
api-7d66f59445-fb2fb
api-7d66f59445-fb2fb
api-7d66f59445-fb2fb
api-7d66f59445-fb2fb
api-7d66f59445-fb2fb

এখান থেকে সবচেয়ে কাজের শিক্ষাটি পাওয়া যায়। kube-proxy অনুরোধ ভাগ করে না, সংযোগ ভাগ করে। ভাগ এলোমেলো, round-robin-এর মতো সমান পালা নয়, তাই ৮, ১০, ১২ এসেছে। আর একটি keep-alive সংযোগে যত অনুরোধই যাক, সব এক Pod-এ যায়। Connection pool রাখা app-এ scale করার পর নতুন Pod প্রায় খালি বসে থাকতে পারে, পুরোনো সংযোগ বন্ধ না হওয়া পর্যন্ত। এজন্য HTTP/2 বা gRPC traffic-এ অনুরোধ-স্তরের load balancing অনেক সময় আলাদা করে লাগে।

তিনটি mode: iptables, IPVS, nftables

এই cluster-এর kube-proxy config-এ mode ফাঁকা, আর Linux-এ ফাঁকা মানে iptables. Node-এর rule আমরা খুলে দেখিনি, কারণ সেটি আমাদের namespace-এর বাইরের এলাকা। নিচে iptables mode-এর rule-এর আকার দেখানো হলো। এটি নমুনা, এই run-এর আউটপুট নয়; chain-এর নাম আসলে hash-এর মতো দেখায়।

-A KUBE-SERVICES -d 10.96.0.20/32 -p tcp --dport 80 -j KUBE-SVC-EXAMPLE
-A KUBE-SVC-EXAMPLE -m statistic --mode random --probability 0.33333 -j KUBE-SEP-AAAA
-A KUBE-SVC-EXAMPLE -m statistic --mode random --probability 0.50000 -j KUBE-SEP-BBBB
-A KUBE-SVC-EXAMPLE -j KUBE-SEP-CCCC
-A KUBE-SEP-AAAA -p tcp -m tcp -j DNAT --to-destination 10.1.0.11:8080

প্রথম rule ১/৩ সম্ভাবনায় ধরে। বাকি ২/৩-এর ১/২ দ্বিতীয় rule ধরে, অর্থাৎ ২/৩ × ১/২ = ১/৩। যা বাকি থাকে, তৃতীয় rule ধরে: ১/৩। তাই প্রতিটি Pod সমান সম্ভাবনা পায়। সব সংখ্যা আলাদা দেখালেও ফল সমান, এটি পরীক্ষায় প্রিয় হিসাব। নিচে নিজে চালিয়ে দেখুন।

// iptables-mode pick: one random choice per NEW connection.
const PODS = ["api-a", "api-b", "api-c"];  // add or remove Pods
const CONNECTIONS = 3000;
const REQUESTS_PER_CONN = 1;               // try 100: keep-alive
function mulberry32(a) {
  return function () {
    a |= 0; a = a + 0x6D2B79F5 | 0;
    let t = Math.imul(a ^ a >>> 15, 1 | a);
    t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
    return ((t ^ t >>> 14) >>> 0) / 4294967296;
  };
}
const rand = mulberry32(45);
const n = PODS.length;
PODS.forEach((p, i) => {
  const left = n - i;
  console.log("rule", i + 1, "->", p, "probability 1/" + left);
});
const conns = Array(n).fill(0);
for (let c = 0; c < CONNECTIONS; c++) {
  for (let i = 0; i < n; i++) {
    if (i === n - 1 || rand() < 1 / (n - i)) { conns[i]++; break; }
  }
}
PODS.forEach((p, i) => console.log(p, "connections:", conns[i],
  "requests:", conns[i] * REQUESTS_PER_CONN));

আউটপুট: rule 1 -> api-a probability 1/3, rule 2 -> api-b probability 1/2, rule 3 -> api-c probability 1/1. গণনা: api-a connections: 1014, api-b connections: 1036, api-c connections: 950. প্রত্যাশা প্রতিটিতে ১০০০, কিন্তু এলোমেলো ভাগে ছোট পার্থক্য থাকবেই। এবার CONNECTIONS = 6 আর REQUESTS_PER_CONN = 100 দিন: ফল আসে 100, 200, 300 request. কম সংযোগে অসমতা কত বড় হতে পারে, এটি তার ছবি।

Modeকীভাবে খোঁজেখেয়াল রাখুন
iptablesRule-এর তালিকা ধরে এগোয়লেখার সময়ে (১.৩৭) Linux-এ default, তবে নথি জানায় ভবিষ্যতে default হবে nftables. Service হাজারের ঘরে গেলে rule বদলাতে সময় লাগে।
IPVSKernel-এর hash tableএকাধিক scheduling পদ্ধতি আছে। Kubernetes ১.৩৫-এ deprecated হয়েছে (KEP-5495), পরে সরানোর পরিকল্পনা আছে।
nftablesKernel-এর mapনতুন পথ। Kubernetes ১.৩৩-এ stable. নতুন kernel লাগে (১.৩৩-এর নথিতে ৫.১৩ বা পরের), আর সব network plugin-এর সাথে নাও মিলতে পারে।
eBPF-ভিত্তিকKernel-এ ছোট প্রোগ্রামযেমন Cilium, kube-proxy-র জায়গা নিতে পারে। এটি CNI-ভেদে আলাদা, Kubernetes-এর নিজস্ব mode নয়।

Session affinity

কোনো কারণে একই client-কে একই Pod-এ রাখতে হলে Service-এ sessionAffinity: ClientIP দেওয়া যায়। এটি sticky session-এর সরল রূপ: ভিত্তি শুধু client-এর IP, cookie নয়।

$ kubectl patch svc api -p '{"spec":{"sessionAffinity":"ClientIP"}}'
service/api patched

$ kubectl get svc api -o jsonpath='{.spec.sessionAffinity} {.spec.sessionAffinityConfig}{"\n"}'
ClientIP {"clientIP":{"timeoutSeconds":10800}}

$ kubectl exec client -- sh -c 'for i in $(seq 1 20); do curl -s http://api/; done | sort | uniq -c'
     20 api-7d66f59445-mn4r9

$ kubectl patch svc api -p '{"spec":{"sessionAffinity":"None"}}'
service/api patched

$ kubectl exec client -- sh -c 'for i in $(seq 1 20); do curl -s http://api/; done | sort | uniq -c'
      7 api-7d66f59445-fb2fb
      6 api-7d66f59445-mn4r9
      7 api-7d66f59445-th48x

২০টির ২০টিই এক Pod-এ, আর default সময়সীমা ১০৮০০ সেকেন্ড, মানে ৩ ঘণ্টা। সাবধান: অনেক client একটি NAT-এর পেছনে থাকলে সবার IP এক দেখায়, তখন সবাই একটি Pod-এ জমা হয়। আসল সমাধান হলো session Redis-এর মতো বাইরের store-এ রাখা, যাতে যেকোনো Pod উত্তর দিতে পারে।

৬ · NodePort আর LoadBalancer: বাইরে থেকে ভেতরে

ClusterIP শুধু cluster-এর ভেতর থেকে পৌঁছানো যায়। বাইরের ব্যবহারকারীর জন্য দুটি পথ। প্রথমটি NodePort: প্রতিটি node একই একটি port খুলে রাখে। আমাদের Service-এ শুধু ধরন আর port বদলানো হলো:

apiVersion: v1
kind: Service
metadata:
  name: api-np
  namespace: abcl-ks
spec:
  type: NodePort
  selector:
    app: api
  ports:
    - name: http
      port: 80
      targetPort: http
      nodePort: 30845
$ kubectl get svc api-np
NAME     TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
api-np   NodePort   10.103.112.35   <none>        80:30845/TCP   1s

# From this machine, outside the cluster, 12 connections
$ for i in 1 2 3 4 5 6 7 8 9 10 11 12; do curl -s --max-time 2 http://127.0.0.1:30845/; done | sort | uniq -c
      3 api-7d66f59445-fb2fb
      4 api-7d66f59445-mn4r9
      5 api-7d66f59445-th48x

# A nodePort outside the allowed range is refused by the API server
$ kubectl apply -f bad.yaml
The Service "api-bad" is invalid: spec.ports[0].nodePort: Invalid value: 20045: provided port is not in the valid range. The range of valid ports is 30000-32767

PORT(S) ঘরের 80:30845/TCP মানে Service port 80, node port 30845. NodePort নিজে থেকেই একটি ClusterIP-ও পেয়েছে, ঠিক ছবি ৪৫.১-এর মতো। nodePort না লিখলে Kubernetes পরিসর থেকে একটি port বেছে দেয়। একটি ছোট পর্যবেক্ষণ: Service তৈরির ঠিক পরপর দ্রুত ৬টি অনুরোধের সবগুলো সংযোগ পায়নি, আর কিছু পরে ১২টির ১২টিই পেয়েছে। Rule বসতে সামান্য সময় লাগে, তাই তৈরির মুহূর্তের একটি ব্যর্থতা দেখেই সিদ্ধান্ত নেবেন না।

  1. বাইরের clientnode:30845যেকোনো node
  2. Node rule:30845Pod বাছাই
  3. SNAT + DNAT→ Pod:8080দুই ঠিকানা বদল
  4. Pod:8080উত্তর
Clientকোনো node-এর ঠিকানা জানে
$ curl http://<node-address>:30845/
Nodeport-এ কোনো app শুনছে না, rule ধরছে
dst port 30845 → Service api-np
pick one Ready endpoint, on any node
নমুনা হিসাবexternalTrafficPolicy: Cluster
before: client → node:30845
after:  node → 10.1.0.11:8080
Pod-এর logএই run-এ, উৎস ছাঁটা
<node address> - - "GET / HTTP/1.…
not the client's own address
ফেরার পথconntrack উল্টে দেয়
Pod → node → client
api-7d66f59445-mn4r9
  1. Client: সে যেকোনো একটি node-এর ঠিকানায় port 30845-এ সংযোগ করে। সব node-এ একই port খোলা থাকে।
  2. Node-এর rule: এই port-এ কোনো process শুনছে না। kube-proxy-র rule সংযোগ ধরে, আর একটি Ready Pod বেছে নেয়। Pod অন্য node-এও থাকতে পারে।
  3. দুটি ঠিকানা বদল: গন্তব্য বদলায় Pod-এ, আর default নিয়মে উৎসও বদলায় node-এ, যাতে উত্তর একই node হয়ে ফেরে।
  4. Pod-এর চোখে: আসল run-এর log-এ উৎস ছিল node-এর ঠিকানা (এখানে ছাঁটা), client-এর নয়। Pod থেকে সরাসরি আসা অনুরোধে ছিল Pod-এর ঠিকানা; এই পার্থক্যটিই SNAT.
  5. ফেরার পথ: Pod node-কে উত্তর দেয়, conntrack দুটি বদল উল্টে দেয়, আর client পায় Pod-এর নাম।

উৎস ঠিকানা হারানো ঠেকাতে Service-এ externalTrafficPolicy: Local দেওয়া যায়। তখন traffic শুধু সেই node-এর Pod-এ যায়, উৎস টিকে থাকে, কিন্তু যে node-এ Pod নেই সেখানে সংযোগ ব্যর্থ হয়, আর ভাগ অসমান হতে পারে। আমাদের single-node cluster-এ আমরা field-টি বদলে দেখেছি: সব Pod এক node-এ বলে ভাগে পার্থক্য দেখা যায় না, আর Pod-এর log-এও উৎস একই থেকেছে। এই বৈশিষ্ট্য ঠিকভাবে পরীক্ষা করতে বহু-node cluster লাগে।

LoadBalancer: এই cluster-এ আসলে কী হলো

LoadBalancer ধরন নিজে কোনো load balancer বানায় না। সে শুধু status-এ একটি ঠিকানা চায়, আর সেটি পূরণ করে cluster-এর বাইরের কোনো controller. তাই আমরা অনুমান না করে চালিয়ে দেখলাম:

apiVersion: v1
kind: Service
metadata:
  name: api-lb
  namespace: abcl-ks
spec:
  type: LoadBalancer
  selector:
    app: api
  ports:
    - name: http
      port: 18045
      targetPort: http
$ kubectl get svc api-lb
NAME     TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)           AGE
api-lb   LoadBalancer   10.108.255.68   localhost     18045:30610/TCP   4s

$ kubectl get svc api-lb -o jsonpath='{.status}{"\n"}'
{"loadBalancer":{"ingress":[{"hostname":"localhost"}]}}

$ for i in 1 2 3 4 5 6; do curl -s --max-time 3 http://127.0.0.1:18045/; done | sort | uniq -c
      3 api-7d66f59445-fb2fb
      2 api-7d66f59445-mn4r9
      1 api-7d66f59445-th48x

$ kubectl delete svc api-lb
service "api-lb" deleted from abcl-ks namespace

এখানে EXTERNAL-IP হলো localhost. Docker Desktop-এর নিজের একটি controller (vpnkit-controller) এই কাজটি করে, আর মেশিনের port 18045 Service-এ জুড়ে দেয়। কোনো আসল load balancer তৈরি হয়নি, আর বিলও আসেনি। লক্ষ করুন, 18045:30610: Service নিজে থেকে একটি NodePort-ও পেয়েছে। আরেকটি সতর্কতা: এই run-এ port 18045 আর NodePort 30845 মেশিনের সব network interface-এ শুনছিল, শুধু 127.0.0.1-এ নয়। তাই local cluster-এ NodePort বা LoadBalancer চালালে কাজ শেষে মুছে ফেলুন।

Provider-এর আচরণ (এই run-এর বাইরের কথা, পরীক্ষা করা হয়নি): Managed cluster-এ cloud controller manager বা provider-এর controller এই Service দেখে আসল load balancer বানায়। পর্ব ১৮-এর target group, health check আর ঘণ্টা-ভিত্তিক খরচ সবই তখন খাটে। Bare-metal বা নিজের server-এ এমন controller না থাকলে EXTERNAL-IP <pending> হয়ে থাকে; তখন MetalLB-এর মতো project লাগে। প্রতিটি LoadBalancer Service সাধারণত একটি আলাদা load balancer, তাই ১০টি app-এ ১০টি বিল হতে পারে। এই খরচ কমানোর পথ পর্ব ৪৬-এর Ingress আর Gateway API.

ধরনClusterIP আছে?কে পৌঁছাতে পারেসাধারণ ব্যবহার
ClusterIPহ্যাঁCluster-এর ভেতরService-এ Service কথা
NodePortহ্যাঁNode-এ পৌঁছাতে পারে এমন সবাইপরীক্ষা, বা বাইরের LB-র পেছনে
LoadBalancerহ্যাঁLB যতদূর খোলাএকটি app-কে বাইরে দেখানো
ExternalNameনাCluster-এর ভেতর (DNS)বাইরের নামকে ভেতরের নাম দেওয়া
Headlessনা (None)Cluster-এর ভেতর (DNS)Pod-কে আলাদা করে চেনা

৭ · ExternalName আর headless: যেখানে proxy নেই

শেষের দুই ধরন শুধু DNS-এর খেলা। এদের জন্য kube-proxy কোনো rule বসায় না। আসল manifest আর আউটপুট দেখুন।

apiVersion: v1
kind: Service
metadata:
  name: api-headless
  namespace: abcl-ks
spec:
  clusterIP: None
  selector:
    app: api
  ports:
    - name: http
      port: 80
      targetPort: http
---
apiVersion: v1
kind: Service
metadata:
  name: pay-gateway
  namespace: abcl-ks
spec:
  type: ExternalName
  externalName: pay.example.com
$ kubectl get svc
NAME           TYPE           CLUSTER-IP       EXTERNAL-IP       PORT(S)   AGE
api            ClusterIP      10.105.249.110   <none>            80/TCP    45s
api-headless   ClusterIP      None             <none>            80/TCP    4s
pay-gateway    ExternalName   <none>           pay.example.com   <none>    4s

$ kubectl exec client -- nslookup api-headless.abcl-ks.svc.cluster.local
…
Name:	api-headless.abcl-ks.svc.cluster.local
Address: 10.1.0.13
Name:	api-headless.abcl-ks.svc.cluster.local
Address: 10.1.0.11
Name:	api-headless.abcl-ks.svc.cluster.local
Address: 10.1.0.16

# Port 80 is only the Service's number: nothing translates it here
$ kubectl exec client -- curl -sS --max-time 2 http://api-headless/
curl: (7) Failed to connect to api-headless port 80 after 1 ms: Could not connect to server

$ kubectl exec client -- curl -sS --max-time 2 http://api-headless:8080/
api-7d66f59445-wjdht

$ kubectl exec client -- nslookup pay-gateway.abcl-ks.svc.cluster.local
…
pay-gateway.abcl-ks.svc.cluster.local	canonical name = pay.example.com

Headless Service-এ DNS সরাসরি Pod-এর ঠিকানাগুলো দিল। মাঝে DNAT নেই, তাই client-কে Pod-এর আসল port 8080-এই যেতে হলো; port: 80 এখানে কোনো অনুবাদ করেনি। আমরা আরেকটি run-এ একটি Pod unready করে দেখেছি, DNS-এর উত্তরে তখন ৩টির বদলে ২টি ঠিকানা এসেছে। StatefulSet-এর প্রতিটি Pod-কে আলাদা নাম দিতে এই ধরনটিই লাগে, যা পর্ব ৪৭-এ দেখবেন। Client নিজে ঠিকানা বেছে নিতে চাইলেও এটি কাজে লাগে, যেমন client-side load balancing করা gRPC.

ExternalName শুধু একটি CNAME ফেরত দেয়। সুবিধা হলো, app-এর config-এ pay-gateway লেখা থাকলে পরে শুধু Service বদলেই গন্তব্য বদলানো যায়। সীমা হলো, এখানে কোনো port অনুবাদ নেই, আর HTTP-এর Host header বা TLS certificate-এ client তখনো ভেতরের নামটিই পাঠাতে পারে। তাই বাইরের HTTPS service-এ এটি প্রায়ই ঠিকঠাক খাটে না, আগে পরীক্ষা করুন।

৮ · port-forward আর Service debug

kubectl port-forward: নিজের জন্য একটি ছোট সুড়ঙ্গ

নিজের মেশিন থেকে cluster-এর ভেতরের কিছু একবার দেখতে চাইলে Service-এর ধরন বদলানোর দরকার নেই। kubectl port-forward API server হয়ে একটি সুড়ঙ্গ বানায়, অনেকটা পর্ব ১২-এর SSH port forwarding-এর মতো।

$ kubectl port-forward --address 127.0.0.1 svc/api 18145:80
Forwarding from 127.0.0.1:18145 -> 8080

# In another terminal, 8 connections
$ for i in 1 2 3 4 5 6 7 8; do curl -s --max-time 3 http://127.0.0.1:18145/; done | sort | uniq -c
      8 api-7d66f59445-th48x

৮টি আলাদা সংযোগ, সব এক Pod-এ! svc/api লিখলেও kubectl শুরুতে Service থেকে একটি Pod বেছে নেয়, আর সেই Pod-এই সুড়ঙ্গ বাঁধে। এটি kube-proxy-র পথ নয়। তাই port-forward কাজ করলেও প্রমাণ হয় না যে Service-এর ভাগ বা kube-proxy ঠিক আছে; আর Pod মরলে সুড়ঙ্গও ভেঙে যায়। kubectl-এর default-ও localhost, আর --address 127.0.0.1 লিখে আমরা সেটি স্পষ্ট করেছি। 0.0.0.0 দিলে একই network-এর অন্যরাও ঢুকতে পারে।

আসল সমস্যা: একটি অক্ষরের ভুল

একটি নতুন Service orders বানানো হলো, কিন্তু selector-এ ভুলে লেখা হলো app: apii. কোথাও কোনো error এলো না, এটিই এই ভুলের বিপদ।

$ kubectl apply -f typo.yaml
service/orders created

$ kubectl get svc orders
NAME     TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
orders   ClusterIP   10.106.98.36   <none>        80/TCP    2s

$ kubectl get endpointslices -l kubernetes.io/service-name=orders
NAME           ADDRESSTYPE   PORTS     ENDPOINTS   AGE
orders-8mtx4   IPv4          <unset>   <unset>     2s

$ kubectl exec client -- getent hosts orders
10.106.98.36      orders.abcl-ks.svc.cluster.local  orders.abcl-ks.svc.cluster.local orders

$ kubectl exec client -- curl -sS --max-time 3 http://orders/
curl: (7) Failed to connect to orders port 80 after 0 ms: Could not connect to server

$ kubectl describe svc orders | grep -E 'Selector|Endpoints|Events'
Selector:                 app=apii
Endpoints:
Events:                   <none>

$ kubectl get pods -l app=apii
No resources found in abcl-ks namespace.

$ kubectl get pods --show-labels -l app=api
NAME                   READY   STATUS    RESTARTS   AGE     LABELS
api-7d66f59445-fb2fb   1/1     Running   0          2m46s   app=api,pod-template-hash=7d66f59445
api-7d66f59445-mn4r9   1/1     Running   0          92s     app=api,pod-template-hash=7d66f59445
api-7d66f59445-th48x   1/1     Running   0          2m46s   app=api,pod-template-hash=7d66f59445

$ kubectl patch svc orders -p '{"spec":{"selector":{"app":"api"}}}'
service/orders patched

$ kubectl get endpointslices -l kubernetes.io/service-name=orders
NAME           ADDRESSTYPE   PORTS   ENDPOINTS                       AGE
orders-8mtx4   IPv4          8080    10.1.0.11,10.1.0.13,10.1.0.40   5s

$ kubectl exec client -- curl -sS --max-time 3 http://orders/
api-7d66f59445-mn4r9

লক্ষণটি পড়তে শিখুন। DNS কাজ করছে, ClusterIP আছে, Service-এর status ঠিক, Events ফাঁকা। শুধু EndpointSlice-এ <unset>, আর সংযোগ সঙ্গে সঙ্গে বাতিল (0 ms). সঙ্গে সঙ্গে বাতিল হওয়া আর timeout আলাদা: অংশ ১-এর মৃত Pod IP-তে ৩ সেকেন্ড অপেক্ষা করে timeout হয়েছিল। এবার আমরা একই Service-এ আরও তিনটি ভুল ইচ্ছা করে ঘটালাম:

ভুলSlice-এর PORTSSlice-এর ENDPOINTScurl
Selector টাইপো<unset><unset>exit 7
targetPort: 9090 (app শোনে 8080)9090৩টি ঠিকানাexit 7
targetPort: web (এই নাম নেই)<unset>৩টি ঠিকানাexit 7
সব Pod unready8080৩টি ঠিকানা (ready: false)exit 7

চারটি ক্ষেত্রেই curl-এর একই error! তাই error দেখে কারণ বলা যায় না, কারণ ধরা পড়ে slice আর Pod পড়ে। শেষের সারিতে kubectl get endpoints api-এর ঘর ফাঁকা ছিল আর ৩টি ঠিকানা ছিল notReadyAddresses-এ। নিচের decision tree এই যুক্তিই সাজিয়ে দেয়। যা দেখলেন সেই অনুযায়ী true বা false বদলে চালান।

// "Service not reachable" decision tree. Set what you observed.
const OBSERVED = {
  serviceExists: true,      // kubectl get svc api
  dnsResolves: true,        // getent hosts api (from a Pod)
  podsMatchSelector: false, // kubectl get pods -l <selector>
  somePodReady: true,       // READY column, or slice conditions
  slicePortSet: true,       // PORTS is not <unset>
  podIpWorks: true,         // curl PodIP:targetPort (from a Pod)
  clusterIpWorks: true,     // curl ClusterIP:port (from a Pod)
  outsideWorks: true,       // from outside the cluster
};
const TREE = [
  ["serviceExists", "Service exists?", "Wrong name or namespace. Check: kubectl get svc -A"],
  ["dnsResolves", "Name resolves?", "Use the full name; check namespace and CoreDNS Pods."],
  ["podsMatchSelector", "Pods match selector?", "Selector or label typo. Compare with --show-labels."],
  ["somePodReady", "Any Pod Ready?", "Read kubectl describe pod: probe, crash or image."],
  ["slicePortSet", "Slice port set?", "targetPort name not on the Pod. Fix the port name."],
  ["podIpWorks", "Pod answers directly?", "App not listening there, wrong targetPort, or NetworkPolicy."],
  ["clusterIpWorks", "ClusterIP answers?", "Service path broken: check port, kube-proxy, CNI, policy."],
  ["outsideWorks", "Reachable from outside?", "Check type, NodePort, firewall, LB health check."],
];
let step = 0;
for (const [key, question, fix] of TREE) {
  step++;
  if (OBSERVED[key]) { console.log(step + ". " + question + " yes"); continue; }
  console.log(step + ". " + question + " NO");
  console.log("STOP -> " + fix);
  step = -1;
  break;
}
if (step !== -1) console.log("All checks pass: test from the failing client itself.");

আউটপুট: 1. Service exists? yes, 2. Name resolves? yes, 3. Pods match selector? NO, তারপর STOP -> Selector or label typo. Compare with --show-labels. এটিই আমাদের orders সমস্যা। ক্রমটি ইচ্ছা করেই ভেতর থেকে বাইরে: Service, নাম, Pod, readiness, port, Pod-এ সরাসরি, ClusterIP, তারপর বাইরের পথ। একটি ধাপে ব্যর্থ হলে পরের ধাপ দেখার দরকার নেই। podIpWorks ঠিক থাকলে app ভালো আছে, সমস্যা তার সামনে কোথাও।

আরও গভীরে: EndpointSlice ডিফল্টে প্রতি slice-এ সর্বোচ্চ ১০০টি ঠিকানা রাখে, আর label kubernetes.io/service-name দিয়ে Service-এর সাথে জোড়া থাকে। Pod বন্ধ হওয়ার সময় ঠিকানাটি ready: false কিন্তু serving থাকতে পারে; কোনো ready ঠিকানা না থাকলে নতুন kube-proxy এমন terminating Pod-কে শেষ ভরসা হিসেবে ব্যবহার করতে পারে। Service-এর trafficDistribution field কাছের Pod-কে অগ্রাধিকার দিতে বলে; এই cluster-এর API server PreferClose, PreferSameZone আর PreferSameNode মান গ্রহণ করেছে, ভুল মান দিলে সে তালিকাটিই error-এ জানায়। DNS-এ named port-এর SRV record-ও থাকে: _http._tcp.api.abcl-ks.svc.cluster.local-এর উত্তরে আসল run-এ এলো service = 0 100 80 api.abcl-ks.svc.cluster.local.

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

  • Service আর Deployment-এর সম্পর্ক কী? সরাসরি কোনো সম্পর্ক নেই; দুটোই label দিয়ে Pod ধরে।
  • Readiness ব্যর্থ হলে কী হয়? Pod চলতে থাকে, শুধু Service-এর traffic বন্ধ হয়। Restart করে liveness probe.
  • ClusterIP ping করা যায় না কেন? এটি virtual, rule শুধু নির্দিষ্ট port-এর TCP বা UDP ধরে, তাই ping-এর উত্তর পাওয়ার নিশ্চয়তা নেই।
  • LoadBalancer <pending> কেন? ঠিকানা দেওয়ার মতো কোনো controller নেই, বা provider-এর দিকে সমস্যা।
  • Headless কখন? Client-কে প্রতিটি Pod আলাদা করে চিনতে হলে, যেমন StatefulSet-এর database.

৯ · AI-এর যুগে Service আর DNS

AI খুব দ্রুত Service-এর YAML লিখে দেয়, আর তা দেখতেও ঠিক লাগে। কিন্তু অংশ ৮-এর মতো ভুল YAML kubectl apply-এ কোনো error দেয় না। তাই “apply সফল” মানে “traffic পৌঁছাচ্ছে” নয়।

AI-এর সাধারণ ভুলফলযাচাই
সব Service-এ type: LoadBalancerপ্রতিটিতে বিল, আর database পর্যন্ত internet-এ খোলাভেতরের Service ClusterIP রাখা
Selector আর Pod label না মেলাEndpoint নেই, কিন্তু কোনো error-ও নেইkubectl get pods -l ...
targetPort-এ ভুল portঠিকানা আছে, সংযোগ নেইPod IP-তে সরাসরি curl
বানানো annotation বা sessionAffinity: Cookieনীরবে উপেক্ষা, বা API-র error--dry-run=server আর provider-এর ডকুমেন্ট
port-forward --address 0.0.0.0Network-এর অন্যদের জন্যও খোলা127.0.0.1-ই রাখা
Config-এ Pod IP লেখা, Endpoints API ধরে scriptপরের rollout-এ ভেঙে যায়Service-এর নাম, আর EndpointSlice
ঠিক না করে kubectl delete বা restart বলাপ্রমাণ মুছে যায়, সমস্যা ফিরে আসেআগে slice আর Events পড়া

নিচের prompt-এ AI-কে আপনার আসল আউটপুট দিন, আর সমাধানের আগে কারণ চান। কোনো secret, token বা kubeconfig পেস্ট করবেন না।

আমার Kubernetes Service-এ পৌঁছাতে পারছি না। নিচে আসল আউটপুট দিলাম, গোপন কিছু নেই: [kubectl get svc, kubectl get pods --show-labels, kubectl get endpointslices -l kubernetes.io/service-name=... -o yaml এর আউটপুট] ১. ধাপে ধাপে বলো: Service, DNS, selector, readiness, targetPort, Pod-এ সরাসরি, ClusterIP — কোন ধাপে সমস্যা, আর আউটপুটের কোন লাইন তা প্রমাণ করে। ২. কোনো কমান্ড মুছে ফেলা বা restart করার হলে আগে সতর্ক করো, আর কেন দরকার বলো। ৩. YAML বদলাতে বললে শুধু বদলের অংশ দাও, আর kubectl apply --dry-run=server দিয়ে যাচাইয়ের কমান্ড দাও। ৪. আমার cluster-এ যে field আছে কি না জানো না, সেটি অনুমান করে লিখো না।

AI-এর Service manifest হাতে পেলে

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

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

প্র ০১Kubernetes প্রতিটি Pod-কে নিজের IP কেন দিল? Docker-এর মতো node-এর port ভাগ করলে কী সমস্যা ছিল?

উত্তরের শিকড় Google-এর Borg-এ। ২০১৫ সালের Borg paper-এর “lessons” অংশে লেখকেরা বলেন, একটি machine-এর সব task এক IP ভাগ করলে port-কেই একটি সম্পদ হিসেবে সামলাতে হয়। Scheduler-কে port-ও বরাদ্দ করতে হয়, app-কে আগে থেকে জানতে হয় সে কোন port পাবে, আর name service-কে IP-র সাথে port-ও জানাতে হয়।

পর্ব ৩৯-এ আপনি নিজেই এই সমস্যার ছোট রূপ দেখেছেন: দুটি container একই host port চাইলে সংঘাত হয়। এখন ভাবুন ১০০টি node, ৫০টি app, প্রতিটিতে port 8080. হাতে port বরাদ্দ করা অসম্ভব। Dynamic port দিলে প্রতিটি app-কে port-এর হিসাব জানতে হয়, যা পুরোনো software-এ বদলানো কঠিন।

Kubernetes সিদ্ধান্ত নিল, প্রতিটি Pod-কে নিজের IP দেবে। তখন প্রতিটি Pod নিজের port-পরিসরের মালিক, nginx ঠিক 80-তেই চলতে পারে। Pod দেখতে একটি ছোট VM-এর মতো, তাই পুরোনো app, port-এর ধারণা আর DNS, সবই আগের মতো খাটে। সেই সাথে NAT-ছাড়া নিয়মের কারণে একটি Pod যে ঠিকানায় নিজেকে চেনে, অন্যরাও তাকে সেই ঠিকানায় চেনে।

দাম কিন্তু আছে। Network-কে এখন অনেক বেশি ঠিকানা সামলাতে হয়। Cloud-এর VPC-native CNI-তে Pod ঠিকানা subnet থেকে নেয়, তাই পর্ব ২৫-এর CIDR পরিকল্পনা ছোট হলে ঠিকানা ফুরিয়ে যেতে পারে। আর Pod নশ্বর বলে ঠিকানা স্থির রাখা যায় না, যার সমাধান হিসেবেই Service-এর জন্ম।

মূল উপলব্ধি: একটি জটিলতা সরালে তা অন্য কোথাও যায়। Kubernetes port-এর জটিলতা সরিয়ে network স্তরে ঠেলে দিয়েছে, যেখানে সেটি একবার সমাধান করলেই সব app উপকার পায়।

প্র ০২DNS-ই তো একাধিক ঠিকানা দিতে পারে। তাহলে Kubernetes শুধু round-robin DNS না দিয়ে ClusterIP-এর মতো virtual IP কেন বানাল?

Kubernetes-এর নিজের ডকুমেন্টেশনই এই প্রশ্নের উত্তর দেয়, আর কারণগুলো DNS-এর বাস্তব আচরণ থেকে আসে। পর্ব ২৯-এ দেখেছেন, DNS-এর উত্তর cache হয়, আর TTL একটি অনুরোধ, আদেশ নয়।

  • TTL না মানা: কিছু DNS client ও library TTL মানেনি, বা নিজের মতো বেশি সময় cache রেখেছে।
  • একবার খোঁজা: কিছু app শুরুতে একবার নাম খুঁজে সারা জীবন সেই ফলই রাখে।
  • কম TTL-এর দাম: Pod দ্রুত বদলায়, তাই TTL শূন্যের কাছে নামালে DNS server-এ চাপ অনেক বাড়ে, আর সব উত্তরে সব Pod থাকে।

আমরা এই পর্বের পরীক্ষায় Pod মুছেছি, scale করেছি। যদি client Pod IP cache করত, পুরোনো ঠিকানায় অংশ ১-এর মতো timeout হতো। ClusterIP-তে সমস্যাটি উল্টে গেছে: নামের উত্তর কখনো বদলায় না, তাই cache নিরাপদ। বদল ঘটে node-এর rule-এ, যা kube-proxy প্রায় সঙ্গে সঙ্গে হালনাগাদ করে।

তবে virtual IP-রও দাম আছে। সংযোগ-স্তরের ভাগ HTTP/2-তে অসম হয়, প্রতিটি node-এ rule রাখতে হয়, আর debugging-এ DNAT আর conntrack বোঝা লাগে। তাই Kubernetes দুটো পথই রেখেছে। যার client-side load balancing আছে, যেমন gRPC, সে headless Service নিয়ে সরাসরি Pod ঠিকানা পেতে পারে। সে ক্ষেত্রে TTL আর পুনরায় খোঁজার দায় client-এর।

মূল উপলব্ধি: “কে ঠিকানা বদল সামলাবে” এই প্রশ্নটিই নকশার মূল। ClusterIP দায়টি client থেকে platform-এ নিয়েছে; headless দায়টি ফিরিয়ে দেয় তাদের, যারা তা চায়।

প্র ০৩ndots:5 এত খরচের হলে default-এ কেন? Production-এ DNS-এর চাপ কীভাবে কমানো হয়?

আমরা আসল log-এ দেখেছি, shop.example.com খুঁজতে ৮টি প্যাকেট গেছে। তবু মান ৫ রাখার পেছনে একটি যুক্তি আছে। Cluster-এর ভেতরের নামে অনেক dot থাকে। যেমন named port-এর SRV খোঁজা: _http._tcp.api.abcl-ks.svc-এ ৪টি dot. ndots ৫ হলে এমন ছোট রূপও search list দিয়ে মিলে যায়। অর্থাৎ নকশাটি ভেতরের নামকে অগ্রাধিকার দিয়েছে, বাইরের নামের দাম দিয়ে। Kubernetes-এর issue আলোচনায় এই কারণটিই বলা হয়েছে বলে জানি, তবে এটিকে ইতিহাসের চূড়ান্ত দলিল ধরবেন না।

একটি microservice প্রতি সেকেন্ডে বাইরের API-কে ১০০০ বার নতুন করে খুঁজলে আর ৮টি করে প্যাকেট গেলে CoreDNS-এ ৮০০০ প্রশ্ন আসে, যেখানে দরকার ছিল ২০০০ (এখানে cache ধরা হয়নি)। ২০১৭-১৮ সালের দিকে কয়েকটি প্রকৌশল ব্লগে দেখা যায়, conntrack-এর একটি race condition-এ UDP DNS প্যাকেট হারিয়ে ৫ সেকেন্ডের অদ্ভুত দেরি হতো। ৫ সেকেন্ড ছিল resolver-এর default timeout. এমন ঘটনায় বাড়তি DNS প্রশ্ন মানে বাড়তি ঝুঁকি।

প্রচলিত ব্যবস্থাগুলো:

  • শেষে dot: Config-এ absolute নাম, যা আমরা log-এ ২টি প্যাকেটে দেখেছি।
  • ndots কমানো: নির্দিষ্ট Pod-এর dnsConfig-এ।
  • NodeLocal DNSCache: প্রতিটি node-এ ছোট cache, যা conntrack-এর পথও এড়াতে সাহায্য করে।
  • CoreDNS scale: Replica বাড়ানো, autoscaling আর cache; CoreDNS-এর autopath plugin-ও আছে।
  • App-এ cache: সংযোগ reuse করা, প্রতি অনুরোধে নতুন করে না খোঁজা।

মূল উপলব্ধি: Default মানে সবচেয়ে ভালো নয়, বরং একটি পক্ষ নেওয়া সিদ্ধান্ত। আপনার traffic কোন দিকের, তা মেপে সেই সিদ্ধান্ত যাচাই করতে হয়।

প্র ০৪Endpoints তো কাজ করছিল। তাহলে EndpointSlice কেন এলো, আর একটি Pod বদলালে হিসাবে কত সাশ্রয় হয়?

পুরোনো নকশায় একটি Service-এর সব Pod ঠিকানা থাকত একটিমাত্র Endpoints object-এ। প্রতিটি node-এর kube-proxy সেই object watch করে। সমস্যা হলো, একটি Pod বদলালেও পুরো object নতুন করে প্রতিটি node-এ যায়। ছোট cluster-এ এটি চোখে পড়ে না, বড় cluster-এ rolling update-এর সময় এটি বড় বোঝা হয়।

একটি অনুমান-ভিত্তিক হিসাব করি। ধরুন একটি Service-এ ৫০০০ Pod, cluster-এ ১০০০ node, আর একটি Endpoints object-এর আকার ১ MB. এক Pod বদলালে ১ MB × ১০০০ = ১ GB ডেটা ছড়ায়। Rolling update-এ ৫০০০ Pod বদলালে ৫০০০ GB-র কাছাকাছি, যদিও বাস্তবে কিছু update জোট বেঁধে যায়।

EndpointSlice একই তালিকাকে ছোট ছোট টুকরায় ভাগ করে, ডিফল্টে প্রতিটিতে সর্বোচ্চ ১০০টি। তাহলে ৫০০০ Pod মানে অন্তত ৫০টি slice, প্রতিটি মোটামুটি ১ MB ÷ ৫০ = ২০ KB. এক Pod বদলালে শুধু একটি slice যায়: ২০ KB × ১০০০ = ২০ MB. ১ GB থেকে ২০ MB, মানে প্রায় ৫০ গুণ কম।

EndpointSlice ১.১৬ নাগাদ alpha হয়ে ১.২১-এ GA হয়। আর আসল run-এই kubectl জানাল, v1 Endpoints is deprecated in v1.33+. তবে আগে থেকে থাকা object এখনো দেখা যায়, যা পুরোনো tool-কে সময় দেয়। পুরোনো Endpoints object ১০০০ ঠিকানার পরে ছেঁটে ফেলা হয় বলেও ডকুমেন্টে জানা যায়। Slice-এর আরেকটি লাভ হলো IPv4 আর IPv6 আলাদা রাখা, আর প্রতিটি ঠিকানায় zone বা node-এর তথ্য রাখা, যা trafficDistribution-এ কাজে লাগে।

মূল উপলব্ধি: ছোট স্কেলের সরল নকশা বড় স্কেলে নিজেই সমস্যা হয়ে দাঁড়ায়। বদল যত ছোট, পাঠানো ডেটাও তত ছোট হওয়া উচিত; একটি object-কে ভাগ করাই এখানে সমাধান।

প্র ০৫kube-proxy কেন বারবার mode বদলাল? iptables-এর সীমা কোথায়, আর nftables বা eBPF কী বদলায়?

শুরুর দিকে kube-proxy সত্যিই একটি proxy ছিল: userspace-এ সংযোগ নিয়ে Pod-এ পাঠাত। প্রতিটি packet-কে kernel থেকে userspace-এ গিয়ে আবার ফিরতে হতো, যা ধীর। এই mode পরে পুরোপুরি সরানো হয়, Kubernetes ১.২৬ নাগাদ।

তারপর iptables mode default হলো। Packet kernel-এই থেকে গেল, গতি বাড়ল। কিন্তু iptables মূলত firewall-এর জন্য বানানো, যেখানে rule অল্প। প্রতিটি Service আর endpoint-এর জন্য rule জমতে থাকে। হিসাবের একটি অসুবিধা হলো, মিল খোঁজা তালিকা ধরে এগোয়, তাই গড়ে অনেক rule পার হতে হয়। আরেকটি হলো হালনাগাদ: পরিবর্তনে বড় rule-তালিকা ঘুরে লিখতে হয়। Service হাজারের ঘরে গেলে একবার sync করতেই চোখে পড়ার মতো সময় লাগে, আর সেই সময়টুকু নতুন Pod traffic পায় না বা মৃত Pod পেতে থাকে।

IPVS এলো hash table নিয়ে, Kubernetes ১.১১ নাগাদ GA. খোঁজা দ্রুত, আর least connection-এর মতো পদ্ধতিও আছে। কিন্তু এটি আলাদা kernel subsystem, আর iptables-এর সব আচরণ এতে হুবহু মেলানো কঠিন ছিল। nftables, iptables-এর আধুনিক উত্তরসূরি: map দিয়ে খোঁজা প্রায় ধ্রুব সময়ে হয়, আর ছোট বদল ছোট transaction-এ যায়। eBPF-ভিত্তিক পথ (যেমন Cilium) packet-এর আরও আগের ধাপে কাজ করে, কখনো conntrack-এর বোঝাও কমায়, তবে তা CNI-এর সাথে বাঁধা।

ছোট cluster-এ এই পার্থক্য প্রায় বোঝাই যায় না। আমাদের এখানে iptables-এর default-এই সব পরীক্ষা ঠিকঠাক চলেছে। সংখ্যা যা-ই বলুক, নিজের স্কেলে মেপে তবেই বদলানো উচিত, আর version অনুযায়ী mode-এর অবস্থা release note থেকে মিলিয়ে নেওয়া উচিত।

মূল উপলব্ধি: Concept একই থেকেছে: “virtual ঠিকানা থেকে একটি Pod.” বদলেছে শুধু কোন স্তরে সেই কাজ হয়। ভালো abstraction-এর লক্ষণ হলো, নিচের স্তর বদলালেও আপনার Service YAML একই থাকে।

অনুশীলন

নিজের মেশিনের local cluster-এ করলে কোনো বিল আসে না। Cloud-এর cluster-এ LoadBalancer-এর খরচ হতে পারে। শেষে নিজের namespace মুছুন: kubectl delete ns <আপনার-namespace>.

  1. হিসাব (iptables): একটি Service-এর পেছনে ৪টি Ready Pod. iptables mode-এ প্রতিটি rule-এর probability কত? ৩ নম্বর Pod-এ পৌঁছানোর সম্ভাবনা কত? ১২০০টি নতুন সংযোগে তার প্রত্যাশিত ভাগ কত? আর সবগুলো সংযোগ যদি ১২০০-এর বদলে ৪টি keep-alive সংযোগ হয়, ফলাফল কী বদলাবে?

    Rule-এর probability: ১/৪, ১/৩, ১/২, ১। ৩ নম্বরে পৌঁছাতে প্রথম দুটি পার হতে হবে, তারপর তৃতীয়টি ধরবে: (৩/৪) × (২/৩) × (১/২) = ১/৪। ১২০০ × ১/৪ = ৩০০টি সংযোগ।

    ৪টি keep-alive সংযোগে প্রতিটি নিজের Pod বেছে নেয়, আর পরে বদলায় না। সব ৪টি এক Pod-এ যাওয়াও সম্ভব। সম্ভাবনা ৪ × (১/৪)৪ = ১/৬৪। তাই গড় সমান হলেও একটি সময়ে ভাগ অনেক অসমান হতে পারে। kube_proxy_pick.js-এ PODS আর CONNECTIONS বদলে মিলিয়ে নিন।

  2. ফল আগেই বলুন: একটি Service-এর selector app=web. শুরুতে app=web-এর ৩টি Ready Pod (P1, P2, P3). আপনি এই ক্রমে কাজ করলেন: (১) P2-এর readiness ব্যর্থ করলেন, (২) P3-এর label বদলে app=old করলেন, (৩) Deployment selector একই আছে, তাই সে একটি নতুন Pod P4 বানাল, যা Ready. এখন kubectl get endpointslices-এর ENDPOINTS ঘরে কয়টি ঠিকানা, ready: true কয়টি, আর ৩০টি নতুন সংযোগ কোন Pod-এ যাবে?

    Label বদলে P3 selector থেকে বেরিয়ে গেছে, তাই তালিকায় P1, P2, P4: ৩টি ঠিকানা। ready: true ২টি (P1, P4), কারণ P2 তালিকায় থাকলেও ready: false. ৩০টি সংযোগ যাবে P1 আর P4-এ, গড়ে ১৫টি করে। P3 চলতেই থাকবে, কিন্তু কোনো traffic পাবে না, আর ReplicaSet-এর হিসাবের বাইরে চলে গেছে। Debug-এর জন্য Pod আলাদা করার এটি একটি পরিচিত কৌশল, তবে পরে নিজ হাতে মুছতে হবে।

  3. নকশা ও সমালোচনা: একটি কাল্পনিক অনলাইন বাজার অ্যাপে AI এই পরিকল্পনা দিয়েছে: frontend, orders, payments আর PostgreSQL, চারটিতেই type: LoadBalancer; orders-এর config-এ payments-এর Pod IP লেখা; orders আর payments দুটোরই Pod-এ app: backend label, আর Service-এর selector শুধু app: backend; session Pod-এর memory-তে, তাই sessionAffinity: ClientIP. কী কী বদলাবেন?
    • ধরন: শুধু frontend বাইরের। অন্যগুলো ClusterIP. Database কখনোই public নয়। বাইরের পথে একটি Ingress বা Gateway রাখলে পর্ব ৪৬-এর মতো একটি load balancer-এই চলবে, খরচও কম।
    • ঠিকানা: Pod IP সরিয়ে payments বা payments.shop.svc.cluster.local নাম।
    • Selector: Selector এক হলে দুটি Service-এই দুই app-এর Pod মিশে যাবে, আর orders-এর অর্ধেক অনুরোধ payments-এ যাবে। আলাদা label দিন, যেমন app: orders, app: payments.
    • Session: Affinity একটি পট্টি। Pod মরলে session হারাবে, আর NAT-এর পেছনের সবাই এক Pod-এ জমবে। Session Redis-এর মতো বাইরের store-এ নিন।
    • যাচাই: সব Service-এ EndpointSlice পড়ুন, আর পর্ব ৪৯-এর NetworkPolicy দিয়ে database-এর পথ orders আর payments-এ সীমিত রাখুন।
  4. কমান্ড ও নির্ণয়: একজন সহকর্মী জানালেন, curl http://reports/ করলে Could not connect to server আসছে। getent hosts reports ঠিকানা দিচ্ছে। Slice-এ PORTS 9000, ENDPOINTS 10.1.0.21,10.1.0.22, দুটিই ready: true. Pod-এর container শোনে 8000-এ। কারণ কী, কোন কমান্ডে প্রমাণ করবেন, আর কীভাবে ঠিক করবেন?

    DNS, selector আর readiness ঠিক আছে, কারণ ঠিকানা আছে আর ready. Slice-এর port 9000 বলছে Service-এর targetPort ভুল। Readiness probe সম্ভবত সরাসরি আসল port 8000 দেখছে, তাই Pod ready দেখাচ্ছে। প্রমাণ:

    # From a test Pod: the real port answers, the Service's port does not
    kubectl exec client -- curl -sS --max-time 3 http://10.1.0.21:8000/
    kubectl exec client -- curl -sS --max-time 3 http://10.1.0.21:9000/
    
    # Fix: point targetPort at the real port, then read the slice again
    kubectl patch svc reports --type=json \
      -p '[{"op":"replace","path":"/spec/ports/0/targetPort","value":8000}]'
    kubectl get endpointslices -l kubernetes.io/service-name=reports

    এই কমান্ডগুলো আপনার নিজের cluster-এর উদাহরণ, আউটপুট তাই দেখানো হয়নি। আরও ভালো হলো Pod-এর port-এ নাম দিয়ে targetPort-এ সেই নাম ব্যবহার করা।

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

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

ক্রমএকটি Pod Service-এর নাম ধরে ডাকলে যা ঘটে, সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. Resolver resolv.conf পড়ে search domain জুড়ে পুরো নাম বানায়
  2. CoreDNS Service-এর ClusterIP ফেরত দেয়
  3. Pod ClusterIP:port-এ সংযোগ শুরু করে
  4. Node-এর rule একটি Ready Pod বেছে গন্তব্য বদলায়
  5. Pod-এর targetPort-এ অনুরোধ পৌঁছায়

একটি Service-এ port: 80, targetPort: 8080, nodePort: 31000 দেওয়া আছে। Cluster-এর ভেতরের একটি Pod ClusterIP দিয়ে কোন port-এ সংযোগ করবে?

  • 8080
  • 31000
  • 80
  • যেকোনো port

একটি Pod-এর readiness probe ব্যর্থ হচ্ছে, কিন্তু container চলছে। তখন কী ঘটে?

  • kubelet container restart করে
  • Pod চলতে থাকে, কিন্তু Service তাকে নতুন traffic পাঠায় না
  • Deployment Pod মুছে নতুন Pod বানায়
  • Service-এর ClusterIP বদলে যায়

একটি Pod-এর resolv.conf-এ ndots:5 আর ৩টি search domain আছে। Alpine-ভিত্তিক image-এ shop.example.com খুঁজলে আর কোনো নাম না মিললে A আর AAAA মিলিয়ে সর্বোচ্চ কতটি DNS প্রশ্ন যায়?

  • ২
  • ৪
  • ৬
  • ৮

একটি Service-এ curl ব্যর্থ। EndpointSlice-এ PORTS আর ENDPOINTS দুটোই <unset>. DNS ঠিকানা দিচ্ছে। সবচেয়ে সম্ভাব্য কারণ কোনটি?

  • Service-এর selector কোনো Pod-এর label-এ মিলছে না
  • CoreDNS বন্ধ হয়ে গেছে
  • targetPort-এর সংখ্যা ভুল
  • সব Pod-এর readiness ব্যর্থ

৩টি Ready Pod-এর পেছনে থাকা ClusterIP Service-এ একটি client একটি keep-alive সংযোগে ১০০টি HTTP অনুরোধ পাঠাল। iptables mode-এ সাধারণত কী হবে?

  • প্রতিটি Pod প্রায় ৩৩টি করে অনুরোধ পাবে
  • ১০০টি অনুরোধই একটি Pod-এ যাবে
  • অনুরোধগুলো পালা করে Pod ১, ২, ৩-এ যাবে
  • Connection reset হবে

একটি নিজের server-এর (bare-metal) cluster-এ type: LoadBalancer Service-এর EXTERNAL-IP অনেকক্ষণ <pending>, আর cloud controller বা MetalLB-র মতো কিছু চালানো নেই। কেন?

  • Service-এর selector ভুল
  • NodePort পরিসর শেষ হয়ে গেছে
  • এই ঠিকানা দেওয়ার মতো কোনো controller চলছে না
  • ClusterIP তৈরি হয়নি

শব্দকোষ

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

মনে রাখুন

  1. Pod নশ্বর, তার IP-ও। Service দেয় স্থির নাম আর স্থির ClusterIP.
  2. Selector ঠিক করে কে তালিকায়, readiness ঠিক করে কে traffic পাবে। পড়ুন conditions.ready, শুধু ENDPOINTS কলাম নয়।
  3. নাম: service.namespace.svc.cluster.local. বাইরের নামে ndots:5 বাড়তি প্রশ্ন আনে।
  4. kube-proxy সংযোগ ভাগ করে, অনুরোধ নয়। NodePort-এর ওপর LoadBalancer, আর তার আসল কাজ করে provider.
  5. Debug ভেতর থেকে বাইরে: Service → DNS → selector → ready → port → Pod → ClusterIP → বাইরের পথ।

পরের পর্বে: ১০টি app-এ ১০টি load balancer না নিয়ে একটি দরজা দিয়ে কীভাবে সব traffic ভেতরে আনবেন? পর্ব ৪৬-এ দেখবেন Ingress আর Gateway API, যেখানে host আর path দেখে Service বেছে নেওয়া হয়।

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

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