Fabirc.js Node 服务内存泄漏问题调查
因为一些前端的限制问题,需要在服务端实现文字转图片的功能,然后前端再将这个图片渲染到画布。这里采用的是 Fabric.js + Node.js 的方案。
由于调用量一直很小所以也没有出现问题,但是最近随着调用量变大,发现内存会缓慢增长。最后有一个 pod 居然占用了 11G 的内存,导致服务器一直在告警,才发现了这个问题。
临时的解决方案是先对 pod 增加了内存限制,超出则会自动 kill 这个 pod 并重启。
resources:
limits:
cpu: "2"
memory: 2Gi
requests:
cpu: 50m
memory: 256Mi
Node 服务的大概处理流程如下:
首先是需要找出是哪里出了问题。这里其实走了不少弯路,也记录一下。
- 以复盘的角度来看,最大的失误是没有在什么都不修改的情况下看一下在本地能否复现内存泄漏的现象。
- 不要使用 WebStorm 的调试功能来做测试,因为调试功能会引入一些额外的开销,导致内存一直上升。
由于刚开始采用的是在 WebStorm 中修改代码,然后测试,导致这些测试结果其实都是错的,浪费了相当多的时间。
之后是直接在命令行中启动 node 服务,删除其中一个步骤,执行一次压力测试。结果比较意外,虽然删除一些步骤后,内存消耗会小一些,但是所有的情况下都没有出现内存泄漏的现象。一度怀疑是不是本地压力测试的力度不够。
然后猜测是不是本地 Node.js 环境的问题,于是将线上使用的 docker 镜像拉到本地运行,结果也是一样,没有出现内存泄漏。
本地没法再现就只能到线上测试环境去测试了,于是对比了下线上环境和本地环境的区别,怀疑是不是 SkyWalking 性能检测插件 skywalking-backend-js 的问题。尝试升级和移除 skywalking-backend-js 并部署到线上测试环境然后测试,结果是没有任何改善,仍然会出现内存泄漏。
最后只能将前面的检验方法移到线上测试环境重新执行了一遍:依次删除一些步骤,然后部署到线上测试环境。最终在移除了注册字体处理后,内存终于稳定了。
有问题的代码如下:
fabric.nodeCanvas.registerFont(path.resolve(process.cwd(), `fonts/${fontName}.ttf`), {
family: fontName,
weight: 'regular',
style: 'normal'
});
这段代码本来是为了动态加载字体,以避免出现字体渲染不对的问题,但同时也导致了同一个字体会被重复加载多次。关于这一点其实刚开始走查代码的时候也发现了这个问题,但是由于当时测试方法的失误,再加上本地测试时并没有发现内存泄漏,所以就忽略了。
这里需要提一下测试时发现的一个比较奇怪的现象。刚开始在本地测试时,加载的字体并没有起作用(字体文件路径是对的,而且字体是加载了的),更奇怪的是后来不知道为什么突然就好了,加载的字体又可以正常渲染了。这个估计是因为操作系统导致的(测试时使用的是 Windows 环境,线上是 Linux 环境)。也可能是因为操作系统不一样,所以本地一直没有出现内存泄漏。
找到了出问题的地方,剩下的就好解决了。为了仍然支持动态加载字体,添加了一个数组来保存已加载的字体。加载字体时先执行一个判断,如果数组中已经存在这个字体,就不再重复加载了。
if (registeredFonts.indexOf(fontName) !== -1) {return}
fabric.nodeCanvas.registerFont(path.resolve(process.cwd(), `fonts/${fontName}.ttf`), {
family: fontName,
weight: 'regular',
style: 'normal'
});
registeredFonts.push(fontName)
另外,为了提高性能,在项目启动时增加了预热处理:将本地已有的字体全部注册到 fabric 中。
最终,Node.js 服务启动后大约占用了 100M 左右的内存,随后会稳定在 150M 左右。