"Gọi Service không được" là mô tả sự cố phổ biến nhất trong Kubernetes, và cũng mơ hồ nhất. Bài này dựng bốn cách hỏng rồi đo xem chúng khác nhau ở đâu.

Bốn Service, kết quả gọi thử, và lệnh phân biệt được cả ba ca hỏng

Bốn Service, một pod

Cùng một pod nginx đứng sau bốn Service khác nhau. Gọi thử từ một pod khách:

dung             DNS  19ms -> 10.96.199.47    KẾT NỐI ĐƯỢC
sai-selector     DNS   2ms -> 10.96.38.39     ConnectionRefused (0 ms)
sai-targetport   DNS   2ms -> 10.96.205.216   ConnectionRefused (0 ms)
co-probe         DNS   3ms -> 10.96.239.205   ConnectionRefused (0 ms)

Ba nguyên nhân hoàn toàn khác nhau — nhãn sai, cổng sai, pod chưa sẵn sàng — cho ra cùng một thông báo lỗi, cùng một thời gian.

Và chú ý: DNS phân giải tốt ở cả ba. Service có ClusterIP ngay khi được tạo, bất kể có pod nào đứng sau hay không. Nên "nslookup chạy được" không chứng minh gì cả — đó là bước kiểm tra hay bị lạm dụng nhất.

Một lệnh phân biệt được cả ba

kubectl get endpointslices -n <ns>
Service Địa chỉ Cổng Sẵn sàng
dung 10.244.1.94 80 true
sai-selector <none> <none> <none>
sai-targetport 10.244.1.94 8080 true
co-probe 10.244.2.94 80 false

Ba dòng hỏng, ba dấu hiệu khác nhau, đọc được ngay trên một bảng:

  • Không có địa chỉ nàoselector không khớp nhãn pod nào.
  • Có địa chỉ nhưng cổng lạtargetPort trỏ vào cổng ứng dụng không nghe.
  • ready=false → pod sống nhưng probe đang trượt.

Với ca thứ ba, gọi thẳng vào IP của pod thì kết nối được — nginx vẫn chạy bình thường. Chính Kubernetes đang cố tình không gửi lưu lượng tới. Đó là hành vi đúng, và cũng là nguồn của rất nhiều phút hoang mang.

Bảng in gọn:

kubectl get endpointslices -n <ns> -o custom-columns=\
'SVC:.metadata.labels.kubernetes\.io/service-name,\
DIACHI:.endpoints[*].addresses,\
CONG:.ports[*].port,\
SANSANG:.endpoints[*].conditions.ready'

Refused hay Timeout — hai họ nguyên nhân

Tôi bật một NetworkPolicy chặn hết rồi gọi lại:

dung          -> TimeoutError (3004 ms)
IP của pod    -> TimeoutError (3004 ms)
endpointslice -> vẫn ready=true

Triệu chứng đổi hẳn. Và đó là thông tin quý nhất trong cả bài:

Triệu chứng Ý nghĩa Nhóm nguyên nhân
ConnectionRefused, ~0 ms Gói tin đến nơi và bị từ chối Không có endpoint, sai cổng, pod chưa sẵn sàng
Timeout, đúng bằng hạn chờ Gói tin bị vứt, không ai trả lời NetworkPolicy, tường lửa, định tuyến giữa node

Đây là phép chia đôi tốt nhất. Thấy Timeout thì đừng mất công đọc endpoint — chỗ hỏng không nằm ở đó, vì đo được endpoint vẫn bình thường trong suốt thời gian bị chặn.

Ngược lại, thấy Refused thì đừng đi tìm NetworkPolicy: gói tin đã tới được nơi có người trả lời, tức là không bị chặn.

Một lưu ý về đo đạc: hạn chờ ở trên là 3 giây và kết quả là 3004 ms. Đo bằng curl mặc định thì hạn chờ tới hàng chục giây, đủ lâu để người ta bỏ cuộc trước khi có kết quả. Luôn đặt hạn chờ ngắn khi thăm dò — curl -m 3, wget -T 3.

Thứ tự lần từ dưới lên

Bốn bước, mỗi bước loại bỏ một tầng:

# 1. Pod có nghe không? -- gọi thẳng IP pod, bỏ qua Service
kubectl get pod <pod> -o wide          # lấy IP
kubectl exec <khach> -- wget -qO- -T3 http://<pod-ip>:<cong>

# 2. Service có endpoint đúng không?
kubectl get endpointslices -n <ns>

# 3. DNS trả về gì?
kubectl exec <khach> -- nslookup <svc>.<ns>.svc.cluster.local

# 4. Có chính sách nào chặn không?
kubectl get networkpolicy -A

Bước 1 quan trọng nhất và hay bị bỏ qua nhất. Nó chia đôi bài toán ngay: gọi thẳng pod mà được thì lỗi nằm ở Service, endpoint hoặc DNS; gọi thẳng pod mà không được thì lỗi nằm ở ứng dụng hoặc tầng mạng, và Service hoàn toàn vô can.

Ba cái bẫy đã đo được ở các phần trước

targetPort là cổng của ứng dụng, port là cổng của Service. Nhầm hai cái cho ra đúng ConnectionRefused như trên. targetPort cũng nhận tên cổng đã khai trong pod — cách này an toàn hơn số, vì đổi cổng ứng dụng thì Service tự theo.

Pod 0/1 Running vẫn là Running. Danh sách pod nhìn lướt qua thấy Running hết, nhưng cột READY0/1, và nó bị loại khỏi endpoint. Luôn nhìn cột READY trước cột STATUS.

Tên DNS thiếu namespace. Gọi web từ namespace khác thì trình phân giải thử web.<ns-cua-minh>.svc.cluster.local trước và trượt. Dùng đủ web.<ns>.svc.cluster.local khi qua namespace — và như đã đo ở phần về DNS, viết đủ còn nhanh hơn hẳn.

Thử ba mươi giây

Tìm mọi Service trong cụm không có endpoint nào — tức là mọi Service chắc chắn đang trả về ConnectionRefused:

kubectl get endpointslices -A -o json | python3 -c '
import sys, json
co = {}
for s in json.load(sys.stdin)["items"]:
    ns = s["metadata"]["namespace"]
    svc = s["metadata"]["labels"].get("kubernetes.io/service-name")
    n = sum(1 for e in (s.get("endpoints") or [])
            if (e.get("conditions") or {}).get("ready"))
    co[(ns, svc)] = co.get((ns, svc), 0) + n
for (ns, svc), n in sorted(co.items()):
    if n == 0: print("khong co endpoint san sang:", ns + "/" + svc)
'

Vài dòng là bình thường — Service không selector dùng cho endpoint ngoài cụm, hoặc Deployment đang scale về 0. Nhưng mỗi dòng đều đáng nhìn qua một lượt: đó là danh sách những chỗ sẽ hỏng ngay khi có ai gọi tới, và không có gì báo động cho tới lúc đó.

Phần sau: Helm — nó thêm gì vào manifest của bạn, và nó giấu đi những gì.