4.8 KiB
4.8 KiB
id, title, type, domain, stack, tags, level, prerequisites, est_hours, related, week, order
| id | title | type | domain | stack | tags | level | prerequisites | est_hours | related | week | order | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| devops.linux.troubleshooting | 线上排查:CPU、内存、磁盘、网络、日志的定位思路 | lesson | devops |
|
|
intermediate |
|
3 |
|
|
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 |
| 磁盘 | 空间和 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看到wa30%,iostat显示磁盘利用率 100%;进一步发现是同一台机器上的数据库在做全表扫描的报表查询。把报表改到从库。 - 服务每天半夜重启:应用日志没有错误,
journalctl -k里有Out of memory: Killed process ... (api)。用 pprof 看 heap,发现一个按天累积的本地缓存没有上限。 ss -ant统计出几千个 CLOSE_WAIT:代码里调用第三方接口后没有关闭响应体。
代码示例
一分钟快照脚本 examples/log-analysis/diag.sh(只读命令)的核心部分:
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(只监听本机端口,不要暴露到公网):
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))
}
# 在服务器上采集 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 面试题:CPU 飙高怎么排查、内存泄漏怎么查、磁盘满了怎么办、TIME_WAIT 和 CLOSE_WAIT。