Files

100 lines
4.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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。