# 🧭 Quy trình troubleshoot: event, describe, log

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

Sau bài này, bạn sẽ:

- Đi đúng thứ tự bốn bước khi một workload có vấn đề: xem trạng thái, đọc event, đọc log, rồi mới vào trong container.
- Biết chính xác lệnh nào trả lời câu hỏi nào, để không mất 10 phút mò mẫm giữa phòng thi.
- Đọc được mục `Events` ở cuối output của `kubectl describe` và biết event nào là manh mối chính.
- Dùng `--previous`, `--tail`, `--since` và exit code để khoanh vùng lỗi thuộc về app hay thuộc về cluster.
- Tránh các bẫy quen thuộc: event đã hết hạn, log của container đã chết, và nhầm namespace.

## Ôn lại nhanh

Trước bài này bạn đã có đủ đồ nghề: `kubectl get -o wide`, `-o yaml`, `jsonpath`, `kubectl explain`, và bạn đã từng troubleshoot PVC `Pending` ở Module 5. Bài này không dạy thêm lệnh mới, nó dạy **thứ tự dùng lệnh**.

> [!NOTE]
> Câu thần chú của mình khi đi thi: **status trước, event sau, log cuối**. Ai mới vào cũng nhảy thẳng vào `logs`, mà 80% lỗi trong đề CKA lại nằm ở event (scheduling, mount, image, probe).

## Lý thuyết

### Bức tranh tổng thể: 4 tầng thông tin

Một pod lỗi để lại dấu vết ở bốn nơi khác nhau. Mỗi nơi trả lời một câu hỏi khác nhau:

| Tầng | Lệnh | Trả lời câu hỏi |
| --- | --- | --- |
| Trạng thái | `kubectl get pod -o wide` | Pod đang ở pha nào, chạy trên node nào, restart bao nhiêu lần |
| Event | `kubectl describe pod`, `kubectl get events` | Control plane đã cố làm gì và thất bại ở đâu |
| Log | `kubectl logs`, `kubectl logs --previous` | Ứng dụng bên trong nói gì trước khi chết |
| Bên trong | `kubectl exec`, `kubectl debug` | Môi trường thực tế: DNS, file, biến, kết nối |

Đi từ trên xuống dưới. Đừng nhảy cóc.

```mermaid
flowchart TD
    Start["🔍<br/>Pod có vấn đề"] --> Get["📋<br/>kubectl get pod -o wide"]
    Get --> Status{"STATUS là gì"}
    Status -->|Pending| Scheduling["📝<br/>describe: FailedScheduling / FailedMount"]
    Status -->|CrashLoopBackOff| Crash["📝<br/>describe: Last State + Exit Code"]
    Status -->|ImagePullBackOff| Image["📝<br/>describe: Failed to pull image"]
    Status -->|Running nhưng sai| Logs["📜<br/>kubectl logs"]
    Crash --> Prev["📜<br/>kubectl logs --previous"]
    Scheduling --> FixA["🛠️<br/>Sửa request, selector, PVC"]
    Image --> FixB["🛠️<br/>Sửa image, imagePullSecret"]
    Prev --> FixC["🛠️<br/>Sửa app, config, probe"]
    Logs --> Exec["🐚<br/>kubectl exec / debug"]
```

### Bước 1: Đọc trạng thái

```bash
kubectl get pods -n shop -o wide
kubectl get pods -A -o wide
```

Bốn cột bạn phải đọc ngay: `READY`, `STATUS`, `RESTARTS`, `AGE`.

- `READY 0/1` nghĩa là container chưa sẵn sàng nhận traffic, kể cả khi `STATUS` là `Running`.
- `RESTARTS` tăng đều là dấu hiệu container chết rồi được restart, không phải app chậm.
- `STATUS` chỉ là bề mặt. `Pending` và `CrashLoopBackOff` là hai hướng điều tra hoàn toàn khác nhau.

Mẹo nhỏ mà mình dùng rất nhiều: `kubectl get pod -o wide` cho thêm cột `NODE`. Khi chỉ một node có pod lỗi, bạn đã khoanh vùng được sang bài học sau (node `NotReady`).

### Bước 2: Đọc event

`kubectl describe` là lệnh quan trọng nhất trong bài này. Với pod, phần đáng đọc nhất nằm ở **cuối output**, mục `Events`:

