Next.js项目内存泄露排查
April 25, 2024
在实现NextBlogger项目的过程中,遇到了一个内存泄露的问题,总结下排查思路及过程。
现象
项目在部署到腾讯轻量云服务器(总内存2G)后,随着用户的访问,next-server的内存占用会随时间缓慢上涨,最高时占用到1.2G左右,然后就会出现内存错误,甚至无法ssh登录。
此时从监控面板上看高峰期机器总内存占用已经到1.5G,磁盘IO大涨,CPU使用率到80%左右。猜测是内存过大受到了swap的影响。由于无法登录只能重启机器。
排查
总体思路上先考虑增加临时解决方法,避免线上的影响。然后逐步排查原因。
预防方法
既然已经确认是内存的问题,就在内存使用量上增加一些限制措施,临时解决内存过大时无法访问的问题。主要增加了以下措施。
总内存限制
使用第三方进程管理工具监控服务内存大小,当内存使用量超过阈值时直接重启。由于next-server编译后的启动过程还是非常快的,用户侧几乎无感知。
node的进程管理工具不少,选择了之前使用过的 pm2。配置当内存占用超过800M时逻辑重启。配置文件如下:
next内存缓存
next的各种缓存(见 Next.js缓存能力 )主要使用了文件系统和部分内存,用来存储静态页、脚本、图片等。由于next没有暴露内存的具体使用大小,无法确认是否有影响,所以此处也计划做些大小限制。
next.config中暴露了cacheMaxMemorySize参数用来控制内存缓存的大小。
看文档默认配置为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数据还挺大的:
上线后发现内存整体大小有下降,但是泄露的趋势并无明显变化。可见此处不是根本原因。继续定位。
使用Chrome DevTools
由于代码层面没有发现异常,所以只能使用Chrome DevTools工具来排查可能的点。大致流程:
- 使用 inspect 模式启动服务: NODE_OPTIONS='--inspect' next start
- 获取第一次快照
- 为了加速现场复现,使用压测工具批量访问页面。
- 获取第二次快照。并对比第一次结果。
反复多次尝试之后,发现了疑点:
大量的代码块string存在嫌疑。在渲染notion的code blocks时使用了shikijs库:
怀疑问题就出在了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这种迭代较快的框架。不确定库兼容性上会出现什么问题。