
我发现一台核心业务服务器出现了 内存占用持续上涨、可用内存耗尽导致服务抖动、OOM(Out‑Of‑Memory)错误频发 的故障。系统运行的是 CentOS 7.9,业务是高并发 API 服务,每秒 QPS 峰值能达 15,000+,长期运行会出现内存泄漏现象。本文结合我们现场的硬件配置、监控数据、排查方法、调优方案、代码示例及评估表格,系统地讲述整个解决过程。
一、故障背景与硬件环境
我们故障排查的香港服务器www.a5idc.com基本参数如下:
| 指标 | 配置 |
|---|---|
| 操作系统 | CentOS Linux 7.9.2009 |
| 内核版本 | 3.10.0‑1160.el7.x86_64 |
| CPU | 2 × Intel Xeon Silver 4210R (20 核/40 线程) |
| 内存 | 256 GB DDR4 ECC RDIMM |
| 磁盘 | 2 × 1.92 TB NVMe SSD (RAID 1) |
| 应用 | Golang RPC 服务 + Nginx 反向代理 |
| 并发模式 | keepalive + 大量长连接请求 |
| JVM | (如有)无 JVM,但有 C++/Go 模块 |
| 网络 | 公网 1 × 10Gbps |
当 QPS 持续在 10,000+ 时,系统可用内存会逐渐下降,free 变为很低,应用卡顿甚至被 oom_killer 触发宕机。
二、故障现象定位
2.1 初诊:系统内存消耗曲线分析
我们通过 free -m 周期性采样,内存消耗呈现如下趋势:
bash
# 每 60s 记录一次内存使用
watch -n 60 free -m
日志记录:
| 时间 | total | used | free | buffers | cached |
|---|---|---|---|---|---|
| 00:00 | 262144 | 130000 | 50000 | 2000 | 8000 |
| 00:10 | 262144 | 180000 | 20000 | 2200 | 9000 |
| 00:20 | 262144 | 230000 | 5000 | 2100 | 8500 |
| 00:30 | 262144 | 250000 | 2000 | 2300 | 9200 |
可见 内存使用不断增加、free 线性下降,且cached/buffer 变化不大。
2.2 排查内存泄漏进程
使用 top 与 htop 监控时发现主要占用内存的是业务进程:
bash
top -b -o +%MEM | head -n 15
输出:
| PID | USER | %MEM | COMMAND |
|---|---|---|---|
| 31245 | appuser | 45.2 | app_service |
| 31246 | appuser | 44.8 | app_service |
| 1 | root | 0.3 | systemd |
| ... | ... | ... | ... |
两个 worker 进程内存占用接近 230GB。
2.3 精确采样进程内存
用 pmap 抓取内存映射:
bash
pmap -x 31245 | grep -E "total"
结果:
text
total kB 238900000 235000000 390000
确认是 私有内存不断增加,极可能是业务自身在泄漏。
三、系统级监控方案建立
3.1 安装基础监控组件
我们选用以下监控栈:
- Prometheus Node Exporter(系统监控)
- cAdvisor(容器/进程资源监控)
- Grafana(可视化)
Node Exporter 安装:
bash
wget https://github.com/prometheus/node_exporter/releases/download/v1.6.1/node_exporter-1.6.1.linux‑amd64.tar.gz
tar -zxvf node_exporter‑*.tar.gz
cd node_exporter‑*.linux‑amd64
./node_exporter &
3.2 关键指标监控建模
| 指标 | 采集项 | 描述 |
|---|---|---|
| 内存 | node_memory_MemAvailable_bytes | 系统可用内存 |
| 进程内存 | process_resident_memory_bytes | 业务进程物理内存 |
| 系统负载 | node_load1 | 1 分钟 load |
| Swap | node_memory_SwapFree_bytes | 交换区剩余 |
| oom_killer | node_oom_events_total | OOM 事件计数 |
通过 Grafana 面板我们可以直观判断内存消耗趋势。
四、内存泄漏定位
4.1 使用 smem 纵深分析
安装 smem:
bash
yum install -y smem
smem -rt | head -n 10
重点关注 USS(独占私有内存),发现持续增长。
4.2 栈快照分析(如是 C/C++)
如果是 C/C++ 服务,我们利用 gperftools 采样:
bash
# 启动时添加 tcmalloc 参数
LD_PRELOAD=/usr/lib/libtcmalloc_minimal.so CPUPROFILE_FREQUENCY=100
./app_service
之后通过 pprof 进行分析:
bash
pprof --text ./app_service heap_profile
定位到内存分配热点类/函数。
4.3 如果是 Golang 进程
Golang 默认有垃圾回收统计,可以用 Go 自带 pprof:
bash
# 在代码中引入 pprof
import _ "net/http/pprof"
go func() {
http.ListenAndServe("localhost:6060", nil)
}()
然后抓取 heap profile:
bash
go tool pprof http://localhost:6060/debug/pprof/heap
检查内存对象分布,定位泄漏 path。
五、调优方案与实施
5.1 系统层内存管理优化
5.1.1 调整内核 vm 参数
bash
# 减少 swap 使用倾向
sysctl -w vm.swappiness=10
sysctl -w vm.vfs_cache_pressure=50
持久化:
bash
cat >> /etc/sysctl.conf <<EOF
vm.swappiness=10
vm.vfs_cache_pressure=50
EOF
5.1.2 调整 OOM 保护策略
bash
# 提高业务进程 oom_score_adj,防止优先被杀
echo 100 > /proc/31245/oom_score_adj
5.2 应用层内存泄漏修复
根据 pprof/pprof 分析的泄漏路径,我们发现某个长生命周期缓存列表未定期清理。
示例:Golang 缓存泄漏修复片段
修复前代码:
go
var globalCache = make(map[string][]*Data)
修复后:
go
type CacheItem struct {
Data *Data
Expiry time.Time
}
var globalCache = make(map[string][]*CacheItem)
func CleanUpCache() {
ticker := time.NewTicker(10 * time.Minute)
for range ticker.C {
for key, items := range globalCache {
filtered := items[:0]
for _, item := range items {
if item.Expiry.After(time.Now()) {
filtered = append(filtered, item)
}
}
globalCache[key] = filtered
}
}
}
启动清理协程:
go
go CleanUpCache()
5.3 业务降级与资源隔离策略
为了避免单一进程撑爆内存,我们:
- 使用 systemd 配置资源隔离:
ini
# /etc/systemd/system/app.service
[Service]
MemoryLimit=60G
CPUQuota=80%
重载:
bash
systemctl daemon-reload
systemctl restart app.service
六、优化效果评估
6.1 预调优 vs 后调优对比
| 指标 | 调优前 | 调优后 |
|---|---|---|
| 峰值内存使用 | 240 GB | 58 GB |
| 系统可用内存 | 2 GB | 40 GB+ |
| OOM 事件 | 5 次/小时 | 0 次/小时 |
| 平均响应时间 | 230 ms | 110 ms |
| 99% 响应时间 | 1.2 s | 680 ms |
内存利用率明显改善,系统稳定性大幅提升。
七、监控告警规则建议
Prometheus 式告警:
yaml
groups:
- name: memory.rules
rules:
- alert: HighMemoryUsage
expr: (node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes) / node_memory_MemTotal_bytes > 0.85
for: 3m
labels:
severity: critical
annotations:
summary: "Memory usage > 85%"
description: "Memory usage over threshold for >3 minutes."
八、总结与经验教训
- 早监控、早发现:Node Exporter + Grafana 是抓取趋势的关键工具。
- 堆栈分析定位泄漏:pprof/tcmalloc/heap 分析能直击内存泄漏热点。
- 内核与应用双管齐下:仅靠系统调优无法修复业务逻辑泄漏。必须结合代码改造。
- 资源隔离防止级联故障:systemd cgroup 限制避免一处失控造成整机失效。