```bash
kubectl describe pod api-7d9f8c6b5-x2k4n -n shop
```

```text
Events:
  Type     Reason            Age   From               Message
  ----     ------            ----  ----               -------
  Warning  FailedScheduling  45s   default-scheduler  0/3 nodes are available:
           3 Insufficient cpu. preemption: 0/3 nodes are available: 3 No preemption victims found
  Normal   Scheduled         40s   default-scheduler  Successfully assigned shop/api-... to node-2
  Normal   Pulled            38s   kubelet            Container image "nginx:1.25" already present
  Warning  Failed            20s   kubelet            Error: container has runAsNonRoot and image will run as root
```

Cách đọc: đọc từ dưới lên, sự kiện mới nhất ở cuối. `Type` có `Warning` là ưu tiên số một.

Các `Reason` bạn gặp nhiều nhất trong đề thi:

| Reason | Tầng lỗi | Hướng sửa |
| --- | --- | --- |
| `FailedScheduling` | Scheduler | Thiếu CPU/memory, `nodeSelector`, taint, PVC chưa bind |
| `FailedMount` | Kubelet, storage | Secret/ConfigMap thiếu, PVC `Pending`, sai `accessModes` |
| `Failed` + `Failed to pull image` | Kubelet, registry | Sai tag, private registry thiếu `imagePullSecret` |
| `BackOff` | Kubelet | Container crash liên tục, xem `Last State` |
| `Unhealthy` | Kubelet | Probe sai đường dẫn, cổng, hoặc app chưa kịp khởi động |
| `Killing` | Kubelet | Bị evict do vượt memory limit, hoặc bị `delete` |

Khi cần xem event của cả namespace hoặc toàn cluster:

```bash
kubectl get events -n shop --sort-by=.metadata.creationTimestamp
kubectl get events -A --sort-by=.lastTimestamp
kubectl get events -n shop --field-selector involvedObject.name=api-7d9f8c6b5-x2k4n
```

> [!WARNING]
> Event có thời hạn. Mặc định kube-apiserver chỉ giữ event trong {{c1::1 giờ}} (`--event-ttl`). Nếu bạn vào lab muộn hoặc vừa `drain` node xong, event có thể đã biến mất. Lúc đó phải suy luận từ trạng thái hiện tại và log, đừng ngồi chờ event quay lại.

### Bước 3: Đọc log

```bash
kubectl logs api-7d9f8c6b5-x2k4n -n shop
kubectl logs api-7d9f8c6b5-x2k4n -n shop --tail=50
kubectl logs api-7d9f8c6b5-x2k4n -n shop --since=10m
kubectl logs api-7d9f8c6b5-x2k4n -n shop -c sidecar
kubectl logs api-7d9f8c6b5-x2k4n -n shop --all-containers=true
```

Điểm ăn điểm nhất: **container đã chết thì log hiện tại trống**. Phải dùng `--previous` (viết tắt `-p`) để xem log của lần chạy trước, tức là lần đã crash:

```bash
kubectl logs api-7d9f8c6b5-x2k4n -n shop --previous
```

Nếu `--previous` cũng trống và bạn thấy `RESTARTS 0` nhưng `STATUS` sai, thì lỗi chưa bao giờ tới được app: nó nằm ở image, ở volume, hoặc ở scheduler.

Với pod nhiều container, thiếu `-c` là lỗi rất hay gặp trong phòng thi. Lệnh sẽ báo `a container name must be specified`. Chạy `kubectl get pod <name> -o jsonpath='{.spec.containers[*].name}'` để lấy danh sách tên.

### Bước 4: Exit code, manh mối cuối cùng

Trong `kubectl describe`, phần `Last State: Terminated` cho bạn `Reason` và `Exit Code`:

```text
Last State:     Terminated
  Reason:       OOMKilled
  Exit Code:    137
```

| Exit code | Ý nghĩa | Thường gặp khi |
| --- | --- | --- |
| `0` | Thoát bình thường | Job chạy xong, container one-shot kết thúc |
| `1` | App tự báo lỗi | Sai config, thiếu biến môi trường, crash khởi động |
| `137` | `128 + 9`, bị SIGKILL | **OOMKilled** hoặc bị kill cứng |
| `139` | Segfault | Binary lỗi, hiếm gặp trong đề |
| `143` | `128 + 15`, SIGTERM | Bị terminate bình thường khi scale down hoặc `delete` |

