Skip to main content

14. Troubleshooting

Mục lục


1. Troubleshooting là gì và tại sao cần?

3 giờ sáng, điện thoại rung. Người dùng không mở được trang web. Trên máy dev mọi thứ vẫn chạy hoàn hảo — cùng một image, cùng một file YAML. Nhưng trên production, kubectl get pods trả về một Pod đứng yên ở Pending, một Pod khác đếm RESTARTS tăng đều mỗi 40 giây, và Service thì im lặng không trả về gì.

Bắt đầu từ đâu?

Câu trả lời sai phổ biến nhất là: xoá hết đi và deploy lại. Đôi khi nó "chữa" được triệu chứng, nhưng lỗi sẽ quay lại vào 3 giờ sáng hôm sau. Câu trả lời đúng là đi theo một trình tự cố định, mỗi bước loại trừ đúng một tầng của hệ thống.

💡 Hình dung: Troubleshooting giống bác sĩ khám bệnh — hỏi triệu chứng trước khi chẩn đoán. Bệnh nhân (cluster) có nhiều triệu chứng khác nhau, mỗi triệu chứng gợi ý một nguyên nhân khác nhau. Bác sĩ giỏi không kê đơn ngay, mà hỏi từng câu để thu hẹp phạm vi.

Ba câu hỏi cần trả lời được sau tài liệu này:

  • Triệu chứng đang nằm ở tầng nào — ứng dụng, control plane, worker node, hay network?
  • Công cụ nào giúp khoanh vùng nhanh nhất ở mỗi tầng?
  • Lỗi phổ biến nhất ở mỗi tầng là gì, và sửa ở đâu?

Kubernetes troubleshooting chia thành 4 tầng: Application, Control Plane, Worker Node, và Network. Tài liệu này đi từ dưới lên — từ triệu chứng người dùng nhìn thấy, ngược về nguyên nhân hệ thống. Lý do của thứ tự này rất thực dụng: 80% sự cố thật sự dừng lại ở tầng Application, và chẩn đoán ở tầng đó chỉ tốn 3 lệnh.


2. Troubleshooting Application Failures

2.1. Tổng quan về kiến trúc ứng dụng

Lấy một ứng dụng hai tầng (two-tier application) làm ví dụ xuyên suốt. Có một web pod chạy web server phục vụ người dùng, và một database pod chạy MySQL. Web pod nói chuyện với database pod qua một database service, còn người dùng nói chuyện với web pod qua một web service.

Trước khi gõ lệnh đầu tiên, hãy vẽ sơ đồ này ra giấy. Nghe có vẻ thừa, nhưng đây là bước tiết kiệm thời gian nhất: sơ đồ cho biết có bao nhiêu mắt xích, và troubleshooting chính là việc kiểm tra từng mắt xích cho đến khi tìm ra mắt xích đứt. Tuỳ mức độ hiểu biết về lỗi, bạn có thể đi từ đầu hoặc từ cuối sơ đồ — nhưng đừng bỏ sót mắt xích nào.

Trước khi đi tiếp, cần thống nhất 4 thuật ngữ sẽ xuất hiện liên tục trong phần này.

  • Label là cặp key-value gắn trên Pod, ví dụ name=mysql. Nó chỉ là một cái nhãn, tự nó không làm gì cả.
  • Selector là cặp key-value khai báo trong Service, nói rằng "tôi phục vụ những Pod nào mang label này".
  • Endpoints là danh sách IP:port thật sự mà Service đang forward request tới. Kubernetes tự sinh danh sách này bằng cách so selector của Service với label của mọi Pod trong cùng namespace. Không Pod nào khớp thì danh sách rỗng, và Service trở thành một cái ngõ cụt.
  • targetPort là port trên container mà Service chuyển request tới. Nó phải trùng với port mà tiến trình bên trong container thật sự đang lắng nghe.
Service tim Pod bang selector, khong bang tenService: mysql-serviceselector: name=mysqlport: 3306targetPort: 3306Endpoints: 10.244.1.7:3306Pod: mysql-5f9clabel: name=mysqlpodIP: 10.244.1.7containerPort: 3306MySQL lang nghe cong nayselector khop labelSai 1 ky tu trong selectorEndpoints: <none>, Service thanh ngo cut

Điểm cần khắc vào đầu: Service không biết Pod nào phục vụ nó qua tên. Liên kết duy nhất giữa hai bên là cặp selectorlabel, và nó so khớp theo đúng từng ký tự, phân biệt hoa thường. Đây là nguồn gốc của phần lớn sự cố "Service không route được" mà bạn sẽ gặp ở mục 3. Chi tiết đầy đủ về Service nằm ở Part 2.4.

2.2. Quy trình 4 bước

Quy trình này đi từ ngoài vào trong. Mỗi bước trả lời đúng một câu hỏi, và khi câu trả lời là "ổn" thì loại trừ được cả một tầng.

Debug Application Failures: 4 buoc tu ngoai vao trongNguoi dung bao loiBuoc 1: Frontend con vao duoc?curl http://NODE_IP:30081Timeout? 502? Trang trang?Buoc 2: Service co Endpoints?kubectl describe svc web-serviceselector khop label cua Pod?Buoc 3: Pod co Running khong?kubectl describe pod / kubectl logsRESTARTS tang dan? CrashLoopBackOff?Buoc 4: DB Service + DB Podkubectl logs mysql --previousSai credentials? Sai port?Trieu chung nguoidung nhin thay.Endpoints: <none>= selector sai.CrashLoopBackOff= container chet lap.Access denied /connection refused.Moi buoc loai tru mot tang. Dung nhay coc.

Bước 1: Kiểm tra frontend của ứng dụng.

Đây là điểm cuối mà người dùng chạm vào. Nếu nó không truy cập được, lỗi có thể nằm ở bất kỳ tầng nào phía sau — nhưng chính cách nó hỏng đã là một manh mối. Với ứng dụng web, gọi thẳng vào NodePort bằng curl.

curl -v http://192.168.1.10:30081

# Output minh họa
# * Trying 192.168.1.10:30081...
# * connect to 192.168.1.10 port 30081 failed: Connection refused
# * Failed to connect to 192.168.1.10 port 30081 after 3 ms: Connection refused

(Output minh họa — cấu trúc và tên field đúng theo lệnh thật, số liệu cụ thể chỉ mang tính ví dụ.)

Connection refused ngay ở bước connect nghĩa là không có gì đang lắng nghe trên port đó của node — thường là nodePort bị đặt sai số, hoặc Service chưa được tạo. Phân biệt với Connection timed out: timeout nghĩa là gói tin bị chặn ở đâu đó trên đường đi (firewall, security group), còn refused nghĩa là gói tin đã tới node và bị node từ chối thẳng.

Bước 2: Kiểm tra Service.

Service là cầu nối giữa người dùng và Pod. Câu hỏi duy nhất ở bước này: Service đã tìm được Pod nào chưa?

kubectl describe svc web-service

# Output minh họa
# Name: web-service
# Namespace: default
# Selector: name=webapp-mysql
# Type: NodePort
# IP: 10.108.190.7
# Port: <unset> 8080/TCP
# TargetPort: 8080/TCP
# NodePort: <unset> 30081/TCP
# Endpoints: <none>

(Output minh họa — cấu trúc và tên field đúng theo lệnh thật, số liệu cụ thể chỉ mang tính ví dụ.)

Dòng cần nhìn là Endpoints: <none>. Nó nói rằng không một Pod nào trong namespace mang label khớp với Selector: name=webapp-mysql, nên Service không có chỗ nào để forward request. Đối chiếu ngay bằng kubectl get pods --show-labels — lệch một ký tự hoa thường cũng đủ cho ra kết quả này.

Kể từ Kubernetes v1.33, API Endpoints đã bị deprecated và API server trả về cảnh báo endpoints is deprecated, use endpointslices instead khi bạn chạy kubectl get endpoints. Dòng Endpoints: trong kubectl describe svc vẫn hiển thị bình thường, nhưng lệnh nên dùng từ nay là:

kubectl get endpointslices -l kubernetes.io/service-name=web-service

# Output minh họa
# NAME ADDRESSTYPE PORTS ENDPOINTS AGE
# web-service-x4k2p IPv4 8080 <unset> 11m

(Output minh họa — cấu trúc và tên field đúng theo lệnh thật, số liệu cụ thể chỉ mang tính ví dụ.)

Cột ENDPOINTS hiện <unset> thay vì một danh sách IP — cùng một chẩn đoán với Endpoints: <none> ở trên. Một Service có thể có nhiều EndpointSlice (ví dụ dual-stack thì có 1 slice IPv4 và 1 slice IPv6), trong khi API Endpoints cũ chỉ biểu diễn được đúng một object.

Bước 3: Kiểm tra Pod.

Pod là nơi container thật sự chạy. Hai con số nói lên gần hết câu chuyện: STATUSRESTARTS.

kubectl get pods

# Output minh họa
# NAME READY STATUS RESTARTS AGE
# webapp-mysql-5d8c9f7b4d-tq7xw 0/1 CrashLoopBackOff 6 (42s ago) 8m
# mysql 1/1 Running 0 8m

(Output minh họa — cấu trúc và tên field đúng theo lệnh thật, số liệu cụ thể chỉ mang tính ví dụ.)

CrashLoopBackOff không phải một lỗi — nó là hành vi của kubelet. Container khởi động, chết, kubelet khởi động lại, container lại chết; sau vài vòng kubelet bắt đầu giãn thời gian chờ giữa các lần thử (backoff) để khỏi đốt CPU. RESTARTS: 6 (42s ago) xác nhận vòng lặp này đang diễn ra. Nguyên nhân thật nằm trong log của lần chết gần nhất, không phải trong trạng thái hiện tại.

Sau đó xem events của Pod, nơi kubelet và scheduler ghi lại lý do từ chối:

kubectl describe pod webapp-mysql-5d8c9f7b4d-tq7xw

# Output minh họa (rút gọn)
# Events:
# Type Reason Age From Message
# ---- ------ ---- ---- -------
# Normal Scheduled 8m default-scheduler Successfully assigned default/webapp-mysql-5d8c9f7b4d-tq7xw to node01
# Normal Pulled 7m (x4 over 8m) kubelet Container image "kodekloud/simple-webapp-mysql" already present on machine
# Warning BackOff 2m (x18 over 7m) kubelet Back-off restarting failed container webapp

(Output minh họa — cấu trúc và tên field đúng theo lệnh thật, số liệu cụ thể chỉ mang tính ví dụ.)

Cột Reason là thứ cần đọc trước: Scheduled nghĩa là scheduler đã đặt được Pod lên node, nên vấn đề không nằm ở scheduling; BackOff nghĩa là container tự chết chứ không phải Kubernetes giết nó. Nếu Pod kẹt ở Pending thì ngược lại — sẽ không có dòng Scheduled nào cả, và Message sẽ ghi rõ lý do scheduler từ chối.

Muốn xem toàn bộ events trong namespace theo thứ tự thời gian, dùng:

kubectl get events --sort-by=.metadata.creationTimestamp

💡 Lưu ý quan trọng: Khi Pod đang restart liên tục, log của container đang chạy thường sạch bong — nó vừa mới khởi động xong và chưa kịp hỏng. Lý do thật nằm trong log của lần chạy trước đó. Dùng kubectl logs <pod> --previous để đọc đúng lần chết vừa rồi, hoặc kubectl logs -f <pod> rồi ngồi chờ nó chết lần nữa.

kubectl logs webapp-mysql-5d8c9f7b4d-tq7xw --previous

# Output minh họa
# [2026-09-23 03:14:22] Starting webapp on :8080
# [2026-09-23 03:14:22] Connecting to mysql-service:3306 ...
# Traceback (most recent call last):
# File "app.py", line 41, in connect
# pymysql.err.OperationalError: (2003, "Can't connect to MySQL server on 'mysql-service' (name does not resolve)")

(Output minh họa — cấu trúc và tên field đúng theo lệnh thật, số liệu cụ thể chỉ mang tính ví dụ.)

Chuỗi name does not resolve là chẩn đoán cuối cùng: đây không phải lỗi mạng, cũng không phải lỗi credentials. Ứng dụng hỏi DNS tên mysql-service và DNS trả lời "không có object nào tên như vậy" — tức Service database hoặc chưa tồn tại, hoặc mang tên khác.

Bước 4: Kiểm tra Database Service và DB Pod.

Nếu web pod đã chạy nhưng dữ liệu trả về sai hoặc rỗng, mắt xích đứt nằm ở tầng database. Lặp lại đúng bước 2 và bước 3, lần này cho mysql-service và pod mysql, rồi đọc log của chính MySQL để tìm lỗi xác thực hoặc lỗi khởi tạo.

