最近用 node+socket.io 做手游服务端,在小范围的线上测试的时候发现有内存泄漏,一开始是以为是 socket.io 的问题
最后这几天通过 heapdump 分析,基本定位了是node-scheduled
这个库导致的(不知道是不是我使用问题)
程序刚启动打一个 snapshot,运行 3 个小时左右再打一个 snapshot,对比这个 2 个 snapshot 发现有大量的 closure (新增 100W+),都是 node-scheduled 这个库产生的
node-scheduled 主要用在游戏内道具的倒计时,一局游戏 3 分钟时长,6 个玩家为一个房间,一个房间内有 20 个左右的道具 游戏一开始每个道具我都用 node-scheduled 启动一个 task 来通知客户端这个道具在什么时候出现 当玩家在游戏过程中吃掉一个道具后,客户端告诉我,我就重新给这个道具一个 task 用来倒计时多长时间后这个道具恢复(一个道具被吃掉后一般 10 多秒就恢复了)
每个房间有一个 object 对象 room,我将这个房间内的所有道具的 task 都绑定在这个 room 对象上,当这局游戏结束的时候,我首先把房间内所有的道具 task 都 cancel 掉,然后再销毁这个 room 对象 但不知道为什么通过 snapshot 查看到还是有 100W+的新增
服务器用的是双核 4G,这个服务器上只允许了这个一个 node 进程,在运行 4 小时左右 loop delay 已经达到 20ms+了,当前进程 CPU 快超过 80%了,运行时间越长 loop delay 能达到上百,进程 CPU 超过 100%
1.上述定时器我改用 settimeout 和 setinterval 会不会好一些? 2.对于只跑一个 node 进程来说 买多核有用吗?
node:8.4,node-scheduled:1.2.4
1
bramblex 2017-12-06 11:24:33 +08:00 1
建议用 setinterval , 全局共用一个 setinterval.
内存管理一定要把他当成 c 一样小心处理. |
2
janxin 2017-12-06 11:29:45 +08:00 2
让客户端去做这个逻辑?服务端只记录上次吃掉的时间和这次吃掉是否合法就行了
|
3
imherer OP @bramblex 全局用一个 setinterval ?那是 1s 执行一次这个 setinterval,然后每次执行的时候去检测有没有道具啥的,去告诉客户端吗?
|
4
imherer OP @janxin 关键是客户端的表现是,一开始这个道具在地图里,然后被吃掉后,这个道具就没了,服务端倒计时完毕,告诉客户端道具刷新了,然后客户端就在同样的位置刷出同样的道具。 让客户端去做的话应该不好实现,而且估计容易作弊
|
5
haozes 2017-12-06 11:37:33 +08:00 1
如果你的回调一直在而且越来越多,就很耗 CPU 和内存。解决的思路是让你的 NODE 进程里未处理的回调变少
我认为你用 settimeout,setinverval 可能都还是有问题。 建议: 1.先看下有多少用户量,如果用户量少,那还是代码问题, 2.首先利用多核,前面另个负载后面起两个进程就行了,PM2 的利用多核具体没用过。 3.其次建议你用 redis 的通知机制来实现定时器,让 redis 通知你的 node |
6
imherer OP @haozes
1.用户量很少的,因为是小范围的测试,同时在线就 100 多点。所以是代码问题 2.按道理单个正常情况下支持个上百人应该没问题吧,所以就暂时没考虑负载均衡了(正式上线肯定是需要的) 3.redis 通知这个一开始有考虑过,后来想到一个道具就 10 多秒就恢复了,就没用 redis 这一层 |
7
janxin 2017-12-06 11:43:46 +08:00 1
@imherer 逻辑放在客户端的话也一样的,定时器放在客户端去做了而已。如果是联机游戏,因为吃掉道具是一定需要通知服务器更新其他用户信息的,只是需要你服务端来验证这次吃掉是不是真吃掉了,本地作弊是没有用的。
|
9
keenwon 2017-12-06 12:27:53 +08:00 1
和 #1 类似,之前做过一个秒杀系统,页面上一大堆倒计时,统一用一个 setInterval 处理 https://github.com/keenwon/Tictac
|
10
jysperm 2017-12-06 12:47:50 +08:00 2
给事件维护一个有序列表(按事件的触发时间排序),然后循环处理列表最前的事件(应该最先被触发的事件),如果该触发的事件已经处理完了,就按照列表里接下来第一个事件来设置一个 setInterval。可以直接用 Redis 的 ZSET,很容易拓展。
|
12
solee 2017-12-06 13:00:03 +08:00
换个思路,因为是消费过段时间通知的模式,感觉采用定时消息队列的方式也可行;吃掉后服务器产生一条定时消息,并设置通知时间;服务器收到消息恢复对应的道具就 ok 了。减少定时器对性能的消耗。
|
13
imherer OP @janxin 嗯。就想#1 说的,全局一个 setinterval,因为我这里的最小单位是 1s,那就让这个 setinterval 1s 执行一次,每次执行的时候 我就去处理是否有相应的逻辑触发?
|
15
solee 2017-12-06 13:08:50 +08:00 1
@imherer 这是阿里云的定时消息和延时消息 https://help.aliyun.com/document_detail/43349.html?spm=5176.doc29532.6.565.h0vG6B
应用角度讲现成方案已经很成熟,当然如果是想研究的话 kafka 等都是消息队列的实现。我也没深入研究过。 |
16
Nitromethane 2017-12-06 13:13:02 +08:00 1
是否考虑单独一个 node 进程服务所有的定时事件,这样子即使定时服务倒了,也不至于影响主要业务
|
17
wxsm 2017-12-06 13:47:54 +08:00 1
node-schedule 确实是存在上述问题,我司已被坑过了。解决方案是另起进程跑 schedules,定时重启。
|
18
fds 2017-12-06 13:51:11 +08:00 1
说的是下面这个库?结尾没有 d 呀?
https://github.com/node-schedule/node-schedule 看起来这个库主要是运行那种定期执行的任务,可能设计时没太注意大量使用的情况?不过看代码 cancel 以后应该删除了,没看出什么问题…… 其实几秒钟的时间用 setTimeout 更方便呀,不知道为什么舍近求远用这个库呢? |
20
fds 2017-12-06 14:19:00 +08:00
@imherer 我死循环不断新建 job 然后 cancel 没有发现有问题
``` let schedule = require("node-schedule"); let count = 0; function scheduleOne() { if (count % 10000 === 0) { console.log(count); } ++count; let j = schedule.scheduleJob("0 * * * 0,4-6", function() { console.log("Today is recognized by Rebecca Black!"); }); setImmediate(() => { j.cancel(); scheduleOne(); }); } scheduleOne(); ``` 看你描述“当玩家在游戏过程中吃掉一个道具后,客户端告诉我,我就重新给这个道具一个 task 用来倒计时多长时间后这个道具恢复”这里旧的 task 有 cancel 么?定时把 node-schedule 里的 scheduledJobs 的大小打出来看看? |
21
imherer OP @fds 旧的 task 没有 cancel,因为我这里的 task 都是只执行一次的,即在某个时间点执行一次,执行之后才会有新的 task 产生,我产生新的 task 是直接把新旧 taks 覆盖掉。不知道这里有没有问题。 因为都是只执行一次而且都是已经执行过了,task 再 cancel 已经没意义了吧?
|
22
chairuosen 2017-12-06 14:31:39 +08:00
@imherer 也许这就是问题,你知道只执行一次,但是 node 不知道呀,闭包一直留着呢
|
23
imherer OP @chairuosen 听你这么一说好像有点道理。 也没深入了解过这个库,我以为这种只执行一次的任务是不需要 cancel 的,我先把这部分修改了
|
24
fds 2017-12-06 14:38:33 +08:00
@imherer 建议你看下源码,只有调用 cancel,才会从 scheduledJobs 这个字典里删除,否则就一直缓存在里面了。
这个设计勉强也可以理解,因为这个库就主要是为了那些会重复执行的 task 设计的。当然在新语法下,这个 scheduledJobs 应该用 WeakMap 比较好。 总之,你那需求应该用 setTimeout ……当然我觉得更好的做法是与客户端同步服务器时间,然后是直接告知在某某时间点某某道具开始有效,不必用定时器。生效前客户端就消耗了道具应该视为无效。正确消耗了,通知下一个时间点即可。 |
25
imherer OP @fds 嗯。让客户端来做倒计时服务端验证应该是最好的。 我看了下 snapshot 确实是好多 scheduledJob 都在_cache 里,应该就是没 cancel 掉。 我先本地测试下,thanks。
|
26
123s 2017-12-06 15:35:49 +08:00
node-scheduled 以前用来做定时爬虫,跑一段时间就挂,我就猜是它
|
27
123s 2017-12-06 15:40:52 +08:00
好吧,看错了,不是这个
|
28
jyf 2017-12-06 15:46:43 +08:00
可以考虑用 libev 定制个这种外部服务 比自己维护好点 到点了来触发下
|