📡 Troubleshoot service và kết nối mạng trong cluster

Pod của bạn đã Running và Ready, nhưng gọi Service thì timeout. Đây là tình huống tôi gặp nhiều nhất trong lúc luyện thi, và cũng là dạng bài ăn điểm nhanh nếu bạn kiểm tra đúng thứ tự. Bài này tập trung vào một câu hỏi duy nhất: luồng traffic đứt ở đâu? Giữa pod và app có 4 chặng dễ đứt: DNS, Endpoints, port mapping và NetworkPolicy. Đi đúng thứ tự thì mỗi ca troubleshoot chỉ mất 2 đến 3 phút.

Mục tiêu bài học

  • Xác định được Service có endpoint hay không chỉ bằng 1 lệnh, và biết vì sao endpoint rỗng.
  • Kiểm tra được chuỗi port → targetPort → containerPort khớp với app đang listen.
  • Nhận diện NetworkPolicy chặn ingress hoặc chặn egress (đặc biệt là chặn DNS).
  • Viết được lệnh test kết nối từ trong cluster bằng pod tạm, không phụ thuộc công cụ ngoài.
  • Có quy trình 6 bước lặp lại được để dùng trong phòng thi.

Ôn nhanh bài trước

Bài trước chốt lại cách đọc describe, events và log để xử lý CrashLoopBackOff, ImagePullBackOff, Pending. Điểm móc nối quan trọng: khi pod đã 1/1 Running với restart count bằng 0 mà app vẫn không truy cập được, lỗi gần như chắc chắn không nằm trong container nữa, mà nằm ở lớp Service, DNS hoặc policy. Đừng đọc log app thêm lần nữa, hãy chuyển sang kiểm tra endpoints.

Ví dụ mẫu: Service có endpoints rỗng

Bối cảnh: namespace shop có Deployment api chạy nginx, container listen port 80. Service api-svc phải trỏ tới nó. Thực tế đang lỗi, ta debug từng bước.

Bước 1. Nhìn tổng thể trong một lệnh.

kubectl -n shop get svc,ep,pods --show-labels

Kết quả đáng ngờ:

NAME              TYPE        CLUSTER-IP     PORT(S)
service/api-svc   ClusterIP   10.96.14.22    80/TCP

NAME                   ENDPOINTS
endpoints/api-svc      <none>

NAME                   READY   STATUS    LABELS
pod/api-6d9f-abcde     1/1     Running   app=api-server,pod-template-hash=6d9f

ENDPOINTS <none> là manh mối số một. Service không có backend thì mọi thứ phía sau đều vô nghĩa.

Bước 2. So selector của Service với label của pod.

kubectl -n shop describe svc api-svc | grep -i selector
kubectl -n shop get pods --show-labels

Service đang chọn app=api, còn pod mang label app=api-server. Lệch một chữ là đủ để endpoint rỗng.

Bước 3. Sửa selector, không sửa pod.

Trong đề thi, câu hỏi thường quy định bên nào được phép sửa (đôi khi là "sửa Service", đôi khi là "sửa pod"). Ở đây đề cho sửa Service:

kubectl -n shop patch svc api-svc -p '{"spec":{"selector":{"app":"api-server"}}}'

Bước 4. Xác nhận endpoint đã có IP.

kubectl -n shop get endpoints api-svc
kubectl -n shop get endpointslices -l kubernetes.io/service-name=api-svc

Lần này phải thấy 1 hoặc 2 địa chỉ IP pod kèm port. Nếu vẫn <none>, kiểm tra pod đã Ready chưa: pod chưa Ready thì không được đưa vào endpoints.

Bước 5. Test từ bên trong cluster, không test từ máy bạn.

kubectl -n shop run tmp --image=busybox:1.28 --rm -it --restart=Never -- \
  wget -qO- --timeout=3 http://api-svc.shop.svc.cluster.local/

Kết quả vẫn timed out. Endpoint có IP rồi mà vẫn hỏng, vậy điểm đứt nằm ở port mapping.

Bước 6. So port với targetPort và containerPort.

kubectl -n shop get svc api-svc -o yaml | grep -A5 ports
kubectl -n shop get deploy api -o jsonpath='{.spec.template.spec.containers[*].ports}'

Service khai port: 80, targetPort: 8080, nhưng nginx trong pod listen port 80. Traffic bị đẩy vào cổng không ai nghe. Sửa:

kubectl -n shop patch svc api-svc -p \
  '{"spec":{"ports":[{"port":80,"targetPort":80,"protocol":"TCP"}]}}'

Lưu ý: patch vào ports là thay thế toàn bộ danh sách, nên phải khai lại đầy đủ port, targetPort, protocol.

Bước 7. Test lại. Lần này wget trả về nội dung trang mặc định của nginx. Luồng traffic đã thông.