2.3. Bảng tra nhanh: triệu chứng → nguyên nhân → lệnh đầu tiên

Bốn bước ở trên cho biết đi theo hướng nào. Bảng dưới rút ngắn đoạn đường: nhìn vào cột STATUS của kubectl get pods (hoặc dòng Endpoints của kubectl describe svc), tra ra nghĩa, rồi chạy đúng một lệnh.

Triệu chứngNghĩa thực sự là gìLệnh chạy đầu tiên
PendingPod đã được tạo trong etcd nhưng chưa node nào nhận — scheduler không tìm được node đủ CPU/RAM, hoặc bị chặn bởi taint, node selector, hostPort đã bị chiếmkubectl describe pod <pod> rồi đọc Events phần FailedScheduling
ImagePullBackOffKéo image thất bại nhiều lần nên kubelet đã giãn nhịp thử lại — sai tên image, sai tag, registry private mà thiếu imagePullSecretskubectl describe pod <pod> rồi đọc Events phần Failed
ErrImagePullLần kéo image đầu tiên vừa thất bại — cùng nhóm nguyên nhân với ImagePullBackOff, chỉ khác là chưa bước vào backoffkubectl describe pod <pod>
CrashLoopBackOffContainer khởi động được nhưng chết ngay, lặp đi lặp lại — lỗi nằm trong ứng dụng, không nằm ở Kuberneteskubectl logs <pod> --previous
OOMKilledContainer vượt memory limit nên bị kernel giết — thấy ở Last State trong kubectl describe pod, thường kèm Exit Code: 137kubectl describe pod <pod> rồi so limits.memory với mức dùng thật
CreateContainerConfigErrorPod tham chiếu tới một ConfigMap hoặc Secret không tồn tại, hoặc thiếu key bên trong nó — container chưa từng được tạo rakubectl describe pod <pod> rồi đọc tên ConfigMap/Secret trong Events
Endpoints: <none>selector của Service không khớp label của bất kỳ Pod nào trong cùng namespace — Service trở thành ngõ cụtkubectl get pods --show-labels rồi so với Selector trong kubectl describe svc
Có Endpoints nhưng vẫn không kết nối đượctargetPort khác với port mà tiến trình trong container thật sự lắng nghekubectl describe svc <svc> rồi so TargetPort với containerPort của Pod

Điểm chung của cả bảng: kubectl describe pod là lệnh trả lời được nhiều câu hỏi nhất, còn kubectl logs --previous là lệnh duy nhất trả lời được CrashLoopBackOff. Nhớ hai lệnh này thì đã xử lý được phần lớn sự cố tầng ứng dụng.


3. Thực hành: 6 kịch bản Application Failures

Sáu kịch bản dưới đây dùng chung một ứng dụng hai tầng, mỗi kịch bản nằm trong một namespace riêng và hỏng theo một kiểu khác nhau. Mẹo đầu tiên: đổi namespace mặc định của context để khỏi phải gõ -n <namespace> ở mọi lệnh.

3.1. Scenario Alpha — Namespace alpha

Vấn đề: ứng dụng báo lỗi can't connect to MySQL server on mysql-service:3306; name does not resolve.

Kiến trúc:

  • Web application: Deployment webapp-mysql, container lắng nghe port 8080.
  • Web Service: kiểu NodePort, nodePort 30081.
  • MySQL Service: port 3306.
  • MySQL Pod.

Quá trình troubleshooting:

kubectl config set-context --current --namespace=alpha
kubectl get pods
kubectl get svc

# Output minh họa
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# mysql ClusterIP 10.102.44.18 <none> 3306/TCP 12m
# web-service NodePort 10.108.190.7 <none> 8080:30081/TCP 12m

(Output minh họa — cấu trúc và tên field đúng theo lệnh thật, số liệu cụ thể chỉ mang tính ví dụ.)

Cột NAME đã nói hết: Service database tên là mysql, trong khi ứng dụng đang gọi DNS tên mysql-service. Hai cái tên này không liên quan gì tới nhau, nên CoreDNS trả về NXDOMAIN và ứng dụng báo name does not resolve. Pod hoàn toàn khoẻ mạnh — sai chỉ nằm ở một cái tên.

Nguyên nhân: Service mang tên mysql, còn ứng dụng kết nối tới mysql-service.

Giải pháp:

# Cach 1: sua ten Service cho khop voi ten ung dung dang goi
kubectl delete svc mysql
kubectl create -f mysql-service.yaml # file YAML co metadata.name: mysql-service

# Cach 2: sua bien moi truong cua ung dung tro ve dung ten Service dang co
kubectl edit deployment webapp-mysql

Không sửa được metadata.name của một Service đang tồn tại — tên là định danh bất biến, nên bắt buộc phải xoá và tạo lại.

3.2. Scenario Beta — Namespace beta

Vấn đề: ứng dụng báo connection refused khi kết nối tới MySQL service. Lần này tên Service đã đúng (mysql-service), nên DNS không còn là nghi phạm.

Tên đúng rồi mà vẫn bị từ chối — nghĩa là gói tin đã tới đúng nơi nhưng không ai mở cửa. Vậy Service đang gõ vào cửa nào?

Quá trình troubleshooting:

kubectl config set-context --current --namespace=beta
kubectl describe svc mysql-service

# Output minh họa
# Name: mysql-service
# Namespace: beta
# Selector: name=mysql
# Type: ClusterIP
# IP: 10.98.12.7
# Port: <unset> 3306/TCP
# TargetPort: 8080/TCP
# Endpoints: 10.244.1.9:8080

(Output minh họa — cấu trúc và tên field đúng theo lệnh thật, số liệu cụ thể chỉ mang tính ví dụ.)

Endpoints có địa chỉ, nên selector khớp và Pod tồn tại — loại trừ luôn nhóm nguyên nhân của bước 2. Nhưng TargetPort: 8080/TCP mới là chỗ sai: Service nhận request ở port 3306 rồi chuyển tiếp vào port 8080 của container, trong khi MySQL chỉ lắng nghe ở 3306. Không có tiến trình nào giữ port 8080 trong container đó, nên kernel trả về ngay connection refused.

Nguyên nhân: Service có targetPort: 8080 nhưng database thật sự chạy trên port 3306.

Giải pháp:

kubectl edit svc mysql-service
# Doi targetPort tu 8080 thanh 3306

targetPort là field sửa tại chỗ được, thay đổi có hiệu lực ngay và không cần restart Pod.

3.3. Scenario Gamma — Namespace gamma

Vấn đề: trang web quay vòng loading mãi, không hiện gì và cũng không báo lỗi.

Loading vô hạn khác hẳn connection refused. Refused là bị từ chối ngay; loading vô hạn nghĩa là request đã được nhận nhưng không bao giờ có ai trả lời — dấu hiệu kinh điển của một Service không có chỗ nào để forward.

Quá trình troubleshooting:

kubectl config set-context --current --namespace=gamma
kubectl describe svc mysql-service

# Output minh họa
# Name: mysql-service
# Namespace: gamma
# Selector: name=mysql
# Type: ClusterIP
# IP: 10.99.240.33
# Port: <unset> 3306/TCP
# TargetPort: 3306/TCP
# Endpoints: <none>

(Output minh họa — cấu trúc và tên field đúng theo lệnh thật, số liệu cụ thể chỉ mang tính ví dụ.)

Endpoints: <none> trong khi Pod mysql vẫn đang Running — đây là mâu thuẫn cần giải thích. So label của Pod với selector của Service:

kubectl get pods --show-labels

# Output minh họa
# NAME READY STATUS RESTARTS AGE LABELS
# mysql 1/1 Running 0 14m name=MySQL
# webapp-mysql-6b7f9d4c88-2xvzq 1/1 Running 0 14m name=webapp-mysql

(Output minh họa — cấu trúc và tên field đúng theo lệnh thật, số liệu cụ thể chỉ mang tính ví dụ.)

Cột LABELS ghi name=MySQL, còn Selector của Service là name=mysql. Label selector so khớp phân biệt hoa thường tuyệt đối, nên với Kubernetes thì đây là hai giá trị hoàn toàn khác nhau và không Pod nào được chọn.

Nguyên nhân: selector của Service là name=mysql nhưng label của Pod là name=MySQL.

Giải pháp:

kubectl edit svc mysql-service
# Doi selector tu name=mysql thanh name=MySQL

Sửa ở phía nào cũng được, miễn hai bên khớp nhau từng ký tự. Trong thực tế nên chuẩn hoá label về chữ thường toàn bộ để không bao giờ phải debug lại kiểu lỗi này.

3.4. Scenario Delta — Namespace delta

Vấn đề: ứng dụng báo access denied for user 'sql-user'.

Thông báo này là tin tốt: kết nối TCP đã thành công, DNS đã phân giải đúng, Service đã route đúng. Chỉ còn tầng cuối cùng — xác thực.

Quá trình troubleshooting:

kubectl config set-context --current --namespace=delta
kubectl describe deployment webapp-mysql

# Output minh họa (rút gọn)
# Pod Template:
# Containers:
# webapp:
# Image: kodekloud/simple-webapp-mysql
# Port: 8080/TCP
# Environment:
# DB_Host: mysql-service
# DB_User: sql_user
# DB_Password: paswrd

(Output minh họa — cấu trúc và tên field đúng theo lệnh thật, số liệu cụ thể chỉ mang tính ví dụ.)

Mục Environment là nơi ứng dụng lấy thông tin đăng nhập. DB_User: sql_user không tồn tại trong MySQL — account duy nhất được tạo sẵn trong pod database là root.

Nguyên nhân: biến môi trường user của ứng dụng được set là sql_user thay vì root.

Giải pháp:

kubectl edit deployment webapp-mysql
# Doi gia tri bien user tu sql_user thanh root

Sửa Deployment sẽ kích hoạt rolling update: Pod cũ bị thay bằng Pod mới mang biến môi trường mới. Đây là điểm khác biệt quan trọng so với kịch bản tiếp theo.

3.5. Scenario Epsilon — Namespace epsilon

Vấn đề: giống Delta, nhưng sau khi đã sửa Deployment thành root, ứng dụng vẫn báo access denied for user 'root'.

Sửa đúng phía client mà vẫn bị từ chối — vậy nghi phạm còn lại chỉ có thể là phía server.

Quá trình troubleshooting:

kubectl config set-context --current --namespace=epsilon
kubectl describe pod mysql

# Output minh họa (rút gọn)
# Containers:
# mysql:
# Image: mysql:8.0
# Port: 3306/TCP
# State: Running
# Environment:
# MYSQL_ROOT_PASSWORD: sqlpassword

(Output minh họa — cấu trúc và tên field đúng theo lệnh thật, số liệu cụ thể chỉ mang tính ví dụ.)

MYSQL_ROOT_PASSWORD: sqlpassword là mật khẩu mà MySQL đặt cho root lúc khởi tạo, và nó khác với DB_Password mà ứng dụng gửi lên. Hai bên phải trùng nhau thì mới xác thực được — sửa bên nào cũng được, miễn khớp.

Nguyên nhân: mật khẩu root của MySQL Pod không trùng với mật khẩu ứng dụng đang dùng.

Giải pháp:

kubectl edit pod mysql
# Doi MYSQL_ROOT_PASSWORD thanh gia tri ung dung dang gui len
# Output minh họa
# error: pods "mysql" is invalid
# # pods "mysql" was not valid:
# # * spec: Forbidden: pod updates may not change fields other than
# # `spec.containers[*].image`, `spec.initContainers[*].image`,
# # `spec.activeDeadlineSeconds`, `spec.tolerations` (only additions)

(Output minh họa — cấu trúc và tên field đúng theo lệnh thật, số liệu cụ thể chỉ mang tính ví dụ.)

Thông báo pod updates may not change fields other than ... là hành vi đúng, không phải lỗi: biến môi trường của một Pod đang chạy là bất biến. Deployment sửa được vì mỗi lần sửa nó tạo ra Pod mới; Pod trần thì phải xoá và dựng lại:

kubectl replace --force -f /tmp/kubectl-edit-xxxx.yaml

--force sẽ xoá Pod cũ rồi tạo lại từ file YAML đã sửa. Bài học rút ra: Pod trần không có cơ chế cập nhật tại chỗ, nên trong thực tế hãy luôn bọc workload trong Deployment.

3.6. Scenario Zeta — Namespace zeta

Vấn đề: trình duyệt báo Bad Gateway ngay từ request đầu tiên, chưa kịp chạm vào ứng dụng.

Quá trình troubleshooting:

kubectl config set-context --current --namespace=zeta
kubectl describe svc web-service

