--- id: devops.linux.troubleshooting title: 线上排查:CPU、内存、磁盘、网络、日志的定位思路 type: lesson domain: devops stack: [linux] tags: [排查, top, 负载, OOM, 磁盘, TIME_WAIT, pprof] level: intermediate prerequisites: [devops.linux.commands] est_hours: 3 related: [devops.linux.scenario.incident-troubleshooting, devops.linux.interview] week: [14] order: 2 --- # 线上排查:CPU、内存、磁盘、网络、日志的定位思路 ## 解释说明 **排查的总顺序**:先止血(回滚、扩容、限流、重启),再定位,最后复盘。定位时**从现象到资源再到代码**: | 资源 | 看什么 | 命令 | | --- | --- | --- | | 负载 | load average 和 CPU 核数的比较;高负载可能是 CPU 忙,也可能是 IO 等待 | `uptime`、`top`(看 `us` / `sy` / `wa`) | | CPU | 哪个进程、哪个线程占用高 | `top -H -p PID`;Go 用 pprof、Python 用 py-spy | | 内存 | 剩余可用内存(看 `available`)、是否用到 swap、是否有 OOM | `free -h`、`journalctl -k | grep -i oom` | | 磁盘 | 空间和 inode 是否用满、IO 是否繁忙 | `df -h`、`df -i`、`du -sh`、`iostat -x 1` | | 网络 | 端口监听、连接状态分布、连接数是否打满 | `ss -lntp`、`ss -ant` 统计状态、`curl -w` 看耗时 | | 日志 | 错误日志、慢请求、时间点 | `journalctl -u 服务`、`grep`、`awk` | **几个关键概念** - **load average**:正在运行和等待运行(包括等待 IO)的进程数的平均值。4 核机器 load 为 4 左右算满载。 - **`wa`(iowait)高**:CPU 在等磁盘,问题在 IO(慢查询扫盘、日志写太多),而不是计算。 - **OOM Killer**:内存耗尽时内核按评分杀掉进程,应用日志里往往什么都没有,只能在内核日志里看到 `Killed process`。 - **TIME_WAIT 很多**:主动关闭连接的一方会进入 TIME_WAIT。大量短连接(每次请求都新建连接)导致,解决办法是复用连接(HTTP keep-alive、连接池)。 - **CLOSE_WAIT 很多**:对方已关闭,本方没有关闭——通常是代码里忘了关闭连接(如 Go 的 `resp.Body.Close()`)。 **语言级工具**:Go 服务开启 `net/http/pprof`(只在内网端口暴露),`go tool pprof http://127.0.0.1:6060/debug/pprof/profile?seconds=30` 采集 CPU 热点,`/debug/pprof/heap` 看内存,`/debug/pprof/goroutine` 看 goroutine 泄漏。Python 用 `py-spy top --pid PID`。 ## 案例 - 接口变慢:`top` 看到 `wa` 30%,`iostat` 显示磁盘利用率 100%;进一步发现是同一台机器上的数据库在做全表扫描的报表查询。把报表改到从库。 - 服务每天半夜重启:应用日志没有错误,`journalctl -k` 里有 `Out of memory: Killed process ... (api)`。用 pprof 看 heap,发现一个按天累积的本地缓存没有上限。 - `ss -ant` 统计出几千个 CLOSE_WAIT:代码里调用第三方接口后没有关闭响应体。 ## 代码示例 一分钟快照脚本 [examples/log-analysis/diag.sh](examples/log-analysis/diag.sh)(只读命令)的核心部分: ```bash uptime ps -eo pid,user,%cpu,%mem,etime,cmd --sort=-%cpu | head -n 10 free -h df -h -x tmpfs -x devtmpfs ss -ant | awk 'NR > 1 {count[$1]++} END {for (s in count) print s, count[s]}' ``` Go 服务暴露 pprof(只监听本机端口,不要暴露到公网): ```go package main import ( "log" "net/http" _ "net/http/pprof" // 注册 /debug/pprof/* 到 http.DefaultServeMux ) func main() { go func() { // 单独的端口,只监听 127.0.0.1;业务服务用自己的 ServeMux,不暴露 pprof log.Println(http.ListenAndServe("127.0.0.1:6060", nil)) }() biz := http.NewServeMux() biz.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("ok")) }) log.Fatal(http.ListenAndServe(":8080", biz)) } ``` ```bash # 在服务器上采集 30 秒 CPU 数据,然后看最耗 CPU 的函数 go tool pprof -top http://127.0.0.1:6060/debug/pprof/profile?seconds=30 # goroutine 数量是否持续增长(泄漏) curl -s http://127.0.0.1:6060/debug/pprof/goroutine?debug=1 | head -n 1 ``` ## 易错点 - **一上来就重启**:现场没了,问题下次还会出现。重启前至少保存快照(diag.sh 的输出、pprof、日志片段)。 - **只看 CPU 不看 iowait**:负载高不一定是计算问题。 - **`free` 看 `free` 列认为内存不够**:Linux 会把空闲内存用作缓存,要看 `available`。 - **在业务端口暴露 pprof**:泄露内部信息,还能被用来消耗资源。 - **删了大文件磁盘空间没释放**:`lsof +L1` 找到仍持有已删除文件的进程。 - **排查结论没有数据支撑**:每一步都记录命令和输出,便于复盘。 ## 面试常问 见 [Linux 面试题](interview.md):CPU 飙高怎么排查、内存泄漏怎么查、磁盘满了怎么办、TIME_WAIT 和 CLOSE_WAIT。