[!TIP] Thứ tự kiểm tra cố định của tôi trong phòng thi: DNS → Endpoints → port/targetPort → NetworkPolicy → kube-proxy/CNI. Đi từ trên xuống, gặp chặng nào sai thì dừng lại sửa, không nhảy cóc.

Thực hành có hướng dẫn

Chuẩn bị chung cho cả 4 bài:

kubectl create namespace shop --dry-run=client -o yaml | kubectl apply -f -

Mỗi bài tập làm theo đúng 4 bước: quan sát → khoanh vùng → sửa → kiểm chứng bằng lệnh test. Đáp án nằm trong phần gập lại, hãy thử ít nhất 3 phút trước khi mở.

Bài tập 1: Endpoint rỗng vì selector lệch

kubectl -n shop apply -f - <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  namespace: shop
spec:
  replicas: 2
  selector:
    matchLabels:
      app: web-frontend
  template:
    metadata:
      labels:
        app: web-frontend
    spec:
      containers:
      - name: nginx
        image: nginx:1.25
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: web-svc
  namespace: shop
spec:
  selector:
    app: web
  ports:
  - port: 80
    targetPort: 80
EOF

Nhiệm vụ: làm cho http://web-svc trả về nội dung từ trong cluster.

Xem gợi ý và đáp án

Gợi ý: kubectl -n shop get endpoints web-svc sẽ ra <none>. Đối chiếu kubectl -n shop get pods --show-labels với selector trong kubectl -n shop describe svc web-svc.

Đáp án (sửa Service):

kubectl -n shop patch svc web-svc -p '{"spec":{"selector":{"app":"web-frontend"}}}'
kubectl -n shop get endpoints web-svc
kubectl -n shop run t1 --image=busybox:1.28 --rm -it --restart=Never -- \
  wget -qO- --timeout=3 http://web-svc.shop.svc.cluster.local/

Cách khác cũng hợp lệ nếu đề cho phép sửa pod: thêm label app=web vào pod template, sau đó rollout lại.

Bài tập 2: Có endpoint nhưng connection refused

kubectl -n shop create deployment api --image=nginx:1.25 --replicas=1
kubectl -n shop expose deployment api --name=api-svc --port=80 --target-port=8080

Nhiệm vụ: api-svc phải phục vụ được trên port 80. Kiểm chứng bằng pod tạm.

Xem gợi ý và đáp án

Gợi ý: kubectl -n shop get endpoints api-svc có IP, nên vấn đề không phải selector. Test bằng busybox sẽ thấy connection refused thay vì timeout. Đọc kubectl -n shop get svc api-svc -o yaml, so targetPort: 8080 với port nginx thực tế listen (80).

Đáp án:

kubectl -n shop patch svc api-svc -p \
  '{"spec":{"ports":[{"port":80,"targetPort":80,"protocol":"TCP"}]}}'
kubectl -n shop run t2 --image=busybox:1.28 --rm -it --restart=Never -- \
  wget -qO- --timeout=3 http://api-svc/

Phân biệt triệu chứng: timeout thường là policy hoặc sai địa chỉ, connection refused thường là đúng IP nhưng sai port.

Bài tập 3: NetworkPolicy chặn ingress

kubectl -n shop apply -f - <<'EOF'
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny
  namespace: shop
spec:
  podSelector: {}
  policyTypes:
  - Ingress
EOF

Nhiệm vụ: giữ nguyên policy default-deny, nhưng cho phép pod có label access=granted gọi được web-svc trên port 80. Test bằng pod tạm có label đó.

Xem gợi ý và đáp án

Gợi ý: kubectl -n shop get networkpolicy để thấy policy đang áp dụng. podSelector: {} nghĩa là áp lên mọi pod trong namespace. Đọc kubectl -n shop describe networkpolicy default-deny để xem chặng nào đang bị deny.

Đáp án:

kubectl -n shop apply -f - <<'EOF'
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-granted
  namespace: shop
spec:
  podSelector:
    matchLabels:
      app: web-frontend
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          access: granted
    ports:
    - protocol: TCP
      port: 80
EOF

Test:

kubectl -n shop run t3 --labels="access=granted" --image=busybox:1.28 --rm -it --restart=Never -- \
  wget -qO- --timeout=3 http://web-svc/

Chạy lại đúng lệnh trên nhưng bỏ --labels, bạn sẽ thấy bị chặn. Đó là bằng chứng policy hoạt động.

Bài tập 4: DNS và egress policy

kubectl create namespace client --dry-run=client -o yaml | kubectl apply -f -
kubectl -n client apply -f - <<'EOF'
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: deny-egress
  namespace: client
spec:
  podSelector: {}
  policyTypes:
  - Egress
EOF

Nhiệm vụ: pod trong namespace client phải gọi được web-svc.shop.svc.cluster.local, và DNS vẫn phải hoạt động. Không được xóa policy.