# Output minh họa
# Name: web-service
# Namespace: zeta
# Selector: name=webapp-mysql
# Type: NodePort
# IP: 10.101.77.5
# Port: <unset> 8080/TCP
# TargetPort: 8080/TCP
# NodePort: <unset> 30088/TCP
# Endpoints: 10.244.1.12:8080

(Output minh họa — cấu trúc và tên field đúng theo lệnh thật, số liệu cụ thể chỉ mang tính ví dụ.)

Endpoints có địa chỉ và TargetPort khớp với container — bên trong cluster mọi thứ đều đúng. Sai duy nhất là NodePort: 30088, trong khi proxy phía trước đang trỏ vào 30081. Không có gì lắng nghe ở 30081 nên proxy trả về Bad Gateway.

Nguyên nhân: nodePort của web-service là 30088 thay vì 30081.

Giải pháp:

kubectl edit svc web-service
# Doi nodePort tu 30088 thanh 30081

Nhớ rằng nodePort chỉ được nằm trong dải 30000-32767; đặt ngoài dải này thì API server từ chối ngay lúc apply.

Sáu kịch bản, sáu nguyên nhân khác nhau, nhưng chỉ dùng đúng ba lệnh: kubectl describe svc, kubectl describe pod/deployment, và kubectl logs --previous. Điều quyết định không phải là biết nhiều lệnh, mà là đọc đúng dòng trong output.


4. Troubleshooting Control Plane Failures

4.1. Tổng quan

Bạn scale một Deployment từ 1 lên 3 replica. kubectl scale báo thành công. Nhưng năm phút sau, kubectl get pods vẫn chỉ có đúng 1 Pod — và không một event nào được ghi ra.

Không có lỗi. Không có cảnh báo. Chỉ là không có gì xảy ra cả. Đó là chữ ký của một control plane hỏng.

💡 Hình dung: Control plane giống bộ não — não khoẻ thì toàn bộ cơ thể phối hợp được; não ngừng ra lệnh thì tay chân vẫn còn nguyên, vẫn khoẻ, nhưng không cử động thêm được gì mới. Các Pod đang chạy vẫn chạy bình thường, chỉ là cluster không nhận thêm lệnh nào nữa.

Trước khi đi vào quy trình, cần ba thuật ngữ.

  • Manifest là file YAML mô tả một Kubernetes object. Ở tầng control plane, nó là file mô tả chính các component như kube-apiserver hay kube-scheduler.
  • Static pod là Pod do kubelet tự quản lý trực tiếp từ một file trên đĩa, không qua API server. Kubelet quét thư mục cấu hình theo chu kỳ và thêm/xoá Pod tương ứng với việc file xuất hiện hay biến mất. Với cluster dựng bằng kubeadm, toàn bộ control plane chạy dưới dạng static pod, manifest nằm ở /etc/kubernetes/manifests/ (đường dẫn này đổi được qua tuỳ chọn staticPodPath của kubelet).
  • kubeconfig là file chứa địa chỉ API server cùng chứng chỉ để xác thực. Mỗi component control plane có file kubeconfig riêng với danh tính riêng, đặt trong /etc/kubernetes/ — ví dụ controller-manager.confscheduler.conf.

Hệ quả thực tế của "static pod": sửa file manifest là kubelet tự restart Pod tương ứng, không cần kubectl apply, cũng không cần systemctl restart. Đổi lại, thay đổi không có hiệu lực tức thì — kubelet chỉ phát hiện ra ở lần quét kế tiếp, nên hãy chờ vài giây rồi mới kiểm tra lại. Và nếu file YAML bị sai cú pháp, kubelet sẽ không tạo được Pod nào cả: Pod đơn giản là biến mất khỏi kubectl get pods -n kube-system mà không có event nào giải thích. Lúc đó lý do nằm trong journalctl -u kubelet.

4.2. Quy trình 3 bước

Control Plane troubleshooting: cay quyet dinhCluster khong phan hoi lenh moihoac Pod ket o Pending.Buoc 1: kubectl get nodesco tra ve ket qua khong?KhongCokubectl khong ket noi duockube-apiserver dang chet.Dung crictl ngay tren node:crictl ps -aBuoc 2: kubectl get pods -n kube-systemComponent nao khong Running?kubeadm: chay dang static pod.Cai native: systemctl status ...Buoc 3: doc log cua component dokubectl logs -n kube-system PODhoac journalctl -u kube-apiserverhoac crictl logs CONTAINER_IDBuoc 4: sua static pod manifest/etc/kubernetes/manifests/<component>.yamlkubelet tu restart pod, khong can apply.

Bước 1: Kiểm tra trạng thái các node.

kubectl get nodes

# Output minh họa
# NAME STATUS ROLES AGE VERSION
# controlplane Ready control-plane 9d v1.37.0
# node01 Ready <none> 9d v1.37.0

(Output minh họa — cấu trúc và tên field đúng theo lệnh thật, số liệu cụ thể chỉ mang tính ví dụ.)

Cả hai node đều Ready, và quan trọng hơn: lệnh trả về được kết quả. Điều đó chứng minh kube-apiserver còn sống và etcd còn đọc được — hai thành phần khó cứu nhất đã được loại khỏi danh sách nghi phạm. Ngược lại, nếu lệnh trả về The connection to the server ... was refused, thì kubectl đã vô dụng và bạn phải chuyển sang mục 4.3.

Bước 2: Kiểm tra các Pod trong namespace kube-system.

Với cluster dựng bằng kubeadm, control plane chạy dưới dạng static pod nên xem được bằng chính kubectl.

kubectl get pods -n kube-system

# Output minh họa (rút gọn)
# NAME READY STATUS RESTARTS AGE
# etcd-controlplane 1/1 Running 0 9d
# kube-apiserver-controlplane 1/1 Running 0 9d
# kube-controller-manager-controlplane 1/1 Running 0 9d
# kube-scheduler-controlplane 0/1 CrashLoopBackOff 7 (35s ago) 6m

(Output minh họa — cấu trúc và tên field đúng theo lệnh thật, số liệu cụ thể chỉ mang tính ví dụ.)

Nghi phạm lộ diện ngay: kube-scheduler-controlplaneCrashLoopBackOff trong khi ba component còn lại đều Running. Đồng thời để ý quy ước đặt tên — static pod luôn mang hậu tố là tên node (-controlplane), nên kubectl logs kube-scheduler sẽ báo không tìm thấy; phải gõ đủ tên.

Nếu cluster được cài theo kiểu native (mỗi component là một systemd service thay vì static pod), thay bước này bằng:

systemctl status kube-apiserver
systemctl status kube-controller-manager
systemctl status kube-scheduler

Bước 3: Đọc log của component đang hỏng.

kubectl logs kube-scheduler-controlplane -n kube-system --previous

Tương ứng với cluster cài native:

journalctl -u kube-apiserver
journalctl -u kube-controller-manager
journalctl -u kube-scheduler

Trên node Linux dùng systemd, kubelet và container runtime ghi log vào journald theo mặc định, nên journalctl -u kubelet là nơi tra khi bản thân kubelet mới là thứ có vấn đề.

4.3. Khi kubectl cũng không dùng được

Có một lỗ hổng trong quy trình trên: cả ba bước đều dựa vào kubectl, mà kubectl lại nói chuyện qua kube-apiserver. Nếu chính API server là thứ đang chết, mọi lệnh đều trả về cùng một câu:

kubectl get pods -n kube-system

# Output minh họa
# The connection to the server 192.168.1.10:6443 was refused - did you specify the right host or port?

(Output minh họa — cấu trúc và tên field đúng theo lệnh thật, số liệu cụ thể chỉ mang tính ví dụ.)

💡 Hình dung: Gọi vào tổng đài để báo rằng tổng đài đang hỏng. Không ai nhấc máy, vì thứ bạn cần báo chính là thứ đáng lẽ phải nhấc máy. Muốn sửa thì phải đi thẳng tới phòng máy.

"Phòng máy" ở đây là container runtime trên chính control plane node. SSH vào node đó và hỏi trực tiếp runtime bằng crictl — công cụ dòng lệnh nói chuyện với mọi runtime tương thích CRI (containerd, CRI-O).

crictl ps -a | grep kube-apiserver

# Output minh họa
# CONTAINER IMAGE CREATED STATE NAME ATTEMPT POD ID
# 8f2b1c4a9d3e a1b2c3d4e5f6 2 minutes ago Exited kube-apiserver 9 7c6d5e4f3a2b

(Output minh họa — cấu trúc và tên field đúng theo lệnh thật, số liệu cụ thể chỉ mang tính ví dụ.)

Cờ -a rất quan trọng: không có nó, crictl ps chỉ liệt kê container đang chạy — và container đang cần tìm thì vừa chết. STATE: Exited cùng ATTEMPT: 9 xác nhận nó đã khởi động lại 9 lần không thành công. Lấy CONTAINER id rồi đọc log:

crictl logs 8f2b1c4a9d3e

Một lưu ý khi đọc tài liệu cũ: trước Kubernetes v1.24, nhiều hướng dẫn debug control plane dùng docker psdocker logs. Dockershim đã bị gỡ khỏi kubelet từ v1.24, nên trên cluster hiện đại các lệnh docker không còn nhìn thấy container của Kubernetes nữa — hãy đổi sang crictl pscrictl logs.

Rút ra một nguyên tắc: còn kubectl thì dùng kubectl; mất kubectl thì xuống một tầng và dùng crictl; mất cả crictl thì lên một tầng nữa và đọc journalctl -u kubelet. Luôn có một tầng thấp hơn để hỏi.


5. Thực hành: 3 kịch bản Control Plane Failures

5.1. Scenario 1 — kube-scheduler CrashLoopBackOff

Vấn đề: Deployment không scale được, Pod mới đứng yên ở trạng thái Pending.

Pending nghĩa là Pod đã tồn tại trong etcd nhưng chưa được gán vào node nào. Ai là người gán Pod vào node? kube-scheduler. Nghi phạm đã có tên ngay từ triệu chứng.

Quá trình troubleshooting:

kubectl get pods

# Output minh họa
# NAME READY STATUS RESTARTS AGE
# app-5f6b7c8d9e-kx2ml 0/1 Pending 0 4m

(Output minh họa — cấu trúc và tên field đúng theo lệnh thật, số liệu cụ thể chỉ mang tính ví dụ.)

kubectl describe pod app-5f6b7c8d9e-kx2ml

# Output minh họa (rút gọn)
# Node: <none>
# Status: Pending
# Events: <none>

(Output minh họa — cấu trúc và tên field đúng theo lệnh thật, số liệu cụ thể chỉ mang tính ví dụ.)

Đây là dấu hiệu quyết định, và nó nằm ở chỗ không có gì. Events: <none> nghĩa là chưa một component nào từng chạm vào Pod này. So sánh: nếu scheduler còn sống nhưng không tìm được node phù hợp, nó sẽ ghi một event FailedScheduling kèm lý do. Hoàn toàn im lặng nghĩa là scheduler không hề chạy.

kubectl get pods -n kube-system

# Output minh họa (rút gọn)
# NAME READY STATUS RESTARTS AGE
# kube-scheduler-controlplane 0/1 CrashLoopBackOff 7 (35s ago) 6m

(Output minh họa — cấu trúc và tên field đúng theo lệnh thật, số liệu cụ thể chỉ mang tính ví dụ.)

kubectl describe pod kube-scheduler-controlplane -n kube-system

# Output minh họa (rút gọn)
# Events:
# Type Reason Age From Message
# ---- ------ ---- ---- -------
# Warning Failed 5m (x6 over 6m) kubelet Error: failed to create containerd task:
# failed to create shim task: OCI runtime create failed: runc create failed:
# unable to start container process: exec: "kube-schedulerr": executable file not found in $PATH

(Output minh họa — cấu trúc và tên field đúng theo lệnh thật, số liệu cụ thể chỉ mang tính ví dụ.)

Chuỗi exec: "kube-schedulerr": executable file not found in $PATH là câu trả lời đầy đủ. Không phải lỗi quyền, không phải lỗi mạng, không phải image hỏng: binary được gọi với tên kube-schedulerr (thừa một chữ r) và trong image không có file nào tên như vậy. Container chết trước cả khi kịp in ra dòng log đầu tiên — nên kubectl logs ở đây sẽ rỗng, và kubectl describe mới là lệnh đúng.

Nguyên nhân gốc: command trong static pod manifest /etc/kubernetes/manifests/kube-scheduler.yaml bị gõ sai.

Giải pháp:

vi /etc/kubernetes/manifests/kube-scheduler.yaml
# Sua dong command tu kube-schedulerr thanh kube-scheduler