`OOMKilled` không phải lỗi app. Nó là lỗi cấu hình: container vượt `resources.limits.memory`. Sửa limit, không sửa code.

### Bước 5: Chỉ khi ba bước trên chưa đủ

```bash
kubectl exec -it api-7d9f8c6b5-x2k4n -n shop -- sh
kubectl debug -it api-7d9f8c6b5-x2k4n -n shop --image=busybox:1.36 --target=api
```

`exec` dùng khi app vẫn chạy nhưng hành vi sai: kiểm tra DNS bằng `nslookup`, kiểm tra file mount, kiểm tra biến môi trường. Nếu image không có shell (distroless), dùng `kubectl debug` với ephemeral container. Đừng dùng `exec` để đoán lỗi khi chưa đọc event, vì `exec` không cho bạn biết quá khứ.

## Ví dụ có hướng dẫn

### Ví dụ 1: Pod `Pending` do thiếu CPU

```bash
kubectl get pods -n shop
NAME                    READY   STATUS    RESTARTS   AGE
api-7d9f8c6b5-x2k4n     0/1     Pending   0          4m
```

Bước 1 cho ta biết: chưa có node nào nhận pod. Bước 2, đọc event:

```bash
kubectl describe pod api-7d9f8c6b5-x2k4n -n shop | tail -n 10
```

```text
Events:
  Type     Reason            Age   From               Message
  Warning  FailedScheduling  4m    default-scheduler  0/3 nodes are available:
           3 Insufficient cpu.
```

Kết luận: `requests.cpu` lớn hơn CPU còn trống trên mọi node. Cách sửa trong phòng thi, chọn cách nhanh nhất:

```bash
kubectl get nodes -o custom-columns=NAME:.metadata.name,CPU:.status.allocatable.cpu
kubectl describe node node-2 | grep -A 5 "Allocated resources"
```

Rồi giảm `requests.cpu` bằng `kubectl edit deployment api -n shop`, hoặc xóa một pod chiếm chỗ. Bước 3 (log) ở đây là vô nghĩa vì container chưa từng chạy. Biết dừng đúng lúc cũng là kỹ năng thi.

### Ví dụ 2: `CrashLoopBackOff` do thiếu biến môi trường

```bash
kubectl get pods -n shop
NAME                    READY   STATUS             RESTARTS   AGE
worker-6b7c9d4f5-h8m2p  0/1     CrashLoopBackOff   6          5m
```

`RESTARTS 6` nghĩa là container có chạy, rồi chết. Vậy log sẽ có nội dung, nhưng phải là log của lần trước:

```bash
kubectl logs worker-6b7c9d4f5-h8m2p -n shop --previous
```

```text
Error: DB_HOST is not set. Exiting.
```

Kiểm tra chéo bằng `describe` để thấy exit code và biến môi trường:

```bash
kubectl describe pod worker-6b7c9d4f5-h8m2p -n shop | grep -A 3 "Last State"
kubectl get pod worker-6b7c9d4f5-h8m2p -n shop -o jsonpath='{.spec.containers[0].env}'
```

```text
Last State:     Terminated
  Reason:       Error
  Exit Code:    1
```

Kết luận: app thoát với code 1 vì thiếu `DB_HOST`. Không phải lỗi cluster. Sửa bằng `kubectl edit deployment worker -n shop` và thêm `env` từ ConfigMap, sau đó `kubectl rollout status deployment/worker -n shop`.

### Ví dụ 3: `ImagePullBackOff` do sai tag

```bash
kubectl describe pod web-5f6d7b8c9-q1w2e -n shop | tail -n 8
```

```text
Events:
  Type     Reason     Age                 From     Message
  Warning  Failed     2m (x4 over 3m)     kubelet  Failed to pull image "nginx:1.999":
           rpc error: code = NotFound ... manifest unknown
  Warning  Failed     2m (x4 over 3m)     kubelet  Error: ErrImagePull
  Normal   BackOff    30s (x6 over 3m)    kubelet  Back-off pulling image "nginx:1.999"
```