Xem gợi ý và đáp án

Gợi ý: chạy test trước, bạn sẽ thấy lỗi không phân giải được tên, dù CoreDNS hoàn toàn khỏe. deny-egress chặn luôn port 53 ra CoreDNS. Cần thêm 2 rule egress: một cho DNS, một cho traffic tới namespace shop.

Đáp án:

kubectl -n client apply -f - <<'EOF'
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns-and-shop
  namespace: client
spec:
  podSelector: {}
  policyTypes:
  - Egress
  egress:
  - to:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: kube-system
    ports:
    - protocol: UDP
      port: 53
    - protocol: TCP
      port: 53
  - to:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: shop
    ports:
    - protocol: TCP
      port: 80
EOF

Test:

kubectl -n client run t4 --image=busybox:1.28 --rm -it --restart=Never -- \
  wget -qO- --timeout=3 http://web-svc.shop.svc.cluster.local/

Bài học lớn nhất ở đây: ==DNS fail trong cluster thường là lỗi NetworkPolicy egress, không phải lỗi CoreDNS==.

Quy trình 6 bước áp dụng cho mọi đề

1. `kubectl -n <ns> get svc,ep,pods --show-labels`: tìm endpoint rỗng trước tiên.
2. `kubectl -n <ns> describe svc <name>`: đọc Selector, Port, TargetPort, Endpoints.
3. `kubectl -n <ns> get endpointslices -l kubernetes.io/service-name=<name>`: xác nhận IP và port đã resolve.
4. `kubectl -n <ns> get networkpolicy` + `describe`: loại trừ ingress/egress bị chặn.
5. Test bằng pod tạm trong đúng namespace: `wget`, `nc -zv` hoặc `nslookup`.
6. Nếu Service hỏng nhưng `kubectl port-forward pod/<pod> 8080:80` vẫn chạy: lỗi nằm ở lớp Service/policy, không phải app.

Lỗi thường gặp và cách xử lý

Triệu chứng Nguyên nhân hay gặp Lệnh kiểm tra
Endpoints <none> Service selector không khớp label pod, hoặc pod chưa Ready get svc,ep --show-labels, describe svc
Timeout khi gọi Service NetworkPolicy chặn, hoặc targetPort sai get networkpolicy, get svc -o yaml
Connection refused Đúng pod nhưng sai port so targetPort với containerPort
Không phân giải được tên Sai namespace, hoặc egress policy chặn port 53 nslookup <svc>.<ns>.svc.cluster.local
Chỉ một số pod lỗi Pod đó ở node mất CNI kubectl -n kube-system get pods -o wide
NodePort không vào được từ ngoài externalTrafficPolicy: Local và node không có pod backend describe svc, get pods -o wide

Một số bẫy cụ thể:

  • Service không có selector (dùng cho Endpoints tự tạo hoặc ExternalName) thì endpoint không bao giờ tự sinh. Phải tạo object Endpoints cùng tên bằng tay.
  • Named port: nếu targetPort: http mà containerPort không đặt name: http, endpoint sẽ không resolve được port. Kiểm tra bằng get endpointslices ... -o yaml.
  • Headless Service (clusterIP: None) không có VIP, DNS trả về IP pod. App nào hard-code ClusterIP sẽ hỏng.
  • kubectl get endpoints vẫn dùng được, nhưng trên cluster mới nên quen với endpointslices.
  • Test bằng curl trong image không có curl: nginx không có curl, busybox không có curl. Dùng wget -qO- hoặc nc -zv <host> <port> cho chắc.

[!WARNING] Đừng sửa cùng lúc nhiều thứ. Mỗi lần chỉ sửa một điểm rồi test lại. Trong phòng thi, sửa 3 chỗ một lúc mà vẫn lỗi thì bạn không biết chỗ nào đúng, và mất luôn khả năng quay lui.

Góc chuyên gia

  • 30 giây đầu tiên luôn dành cho endpoints. Chín trên mười ca Service hỏng trong đề CKA là do selector hoặc targetPort, không phải do DNS.
  • Test từ đúng namespace. Gọi tên ngắn web-svc chỉ đúng khi client ở cùng namespace. Khác namespace phải dùng web-svc.shop.svc.cluster.local hoặc ít nhất web-svc.shop.
  • Port-forward là máy đo chia đôi vùng lỗi. kubectl port-forward pod/<pod> 8080:80 chạy được nghĩa là app ổn, vấn đề ở Service hoặc policy. Port-forward tới svc/ mà lỗi nghĩa là Service không có endpoint.
  • NetworkPolicy có tính cộng dồn: nhiều policy, chỉ cần một policy cho phép là traffic đi qua. Đừng cố tìm "policy đúng duy nhất".
  • Mặc định không có NetworkPolicy nào thì mọi traffic được phép. Nếu namespace có đúng một policy deny-all, hãy nhớ nó chỉ áp lên các pod khớp podSelector.
  • Kiểm tra image có sẵn trước khi chạy pod tạm. Nếu busybox không pull được, hãy kubectl exec vào một pod đang chạy sẵn trong cluster. Đừng mất 5 phút chờ ImagePullBackOff.
  • Ghi lại bằng chứng vào đáp án khi được yêu cầu. Vài đề yêu cầu dán output lệnh test, hãy chạy đúng lệnh mà đề mô tả.

