5.0 KiB
5.0 KiB
id, title, type, domain, stack, tags, level, prerequisites, est_hours, related, week
| id | title | type | domain | stack | tags | level | prerequisites | est_hours | related | week | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| devops.linux.scenario.incident-troubleshooting | 线上故障排查:接口变慢、磁盘写满、进程被 OOM 杀掉 | scenario | devops |
|
|
intermediate |
|
2 |
|
|
线上故障排查:接口变慢、磁盘写满、进程被 OOM 杀掉
业务背景与问题
一个小团队维护的 SaaS 系统,部署在两台云服务器上(Nginx + Go API + PostgreSQL),没有专职运维。某天上午:用户反馈页面很慢,部分请求 502;中午服务自己恢复了;下午又出现。你被要求“查清楚原因并保证不再发生”。
约束
- 业务优先:排查不能让故障时间更长。
- 只有 SSH 权限和基本的云监控(CPU、内存、磁盘曲线),没有完整的监控平台。
- 需要给出书面复盘。
方案设计
按“止血 → 采集现场 → 定位 → 修复 → 预防”推进:
- 止血:确认影响范围(Nginx 日志里 5xx 比例),如果最近有发布先回滚;必要时重启服务恢复,但重启前先采集现场。
- 采集现场:运行
diag.sh保存系统快照;保存最近的 Nginx 错误日志、应用日志;对 Go 服务抓 pprof(heap、goroutine)。 - 定位(本例的实际线索):
- Nginx 错误日志:
connect() failed (111: Connection refused) while connecting to upstream→ 502 是因为 API 进程不在了; journalctl -k:Out of memory: Killed process 2345 (api)→ API 被 OOM 杀掉,systemd 自动拉起,所以“自己恢复了”;- 内存曲线:API 内存从启动开始持续上涨;
- pprof heap:大部分内存在一个“导出任务结果”的 map 里——导出结果放在内存里等用户下载,从不清理。
- 同时发现
df -h磁盘使用 92%:应用日志没有切割。
- Nginx 错误日志:
- 修复:导出结果改为写入对象存储 / 临时文件并设置过期清理;本地缓存加容量上限;配置 logrotate;systemd 设置
MemoryMax让问题早暴露、不拖垮整机。 - 预防:增加内存、磁盘、5xx 比例的告警;发布后观察内存曲线;复盘文档记录时间线和改进项。
关键代码
现场采集(只读):
bash diag.sh api > /tmp/diag-$(date +%F-%H%M).txt
journalctl -k --since "today" | grep -i -E "oom|killed process"
tail -n 200 /var/log/nginx/error.log | grep -E "upstream|connect\(\)"
curl -s -o /tmp/heap.pb.gz http://127.0.0.1:6060/debug/pprof/heap
go tool pprof -top -inuse_space /tmp/heap.pb.gz | head -n 15
logrotate 配置 /etc/logrotate.d/api:
/srv/api/logs/*.log {
daily
rotate 14
compress
missingok
notifempty
copytruncate
}
带上限的本地缓存(修复思路,替代“无限增长的 map”):
// 最多保留 max 个条目,超过时淘汰最早写入的(简单的 FIFO,按需换成 LRU)
type BoundedCache[K comparable, V any] struct {
mu sync.Mutex
max int
order []K
data map[K]V
}
func NewBoundedCache[K comparable, V any](max int) *BoundedCache[K, V] {
return &BoundedCache[K, V]{max: max, data: make(map[K]V)}
}
func (c *BoundedCache[K, V]) Put(k K, v V) {
c.mu.Lock()
defer c.mu.Unlock()
if _, ok := c.data[k]; !ok {
c.order = append(c.order, k)
if len(c.order) > c.max {
delete(c.data, c.order[0])
c.order = c.order[1:]
}
}
c.data[k] = v
}
系统快照脚本和日志分析脚本见 examples/log-analysis。
取舍
| 做法 | 优点 | 缺点 |
|---|---|---|
| 直接重启 | 恢复最快 | 丢失现场,问题会再次发生 |
| 先采集现场再重启(本方案) | 能定位根因 | 多花一两分钟 |
| 加内存 | 立即缓解 | 掩盖泄漏,只是延后 |
systemd MemoryMax 限制 |
防止拖垮整机、问题早暴露 | 进程会更早被杀,需要配合告警 |
易错点
- 只看应用日志,没看内核日志:OOM 在应用日志里没有任何痕迹。
- 把“自己恢复了”当成没问题:其实是 systemd 自动重启掩盖了崩溃。
- 删除正在写的日志文件释放空间:空间不会释放,用
copytruncate或让程序重新打开文件。 - 复盘只写“已修复”:要写时间线、根因、为什么没有提前发现、改进项和负责人。
面试怎么讲
“有一次服务间歇性 502,自己又恢复。我先看 Nginx 错误日志,是连接后端被拒绝,说明进程挂了;再查内核日志发现是 OOM 被杀,systemd 自动重启所以看起来自己恢复了。用 pprof 看 heap,发现导出结果一直放在内存 map 里没清理。修复后又加了缓存上限、日志切割、内存和 5xx 告警,并写了复盘。”
如果你在前端岗位做过类似的线上排查(比如内存泄漏导致页面卡死、静态资源 404),也可以用同样的结构讲:现象 → 证据 → 根因 → 修复 → 预防。