Đọc đúng dòng quan trọng: `manifest unknown` chứng minh tag không tồn tại, không phải lỗi mạng. Nếu message là `unauthorized` hoặc `authentication required`, hướng sửa là `imagePullSecret`. Nếu là `dial tcp ... i/o timeout`, mới là mạng hoặc registry nội bộ.

Lệnh kiểm tra tag nhanh trước khi sửa:

```bash
kubectl set image deployment/web web=nginx:1.25 -n shop
kubectl rollout status deployment/web -n shop
```

### Ví dụ 4: Event trực tiếp và log của pod đang chạy sai

Khi không có pod nào `Pending` hay `Crash`, nhưng app trả lỗi, đọc log theo thời gian:

```bash
kubectl logs -n shop -l app=api --tail=100 --since=15m --prefix=true
```

`-l` chọn theo label, rất tiện khi Deployment có nhiều replica và bạn không muốn copy tên pod. `--prefix=true` thêm tên pod vào mỗi dòng, giúp so sánh giữa các replica.

Nếu log cho thấy app không resolve được tên service, đó là lỗi DNS và thuộc bài 6.4, không phải bài này. Ghi nhận rồi đi tiếp.

## Luyện tập có hướng dẫn

Làm từng bài theo đúng thứ tự 4 bước. Ghi ra giấy bạn đang ở bước nào. Đáp án ở phần gập bên dưới, chỉ mở sau khi tự trả lời.

### Bài 1: Đọc trạng thái trước

```bash
kubectl get pods -n demo
NAME                       READY   STATUS             RESTARTS   AGE
frontend-9c8b7d6f5-abc12   0/1     Pending            0          6m
frontend-9c8b7d6f5-def34   1/1     Running            0          6m
cache-5d4c3b2a1-ghi56      0/1     CrashLoopBackOff   12         6m
```

Trả lời: trong ba pod, pod nào có lỗi thuộc về scheduler, pod nào có lỗi thuộc về app? Lệnh tiếp theo bạn chạy cho từng pod là gì?

<details><summary>Xem gợi ý</summary>

`abc12` là `Pending` nên là lỗi scheduler, xem `kubectl describe pod frontend-9c8b7d6f5-abc12 -n demo`, đọc `FailedScheduling`. `ghi56` là `CrashLoopBackOff` với 12 lần restart nên là lỗi app hoặc config, xem `kubectl logs cache-5d4c3b2a1-ghi56 -n demo --previous`. Riêng `def34` đang `Running 1/1`, nếu nó cũng lỗi thì đó là lỗi phía sau service, không phải pod.

Lưu ý bẫy: hai pod `frontend` khác trạng thái nhau nghĩa là manifest giống nhau nhưng node khác nhau, hoặc một pod bị kẹt từ trước. Đừng sửa Deployment ngay.

</details>

### Bài 2: Khoanh vùng bằng event

```text
Events:
  Type     Reason       Age   From     Message
  Warning  FailedMount  20s   kubelet  MountVolume.SetUp failed for volume "cfg"
           : configmap "app-config" not found
```

Trả lời: lỗi nằm ở đâu? Sửa bằng lệnh gì? Pod sẽ chuyển sang trạng thái gì sau khi bạn sửa?

<details><summary>Xem gợi ý</summary>

Lỗi ở ConfigMap chứ không phải image hay app. Kiểm tra bằng `kubectl get configmap -n demo`, tạo lại hoặc sửa tên trong Deployment. Sau khi ConfigMap xuất hiện, kubelet tự mount lại trong khoảng một phút, pod chuyển từ `ContainerCreating` sang `Running`. Không cần xóa pod, nhưng nếu đang vội trong phòng thi thì `kubectl delete pod` để nó được tạo lại ngay là cách nhanh hơn.

</details>

### Bài 3: Chọn đúng lệnh log

Container `api` vừa crash lần thứ 5 và đang trong backoff. Bạn chạy `kubectl logs api-xyz -n demo` và nhận output rỗng.

Trả lời: tại sao rỗng và lệnh đúng là gì?

<details><summary>Xem gợi ý</summary>

Vì container hiện tại chưa chạy hoặc vừa mới được tạo lại, nên log hiện tại chưa có gì. Lệnh đúng:

```bash
kubectl logs api-xyz -n demo --previous
```