Không cần kubectl apply, cũng không cần restart kubelet. Lưu file là xong — kubelet sẽ phát hiện thay đổi ở lần quét thư mục kế tiếp và dựng lại Pod. Chờ vài giây rồi kiểm tra lại bằng kubectl get pods -n kube-system.

5.2. Scenario 2 — kube-controller-manager sai đường dẫn kubeconfig

Vấn đề: Deployment không scale từ 1 lên 2 replica, và lần này không có Pod nào ở Pending cả — đơn giản là ReplicaSet không tạo thêm Pod nào.

Khác biệt so với kịch bản 5.1 rất đáng chú ý. Ở đó Pod được tạo ra nhưng không ai xếp chỗ (scheduler chết). Ở đây Pod thậm chí không được tạo ra — nghĩa là thứ đang chết nằm sớm hơn trong chuỗi: kube-controller-manager, component chịu trách nhiệm đưa số lượng Pod thực tế về đúng số replica mong muốn.

Quá trình troubleshooting:

kubectl get pods -n kube-system

# Output minh họa (rút gọn)
# NAME READY STATUS RESTARTS AGE
# kube-controller-manager-controlplane 0/1 CrashLoopBackOff 5 (18s ago) 3m

(Output minh họa — cấu trúc và tên field đúng theo lệnh thật, số liệu cụ thể chỉ mang tính ví dụ.)

Lần này container khởi động được rồi mới chết, nên kubectl logs sẽ có nội dung. Dùng --previous để đọc lần chạy vừa chết thay vì lần đang khởi động dở:

kubectl logs kube-controller-manager-controlplane -n kube-system --previous

# Output minh họa
# I0923 03:41:07.882134 1 serving.go:348] Generated self-signed cert in-memory
# F0923 03:41:07.884901 1 controllermanager.go:240] error building controller context:
# failed to build kubeconfig: stat /etc/kubernetes/controller-manager-XXXX.conf:
# no such file or directory

(Output minh họa — cấu trúc và tên field đúng theo lệnh thật, số liệu cụ thể chỉ mang tính ví dụ.)

Ký tự F đầu dòng là mức log Fatal — component tự thoát ngay sau dòng này, nên đây luôn là dòng cần đọc đầu tiên trong log của control plane. Nội dung thì rất cụ thể: nó tìm file /etc/kubernetes/controller-manager-XXXX.conf và không thấy. Đối chiếu với thực tế trên đĩa:

ls /etc/kubernetes/

# Output minh họa
# admin.conf controller-manager.conf kubelet.conf manifests pki scheduler.conf super-admin.conf

(Output minh họa — cấu trúc và tên field đúng theo lệnh thật, số liệu cụ thể chỉ mang tính ví dụ.)

File thật tên là controller-manager.conf, không có hậu tố -XXXX. Đây chính là file kubeconfig mà kubeadm sinh ra để controller-manager tự xác thực với API server, và cờ --kubeconfig trong manifest đang trỏ sai.

Nguyên nhân gốc: giá trị của cờ --kubeconfig trong /etc/kubernetes/manifests/kube-controller-manager.yaml trỏ tới một file không tồn tại.

Giải pháp:

vi /etc/kubernetes/manifests/kube-controller-manager.yaml
# Sua --kubeconfig=/etc/kubernetes/controller-manager-XXXX.conf
# thanh --kubeconfig=/etc/kubernetes/controller-manager.conf

Mẹo áp dụng cho mọi lỗi kiểu này: đừng gõ lại đường dẫn từ trí nhớ. Chạy ls trong thư mục tương ứng rồi copy đúng tên file ra — sai một ký tự là component lại crash và bạn mất thêm một vòng chẩn đoán.

5.3. Scenario 3 — kube-controller-manager không load được client CA file

Vấn đề: Deployment không scale từ 2 lên 3 replica. Triệu chứng giống hệt kịch bản 5.2, nhưng nguyên nhân nằm ở một tầng khác.

Quá trình troubleshooting:

kubectl logs kube-controller-manager-controlplane -n kube-system --previous

# Output minh họa
# F0923 04:02:55.117402 1 controllermanager.go:240] error building controller context:
# unable to load client CA file "/etc/kubernetes/pki/ca.crt":
# open /etc/kubernetes/pki/ca.crt: no such file or directory

(Output minh họa — cấu trúc và tên field đúng theo lệnh thật, số liệu cụ thể chỉ mang tính ví dụ.)

Cờ --client-ca-file chỉ tới chứng chỉ CA dùng để xác thực client gửi request tới component. Thông báo nói file /etc/kubernetes/pki/ca.crt không tồn tại — nhưng kiểm tra trên node thì nó nằm đúng chỗ:

ls /etc/kubernetes/pki/ca.crt

# Output minh họa
# /etc/kubernetes/pki/ca.crt

(Output minh họa — cấu trúc và tên field đúng theo lệnh thật, số liệu cụ thể chỉ mang tính ví dụ.)

File có trên node, nhưng component lại không thấy. Mâu thuẫn này chỉ có một cách giải thích: component chạy bên trong container, và nó chỉ nhìn thấy những gì được mount vào. Đường dẫn sai không nằm ở cờ, mà nằm ở volume.

grep -A 4 "k8s-certs" /etc/kubernetes/manifests/kube-controller-manager.yaml

# Output minh họa
# - hostPath:
# path: /etc/kubernetes/WRONG-PKI-DIR
# type: DirectoryOrCreate
# name: k8s-certs

(Output minh họa — cấu trúc và tên field đúng theo lệnh thật, số liệu cụ thể chỉ mang tính ví dụ.)

Volume k8s-certs đang mount thư mục /etc/kubernetes/WRONG-PKI-DIR của node vào vị trí /etc/kubernetes/pki bên trong container. Thư mục đó rỗng — và vì type: DirectoryOrCreate, kubelet còn lặng lẽ tạo mới nó thay vì báo lỗi. Kết quả: container nhìn vào /etc/kubernetes/pki và thấy một thư mục trống rỗng.

Nguyên nhân gốc: hostPath.path của volume k8s-certs trong /etc/kubernetes/manifests/kube-controller-manager.yaml trỏ sai thư mục.

Giải pháp:

vi /etc/kubernetes/manifests/kube-controller-manager.yaml
# Trong muc volumes, tim volume ten k8s-certs
# Sua hostPath.path tu /etc/kubernetes/WRONG-PKI-DIR thanh /etc/kubernetes/pki

Ba kịch bản, ba tầng hỏng khác nhau: sai binary (5.1), sai đường dẫn file cấu hình (5.2), sai volume mount (5.3). Cả ba đều sửa trong cùng một thư mục /etc/kubernetes/manifests/, và cả ba đều tự hồi phục sau khi lưu file. Vì vậy thói quen đáng giá nhất khi động vào control plane là cp file manifest ra một chỗ an toàn trước khi sửa — còn bản gốc thì mọi sai lầm đều lùi lại được trong vài giây.


6. Troubleshooting Worker Node Failures

6.1. Triệu chứng: một node lặng lẽ rời khỏi cluster

Bắt đầu bằng một buổi sáng rất thật. Monitoring báo một nửa số pod của application vừa restart, latency tăng gấp đôi, nhưng deployment không có ai đụng vào từ hôm qua. Bạn gõ lệnh đầu tiên:

kubectl get nodes

# Output minh họa
# NAME STATUS ROLES AGE VERSION
# controlplane Ready control-plane 12d v1.37.0
# node01 NotReady <none> 12d v1.37.0
# node02 Ready <none> 12d v1.37.0

(Output minh họa — cấu trúc và tên cột đúng theo lệnh thật, số liệu cụ thể chỉ mang tính ví dụ.)

Cột STATUS của node01NotReady. Đây là trạng thái cần hiểu đúng trước khi làm bất cứ điều gì khác.

Trên mỗi worker node có một agent tên kubelet — đây là thành phần duy nhất của Kubernetes chạy trực tiếp trên node như một service của hệ điều hành, chịu trách nhiệm khởi động container và báo cáo tình hình về control plane. Cứ vài giây, kubelet gửi một heartbeat (tín hiệu "tôi còn sống và khoẻ") lên kube-apiserver. Khi control plane không nhận được heartbeat khoẻ mạnh nữa trong khoảng node-monitor-grace-period (mặc định 50 giây), nó đánh dấu node là NotReady.

Điểm mấu chốt: NotReady không nói cho bạn biết cái gì hỏng. Nó chỉ nói rằng control plane đã mất liên lạc với kubelet trên node đó. Máy có thể vẫn đang chạy ngon lành, mạng vẫn thông, chỉ là kubelet chết. Hoặc ngược lại — kubelet chạy tốt nhưng không gọi được tới API server. Công việc của bạn là thu hẹp dần khoảng cách giữa hai khả năng đó.

Và chiếc đồng hồ bắt đầu chạy ngay lúc node chuyển sang NotReady: node controller sẽ gắn taint node.kubernetes.io/not-ready lên node, và sau một khoảng ân hạn, pod trên node bị evict để lên lại ở nơi khác. Đó chính là lý do application restart hàng loạt trong khi không ai deploy gì.

💡 Hình dung: kubelet giống người gác đêm gọi điện về trung tâm mỗi phút một lần để báo "khu vực an toàn". Trung tâm không nhìn thấy khu vực đó — nó chỉ nghe điện thoại. Khi cuộc gọi ngừng, trung tâm chỉ biết đúng một điều: điện thoại im. Người gác ngủ quên, máy hết pin, hay sóng chập chờn — muốn biết thì phải cử người tới tận nơi.

6.2. Node conditions — bảng đồng hồ có sẵn trên mỗi node

Trước khi chạy tới tận node, hãy tận dụng thứ mà node đã tự khai báo. Mỗi Node object mang một tập conditions — các chỉ báo sức khoẻ mà kubelet cập nhật liên tục, mỗi cái là một câu trả lời True / False / Unknown.

ConditionÝ nghĩa khi status: "True"
ReadyNode khoẻ và sẵn sàng nhận pod. False là node không khoẻ; Unknown là control plane đã mất liên lạc với kubelet.
MemoryPressureNode sắp cạn memory.
DiskPressureDung lượng disk còn lại xuống thấp.
PIDPressureQuá nhiều process đang chạy trên node.
NetworkUnavailableNetwork của node chưa được cấu hình đúng — thường gặp khi CNI plugin chưa cài xong.

Một lưu ý nhỏ nhưng hay làm người đọc tài liệu cũ bối rối: condition OutOfDisk xuất hiện trong rất nhiều bài viết và slide đời trước, nhưng nó đã bị gỡ khỏi Kubernetes từ v1.13 và được thay hẳn bằng DiskPressure. Nếu thấy OutOfDisk trong tài liệu nào, hãy đọc nó như DiskPressure.

Phân biệt FalseUnknown của condition Ready là bước phân luồng quan trọng nhất trong cả phần này:

  • Ready: "False" nghĩa là kubelet vẫn đang báo cáo, và nó tự nói rằng mình chưa sẵn sàng. Nguyên nhân thường nằm ở tầng bên dưới kubelet — container runtime chưa lên, CNI plugin chưa cấu hình xong.
  • Ready: "Unknown" nghĩa là kubelet đã ngừng báo cáo. Node có thể đã tắt, mạng đứt, hoặc kubelet chết hẳn.

6.3. Quy trình 4 bước

Có một trật tự cố định nên theo, và lý do của trật tự đó rất đơn giản: mỗi bước đều rẻ hơn bước sau nó. Đọc trạng thái từ control plane không tốn gì cả; SSH vào node thì mất thời gian; đọc log thì mất nhiều thời gian hơn nữa. Chỉ đi sâu thêm một tầng khi tầng hiện tại đã hết thông tin.

Worker Node Troubleshooting: Decision TreeNode STATUS = NotReadyPods stop being scheduled herekubectl describe node NODEConditions + LastHeartbeatTimeMemoryPressure / DiskPressure/ PIDPressure = True?yesFree disk / memoryon the nodenoSSH to node: systemctl status kubeletRead the Active: lineActive: inactive (dead)?yessystemctl start kubeletnoActive: activating (auto-restart)?yesjournalctl -u kubeletnokubelet active, node still NotReadyCheck kubelet.conf server addressEach no goes one layer deeper: cluster, node, kubelet, config

Bước 1 — Nhìn toàn cảnh từ control plane

kubectl get nodes

Lệnh này trả lời đúng một câu hỏi: có bao nhiêu node, và node nào đang NotReady. Nếu tất cả node cùng NotReady một lúc, vấn đề gần như chắc chắn không nằm ở các node — hãy quay sang kiểm tra control plane trước khi SSH đi đâu cả.

Bước 2 — Đọc chi tiết node

kubectl describe node node01

