Bác sĩ giỏi không đoán bệnh từ mỗi câu "tôi thấy mệt". Mệt có thể là thiếu máu, là tuyến giáp, là trầm cảm, là mất ngủ — cùng một lời than, cả chục nguyên nhân. Cái tách chúng ra không phải triệu chứng mà là một xét nghiệm phân biệt: một phép đo rẻ mà mỗi bệnh cho một kết quả khác nhau. Gỡ lỗi mạng trong Kubernetes cũng đúng như vậy. "Gọi Service không được" là câu than mơ hồ nhất, và ba nguyên nhân khác hẳn nhau lại cho ra y hệt một thông báo. Bài này dựng bốn cách hỏng rồi tìm ra đúng cái "xét nghiệm" tách được chú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à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.
Mẫu số chung: một triệu chứng không phải một nguyên nhân — hãy tìm phép đo tách chúng ra
Bài này thật ra là một bài về chẩn đoán phân biệt, và cách nghĩ đó dùng được ở mọi tầng. Nguyên tắc lõi: cùng một triệu chứng có thể do nhiều nguyên nhân khác hẳn nhau, nên đừng đoán từ triệu chứng — hãy tìm một phép quan sát rẻ mà mỗi nguyên nhân cho một kết quả khác nhau, rồi để nó chia bài toán làm đôi. Ở đây "xét nghiệm" đó là bảng endpointslices: ba lỗi cùng ra ConnectionRefused nhưng để lại ba dấu vân tay khác nhau.
Và có một phép phân biệt mang đi được khắp nơi: sự có mặt và thời điểm của một phản hồi tự nó là tín hiệu. Lỗi trả về ngay lập tức nghĩa là gói tin đã tới nơi và bị chủ động từ chối; im lặng cho tới hết hạn chờ nghĩa là gói tin bị vứt giữa đường, chẳng ai từng nghe thấy. Giống gọi điện: máy đổ chuông rồi báo "máy bận / từ chối" là cuộc gọi đã tới tổng đài bên kia; còn đổ chuông mãi tới lúc tự tắt là bạn không biết nó có tới hay không — hai chẩn đoán hoàn toàn khác nhau.
Trong thế giới lập trình mạng bất kể ngôn ngữ, đây chính là TCP RST so với gói SYN bị tường lửa nuốt: "connection refused" tức thì (cổng đóng, không ai nghe, sai cổng) so với "connect timeout" (tường lửa chặn, host im lặng). Một client cơ sở dữ liệu phân biệt y hệt: Connection refused là DB không nghe ở cổng đó / sai cổng, còn treo tới connectTimeout là bị firewall chặn hoặc DB quá tải không kịp bắt tay. Client HTTP cũng vậy: fail-fast (RST, hoặc 4xx có nội dung) so với treo tới read-timeout. Cùng một cây quyết định. Và bài học phụ — "DNS phân giải được" — là trường hợp riêng của một sai lầm chung: một tầng sáng đèn không chứng minh hệ thống chạy; health check chỉ ping mà không thử phụ thuộc thật cũng mắc đúng lỗi đó.
Mẫu số chung: khi một triệu chứng có nhiều nghi phạm, đừng sửa theo linh cảm — tìm phép đo rẻ nhất phân hoạch được các nguyên nhân (với mạng: lỗi-tức-thì so với im-lặng-đến-hết-giờ), và đừng tin một tầng "xanh" là bằng chứng cho cả hệ.
Muốn tự soi, 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ì.