"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, 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ào →
selectorkhông khớp nhãn pod nào. - Có địa chỉ nhưng cổng lạ →
targetPorttrỏ 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 READY là 0/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ì.