# Output minh họa (rút gọn)
# Name: node01
# Taints: node.kubernetes.io/unreachable:NoSchedule
# Conditions:
# Type Status LastHeartbeatTime Reason
# MemoryPressure Unknown Fri, 19 Sep 2026 09:12:04 +0700 NodeStatusUnknown
# DiskPressure Unknown Fri, 19 Sep 2026 09:12:04 +0700 NodeStatusUnknown
# PIDPressure Unknown Fri, 19 Sep 2026 09:12:04 +0700 NodeStatusUnknown
# Ready Unknown Fri, 19 Sep 2026 09:12:04 +0700 NodeStatusUnknown
# Allocatable:
# cpu: 2
# memory: 4025556Ki

(Output minh họa — cấu trúc và tên field đúng theo lệnh thật, số liệu cụ thể chỉ mang tính ví dụ.)

Cả bốn condition đều là Unknown với Reason: NodeStatusUnknown. Đó là chữ ký rõ ràng của "kubelet ngừng gửi heartbeat" — không phải node hết disk, không phải node hết memory. Nếu thay vào đó bạn thấy DiskPressure True, câu chuyện hoàn toàn khác và bạn đã có ngay câu trả lời mà không cần SSH.

Bước 3 — LastHeartbeatTime cho biết node chết lúc nào

Cột LastHeartbeatTime là dấu thời gian của lần cuối kubelet còn nói chuyện được. So nó với đồng hồ hiện tại, bạn có ngay khoảng thời gian node đã im lặng — và quan trọng hơn, bạn có mốc thời gian để đối chiếu với log của hệ thống khác: lần deploy gần nhất, lần restart mạng, lần nâng cấp kernel.

kubectl get node node01 -o jsonpath='{.status.conditions[?(@.type=="Ready")].lastHeartbeatTime}'

# Output minh họa
# 2026-09-19T02:12:04Z

(Output minh họa — cấu trúc và tên field đúng theo lệnh thật, số liệu cụ thể chỉ mang tính ví dụ.)

Một mốc thời gian trùng khít với một sự kiện khác trên hệ thống thường rút ngắn cả buổi debug xuống vài phút.

Bước 4 — Lên tận node

Đến đây control plane đã hết thông tin để cho. Bạn cần vào thẳng máy:

ssh node01

# Node còn sống không, tài nguyên còn không?
top
df -h

# kubelet đang ở trạng thái nào?
systemctl status kubelet

# kubelet nói gì trước khi chết?
journalctl -u kubelet -n 50 --no-pager

Thứ tự này có lý do. topdf -h trả lời câu hỏi rẻ nhất trước — node có còn CPU, memory và disk không. Một node đầy disk sẽ làm kubelet chết theo những cách rất khó hiểu, và bạn sẽ mất hàng giờ đọc log kubelet nếu bỏ qua bước này.

6.4. Khi kubelet chạy nhưng container thì không: crictl

Đôi khi systemctl status kubelet xanh, node vẫn Ready, nhưng pod trên node kẹt ở ContainerCreating mãi không lên. Lúc đó vấn đề nằm ở tầng dưới kubelet: container runtime — chương trình thực sự chạy container trên node, thường là containerd hoặc CRI-O.

Công cụ để soi tầng này là crictl, CLI nói chuyện trực tiếp với container runtime qua chuẩn CRI (Container Runtime Interface):

# Liệt kê pod sandbox mà runtime đang giữ
crictl pods

# Liệt kê container, kể cả container đã exit
crictl ps -a

# Đọc log của một container cụ thể
crictl logs <container-id>

Một cái bẫy rất phổ biến trong tài liệu cũ: các bài hướng dẫn đời trước bảo bạn chạy docker psdocker logs trên node. Cách đó không còn dùng được — dockershim, lớp cầu nối giữa kubelet và Docker Engine, đã bị gỡ khỏi kubelet từ Kubernetes v1.24. Trên cluster hiện đại, docker ps hoặc là không có lệnh, hoặc trả về danh sách rỗng vì Docker không phải thứ đang chạy pod. Cứ thay bằng crictl là đúng.

Điều nên mang theo từ phần này: đừng bắt đầu bằng việc SSH. Ba lệnh đầu tiên đều chạy từ control plane và miễn phí, và rất nhiều sự cố kết thúc ngay ở bước 2 — chỉ cần một condition True là đủ để biết phải làm gì.


7. Thực hành: 3 kịch bản Worker Node Failures

Phần trên đã dựng khung quy trình. Giờ là lúc chạy nó trên ba sự cố có thật, cả ba đều làm node rơi vào NotReady — nhưng vì ba lý do hoàn toàn khác nhau, nằm ở ba mắt xích khác nhau trong chuỗi khởi động của kubelet.

Kubelet startup chain: three break pointssystemd unitkubelet.servicekubelet config/var/lib/kubelet/config.yamlkubeconfig/etc/kubernetes/kubelet.confAPI serverport 6443Scenario 1service never startedinactive (dead)Scenario 2clientCAFile points ata file that is missingScenario 3server port is 6553,not 6443All three end the same way: the node reports NotReady.

Hai file trong sơ đồ trên đáng được giới thiệu tử tế, vì cả phần thực hành xoay quanh chúng:

  • /var/lib/kubelet/config.yamlkubelet config file — file cấu hình hành vi của chính kubelet: nó xác thực client bằng CA nào, tìm static pod ở đâu, dùng DNS server nào cho pod.
  • /etc/kubernetes/kubelet.confkubeconfig của kubelet — file chứa địa chỉ API server cần gọi và chứng chỉ để tự xác thực với API server đó.

Nhầm lẫn hai file này là nguyên nhân số một khiến người mới sửa sai chỗ. Câu thần chú ngắn: config.yaml nói về bản thân kubelet, kubelet.conf nói về đường tới API server.

7.1. Thiết lập ban đầu

Trước khi bắt đầu, hai tiện ích nhỏ này tiết kiệm rất nhiều thời gian gõ phím — đặc biệt khi làm bài thi CKA, nơi mỗi giây đều đáng giá.

# Rút gọn kubectl thành k
alias k=kubectl

# Bật autocompletion cho kubectl
source <(kubectl completion bash)
echo 'source <(kubectl completion bash)' >> ~/.bashrc

Lưu ý một chi tiết: alias k=kubectl không tự động kéo theo autocompletion cho k. Muốn k cũng gợi ý được tên resource, cần khai báo thêm:

complete -o default -F __start_kubectl k

7.2. Scenario 1: kubelet service inactive

Triệu chứng: node01 ở trạng thái NotReady.

Đi theo quy trình. Trước tiên nhìn từ control plane:

kubectl get nodes

# Output minh họa
# NAME STATUS ROLES AGE VERSION
# controlplane Ready control-plane 12d v1.37.0
# node01 NotReady <none> 12d v1.37.0

(Output minh họa — cấu trúc và tên cột đúng theo lệnh thật, số liệu cụ thể chỉ mang tính ví dụ.)

kubectl describe node node01 | grep -A6 Conditions

# Output minh họa (rút gọn)
# Conditions:
# Type Status Reason Message
# MemoryPressure Unknown NodeStatusUnknown Kubelet stopped posting node status.
# DiskPressure Unknown NodeStatusUnknown Kubelet stopped posting node status.
# PIDPressure Unknown NodeStatusUnknown Kubelet stopped posting node status.
# Ready Unknown NodeStatusUnknown Kubelet stopped posting node status.

(Output minh họa — cấu trúc và tên field đúng theo lệnh thật, số liệu cụ thể chỉ mang tính ví dụ.)

Dòng Message nói thẳng ra câu trả lời: Kubelet stopped posting node status. Không có condition nào là True, nên đây không phải chuyện tài nguyên. Đến lúc SSH.

ssh node01
systemctl status kubelet

# Output minh họa
# ● kubelet.service - kubelet: The Kubernetes Node Agent
# Loaded: loaded (/usr/lib/systemd/system/kubelet.service; enabled; preset: enabled)
# Drop-In: /usr/lib/systemd/system/kubelet.service.d
# └─10-kubeadm.conf
# Active: inactive (dead) since Fri 2026-09-19 09:12:01 +07; 41min ago
# Docs: https://kubernetes.io/docs/
# Process: 2143 ExecStart=/usr/bin/kubelet $KUBELET_KUBEACONFIG_ARGS ... (code=exited, status=0/SUCCESS)

(Output minh họa — cấu trúc và tên field đúng theo lệnh thật, số liệu cụ thể chỉ mang tính ví dụ.)

Dòng cần đọc là dòng Active: — ở đây là inactive (dead). Đây là trạng thái đơn giản nhất trong ba kịch bản: service không chạy, và cũng không cố chạy. Không có lỗi, không có crash loop — đơn giản là ai đó (hoặc script nào đó) đã stop nó.

Cách xử lý:

systemctl start kubelet
systemctl status kubelet

# Output minh họa
# Active: active (running) since Fri 2026-09-19 09:53:40 +07; 3s ago
# Main PID: 4821 (kubelet)

(Output minh họa — cấu trúc và tên field đúng theo lệnh thật, số liệu cụ thể chỉ mang tính ví dụ.)

Quay lại control plane và xác nhận. Node không chuyển sang Ready ngay lập tức — nó cần vài chục giây để gửi heartbeat và để node controller ghi nhận:

exit
kubectl get nodes

# Output minh họa
# NAME STATUS ROLES AGE VERSION
# controlplane Ready control-plane 12d v1.37.0
# node01 Ready <none> 12d v1.37.0

(Output minh họa — cấu trúc và tên cột đúng theo lệnh thật, số liệu cụ thể chỉ mang tính ví dụ.)

Một việc nên làm thêm mà nhiều người quên: systemctl enable kubelet. Nếu service không được enable, node sẽ lại NotReady ngay lần reboot kế tiếp, và bạn sẽ debug đúng sự cố này lần thứ hai.

7.3. Scenario 2: kubelet không start được

Triệu chứng: kubelet liên tục exit với exit code 255, node vẫn NotReady.

ssh node01
systemctl status kubelet

# Output minh họa
# ● kubelet.service - kubelet: The Kubernetes Node Agent
# Active: activating (auto-restart) (Result: exit-code) since Fri 2026-09-19 10:04:12 +07; 6s ago
# Process: 5310 ExecStart=/usr/bin/kubelet ... (code=exited, status=255/EXCEPTION)
# Main PID: 5310 (code=exited, status=255/EXCEPTION)

(Output minh họa — cấu trúc và tên field đúng theo lệnh thật, số liệu cụ thể chỉ mang tính ví dụ.)

Lần này dòng Active:activating (auto-restart), kèm status=255/EXCEPTION. Khác hẳn scenario 1: ở đây kubelet đang cố chạy và chết ngay mỗi lần, rồi systemd lại khởi động nó lên. Vòng lặp này nghĩa là có một lỗi cụ thể lúc khởi động — và lỗi đó nằm trong log.

journalctl -u kubelet -n 20 --no-pager

# Output minh họa (rút gọn)
# Sep 19 10:04:12 node01 kubelet[5310]: E0919 10:04:12.884 server.go:274] "Failed to run kubelet"
# err="failed to run Kubelet: unable to load client CA file /etc/kubernetes/pki/WRONG-CA-FILE:
# open /etc/kubernetes/pki/WRONG-CA-FILE: no such file or directory"
# Sep 19 10:04:12 node01 systemd[1]: kubelet.service: Main process exited, code=exited, status=255/EXCEPTION

(Output minh họa — cấu trúc và tên field đúng theo lệnh thật, đường dẫn và số liệu cụ thể chỉ mang tính ví dụ.)

Log chỉ đích danh: unable to load client CA file. kubelet mở một API nhỏ của riêng nó trên port 10250 để kube-apiserver gọi vào (đây là đường đi của kubectl logskubectl exec). Để biết ai được phép gọi, kubelet cần một clientCAFile — đường dẫn tới certificate của Certificate Authority đã ký chứng chỉ cho các client hợp lệ. Không mở được file đó, kubelet từ chối khởi động hẳn, vì chạy tiếp đồng nghĩa với mở một endpoint không kiểm soát được ai gọi.

Mở file cấu hình ra xem:

cat /var/lib/kubelet/config.yaml

# Output minh họa (rút gọn)
# apiVersion: kubelet.config.k8s.io/v1beta1
# kind: KubeletConfiguration
# authentication:
# x509:
# clientCAFile: /etc/kubernetes/pki/WRONG-CA-FILE
# staticPodPath: /etc/kubernetes/manifests
# clusterDNS:
# - 10.96.0.10
# clusterDomain: cluster.local

(Output minh họa — cấu trúc và tên field đúng theo lệnh thật, đường dẫn cụ thể chỉ mang tính ví dụ.)

