---
id: case-outage
title: "App tự sập, điều tra nguyên nhân"
shortTitle: "App sập"
track: cases
trackTitle: "Tình huống vận hành"
order: 15
level: core
status: published
estimatedMinutes: 40
prerequisites:
  - case-502
  - linux-process-management
next: case-ddos
reviewedAgainst: 2026-10-12
sources:
  - title: "journalctl"
    url: https://man7.org/linux/man-pages/man1/journalctl.1.html
  - title: "Assign Memory Resources to Containers"
    url: https://kubernetes.io/docs/tasks/configure-pod-container/assign-memory-resource/
---

# App tự sập, điều tra nguyên nhân

App sập nghĩa là process không còn phục vụ được nữa, không phải chỉ là một status code lẻ. Việc đầu tiên là biết nó chết từ lúc nào và vì sao process biến mất. Nếu bạn restart ngay, bằng chứng mất theo, và lỗi sẽ quay lại sau vài phút.

## Để làm gì

Bạn cần khoanh phạm vi sự cố, giữ lại log của lần chết, rồi mới đưa service trở lại. Đừng đổi năm thứ cùng một lúc.

## Gồm những gì

- Phạm vi: mọi user hay chỉ một đường đi, bắt đầu từ giờ nào.
- Process còn sống không, exit code là gì.
- Các nguyên nhân: OOM, đĩa, deploy, dependency và quyền file.

Bài không hướng dẫn viết postmortem đầy đủ, mà chỉ tập trung vào mười phút đầu.

## Dùng khi nào

Health check chuyển đỏ, Pod rơi vào `CrashLoopBackOff`, hoặc systemd báo service failed. User thấy timeout hoặc 502 hàng loạt.

Đừng dùng bài này cho một request 500 lẻ tẻ. Đó là lỗi nghiệp vụ, nằm trong log của chính request đó. Cả process chết là một chuyện khác.

## Kiến thức cốt lõi

Trả lời ba câu hỏi trước khi gõ bất kỳ lệnh sửa nào, vì câu trả lời quyết định bạn sẽ rollback hay đào sâu:

1. Lỗi bắt đầu từ giờ nào, theo timestamp trong log.
2. Hỏng toàn bộ hay một phần: một zone, một path, một tenant.
3. Trong cửa sổ đó có deploy, đổi config, certificate hết hạn hay đĩa đầy không.

Sau đó mới nhìn vào process:

| Dấu hiệu | Hướng điều tra |
| --- | --- |
| `active (running)` nhưng không trả lời | Treo, deadlock, pool connection kẹt. Đừng kết luận là đã chết |
| `failed`, exit 1 | App tự thoát. Đọc dòng log ngay trước lúc thoát |
| Exit 137, Kubernetes ghi `OOMKilled` | Bị SIGKILL. Trên Kubernetes, `describe pod` ghi rõ OOM. Riêng exit 137 trên một máy Linux chưa đủ để chứng minh là OOM |
| `CrashLoopBackOff` | Process chết rồi được kéo lên lại. Log của lần chạy trước quan trọng hơn log của lần đang chạy |

`journalctl -u app -n 100 --no-pager` đọc phần cuối log của unit, và `--since` giúp thu hẹp cửa sổ thời gian. Trên Kubernetes, `kubectl logs pod --previous` là log của container đã chết, không phải của container vừa được tạo lại.

Nếu user đang mất dịch vụ và lần deploy gần nhất trùng với giờ app chết, hãy rollback trước khi đọc hết stack trace. Điều kiện rollback đã có trong bài rollback. Sau khi user dùng lại được, hãy giữ Pod lỗi hoặc file log để đọc. Nếu xóa Pod rồi mới điều tra, bạn sẽ mất `--previous` khi không còn replica nào giữ log đó.

Kiểm tra các nguyên nhân hay gặp theo thứ tự từ rẻ tới đắt:

