100 lines
4.8 KiB
Markdown
100 lines
4.8 KiB
Markdown
---
|
||
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。
|