Và đối chiếu với những gì thực sự có trên đĩa:

ls /etc/kubernetes/pki/

# Output minh họa
# ca.crt

(Output minh họa — cấu trúc và tên file đúng theo lệnh thật, danh sách cụ thể chỉ mang tính ví dụ.)

Không có file nào tên WRONG-CA-FILE. File thật là ca.crt — đây là CA certificate gốc mà kubeadm sinh ra khi khởi tạo cluster.

Nhân tiện, để ý field staticPodPath trong output ở trên. Static pod là pod do kubelet tự đọc từ file YAML trong thư mục đó và tự chạy, không qua scheduler và không cần API server. Đây chính là cơ chế giữ cho các thành phần control plane sống được trước khi có control plane. Trên worker node, thư mục này thường rỗng — nhưng biết nó tồn tại sẽ có ích ở những phần khác của khoá.

Cách xử lý:

vi /var/lib/kubelet/config.yaml
# Sửa clientCAFile thành /etc/kubernetes/pki/ca.crt

systemctl restart kubelet
systemctl status kubelet

# Output minh họa
# Active: active (running) since Fri 2026-09-19 10:09:58 +07; 5s ago

(Output minh họa — cấu trúc và tên field đúng theo lệnh thật, số liệu cụ thể chỉ mang tính ví dụ.)

7.4. Scenario 3: sai port của API server

Triệu chứng: systemctl status kubelet báo active (running), nhưng node vẫn NotReady.

Đây là kịch bản gây bối rối nhất, vì mọi thứ ở tầng hệ điều hành đều xanh. kubelet chạy, không crash, không restart. Vậy vấn đề ở đâu?

Câu hỏi ngược lại chính là manh mối: nếu kubelet đang chạy mà control plane vẫn không nghe thấy gì, thì vấn đề không nằm ở việc kubelet sống hay chết, mà ở việc nó đang nói chuyện với ai.

ssh node01
journalctl -u kubelet -n 20 --no-pager

# Output minh họa (rút gọn)
# Sep 19 10:31:44 node01 kubelet[6042]: E0919 10:31:44.117 kubelet_node_status.go:96]
# "Unable to register node with API server" err="Post
# \"https://10.54.13.2:6553/api/v1/nodes\": dial tcp 10.54.13.2:6553: connect: connection refused"
# node="node01"
# Sep 19 10:31:45 node01 kubelet[6042]: E0919 10:31:45.882 reflector.go:158] Failed to watch *v1.Node:
# failed to list *v1.Node: Get "https://10.54.13.2:6553/api/v1/nodes?limit=500": dial tcp
# 10.54.13.2:6553: connect: connection refused

(Output minh họa — cấu trúc và tên field đúng theo lệnh thật, địa chỉ IP và port cụ thể chỉ mang tính ví dụ.)

connection refused trên port 6553. Đây là dấu hiệu rất đặc trưng: connection refused nghĩa là gói tin đã tới nơi nhưng không có ai lắng nghe ở port đó — khác hẳn connection timed out, thường là firewall chặn hoặc sai địa chỉ IP. Cổng chuẩn của kube-apiserver là 6443. Con số 6553 là lỗi đánh máy đảo hai chữ số.

Kiểm tra kubeconfig của kubelet:

grep server /etc/kubernetes/kubelet.conf

# Output minh họa
# server: https://10.54.13.2:6553

(Output minh họa — cấu trúc và tên field đúng theo lệnh thật, địa chỉ cụ thể chỉ mang tính ví dụ.)

Cách xử lý:

vi /etc/kubernetes/kubelet.conf
# Sửa port 6553 thành 6443

systemctl restart kubelet
systemctl status kubelet

Rồi quay lại control plane và chờ khoảng một phút để node gửi heartbeat trở lại.

💡 Hình dung: kubelet ở scenario 3 giống một nhân viên vẫn đi làm đúng giờ và vẫn gọi điện báo cáo đều đặn — chỉ có điều anh ta đang bấm nhầm một số máy nhánh không tồn tại. Máy vẫn quay, người vẫn nói, nhưng đầu bên kia không ai nhấc. Nhìn từ văn phòng trung tâm, nó không khác gì nhân viên nghỉ việc.

7.5. Tóm tắt quy trình

Ba kịch bản, ba mắt xích, nhưng cùng một đường đi:

  1. Kiểm tra trạng thái từ control plane trước bằng kubectl get nodeskubectl describe node.
  2. SSH vào đúng node đang có vấn đề, không SSH lung tung.
  3. Đọc dòng Active: của systemctl status kubelet — chính dòng này phân loại sự cố thành ba nhánh.
  4. Nếu dòng Active:active (running) mà node vẫn NotReady, đọc journalctl -u kubelet và tìm những gì kubelet đang cố kết nối tới.
Dòng Active:Sự cố thuộc loại nàoLệnh tiếp theo
inactive (dead)Service chưa được khởi động.systemctl start kubelet rồi systemctl enable kubelet.
activating (auto-restart)kubelet crash ngay lúc start, thường do file cấu hình.journalctl -u kubelet để đọc lỗi cụ thể.
active (running) nhưng node NotReadykubelet sống nhưng không gọi được API server.grep server /etc/kubernetes/kubelet.conf.

Điều nên mang theo từ phần này: một dòng text duy nhất — dòng Active: — quyết định bạn đi nhánh nào trong ba nhánh. Đọc kỹ nó trước khi gõ lệnh thứ hai.


8. Troubleshooting Networking

8.1. Tổng quan về Kubernetes Networking

Lần này triệu chứng khác hẳn. Node đều Ready, pod đều Running, restart count bằng 0 — nhưng application trả về lỗi "could not connect to database" đều đặn. Không có gì "đỏ" trên màn hình để chỉ tay vào.

Đây là loại sự cố khó nhất, vì nó không nằm ở một object nào cả mà nằm ở đường đi giữa các object. Muốn debug nó, trước hết cần biết đường đi đó gồm những chặng nào.

Kubernetes networking cho phép pod nói chuyện với nhau, với dịch vụ bên ngoài, và với người dùng cuối. Nguyên tắc nền tảng: mỗi pod nhận một IP riêng, và các pod nói chuyện trực tiếp với nhau bằng IP đó mà không cần NAT (Network Address Translation — kỹ thuật viết lại địa chỉ gói tin khi đi qua biên giới mạng).

Năm thành phần dưới đây là toàn bộ dàn diễn viên của phần này:

Thành phầnVai trò
PodĐơn vị chạy ứng dụng, mỗi pod có một IP riêng trong cluster.
ServiceMột tên và một IP ảo cố định đứng trước một nhóm pod hay thay đổi.
EndpointSliceDanh sách IP:port thật của những pod đang sẵn sàng phía sau một Service.
CoreDNSDNS server của cluster, dịch tên Service thành IP của Service.
CNI pluginChương trình cắm pod vào mạng — cấp IP, tạo network interface, dựng đường đi giữa các node.
kube-proxyAgent chạy trên mỗi node, biến IP ảo của Service thành IP thật của pod bằng rule trong kernel.

Hai cái tên trong bảng cần nói rõ hơn ngay, vì cả phần còn lại dựa vào chúng.

CNI là viết tắt của Container Network Interface — một chuẩn quy định cách container runtime gọi một chương trình bên ngoài để cắm container vào mạng. Kubernetes không tự làm networking cho pod; nó giao việc đó cho một CNI plugin cài thêm, như Flannel, Calico hay Cilium. Không có CNI plugin, pod không bao giờ có IP.

EndpointSlice là object ghi lại danh sách địa chỉ thật phía sau một Service. Ở tài liệu và cluster đời cũ, danh sách này nằm trong object tên Endpoints; API đó đã bị đánh dấu deprecated từ Kubernetes v1.33 và EndpointSlice là thứ thay thế. Bạn vẫn sẽ thấy chữ Endpoints: trong output của kubectl describe service — đó là kubectl hiển thị cho dễ đọc, dữ liệu bên dưới đã là EndpointSlice.

8.2. Đường đi của một request

Trước khi sửa bất cứ thứ gì, hãy nhìn toàn bộ chặng đường mà một lệnh curl http://web-svc phải đi qua. Mỗi mũi tên trong sơ đồ là một điểm có thể hỏng, và toàn bộ phần 8 chẳng qua là đi lần lượt từng mũi tên.

Service resolution path: from name to podClient Podcurl http://web-svc1. resolve name via DNSCoreDNSweb-svc.default.svc.cluster.localresolves to ClusterIP 10.96.0.102. packet sent to 10.96.0.10:80kube-proxy rules on the nodeiptables / nftables DNAT10.96.0.10:80 to 10.244.1.7:803. backend picked fromEndpointSliceready pod IPs behind the Service10.244.1.7:80, 10.244.2.4:804. routed by the CNI pluginTarget Pod10.244.1.7:80Every arrow is a failure point: wrong DNS, empty EndpointSlice,stale kube-proxy rules, or a CNI plugin that never came up.

Sơ đồ này cũng là thứ tự kiểm tra nên theo, nhưng đi ngược từ dưới lên: xác nhận pod đích thật sự phục vụ được trước, rồi mới leo dần lên Service, kube-proxy và DNS. Lý do rất thực tế — nếu pod đích vốn đã hỏng thì mọi tầng phía trên đều vô can, và bạn sẽ đỡ mất công đào bới nhầm chỗ.

8.3. Troubleshooting Service layer

Bước 1: Pod có thật sự đang chạy không?

kubectl get pods --selector=app=web

# Output minh họa
# NAME READY STATUS RESTARTS AGE
# web-6d4b9c7f8d-2xq4z 1/1 Running 0 3h
# web-6d4b9c7f8d-7hmkr 0/1 CrashLoopBackOff 7 (2m ago) 3h

(Output minh họa — cấu trúc và tên cột đúng theo lệnh thật, số liệu cụ thể chỉ mang tính ví dụ.)

Ba cột cần đọc, mỗi cột trả lời một câu hỏi khác nhau:

  • Cột READY dạng 1/1 nghĩa là toàn bộ container trong pod đã pass readiness probe. 0/1 nghĩa là pod đang chạy nhưng chưa sẵn sàng nhận traffic — và pod chưa sẵn sàng thì không được đưa vào EndpointSlice.
  • Cột STATUS nên là Running. CrashLoopBackOff nghĩa là container khởi động rồi chết, lặp đi lặp lại.
  • Cột RESTARTS nên là 0. Con số lớn dần là dấu hiệu vấn đề nằm bên trong pod, không phải ở tầng network.

Ở output trên, pod thứ hai 0/1 đã giải thích một nửa sự cố: Service chỉ còn một backend thay vì hai, nên khoảng một nửa số request thất bại — đúng kiểu lỗi chập chờn khó chịu nhất.

Bước 2: Kết nối thẳng vào pod IP

Bỏ qua Service, gọi thẳng vào pod. Nếu bước này chạy được, bạn vừa loại trừ được toàn bộ tầng ứng dụng và có thể tập trung vào network.

kubectl get pods -o custom-columns='NAME:.metadata.name,IP:.status.podIP'

# Output minh họa
# NAME IP
# web-6d4b9c7f8d-2xq4z 10.244.1.7
# web-6d4b9c7f8d-7hmkr 10.244.2.4

(Output minh họa — cấu trúc và tên cột đúng theo lệnh thật, địa chỉ cụ thể chỉ mang tính ví dụ.)

kubectl run tmp-curl --image=curlimages/curl:latest --rm -it --restart=Never -- curl -s -m 5 http://10.244.1.7:80

# Output minh họa
# <!DOCTYPE html>
# <html><head><title>Welcome to nginx!</title></head>
# pod "tmp-curl" deleted

(Output minh họa — cấu trúc đúng theo lệnh thật, nội dung cụ thể chỉ mang tính ví dụ.)

Có HTML trả về nghĩa là pod phục vụ traffic bình thường. Nếu bước này treo hoặc báo connection refused trong khi pod là Running, vấn đề nằm ở chính ứng dụng — nó không lắng nghe đúng port — hoặc ở CNI plugin, chứ không phải ở Service.

Bước 3: Service definition và EndpointSlice

kubectl describe service web-svc

# Output minh họa (rút gọn)
# Name: web-svc
# Selector: app=web-frontend
# Type: ClusterIP
# IP: 10.96.0.10
# Port: <unset> 80/TCP
# TargetPort: 8080/TCP
# Endpoints: <none>

(Output minh họa — cấu trúc và tên field đúng theo lệnh thật, địa chỉ cụ thể chỉ mang tính ví dụ.)

Dòng quyết định là Endpoints: <none>. Service tồn tại, có ClusterIP hẳn hoi, nhưng phía sau nó không có địa chỉ nào cả — mọi request tới nó đều rơi vào hư không.

