阶段2(下):运维专题 Docker/Nginx/Linux/CI-CD/Git 的课程、企业场景、面试题和可运行示例

This commit is contained in:
Shikui Wang committed 2026-10-23 15:36:06 +08:00
1 parent 5eb67c5d08
commit b8010a74f3
42 files changed
+3533 -55

No files matched your search

@@ -0,0 +1,118 @@
---
id: devops.linux.scenario.incident-troubleshooting
title: 线上故障排查:接口变慢、磁盘写满、进程被 OOM 杀掉
type: scenario
domain: devops
stack: [linux, go, nginx]
tags: [故障排查, OOM, 磁盘满, pprof, 复盘]
level: intermediate
prerequisites: [devops.linux.troubleshooting]
est_hours: 2
related: [devops.linux.interview, devops.nginx.interview]
week: [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
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`:
```text
/srv/api/logs/*.log {
daily
rotate 14
compress
missingok
notifempty
copytruncate
}
```
带上限的本地缓存(修复思路,替代“无限增长的 map”):
```go
// 最多保留 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](../examples/log-analysis/)。
## 取舍
| 做法 | 优点 | 缺点 |
| --- | --- | --- |
| 直接重启 | 恢复最快 | 丢失现场,问题会再次发生 |
| **先采集现场再重启(本方案)** | 能定位根因 | 多花一两分钟 |
| 加内存 | 立即缓解 | 掩盖泄漏,只是延后 |
| systemd `MemoryMax` 限制 | 防止拖垮整机、问题早暴露 | 进程会更早被杀,需要配合告警 |
## 易错点
- 只看应用日志,没看内核日志:OOM 在应用日志里没有任何痕迹。
- 把“自己恢复了”当成没问题:其实是 systemd 自动重启掩盖了崩溃。
- 删除正在写的日志文件释放空间:空间不会释放,用 `copytruncate` 或让程序重新打开文件。
- 复盘只写“已修复”:要写时间线、根因、为什么没有提前发现、改进项和负责人。
## 面试怎么讲
> “有一次服务间歇性 502,自己又恢复。我先看 Nginx 错误日志,是连接后端被拒绝,说明进程挂了;再查内核日志发现是 OOM 被杀,systemd 自动重启所以看起来自己恢复了。用 pprof 看 heap,发现导出结果一直放在内存 map 里没清理。修复后又加了缓存上限、日志切割、内存和 5xx 告警,并写了复盘。”
如果你在前端岗位做过类似的线上排查(比如内存泄漏导致页面卡死、静态资源 404),也可以用同样的结构讲:现象 → 证据 → 根因 → 修复 → 预防。