上一篇 下一篇 分享链接 返回 返回顶部

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

发布人:Minchunlin 发布时间:2025-12-03 09:16 阅读量:534


我作为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 稳定运行 — 而不是靠 “重启 + 暂时缓解” 的临时措施。

目录结构
全文