阶段2(下):运维专题 Docker/Nginx/Linux/CI-CD/Git 的课程、企业场景、面试题和可运行示例
This commit is contained in:
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),也可以用同样的结构讲:现象 → 证据 → 根因 → 修复 → 预防。
|
||||
Reference in new issue
Block a user