Files
zhuangang/topics/devops/linux/scenarios/incident-troubleshooting.md
T

5.0 KiB
Raw Blame History

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
linux
go
nginx
故障排查
OOM
磁盘满
pprof
复盘
intermediate
devops.linux.troubleshooting
2
devops.linux.interview
devops.nginx.interview
14

线上故障排查:接口变慢、磁盘写满、进程被 OOM 杀掉

业务背景与问题

一个小团队维护的 SaaS 系统,部署在两台云服务器上(Nginx + Go API + PostgreSQL),没有专职运维。某天上午:用户反馈页面很慢,部分请求 502;中午服务自己恢复了;下午又出现。你被要求“查清楚原因并保证不再发生”。

约束

  • 业务优先:排查不能让故障时间更长。
  • 只有 SSH 权限和基本的云监控(CPU、内存、磁盘曲线),没有完整的监控平台。
  • 需要给出书面复盘。

方案设计

按“止血 → 采集现场 → 定位 → 修复 → 预防”推进:

  1. 止血:确认影响范围(Nginx 日志里 5xx 比例),如果最近有发布先回滚;必要时重启服务恢复,但重启前先采集现场。
  2. 采集现场:运行 diag.sh 保存系统快照;保存最近的 Nginx 错误日志、应用日志;对 Go 服务抓 pprof(heap、goroutine)。
  3. 定位(本例的实际线索):
    • 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%:应用日志没有切割。
  4. 修复:导出结果改为写入对象存储 / 临时文件并设置过期清理;本地缓存加容量上限;配置 logrotate;systemd 设置 MemoryMax 让问题早暴露、不拖垮整机。
  5. 预防:增加内存、磁盘、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),也可以用同样的结构讲:现象 → 证据 → 根因 → 修复 → 预防。