1. Deploy hoặc config vừa thay đổi. Mục này đứng đầu vì bạn chỉ cần đối chiếu giờ thay đổi với giờ lỗi là biết.
2. Dependency từ chối kết nối: database, DNS, Redis.
3. Đĩa đầy khiến app không ghi được log rồi chết.
4. Hết RAM và bị kill.
5. File hoặc socket không đủ quyền. Phần này có bài riêng ở cuối nhóm.
6. Traffic bất thường. Bài DDoS tách riêng phần này; đừng scale vô hạn khi chưa biết đó có phải user thật hay không.

Mỗi lần chỉ đổi một thứ. Nếu bạn restart, tăng RAM và rollback cùng một lúc, thì dù hết lỗi bạn cũng không biết cái nào đã cứu hệ thống.

Bạn coi là đã ổn định khi đúng health check đã báo đỏ lúc đầu chuyển xanh trở lại, và Pod hoặc service không restart thêm lần nào trong một khoảng theo dõi.

## Lệnh và code mẫu

Trên host lab; không đụng tới production khi chưa có quyền:

```bash
systemctl status app --no-pager
journalctl -u app -n 80 --no-pager
df -h
```

Tên unit khác nhau tùy máy. Nếu máy không có unit `app`, lệnh sẽ báo không tìm thấy, và đó cũng là một kết quả.

Trên cluster, khi đã chắc chắn đúng context:

```bash
kubectl get pods -l app=api
kubectl describe pod <pod> | sed -n '/State:/,/Events:/p'
kubectl logs <pod> --previous --tail=80
```

`--previous` sẽ fail nếu container chưa từng chết. Khi đó process đang sống, bạn chuyển sang kiểm tra timeout và dependency, không cần tìm exit code.

## Thực hành

1. Viết năm dòng cho một sự cố giả: giờ bắt đầu, phạm vi, có deploy không, process còn chạy không, và lệnh tiếp theo.
2. Exit 137 trên Kubernetes và `describe` ghi `OOMKilled`. Việc tiếp theo là xem memory limit và khả năng leak, chưa phải đổi image ngay.
3. Process vẫn `running` nhưng health check timeout. Việc tiếp theo là gửi một request và đọc log của dependency, không phải `kill -9`.

## Kết quả mong đợi

Bạn chọn được một nhánh: process chết, process treo, hoặc dependency chết. Bạn không restart trước khi giữ lại log của lần chết, nếu vẫn còn cách giữ.

## Checklist

- [ ] Ghi lại giờ và phạm vi trước khi sửa.
- [ ] Đọc log của lần chết, kể cả `--previous` nếu Pod đã được tạo lại.
- [ ] Kiểm tra đĩa và bộ nhớ trước khi kết luận code sai.
- [ ] Mỗi lần chỉ đổi một thứ, rồi đo lại bằng đúng health check ban đầu.
- [ ] Rollback nếu deploy trùng giờ và bản trước vẫn chạy được.

## Lỗi thường gặp

**`kubectl delete pod` để nó chạy lại.** ReplicaSet tạo Pod mới, và log tại chỗ mất theo nếu bạn không có log tập trung. Trong khi đó, `logs --previous` trên Pod cũ vẫn đọc được lần chết. Chỉ delete sau khi đã copy log ra.

**Thấy 137 là tăng limit gấp đôi, lần nào cũng vậy.** Nếu process bị leak, nó sẽ ăn đầy limit mới. Hãy tăng một lần để phục vụ user, đồng thời lấy heap dump hoặc metric bộ nhớ. Không có số liệu thì lần sau app vẫn chết.

**Sửa production khi mới có một user báo lỗi.** Hãy kiểm tra phạm vi trước. Một client cấu hình sai certificate không có nghĩa là app sập.

## Nguồn

1. [journalctl(1)](https://man7.org/linux/man-pages/man1/journalctl.1.html) — `-u`, `--since`, và log của service.
2. [Assign memory resources](https://kubernetes.io/docs/tasks/configure-pod-container/assign-memory-resource/) — limit bộ nhớ và trạng thái OOMKilled.

## Tiếp theo

[Xử lý khi bị DDoS](./16-ddos.md)
