如何解决香港服务器长时间运行导致的内存泄漏问题?一线运维经验分享与技术解决方案

我作为A5IDC香港服务器租用与托管服务的运维工程师,我对“服务器重启 = 解决问题”的做法一直存疑。我们的客户通常希望服务能 365×24 h 不间断运行——尤其是跨境电商、直播、短视频或游戏等对可用性要求极高的业务。
几个月前,有一次因为预估不足,一个客户将一个 Node.js 服务部署在我们的一台 32 G RAM 的香港物理服务器上。业务看起来平稳:白天峰值 PV/并发不算高,夜间几乎空闲。服务上线后稳定运行了将近三个月,直到某天我们收到告警:系统剩余可用内存一直下降,Swap 开始使用, eventually 出现 OOM(Out‑Of‑Memory)崩溃。
重启一次虽然暂时恢复了服务,但重启不是办法。客户也不希望我们后台随时重启他的业务——这对跨境电商/直播/短视频这种业务形态是不可接受的。
于是,我带上笔记本、KVM 控制台,亲自去香港机房 — 开始了一场“逐行代码 + 逐进程 + 逐页面”的内存泄漏排查。下面,就是我当时的全过程记录。
一、现场硬件 / 系统 / 服务基本情况
| 项目 | 配置 / 描述 |
|---|---|
| 服务器型号 | Dell R650 (双 2.8 GHz Xeon Gold, 2 CPU, 共 32 核) |
| 内存 | 32 GB DDR4 ECC 2666 MHz |
| 操作系统 | Ubuntu 22.04 LTS, 64-bit, 内核 5.15.x |
| 存储 | NVMe SSD 1 TB (主要存放日志与临时文件) + 4 TB RAID1 SATA 用于静态文件 |
| 业务服务 | Node.js v18 + Express + 自研业务逻辑 + Redis 作为缓存 / session 存储 + MySQL 后端 |
| 部署方式 | systemd 管理,服务名为 myapp.service,ExecStart 为 node /opt/myapp/app.js |
上线初期一切正常 —— CPU、磁盘 I/O 使用率很低;RAM 使用量稳定在 ~4‑5 GB(包括 OS 缓存),Swap 使用几乎为 0。
但是经过 8–10 周后,我开始在监控系统中观察到下列异常趋势:
- free → available RAM 每天缓慢减少,趋势线基本是直线下降
- Swap usage 从 0 逐渐升至 1‑2 GB,然后不断上涨
- 系统的负载(load average)没有明显提升,CPU 利用率也正常 —— 说明不是业务量增加,也不是 I/O 峰值
- 初步判断:极可能是内存泄漏(memory leak)。
二、基本监控与初步定位 —— 用系统工具确认“谁在吃内存”
使用 top / htop / /proc 观察进程内存使用
我通过 htop 观察到 node 进程的 RSS(常驻集大小)不断增长。又通过 /proc/[pid]/status 发现:
- VmRSS 随时间逐渐增加(从启动时 ~100 MB → 数月后达到 5‑6 GB)
- VmSize/VmData 也同步增长
- 这是典型的用户态内存泄漏表现。
同时,我还用 smem 看了进程的 USS / PSS / RSS,确认这块增长主要是该进程 独占 (USS) 的内存。
用 pmap / smaps 分析内存分配结构
为了确认泄漏是否来自 heap(堆)区,我定期对该 node 进程运行:
pmap <PID> | tail -n 10
以及:
cat /proc/<PID>/smaps
发现随着时间推移:
- [ anon ] 区域(匿名内存 / heap)不断扩大
- shared-library 部分变化很小
这基本可以排除内存来自共享库或 mmap 文件映射,而更可能是堆(heap)分配未释放。
三、深入分析 —— 为什么会泄漏 & 是什么导致的?
经过初步确认后,我怀疑是业务代码或第三方包有内存管理缺陷,但因为 Node.js / JavaScript 有 GC(垃圾回收),理应自动回收 — 為何还会泄漏?
我进一步分析,原因主要如下:
- JavaScript 层没有明确调用 native addon 或 C/C++ 扩展,但是底层可能通过 Buffer / native module 使用了 C++ 内存分配/释放;
- 或者业务逻辑中对 Buffer / Buffer pools / 缓存对象 / 全局变量 / singleton / 长生命周期对象 引用了大量数据,导致 GC 无法回收;
- 还有可能是某些长期缓存 / 数据结构(例如用户 session cache、连接池、消息队列、Timer/Interval、Closure 等)不断累积未释放。
此外,我还考虑了更深层的问题:底层 C runtime 的内存分配策略。之前遇到过,正如在 stl + glibc 环境下,即使业务看起来 free 了 std::list / vector,也可能因为 glibc 内部的 memory‑pool / fast‑bin / top‑chunk 策略,内存并未及时返还给操作系统,出现“伪泄漏 (memory fragmentation / retention)”现象。
如果底层 native module 或某些 C++ 扩展也用了类似机制,就会造成即便 JS 层没有持续增长,但系统 RAM 却不断下降。
鉴于 Node.js 中经常被用于 Buffer、native module、stream / buffer pool / file‑I/O 等,这种可能性不能忽视。
四、深度排查方法 —— 工具 + 实验 + 复现
为彻底定位,我采取了以下手段。
(1) 在预生产/测试环境复现 + 用工具分析
我将服务复制到我们另外一台同类型的测试服务器上(同样 32 GB RAM),并构造一个 长时间稳定但并不高负载 的请求模拟脚本 —— 模拟用户登录 / 浏览 / session 维持 / 缓存命中 / 关键逻辑循环,以尽量还原真实运行环境。
然后我用了下列工具:
Valgrind (memcheck) :对 Node.js + native module 的可执行文件重新编译或使用带 debug 符号的版本,在测试服务器上执行,通过 valgrind 检测是否有 malloc → free 不配对、非法内存访问/泄漏。
(如果 Node.js 本身不能方便用 Valgrind 跑,也考虑在 native addon 模块层面做单独测试)
如果 native module 调用较复杂,也可以考虑用更高级的 memory‑profiling / instrumentation 工具。比如一些商业 / 商用工具(如 purify/Insure++ 等),或者自研 debug hook / custom memory‑tracking。
结果:在测试环境用 valgrind 运行 48–72 小时后,发现 某个 native addon —— 用于处理压缩/加密/图片处理的模块,在处理 buffer 时,对部分 buffer 的释放存在遗漏 (free),导致 native heap 不断增长。
也就是说——并非 JS 层代码的问题,而是底层 native 模块里 “忘记释放 buffer / memory” 的问题。
(2) 如果无法方便用 Valgrind,也可以做 “观察 + 定期触发释放 + 内存 trim” 实验
如果因为性能或者兼容性原因不能用 Valgrind(生产环境通常不能在正常服务上跑 Valgrind,因为会显著降低性能甚至挂掉服务),可以考虑在服务中加一个定时 “内存整理 / 释放 + 套用 malloc_trim / manual GC + cache 清理” 的机制。
例如,在 Node.js 的主逻辑中,定期调用逻辑清理缓存(例如清空过期 session、清理不再使用的 Buffer pool、释放 large‑object 等);如果 native addon 支持,也调用其 cleanup 接口。
对于底层 C runtime (glibc) 的 “fast‑bins + top‑chunk” 的问题,也可以在适当时机手动触发 malloc_trim(),让释放的小块内存真正归还给操作系统。
不过,这种“缓释 + 手动回收”方法只能算是权宜之计,不是根本解决方案 — 根本还是要修复 native module 的内存管理逻辑。
五、我当时遇到的坑 —— 现场故事 & 血泪教训
说到这里,有几处“现场经验”值得记录,也提醒后来者注意:
在生产环境上用 Valgrind 很难落地 —— 虽然 Valgrind 是定位 native 内存泄漏的利器,但它对性能影响巨大 (通常只剩正常速度的 ~20–25%)。把生产线上正常业务丢给 Valgrind,很可能导致响应超时、服务堵塞,甚至触发其他问题。
我曾尝试在夜间低峰时刻做一次测试,但结果因为响应延迟太高,客户报警 — 不得不立刻终止测试。
日志 / 监控不足 —— 我们之前对于内存使用变化的监控只有整体 free / used / swap 的告警,但没有对单个进程 (尤其是 native addon) 的 heap growth 做历史趋势监控。导致我们直到 OOM 才触发警报。
后来补上监控后,再次类似问题才可能在“用内存持续增长但仍低于阈值”的阶段就被预警到。
隐藏很深 / 间歇触发 —— 泄漏并不是每一次 Buffer 使用都必现,有可能是某些极端 code path(比如图片批量处理、压缩、加密、解密、编码转换等)引起 native memory 分配,然后忘记释放。平时流量低甚至可能几年都不触发,但一旦流量高或功能被触发,就可能快速释放不当 —— 导致 memory leak。这个不稳定、隐蔽性强,让重现变得非常困难。
glibc internal behavior 导致误判 —— 有时候即便 native addon free 了内存,也因为 glibc 的 fast‑bin / top‑chunk 策略,小块 free 的内存不会立即归还给操作系统。导致 free 之后,free + delete 看似正确,但系统整体 RAM 无下降 (仍被保留) —— 看起来像“泄漏没被释放”。这个坑曾让我以为 addon 仍有问题,但实际上只是 runtime 的内存池策略。
— 也就是说,即便 code 写得“没有 leak”,也可能由于 glibc 内部机制导致 “memory retention / fragmentation” → 对于长期稳定服务,也是致命的。
六、最终解决方案 —— 修复 + 预防 + 长期监控
基于上面排查结果,我采取了如下综合措施,并成功让服务稳定运行再也没有 OOM。
修复 native module 内存释放逻辑
联系了模块作者 / 维护团队,指出在某些 code path 下 buffer 没有 free / release;要求在每一次 alloc + use → ensure free / release(包括异常路径、error handler、early return 等)
本地 fork & patch:在所有可能分支末尾加上释放逻辑 (例如 free() / delete[] / appropriate dealloc),并对 buffer pools 做 clear / shrink 操作
修复后,在测试环境用 Valgrind/stress 模拟 1 周,heap size 基本稳定,没有持续上升趋势。
在服务中加入定期内存整理机制
为了防止遗漏或 glibc 内部缓存导致的 memory‑pool retention,我在服务中加入了如下逻辑(伪代码 / Node.js + native addon):
// 每天凌晨 3 点执行
function dailyMaintenance() {
// 1. 清理缓存 (session / temporary data / buffer pool)
cache.clearExpired();
// 2. 调用 native addon 的 cleanup 接口 (如果有的话)
if (nativeAddon.cleanup) {
nativeAddon.cleanup();
}
// 3. 强制执行 V8 GC (若业务允许)
if (global.gc) {
global.gc();
}
// 4. 如果系统支持,也尝试触发 memory trim(需要通过 addon 或扩展支持)
// e.g., nativeAddon.malloc_trim && nativeAddon.malloc_trim();
}
setInterval(dailyMaintenance, 24 * 3600 * 1000);
对于 Node.js,我启用了 --expose-gc,并适当调用 global.gc();
如果你使用的是 native addon,也应提供类似 cleanup / shrink 的接口;
对于系统层面 (glibc) 的 pool,也建议在低峰期执行一次 malloc_trim(),将空闲的小块合并并归还内存。
增加监控 & 告警机制
为生产环境加入针对关键服务 (node + native addon) 的定期内存指标采集 (RSS / USS / Heap / Native heap if possible),并画 trend graph;
如果内存使用量持续增长(超过设定阈值,例如超过 baseline 的 20% 且持续 24h),触发告警;
强制要求:非到 OOM 之前,就必须进行人工介入 / 自动重启 / 暂停服务 / 转流。
所有第三方 native module 都做版本审核 / code review
对于任何业务 critical 的模块,只要是 native addon,要特别注意其内存管理是否健壮;必要时,做 stress + long‑running 测试。
七、长年运行 ≠ “放任不管” — 内存泄漏必须主动治理
这次实战让我深刻体会到:在香港物理服务器上跑长期在线服务,尤其是对稳定性要求高、业务持续时间长的跨境电商、直播、短视频、游戏等场景中,“先部署上线就走人”是一种赌博。内存泄漏往往像“慢性病”,早期不痛不痒,不会立即影响业务,但时间一长,就可能引发 OOM 崩溃,给客户带来灾难性影响。
因此,我强烈建议:
在上线前做长期 (weeks~months) 的稳定性测试,包含内存 leak 检查;
对 native module 做严格审查,对内存管理有保障;
上线后持续监控内存使用趋势,做到“有异常立即警报”;
必要时加入定期清理 + 内存池整理机制 (GC + malloc_trim);
只有如此,才能真正做到“我们是专业的香港服务器托管商 / 运维团队”,保障客户业务的 24/7 稳定运行 — 而不是靠 “重启 + 暂时缓解” 的临时措施。