Nguyên nhân nằm ngay ở dòng trên: Selector: app=web-frontend, trong khi pod thực tế mang label app=web. Lệch một chữ là đủ.

kubectl get endpointslices -l kubernetes.io/service-name=web-svc

# Output minh họa
# NAME ADDRESSTYPE PORTS ENDPOINTS AGE
# web-svc-4nx7q IPv4 <unset> <unset> 3h

(Output minh họa — cấu trúc và tên cột đúng theo lệnh thật, số liệu cụ thể chỉ mang tính ví dụ.)

Hai thứ cần đối chiếu mỗi khi Service không route được traffic:

  • selector của Service phải khớp chính xác label của pod đang khoẻ. So bằng kubectl get pods --show-labels.
  • targetPort của Service phải trùng port mà container thực sự lắng nghe, không phải port mà Service expose ra ngoài.

💡 Lưu ý: cột ENDPOINTS rỗng hoặc Endpoints: <none> luôn có nghĩa là selector của Service không tìm thấy pod nào đang ở trạng thái ready. Đây là lỗi network phổ biến nhất trong Kubernetes, và nó thậm chí không phải lỗi network.

8.4. Troubleshooting CoreDNS

Kiến trúc CoreDNS

CoreDNS là DNS server chạy bên trong cluster — nó là thứ biến web-svc thành 10.96.0.10. Nó trở thành DNS add-on mặc định của kubeadm từ Kubernetes v1.13, thay cho kube-dns trước đó. Ở Kubernetes v1.37, kubeadm triển khai CoreDNS phiên bản v1.14.6.

Cách nó ráp vào cluster gồm ba mảnh:

  • CoreDNS chạy như một Deployment trong namespace kube-system, thường 2 replica cho dự phòng.
  • Một Service tên kube-dns đứng trước các pod đó và expose port 53. Tên kube-dns được giữ lại vì lý do tương thích ngược, dù phần mềm bên dưới đã là CoreDNS.
  • File /etc/resolv.conf bên trong mỗi pod trỏ nameserver về ClusterIP của Service kube-dns. kubelet là thứ ghi file này, lấy giá trị từ field clusterDNS trong /var/lib/kubelet/config.yaml.

Cấu hình của CoreDNS nằm trong một file tên Corefile — file khai báo CoreDNS bật những plugin nào và xử lý mỗi loại query ra sao. Trong container nó nằm ở /etc/coredns/Corefile, và được mount vào từ ConfigMap coredns trong namespace kube-system. Nghĩa là muốn sửa hành vi DNS của cả cluster, bạn sửa đúng một ConfigMap.

Bước 1: CoreDNS pod có chạy không?

kubectl get pods -n kube-system -l k8s-app=kube-dns

# Output minh họa
# NAME READY STATUS RESTARTS AGE
# coredns-668d6bf9bc-9k2rt 0/1 Pending 0 14m
# coredns-668d6bf9bc-vd8xz 0/1 Pending 0 14m

(Output minh họa — cấu trúc và tên cột đúng theo lệnh thật, số liệu cụ thể chỉ mang tính ví dụ.)

Hai trạng thái này có ý nghĩa rất khác nhau:

  • Pending nghĩa là pod chưa được đặt lên node nào. Với CoreDNS, nguyên nhân gần như luôn là CNI plugin chưa được cài — không có mạng pod thì không pod nào chạy được. Đây là hành vi bình thường ngay sau kubeadm init và tự hết khi bạn cài network add-on.
  • CrashLoopBackOff nghĩa là container khởi động rồi chết liên tục. Lần này hãy đọc log, và nghi ngờ Corefile trước tiên.
kubectl logs -n kube-system -l k8s-app=kube-dns --tail=20

# Output minh họa (rút gọn)
# [INFO] plugin/reload: Running configuration SHA512 = 591cf328cccc12bc...
# [ERROR] plugin/errors: 2 web-svc.default.svc.cluster.local. A: read udp 10.244.0.5:41234->8.8.8.8:53: i/o timeout

(Output minh họa — cấu trúc và định dạng log đúng theo lệnh thật, nội dung cụ thể chỉ mang tính ví dụ.)

Bước 2: kube-dns có endpoint không?

kubectl get endpointslices -n kube-system -l kubernetes.io/service-name=kube-dns

# Output minh họa
# NAME ADDRESSTYPE PORTS ENDPOINTS AGE
# kube-dns-brz7t IPv4 53 10.244.0.5,10.244.0.6 12d

(Output minh họa — cấu trúc và tên cột đúng theo lệnh thật, địa chỉ cụ thể chỉ mang tính ví dụ.)

Cột ENDPOINTS phải liệt kê IP của các pod CoreDNS. Nếu rỗng, nguyên nhân giống hệt mục 8.3 — selector của Service không khớp label pod — chỉ khác là lần này nó làm hỏng DNS của toàn bộ cluster.

Bước 3: Pod đang hỏi DNS server nào?

kubectl exec -it web-6d4b9c7f8d-2xq4z -- cat /etc/resolv.conf

# Output minh họa
# search default.svc.cluster.local svc.cluster.local cluster.local
# nameserver 10.96.0.10
# options ndots:5

(Output minh họa — cấu trúc và tên field đúng theo lệnh thật, địa chỉ cụ thể chỉ mang tính ví dụ.)

Hai dòng đáng để hiểu kỹ:

  • nameserver phải bằng đúng ClusterIP của Service kube-dns. Đối chiếu bằng kubectl get svc kube-dns -n kube-system. Lệch một chữ số ở đây là DNS trong pod chết hẳn.
  • options ndots:5 nghĩa là: tên nào có ít hơn 5 dấu chấm sẽ được thử ghép lần lượt với từng hậu tố trong dòng search trước khi được hỏi như tên tuyệt đối. Nhờ nó mà curl http://web-svc hoạt động được. Cũng chính nó là lý do một tên ngoài internet như api.stripe.com phải đi qua 3-4 lần hỏi hụt trước khi ra kết quả đúng.

Bước 4: Thử resolve thật

kubectl run dnsutils --image=registry.k8s.io/e2e-test-images/agnhost:2.39 --rm -it --restart=Never -- nslookup kubernetes.default

# Output minh họa (thành công)
# Server: 10.96.0.10
# Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local
# Name: kubernetes.default
# Address 1: 10.96.0.1 kubernetes.default.svc.cluster.local

(Output minh họa — cấu trúc và tên field đúng theo lệnh thật, địa chỉ cụ thể chỉ mang tính ví dụ.)

Và đây là hình dáng của một lần thất bại:

kubectl exec -it dnsutils -- nslookup web-svc.default.svc.cluster.local

# Output minh họa (thất bại)
# ;; connection timed out; no servers could be reached
# command terminated with exit code 1

(Output minh họa — cấu trúc thông báo đúng theo lệnh thật, nội dung cụ thể chỉ mang tính ví dụ.)