Tóm tắt và bước tiếp theo

Bốn dòng tôi tự nhắc trước giờ thi:

  • Endpoint rỗng thì nhìn selector trước, đừng nhìn DNS.
  • Endpoint có IP mà vẫn lỗi thì so targetPort với containerPort.
  • Có policy trong namespace thì đọc describe networkpolicy trước khi nghi ngờ CNI.
  • DNS fail mà CoreDNS vẫn chạy: nghĩ ngay tới egress policy chặn port 53.

Bài tiếp theo mở rộng sang đọc log control plane và journalctl: khi cả service lẫn pod đều ổn mà cluster vẫn có vấn đề, bạn sẽ cần đọc log của kube-apiserver, etcd, kube-scheduler và journalctl -u kubelet để tìm nguyên nhân gốc. Phần này cũng là nền để bạn xử lý các ca node NotReady nặng hơn.

Kiểm tra nhanh trước khi chuyển bài:

1. Khi Service không có backend nào, lệnh đầu tiên nên chạy là `kubectl get {{c1::endpoints}} <tên-service>`.
2. Nếu Service khai `port: 80` mà bỏ trống `targetPort`, traffic sẽ đi tới container port {{c2::80}}.
3. Tên DNS đầy đủ của Service `api` trong namespace `shop` là `api.shop.{{c3::svc.cluster.local}}`.
4. NetworkPolicy với `podSelector: {}` và `policyTypes: [Ingress]` sẽ {{c4::chặn toàn bộ}} traffic ingress vào mọi pod trong namespace đó.
5. Nếu policy egress chặn hết, triệu chứng thường thấy là pod không {{c5::phân giải được DNS}} dù CoreDNS vẫn chạy bình thường.
6. Service có `clusterIP: None` được gọi là {{c6::headless}} Service.
[
  { "front": "Lệnh nhanh nhất để biết Service có backend hay không?", "back": "kubectl get endpoints <svc>, hoặc get endpointslices -l kubernetes.io/service-name=<svc>." },
  { "front": "Endpoints rỗng thường do nguyên nhân gì?", "back": "Selector của Service không khớp label pod, hoặc pod chưa Ready." },
  { "front": "Service tự sinh Endpoints khi nào?", "back": "Khi Service có selector khớp pod Ready; Service không có selector phải tạo Endpoints thủ công." },
  { "front": "targetPort bỏ trống thì lấy giá trị nào?", "back": "Mặc định bằng giá trị của port." },
  { "front": "Tên DNS đầy đủ của Service?", "back": "<service>.<namespace>.svc.cluster.local." },
  { "front": "Cách test kết nối từ trong cluster bằng pod tạm?", "back": "kubectl run tmp --image=busybox:1.28 --rm -it --restart=Never -- wget -qO- --timeout=3 http://<svc>:<port>." },
  { "front": "Port-forward tới pod chạy được nhưng tới Service thì không, kết luận gì?", "back": "App ổn; lỗi nằm ở Service, Endpoints hoặc NetworkPolicy." },
  { "front": "NetworkPolicy podSelector rỗng với policyTypes Ingress nghĩa là gì?", "back": "Chặn toàn bộ ingress vào mọi pod trong namespace." },
  { "front": "Vì sao policy egress chặn hết lại làm DNS fail?", "back": "Vì nó chặn luôn port 53 ra CoreDNS, dù CoreDNS vẫn khỏe." },
  { "front": "NetworkPolicy có tính cộng dồn không?", "back": "Có, chỉ cần một policy cho phép là traffic được đi qua." },
  { "front": "clusterIP: None nghĩa là gì?", "back": "Headless Service, DNS trả về IP của từng pod, không có VIP." },
  { "front": "Dấu hiệu CNI lỗi trên một node?", "back": "Pod trên node đó mất kết nối mạng và pod CNI trong kube-system không Running." },
  { "front": "externalTrafficPolicy: Local gây lỗi gì với NodePort?", "back": "Node không có pod backend sẽ drop traffic thay vì chuyển sang node khác." },
  { "front": "Phân biệt timeout và connection refused nói lên điều gì?", "back": "Timeout thường do policy hoặc sai địa chỉ; connection refused thường do đúng IP nhưng sai port." }
]