Next.js项目内存泄露排查

April 25, 2024

在实现NextBlogger项目的过程中,遇到了一个内存泄露的问题,总结下排查思路及过程。

现象

项目在部署到腾讯轻量云服务器(总内存2G)后,随着用户的访问,next-server的内存占用会随时间缓慢上涨,最高时占用到1.2G左右,然后就会出现内存错误,甚至无法ssh登录。

此时从监控面板上看高峰期机器总内存占用已经到1.5G,磁盘IO大涨,CPU使用率到80%左右。猜测是内存过大受到了swap的影响。由于无法登录只能重启机器。

排查

总体思路上先考虑增加临时解决方法,避免线上的影响。然后逐步排查原因。

预防方法

既然已经确认是内存的问题,就在内存使用量上增加一些限制措施,临时解决内存过大时无法访问的问题。主要增加了以下措施。

总内存限制

使用第三方进程管理工具监控服务内存大小,当内存使用量超过阈值时直接重启。由于next-server编译后的启动过程还是非常快的,用户侧几乎无感知。

node的进程管理工具不少,选择了之前使用过的 pm2。配置当内存占用超过800M时逻辑重启。配置文件如下:

Loading...

next内存缓存

next的各种缓存(见 Next.js缓存能力 )主要使用了文件系统和部分内存,用来存储静态页、脚本、图片等。由于next没有暴露内存的具体使用大小,无法确认是否有影响,所以此处也计划做些大小限制。

next.config中暴露了cacheMaxMemorySize参数用来控制内存缓存的大小。

Loading...

看文档默认配置为50M,问题不大,稍微做了一些调整。

PS:next提供的这个配置就是黑盒,也不知道cache大小控制实际上是否生效了。

其他手段

  • next没有提供类似nginx类似的日志能力,所以增加了日志中间件。用于记录访问时间、url等。目的是为了结合内存监控已经访问记录,判断内存上涨是否跟特定url有关。

问题定位

使用外部内存缓存

服务的大致流程是将notion block读取出来,然后按照DFS方式渲染blocks树。同时为了减少延迟,使用unstable_cache将block数据缓存。除App路由使用到的数据外,next也会换成Page数据,所以初步怀疑内存泄露的原因可能是部分内存缓存没有释放,或者使用缓存的姿势不对。排查代码没有发现异常,所以初步想法是将next缓存迁移到外部第三方存储。

next暴露了CacheHandler用来存储App路由、Page的缓存数据,这里使用了第三方lib `@neshca/cache-handler` 将缓存存储到redis。

迁移完成后可以看到redis中已经存储了大量的page以及route数据,尤其page数据还挺大的:

Loading...

上线后发现内存整体大小有下降,但是泄露的趋势并无明显变化。可见此处不是根本原因。继续定位。

使用Chrome DevTools

由于代码层面没有发现异常,所以只能使用Chrome DevTools工具来排查可能的点。大致流程:

  • 使用 inspect 模式启动服务: NODE_OPTIONS='--inspect' next start
  • 获取第一次快照
  • 为了加速现场复现,使用压测工具批量访问页面。
  • 获取第二次快照。并对比第一次结果。

反复多次尝试之后,发现了疑点:

大量的代码块string存在嫌疑。在渲染notion的code blocks时使用了shikijs库:

Loading...

怀疑问题就出在了shikijs的渲染结果没有被释放上面。但是反复观看了shikijs的文档,也没有发现使用异常的地方。

但是Devtools已经判断是这里了,所以将代码中的code block渲染直接屏蔽掉,然后再次执行压测。这次没有出现内存泄露的问题,内存稳定在100M左右。可以确认问题就是shikijs的引入导致。

修复

搜了一圈之后,在git上也有人在next项目中遇到过类似的问题。

原因没有官方回复,但是有人给出的建议是避免每次渲染复用hilighter对象,提升性能。应该是在next server component中使用时每次创建hilighter会有内存泄露的风险。

所以调整后的代码是使用shiki的默认hilighter,避免每次创建。

修改完毕后发现内存上涨明显没有之前的严重了。运行过程中,shikiji还是会占用200左右的内存,按照官方文档的说法shikijs为了避免性能开销,也会按照theme、language缓存hilighter。属于正常现象,内存大小也可以接受。

如果还想进一步减少内存占用,可以按照官方文档的做法,按需加载theme、lauguage等优化手段,比较麻烦,后续再考虑。

总结

  • 线上问题首先考虑缓解方法,避免定时时长不确定造成的影响。
  • 内存问题先考虑使用内存定位工具,避免肉眼扫描代码。
  • 大胆猜测,尤其是next这种迭代较快的框架。不确定库兼容性上会出现什么问题。
See all postsSee all posts