「回顾2022,展望2023,我正在参与2022年终总结征文大赛活动」
前言
在 Nodejs
服务端开发的场景中,内存走漏
肯定是最令人头疼的问题;
可是只需项目一向在开发迭代,那么呈现 内存走漏
的问题肯定不可避免,仅仅呈现的时刻迟早罢了。所以体系性掌握有用的 内存走漏
排查方法是一名Nodejs
工程师最基础、最核心的才能。
内存走漏处理的难点就是如何能在很多的功用、函数中找到详细是哪一个功用中的哪一个函数的第多少行到多少行引起了内存走漏。
很遗憾现在市面上没有能够轻松定位内存走漏的东西,所以很多初度遇到这种问题的工程师会感到茫然,一下子不知道该如何处理。
这儿我以22年的一次排查 内存走漏
的案例共享一下我的处理思路。
问题描绘
2022 Q4
某天,研制用户群中反应咱们的研制平台不能拜访,后台中呈现了很多的反常任务未完成。
第一反应就是或许呈现了内存走漏还好服务接入了监控(prometheus
+ grafana
),在grafana
监控面板中发现在 10.00 后内存一向在涨没有下来过呈现了显着的数据走漏。
阐明:
process memory
:rss
(Resident Set Size),进程的常驻内存巨细。heapTotal
: V8 堆的总巨细。heapUsed
: V8 堆已运用的巨细。external
: V8 堆外的内存运用量。在
Nodejs
中能够调用大局方法process.memoryUsage()
获取这些数据其中heapTotal
和heapUsed
是 V8 堆的运用状况,V8 堆是Node.js
中 JavaScript 目标存储的当地。而external
则表明非 V8 堆中分配的内存,例如 C++ 目标。rss
则是进程一切内存的运用量。一般看监控数据的时分重点关注heapUsed
的目标就行了
内存走漏类型
内存走漏首要分为:
- 大局性走漏
- 局部性走漏
其实不管是大局性内存走漏还是局部性的内存走漏,要做的都是尽或许缩小扫除规模。
大局性内存走漏
大局性内容走漏呈现一般高发于:中间件
与组件
中,这种类型的内存走漏排查起来也是最简单的。
很遗憾我在 2022 Q4
中遇到的内存走漏不属于这个类型,所以还得依照局部性走漏的思路进行剖析。
二分法排查
这种类型我就不讲其它科学的剖析方法了,这种状况下我认为运用二分法排查是最快的。
流程流程:
- 先注释一半的代码(削减一半
中间件
、组件
、或其它共用逻辑的运用) - 随便选择一个接口或新写一个测试接口进行压测
- 假如呈现内存走漏,那么走漏点就在当时运用的代码之中,若没有走漏则走漏点呈现在
- 然后一向循环往复上述流程大约 20 ~ 60 min 必定能够定位到内存走漏的详细位置
2020 年的时分我在做基于
Nuxt
SSR 运用时,上线前压测发现运用内存走漏,判断定为大局性的走漏之后,选用二分法排查大约花了 30min 就成功定位了问题。
当时走漏的原因是咱们在服务端运用axios
导致的走漏,后来一致axios
相关的全换成node-fetch
后就处理了,从此换上了axios PDST
后来肯定不会在Node
服务中运用axios
了
局部性内存走漏排查
大多数内存走漏的状况都是局部性的走漏,走漏点或许存在与某个中间件
、某个接口
、某个异步任务
中,因为这样的特性它的排查难度也较大。这种状况都会做 heapdump
进行剖析。
这儿首要讲我这个案例中的思路关于heapdump
的详细阐明我放在下个阶段,
Heap Dump
:堆转储, 后面部分都运用heapdump
表明,做heapdump
的东西和教程也十分多比如:chrome、vscode、heapdump 这个开源库。我用的 heapdump 库做的网上教程十分多这儿不打开了。
局部性内存走漏排查需求必定的内存走漏排查经历,每次遇到都把它当成对自己的一次磨砺,这样的经历堆集多了今后排查内存走漏问题会越来越快。
1. 确认内存走漏呈现的时刻规模
这一点十分重要,明确了这一点能够大幅度缩小排查规模。
经常会呈现这种状况,这个迭代做了A、B、C 三个功用,压测时或上线后呈现了内存走漏。那么就能够直接确认,内存走漏发生小这三个新的功用之中。这种状况下就不需求十分费事的去出产做 heapdump
咱们在本地经过一些东西就能够很轻松的剖析定位出内存走漏点。
因为咱们 20年Q4
的一些特殊状况,当咱们发现存在内存走漏的时分已经很难确认内存走漏初度呈现在什么时刻点了,只能大约确认在 1 月的时刻内。这一个月中咱们又经历了一个大版别迭代,假如一一排查这些功用与接口本钱必然十分高。
所以还需求结合更多的数据进行进一步剖析
2. 收集 heapdump 数据
- 出产启动时
node
添加--expose-gc
,这个参数会向大局注入gc()
方法,便利手动触发 GC 获取更精确的堆快照
数据 - 这儿我加入了两个接口并带上了自己的专属权限,
- 手动触发 GC
- 打印堆快照
-
heapdump
- 项目启动后第一次打印快照数据
- 内存上涨 100M 后:先触发 GC,再第二次打印堆快照数据
- 内存挨近临界时再次触发 GC 然后打印堆快照
收集堆快照数据时需求特别注意的一些点!
- 在
heapdump
时 Node 服务会中止,根据当时服务器内存巨细这个时刻会在 2 ~ 30min 左右。在出产环境做heapdump
需求和运维一同拟定合理的战略。我在这儿是运用了主、备两个pod
, 当主pod
停掉之后,事务恳求会经过负载均衡到备用pod
由此保证出产事务的正常进行。(这个进程必定是一个与运维密切配合的进程,毕竟heapdump
玩抽还需求经过他们拿到服务器中堆快照
文件)- 上述挨近临界点打印快照仅仅一个模糊的描绘,假如你试过就知道等十分挨近临界点再打印内存快照就打印不出来了。所以挨近这个度需求自己掌握。
- 做至少 3 次
heapdump
(实际上为了拿到最详细的数据我是做了 5 次)
3. 结合监控面板的数据进行剖析
需求你的运用服务接入监控,我这儿运用是运用prometheus
+ grafana
做的监控, 首要监控服务的以下目标
-
QPS
(每秒恳求拜访量) ,恳求状况,及其拜访途径 -
ART
(平均接口呼应时刻) 及其拜访数据 -
NodeJs
版别 -
Actice Handlers
(句柄) -
Event Loop Lag
(事件滞后) - 服务进程重启次数
- CPU 运用率
- 内存运用:
rss
、heapTotal
、heapUsed
、external
、heapAvailableDetail
只有
heapdump
数据是不够的,heapdump
数据十分不流畅,就算在可视化东西的加持下也难以精确认位问题。这个时分我是结合了grafana
的一些数据一同看。
我的剖析处理结果
因为当时的对快照数据丢失了,我这儿模仿一下当时的场景。
-
经过
grafana
监控面看看到内存一向在涨一向下不来,但一起我也注意到,服务中的句柄
数也在疯涨一向不掉。
-
这是我回顾了一下呈现走漏的那一个月中新增的功用怀疑或许是在运用
bull
消息行列组件造成的内存走漏。先去剖析了相关运用代码,但并看不出那里写的有问题导致了内存走漏, 结合 1 中句柄走漏的问题感觉是在运用bull
后需求手动的去开释某些资源,在这个时分还不太确认详细原因。 -
然后对 5 次的
heapdunmp
数据进行了剖析,数据导入chrome
对 5 次堆快照进行比照后,发现每次创建行列后 TCP、Socket、EventEmitter 的事件都没有被开释到。到这儿基本能够确认是因为对bull
的运用不标准导致的。在bull
通常不会频频创建行列,行列占用的体系资源并不会被自动开释,若有需求,需手动开释。
-
在调整完代码后从头进行了压测,问题处理。
Tips: Nodejs 中的
句柄
是一种指针,指向底层体系资源(如文件、网络连接等)。句柄答应 Node.js 程序拜访和操作这些资源,而无需直接与底层体系交互。句柄能够是整数或目标,详细取决于 Node.js 库或模块运用的句柄类型。常见句柄
:
fs.open()
回来的文件句柄net.createServer()
回来的网络服务器句柄dgram.createSocket()
回来的 UDP socket 句柄child_process.spawn()
回来的子进程句柄crypto.createHash()
回来的哈希句柄zlib.createGzip()
回来的紧缩句柄
heapdump 剖析总结
通常很多人第一次拿到堆快照
数据是懵的,我也是。在看了网上很多的剖析技巧结合本身实战后总结了一些比较好用的技巧,一些基础的运用教程这儿就不讲了。这儿首要讲数据导入 chrome
后如何看图;
Summary 视图
看这个视图的时分一般会先对 Retained Size 进行排查,然后观察其中目标的巨细与数量,有经历的工程师,能够快速判断出某些目标数量反常。在这个视图中除了关心自己界说的一些目标之外, 一些容易发生内存走漏的目标也需求注意如:
TCP
Socket
EventEmitter
global
Comparison 视图
假如经过 Summary
视图, 不能定位到问题这时咱们一般会运用 Comparison
视图。经过这个视图咱们能比照两个堆快照中目标个数、与目标占有内存的改变;
经过这些信息咱们能够判断在一段时刻(某些操作)之后,堆中的目标与内存改变的数值,经过这些数值咱们能够找出一些反常的目标。经过这些目标的名称属性或作用能够缩小咱们内存走漏的排查规模。
在 Comparison
视图中选择两个堆快照,并在它们之间进行比较。您能够检查哪些目标在两个堆快照之间新增,哪些目标在两个堆快照之间削减,以及哪些目标的巨细发生了改变。
Comparison
视图还答应检查目标之间的联系,以及目标的详细信息,如类型、巨细和引证计数。经过这些信息,能够了解哪些目标是导致内存走漏的原因。
Containment 视图
显现了目标之间的一切可达的引证联系。每个目标都被表明为一个圆点,并由一条线条连接到它的父目标。经过这种方式能够检查目标之间的层次联系,并了解哪些目标是导致内存走漏的原因。
Statistics 视图
这个图很简单不打开讲了
内存走漏场景
- 大局变量:大局变量不会被回收
- 缓存:运用了内存密集型的第三方库如
lru-cache
存的太多就会导致内存不够用,在 Nodejs 服务中建议运用redis
替代lru-cache
- 句柄走漏:调用完体系资源没有开释
- 事件监听
- 闭包
- 循环引证
总结
- 服务需求接入监控,便利第一时刻确认问题类型
- 判断内存走漏是大局性的还是局部性的
- 大局性内存走漏运用二分法快速排查定位
- 局部内存走漏
- 确认内存走漏呈现时刻点,快速定位问题功用
- 采堆快照数据,至少 3 次
- 结合监控数据、堆快照数据、与呈现走漏事时刻点内的新功用对内存走漏点进行定位
遇到内存走漏的问题不要害怕,多堆集内存走漏问题的排查经历处理经历多了找起来就十分快了。每次处理之后做复盘总结回头再多看看
堆快照
数据利于更快的堆集相关经历
其它
- 压测东西:wrk