Nếu vẫn rỗng, kiểm tra `kubectl describe pod api-xyz -n demo`, phần `Last State`, để xem container có thực sự chạy lần nào chưa. Chưa chạy thì không có log, và lỗi nằm ở tầng image hoặc volume.

</details>

### Bài 4: Tự viết quy trình

Cho một pod có `STATUS` là `Running`, `READY 0/1`, `RESTARTS 0`. Viết ra ba lệnh bạn sẽ chạy, theo đúng thứ tự, và nói mỗi lệnh để tìm gì.

<details><summary>Xem gợi ý</summary>

1. `kubectl describe pod <name> -n <ns>`, tìm event `Unhealthy` ở cuối output. `READY 0/1` với `RESTARTS 0` là dấu hiệu kinh điển của readiness probe sai.
2. `kubectl get pod <name> -n <ns> -o yaml`, xem `status.containerStatuses[].ready` và cấu hình probe: `initialDelaySeconds`, `periodSeconds`, `httpGet.path`, `port`.
3. `kubectl logs <name> -n <ns>`, xem app có thực sự listen đúng cổng và đường dẫn mà probe gọi hay không.

Nếu probe gọi `/healthz` mà app chỉ phục vụ `/health`, đó là lỗi cấu hình probe, không phải lỗi app. Sửa probe hoặc sửa app, cả hai đều được tính điểm nếu đề yêu cầu pod ở trạng thái `Ready`.

</details>

### Bài 5: Tốc độ

Trong 60 giây, hãy lấy ra 5 event `Warning` mới nhất của namespace `demo` và tên pod liên quan.

<details><summary>Xem gợi ý</summary>

```bash
kubectl get events -n demo --field-selector type=Warning \
  --sort-by=.metadata.creationTimestamp -o custom-columns=OBJ:.involvedObject.name,REASON:.reason,MSG:.message
```

Đọc từ dưới lên để thấy sự kiện mới nhất. Trong phòng thi, mình thường gõ `--sort-by=.lastTimestamp` vì một số event được cập nhật thay vì tạo mới. Cả hai đều đúng, chọn cái bạn nhớ và gõ nhanh.

</details>

## Góc chuyên gia

**Bốn câu mình luôn tự hỏi theo thứ tự:**

1. Container đã từng chạy chưa? Nếu chưa, đừng đọc log, hãy đọc event.
2. Control plane có báo lỗi gì không? Event `Warning` là câu trả lời.
3. App có nói gì trước khi chết không? `--previous` là bạn của bạn.
4. Có phải lỗi cấu hình tài nguyên không? `OOMKilled` và `FailedScheduling` chiếm rất nhiều điểm trong đề.

**Lỗi thường gặp của người mới:**

- Chạy `kubectl logs` với pod `Pending` rồi kết luận "không có log, chắc tại app". Pod `Pending` chưa từng được tạo container.
- Quên `-n`, mặc định là `default`, trong khi đề dùng namespace riêng. Luôn kiểm tra `kubectl config view --minify -o jsonpath='{.contexts[0].context.namespace}'`.
- Sửa Deployment rồi không chờ rollout, dẫn tới tưởng là sửa sai. Luôn `kubectl rollout status`.
- Quên `-c` với pod nhiều container, mất thời gian đọc thông báo lỗi.
- Xóa pod ngay khi gặp lỗi, làm mất bằng chứng trong event và `Last State`. Đọc trước, xóa sau.

> [!TIP]
> Trong phòng thi, mỗi câu troubleshoot bạn nên có một "câu chốt" viết ra giấy nháp: lỗi ở tầng nào và sửa bằng lệnh gì. Nếu chưa viết được câu chốt, bạn chưa hiểu lỗi, đừng sửa bừa.

**Quy tắc 2 phút:** nếu sau hai phút đọc event và log bạn vẫn chưa khoanh được vùng, hãy đổi đối tượng: từ pod sang node, sang PVC, hoặc sang Deployment. Đừng đốt 15 phút cho một câu.

**Chỗ hay mất điểm oan:** câu hỏi yêu cầu "pod phải ở trạng thái Running" nhưng bạn sửa app xong vẫn `CrashLoopBackOff` vì probe cũ vẫn sai. Sửa cả hai, rồi kiểm tra lại bằng `kubectl get pod -w` trước khi sang câu tiếp theo.