Phân biệt hai kiểu thất bại sẽ tiết kiệm cho bạn rất nhiều thời gian:

  • connection timed out; no servers could be reached nghĩa là pod không chạm tới được DNS server. Nghi ngờ theo thứ tự: CoreDNS pod không chạy, kube-dns không có endpoint, hoặc CNI plugin hỏng.
  • NXDOMAIN (server can't find ...) nghĩa là DNS server trả lời được nhưng không có record đó. Lúc này DNS khoẻ — chỉ là Service bạn hỏi không tồn tại, hoặc bạn gõ sai namespace trong tên.

8.5. Troubleshooting CNI plugins

CNI plugin làm gì

Mỗi khi một pod được tạo, container runtime gọi CNI plugin để làm ba việc: xin một IP từ dải IP của node, tạo network interface bên trong pod, và dựng đường đi để gói tin từ pod này tới được pod trên node khác. Không có bước này, pod sẽ kẹt vĩnh viễn ở ContainerCreating.

Hai plugin hay gặp nhất:

  • Flannel đơn giản và dễ cài, dựng một overlay network phủ lên mạng vật lý. Điểm cần biết: bản mặc định không enforce NetworkPolicy — object vẫn tạo được nhưng không chặn traffic thật.
  • Calico mạnh về network policy, chạy được cả có lẫn không có overlay.

File cấu hình nằm ở đâu

Hai thư mục cần nhớ, và chúng nằm trên node chứ không phải trong cluster:

  • /etc/cni/net.d/ chứa file cấu hình CNI dạng JSON — khai báo dùng plugin nào, dải IP nào.
  • /opt/cni/bin/ chứa các binary CNI thật sự được gọi để thực thi.
ssh node01
ls /etc/cni/net.d/

# Output minh họa
# 10-flannel.conflist

(Output minh họa — cấu trúc và tên file đúng theo lệnh thật, danh sách cụ thể chỉ mang tính ví dụ.)

Nếu thư mục này rỗng, bạn đã tìm ra nguyên nhân của mọi pod kẹt ContainerCreating trên node đó: chưa có CNI plugin nào được cài.

💡 Lưu ý quan trọng: khi thư mục có nhiều hơn một file cấu hình, chỉ một file được dùng — file đứng đầu theo thứ tự alphabet (10-flannel.conflist thắng 99-calico.conflist). Một chi tiết hay bị nhắc sai trong tài liệu cũ: từ Kubernetes v1.24, kubelet không còn quản lý CNI nữa; việc chọn và nạp file cấu hình đã chuyển hẳn sang container runtime (containerd hoặc CRI-O). Khi debug, đó mới là nơi cần nhìn.

Kiểm tra CNI pod

Hầu hết CNI plugin tự chạy như DaemonSet trong cluster, nên bạn kiểm tra chúng như mọi pod khác:

kubectl get pods -n kube-flannel
kubectl get pods -n calico-system

# Output minh họa
# NAME READY STATUS RESTARTS AGE
# kube-flannel-ds-8qkzn 0/1 Init:CrashLoopBackOff 5 6m
# kube-flannel-ds-m4t2v 1/1 Running 0 12d

(Output minh họa — cấu trúc và tên cột đúng theo lệnh thật, số liệu cụ thể chỉ mang tính ví dụ.)

Một CNI pod chết trên đúng node đang có vấn đề là bằng chứng rất mạnh. Đọc log của nó:

kubectl logs kube-flannel-ds-8qkzn -n kube-flannel

# Output minh họa (rút gọn)
# Error registering network: failed to acquire lease: node "node01" pod cidr not assigned

(Output minh họa — cấu trúc thông báo đúng theo lệnh thật, nội dung cụ thể chỉ mang tính ví dụ.)

Dòng lỗi pod cidr not assigned dẫn thẳng tới nguyên nhân: node chưa được cấp dải IP cho pod, thường do cluster khởi tạo mà thiếu --pod-network-cidr khớp với cấu hình của Flannel.

8.6. Troubleshooting kube-proxy

kube-proxy làm gì

kube-proxy chạy trên mỗi node dưới dạng DaemonSet. Nhiệm vụ của nó: theo dõi Service và EndpointSlice qua API server, rồi dịch chúng thành rule trong kernel Linux sao cho gói tin gửi tới ClusterIP được viết lại địa chỉ đích thành IP của một pod thật. Phép viết lại đó gọi là DNAT (Destination NAT).

Ở đây có một chi tiết dễ gây hiểu nhầm: ClusterIP là IP ảo, không gắn với bất kỳ network interface nào trên bất kỳ máy nào. Ping nó sẽ không ăn thua. Nó chỉ tồn tại dưới dạng một dòng rule trong kernel.

Kernel cũng ghi nhớ mỗi kết nối đã được DNAT trong một bảng gọi là conntrack (connection tracking), để gói tin trả về biết đường quay lại đúng client. Đây là lý do một sự cố kinh điển: khi một pod backend bị xoá, các kết nối cũ trong bảng conntrack vẫn trỏ tới IP đã chết, và ứng dụng treo cho đến khi entry đó hết hạn.

💡 Hình dung: kube-proxy giống lễ tân ghi sẵn một tờ giấy dán ở cửa: "ai hỏi phòng 10.96.0.10 thì dẫn lên phòng 10.244.1.7". Khách không bao giờ gặp "phòng 10.96.0.10" — phòng đó không tồn tại, chỉ có tờ giấy. Lễ tân cũng ghi lại từng vị khách đã dẫn đi đâu, để lúc khách quay ra còn biết trả lại đúng cửa.

Các mode hiện hành

Đây là chỗ nhiều tài liệu đã lạc hậu, nên cần nói rõ tình hình ở Kubernetes v1.37:

ModeTình trạng ở v1.37
iptablesVẫn là default trên Linux. Ổn định, nhưng rule dạng danh sách tuyến tính nên chậm dần khi số Service tăng lên hàng nghìn.
nftablesĐã GA từ v1.33. Nhanh hơn iptables đáng kể ở cluster lớn, và là hướng đi lâu dài — Kubernetes đã công bố sẽ chuyển default sang nftables ở một phiên bản tương lai.
ipvsĐã bị đánh dấu deprecated từ v1.35. Vẫn chạy được ở v1.37 nhưng kube-proxy sẽ in cảnh báo lúc khởi động. Dự kiến tắt mặc định ở v1.40 và gỡ hẳn ở v1.43.
userspaceĐã bị gỡ bỏ từ lâu, chỉ còn gặp trong tài liệu rất cũ.

Một điểm rất đáng làm ngay: từ v1.37, kube-proxy sẽ in cảnh báo nếu mode không được khai báo tường minh, vì default sẽ đổi trong tương lai. Hãy ghi rõ mode trong ConfigMap của kube-proxy thay vì dựa vào giá trị mặc định — nếu không, một lần nâng cấp cluster có thể lặng lẽ đổi cả data plane của bạn.

Các bước kiểm tra

kubectl get pods -n kube-system -l k8s-app=kube-proxy

# Output minh họa
# NAME READY STATUS RESTARTS AGE
# kube-proxy-5n8wd 1/1 Running 0 12d
# kube-proxy-tq2jx 0/1 Error 4 9m

(Output minh họa — cấu trúc và tên cột đúng theo lệnh thật, số liệu cụ thể chỉ mang tính ví dụ.)

Phải có đúng một kube-proxy pod trên mỗi node. Thiếu một cái nghĩa là Service không hoạt động trên node đó — và triệu chứng sẽ là "ứng dụng lúc được lúc không", tuỳ vào việc client rơi vào node nào.

kubectl logs kube-proxy-tq2jx -n kube-system | head -20

# Output minh họa (rút gọn)
# I0919 10:22:11.402 server_linux.go:66] "Using iptables proxy"
# E0919 10:22:11.938 server.go:558] "Error running ProxyServer" err="failed to initialize iptables: exit status 3"

(Output minh họa — cấu trúc và định dạng log đúng theo lệnh thật, nội dung cụ thể chỉ mang tính ví dụ.)

Dòng Using iptables proxy cho bạn biết mode đang chạy thật sự — hữu ích hơn việc đoán từ file cấu hình.

kubectl get configmap kube-proxy -n kube-system -o yaml | grep -E 'mode|clusterCIDR'

# Output minh họa
# clusterCIDR: 10.244.0.0/16
# mode: "iptables"

(Output minh họa — cấu trúc và tên field đúng theo lệnh thật, giá trị cụ thể chỉ mang tính ví dụ.)

Hai field này quan trọng. mode rỗng nghĩa là cluster đang dựa vào giá trị mặc định — nên sửa. clusterCIDR phải khớp dải IP mà CNI plugin thực sự cấp cho pod; lệch nhau là nguồn của những lỗi routing rất khó tìm.

Cuối cùng, xem rule thật trên node:

ssh node01
iptables -t nat -L KUBE-SERVICES -n | grep web-svc

# Output minh họa (rút gọn)
# KUBE-SVC-XQ3T5LMNPR2 tcp -- 0.0.0.0/0 10.96.0.10 /* default/web-svc:http cluster IP */ tcp dpt:80

(Output minh họa — cấu trúc và tên chain đúng theo lệnh thật, địa chỉ cụ thể chỉ mang tính ví dụ.)

Nếu cluster đang chạy mode nftables thì lệnh tương đương là:

nft list table ip kube-proxy | head -20

# Output minh họa (rút gọn)
# table ip kube-proxy {
# chain services {
# ip daddr 10.96.0.10 tcp dport 80 goto service-XQ3T5LMNPR2
# }
# }

(Output minh họa — cấu trúc đúng theo lệnh thật, địa chỉ cụ thể chỉ mang tính ví dụ.)

Và với các cluster cũ còn chạy ipvs, rule nằm ở chỗ khác:

ipvsadm -Ln

# Output minh họa (rút gọn)
# TCP 10.96.0.10:80 rr
# -> 10.244.1.7:80 Masq 1 0 0
# -> 10.244.2.4:80 Masq 1 0 0

(Output minh họa — cấu trúc và tên cột đúng theo lệnh thật, số liệu cụ thể chỉ mang tính ví dụ.)

Không thấy ClusterIP của Service xuất hiện trong output tương ứng với mode đang dùng? Vậy kube-proxy trên node đó chưa kịp (hoặc không thể) tạo rule — quay lại đọc log của nó.

8.7. Bảng tra nhanh: triệu chứng đến nguyên nhân

Khi đang trong một sự cố thật, thứ bạn cần không phải là lý thuyết mà là lệnh tiếp theo. Bảng này gom lại năm triệu chứng hay gặp nhất của cả phần node lẫn phần network.

Triệu chứngNghĩa là gìLệnh chạy đầu tiên
Node ở trạng thái NotReadyControl plane không còn nhận heartbeat khoẻ từ kubelet trên node đó. Chưa nói gì về nguyên nhân.kubectl describe node <node> và đọc mục Conditions.
kubelet ở trạng thái inactive (dead)Service không chạy và cũng không cố chạy — thường do bị stop thủ công hoặc chưa enable sau reboot.systemctl start kubelet rồi systemctl enable kubelet.
Pod kẹt ở ContainerCreatingContainer runtime chưa cắm được pod vào mạng: CNI plugin chưa cài, hoặc file cấu hình CNI sai.kubectl describe pod <pod> rồi ls /etc/cni/net.d/ trên node.
DNS resolution thất bại trong podCoreDNS không chạy, kube-dns không có endpoint, hoặc /etc/resolv.conf trỏ sai nameserver.kubectl get pods -n kube-system -l k8s-app=kube-dns.
Service gọi được bằng IP nhưng không gọi được bằng tênTầng Service và kube-proxy đều khoẻ; vấn đề nằm đúng ở DNS.kubectl exec <pod> -- cat /etc/resolv.conf rồi thử nslookup.

Hàng cuối cùng đáng được nhấn mạnh, vì nó là phép thử phân đôi hiệu quả nhất trong toàn bộ phần network: gọi bằng ClusterIP thành công còn gọi bằng tên thất bại thì bạn đã loại được Service, EndpointSlice, kube-proxy và CNI chỉ bằng một lệnh curl. Chỉ còn lại DNS.

Điều nên mang theo từ phần này: đừng debug network theo trực giác. Đi theo đúng đường đi của gói tin trong sơ đồ ở mục 8.2, mỗi lần một chặng, và luôn bắt đầu bằng phép thử rẻ nhất — gọi thẳng vào pod IP.


Nguồn tham khảo

Nguồn gốc: Khóa "Certified Kubernetes Administrator (CKA)" — các bài "Troubleshooting" (Phần 14: Application Failures, Control Plane Failures, Worker Node Failures, Networking), nền tảng KodeKloud. Giảng viên: Mumshad Mannambeth.

Repo ghi chú, link tài liệu, và đáp án các practice question của toàn bộ khóa học: kodekloudhub/certified-kubernetes-administrator-course.

Fact-check:

  • Static pod manifest nằm ở /etc/kubernetes/manifests, đổi được qua staticPodPath — 2026-09-23, Local Files And Paths Used By The Kubelet
  • Sửa file trong /etc/kubernetes/manifests là kubelet tự restart static pod tương ứng — 2026-09-23, Reconfiguring a kubeadm cluster
  • Kubelet quét thư mục manifest theo chu kỳ và thêm/xoá Pod theo file — 2026-09-23, Static Pods
  • kubeadm sinh kubeconfig riêng cho controller-manager và scheduler trong /etc/kubernetes/, chứng chỉ trong /etc/kubernetes/pki/ — 2026-09-23, kubeadm Implementation details
  • Cờ --kubeconfig--client-ca-file của kube-controller-manager vẫn hiện hành trong v1.37 — 2026-09-23, kube-controller-manager v1.37
  • kubeadm ghi kubelet config file tại /var/lib/kubelet/config.yaml, kubeconfig của kubelet tại /etc/kubernetes/kubelet.conf, và CA gốc tại /etc/kubernetes/pki/ca.crt — 2026-09-23, kubeadm Implementation details
  • Field authentication.x509.clientCAFile của KubeletConfiguration chỉ định CA certificate dùng để xác thực client gọi vào kubelet API — 2026-09-23, Kubelet Configuration (v1beta1) API reference
  • kube-apiserver lắng nghe trên port 6443, kubelet API trên port 10250 — 2026-09-23, Ports and Protocols
  • Node conditions hiện hành gồm Ready, MemoryPressure, DiskPressure, PIDPressure, NetworkUnavailable; Ready chuyển sang Unknown khi node controller không nhận heartbeat trong node-monitor-grace-period (mặc định 50 giây) — 2026-09-23, Node Status
  • Condition OutOfDisk đã bị gỡ khỏi Kubernetes và được thay bằng DiskPressure — 2026-09-23, kubernetes/kubernetes PR #72420
  • Node mất liên lạc bị node controller gắn taint node.kubernetes.io/not-ready, dẫn tới evict pod trên node đó — 2026-09-23, Taints and Tolerations
  • Quy trình debug kubelet trên node dùng systemctl status kubeletjournalctl -xeu kubelet — 2026-09-23, Troubleshooting kubeadm
  • Trên node Linux dùng systemd, kubelet ghi log vào journald, đọc bằng journalctl -u kubelet — 2026-09-23, Logging Architecture
  • Dockershim bị gỡ khỏi kubelet từ v1.24 nên lệnh docker không còn thấy container của Kubernetes — 2026-09-23, Updated: Dockershim Removal FAQ
  • Công cụ thay thế để inspect container runtime trên node là crictl (crictl pods, crictl ps -a, crictl logs) — 2026-09-23, Debugging Kubernetes nodes with crictl
  • kubectl logs --previous đọc log của lần chạy container trước đó — 2026-09-23, Logging Architecture
  • kubectl get events --sort-by=.metadata.creationTimestamp sắp xếp events theo thời gian — 2026-09-23, kubectl Quick Reference
  • Pod Pending và Pod Waiting chẩn đoán bằng kubectl describe pod, so label với selector bằng kubectl get pods --selector — 2026-09-23, Debug Pods
  • API Endpoints deprecated từ v1.33, API server trả cảnh báo endpoints is deprecated, use endpointslices instead — 2026-09-23, Continuing the transition from Endpoints to EndpointSlices
  • kubectl get endpointslices -l kubernetes.io/service-name=<svc> là lệnh thay thế — 2026-09-23, EndpointSlices
  • kube-proxy trên Linux có ba mode iptables, ipvs, nftables; ở Kubernetes v1.37 default vẫn là iptables — 2026-09-23, Virtual IPs and Service Proxies
  • Mode nftables của kube-proxy GA từ Kubernetes v1.33 — 2026-09-23, NFTables mode for kube-proxy
  • Mode ipvs của kube-proxy deprecated từ Kubernetes v1.35, dự kiến tắt mặc định ở v1.40 và gỡ hẳn ở v1.43 — 2026-09-23, KEP-5495: Deprecate ipvs mode in kube-proxy
  • Từ Kubernetes v1.37 kube-proxy in cảnh báo nếu proxy mode không được khai báo tường minh — 2026-09-23, CHANGELOG-1.37
  • CoreDNS là DNS add-on mặc định của kubeadm từ Kubernetes v1.13 — 2026-09-23, Kubernetes 1.13 Release Announcement
  • kubeadm của Kubernetes v1.37 triển khai CoreDNS phiên bản v1.14.6 — 2026-09-23, kubeadm constants.go, nhánh release-1.37
  • Corefile của CoreDNS được mount từ ConfigMap coredns trong namespace kube-system và nằm tại /etc/coredns/Corefile trong container — 2026-09-23, Using CoreDNS for Service Discovery
  • Quy trình debug DNS chuẩn dùng nslookup kubernetes.default từ pod dnsutils, kiểm tra /etc/resolv.conf, và log của CoreDNS — 2026-09-23, Debugging DNS Resolution
  • Thư mục cấu hình CNI mặc định là /etc/cni/net.d, thư mục binary là /opt/cni/bin; từ Kubernetes v1.24 việc nạp CNI plugin do container runtime đảm nhiệm — 2026-09-23, Network Plugins
  • Khi có nhiều file cấu hình trong /etc/cni/net.d, runtime chọn file theo thứ tự alphabet — 2026-09-23, CRI-O: CNI configuration
  • CoreDNS pod ở trạng thái Pending ngay sau kubeadm init là hành vi mong đợi cho tới khi pod network add-on được cài — 2026-09-23, Troubleshooting kubeadm