```cloze
Khi một container đã crash, xem log của lần chạy trước bằng cách thêm cờ {{c1::--previous}}.
Exit code {{c2::137}} thường chỉ ra container bị SIGKILL, hay gặp nhất là OOMKilled.
Muốn lọc event theo tên pod, dùng {{c3::--field-selector involvedObject.name=<tên-pod>}}.
Trong output của kubectl describe pod, phần cần đọc nhất nằm ở cuối và có tên là {{c4::Events}}.
Nếu pod ở trạng thái {{c5::Pending}} thì chưa từng có container nào chạy, nên đọc log là vô nghĩa.
```

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

Chốt lại trong bốn dòng:

- `kubectl get -o wide` để biết đang ở pha nào và pod nằm trên node nào.
- `kubectl describe` để đọc event, luôn đọc từ dưới lên và ưu tiên `Warning`.
- `kubectl logs --previous` để nghe app nói lần cuối trước khi chết, cộng với exit code.
- Chỉ `exec` hoặc `debug` khi ba bước trên chưa đủ, vì nó không cho bạn biết quá khứ.

Bài này cho bạn quy trình. Bài tiếp theo, **Troubleshoot node NotReady và kubelet**, sẽ áp đúng quy trình đó lên một node: xem `kubectl get nodes`, đọc event của node, rồi `journalctl -u kubelet` trên node đó. Bạn sẽ thấy cùng một logic, chỉ khác đối tượng và khác nguồn log.

```flashcard
[
  { "front": "Lệnh đầu tiên khi một pod có vấn đề là gì?", "back": "kubectl get pod -o wide, để biết STATUS, RESTARTS và NODE trước khi đi sâu." },
  { "front": "Bạn tìm Events ở đâu trong output của kubectl describe pod?", "back": "Ở cuối output, mục Events, đọc từ dưới lên vì sự kiện mới nhất nằm cuối." },
  { "front": "Event mặc định được lưu bao lâu trong cluster?", "back": "1 giờ, cấu hình bằng --event-ttl của kube-apiserver, nên đừng chờ event cũ quay lại." },
  { "front": "Xem log của container vừa crash bằng lệnh nào?", "back": "kubectl logs <pod> --previous, viết tắt -p." },
  { "front": "Exit code 137 nghĩa là gì?", "back": "128 cộng 9, tức bị SIGKILL, thường gặp nhất là OOMKilled hoặc bị kill cứng." },
  { "front": "Exit code 143 nghĩa là gì?", "back": "128 cộng 15, tức SIGTERM, container bị terminate bình thường." },
  { "front": "Event Reason FailedScheduling cho biết điều gì?", "back": "Scheduler không tìm được node phù hợp, do thiếu tài nguyên, nodeSelector, taint hoặc PVC chưa bind." },
  { "front": "Event Reason FailedMount thường do đâu?", "back": "Thiếu ConfigMap hoặc Secret, PVC chưa bind, hoặc sai accessModes." },
  { "front": "Lệnh sắp xếp event theo thời gian tạo?", "back": "kubectl get events --sort-by=.metadata.creationTimestamp, hoặc --sort-by=.lastTimestamp." },
  { "front": "Lọc event chỉ của một pod bằng cách nào?", "back": "kubectl get events --field-selector involvedObject.name=<tên-pod>." },
  { "front": "Xem log của tất cả container trong một pod?", "back": "kubectl logs <pod> --all-containers=true, hoặc chỉ định từng container bằng -c." },
  { "front": "Cờ --tail và --since của kubectl logs dùng để làm gì?", "back": "--tail giới hạn số dòng cuối, --since lấy log từ một mốc thời gian, ví dụ 10m." },
  { "front": "Khi nào nên dùng kubectl exec thay vì đọc log?", "back": "Khi app vẫn chạy nhưng hành vi sai, cần kiểm tra DNS, file mount, biến môi trường bên trong container." },
  { "front": "Pod đang Pending thì có log để đọc không?", "back": "Không, container chưa từng được tạo, phải đọc event trong kubectl describe." },
  { "front": "Vì sao không nên xóa pod ngay khi gặp lỗi?", "back": "Vì sẽ mất bằng chứng trong Events và Last State, là nguồn thông tin chính để chẩn đoán." }
]
```
