nodejs 中大量短时间定时器实现方案?

2017-12-06 11:12:27 +08:00
 imherer

最近用 node+socket.io 做手游服务端,在小范围的线上测试的时候发现有内存泄漏,一开始是以为是 socket.io 的问题 最后这几天通过 heapdump 分析,基本定位了是node-scheduled这个库导致的(不知道是不是我使用问题)

heapdump 分析

程序刚启动打一个 snapshot,运行 3 个小时左右再打一个 snapshot,对比这个 2 个 snapshot 发现有大量的 closure (新增 100W+),都是 node-scheduled 这个库产生的

服务器用的是双核 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

7005 次点击
所在节点    Node.js
28 条回复
bramblex
2017-12-06 11:24:33 +08:00
建议用 setinterval , 全局共用一个 setinterval.

内存管理一定要把他当成 c 一样小心处理.
janxin
2017-12-06 11:29:45 +08:00
让客户端去做这个逻辑?服务端只记录上次吃掉的时间和这次吃掉是否合法就行了
imherer
2017-12-06 11:36:00 +08:00
@bramblex 全局用一个 setinterval ?那是 1s 执行一次这个 setinterval,然后每次执行的时候去检测有没有道具啥的,去告诉客户端吗?
imherer
2017-12-06 11:37:24 +08:00
@janxin 关键是客户端的表现是,一开始这个道具在地图里,然后被吃掉后,这个道具就没了,服务端倒计时完毕,告诉客户端道具刷新了,然后客户端就在同样的位置刷出同样的道具。 让客户端去做的话应该不好实现,而且估计容易作弊
haozes
2017-12-06 11:37:33 +08:00
如果你的回调一直在而且越来越多,就很耗 CPU 和内存。解决的思路是让你的 NODE 进程里未处理的回调变少

我认为你用 settimeout,setinverval 可能都还是有问题。
建议:
1.先看下有多少用户量,如果用户量少,那还是代码问题,
2.首先利用多核,前面另个负载后面起两个进程就行了,PM2 的利用多核具体没用过。
3.其次建议你用 redis 的通知机制来实现定时器,让 redis 通知你的 node
imherer
2017-12-06 11:41:05 +08:00
@haozes
1.用户量很少的,因为是小范围的测试,同时在线就 100 多点。所以是代码问题
2.按道理单个正常情况下支持个上百人应该没问题吧,所以就暂时没考虑负载均衡了(正式上线肯定是需要的)
3.redis 通知这个一开始有考虑过,后来想到一个道具就 10 多秒就恢复了,就没用 redis 这一层
janxin
2017-12-06 11:43:46 +08:00
@imherer 逻辑放在客户端的话也一样的,定时器放在客户端去做了而已。如果是联机游戏,因为吃掉道具是一定需要通知服务器更新其他用户信息的,只是需要你服务端来验证这次吃掉是不是真吃掉了,本地作弊是没有用的。
imherer
2017-12-06 11:44:59 +08:00
@janxin 嗯,感谢。 是联机游戏,我和客户端讨论下。
keenwon
2017-12-06 12:27:53 +08:00
和 #1 类似,之前做过一个秒杀系统,页面上一大堆倒计时,统一用一个 setInterval 处理 https://github.com/keenwon/Tictac
jysperm
2017-12-06 12:47:50 +08:00
给事件维护一个有序列表(按事件的触发时间排序),然后循环处理列表最前的事件(应该最先被触发的事件),如果该触发的事件已经处理完了,就按照列表里接下来第一个事件来设置一个 setInterval。可以直接用 Redis 的 ZSET,很容易拓展。
janxin
2017-12-06 12:56:33 +08:00
@imherer 只是一种实现思路,不过现阶段直接用 setinterval 可以试一下
solee
2017-12-06 13:00:03 +08:00
换个思路,因为是消费过段时间通知的模式,感觉采用定时消息队列的方式也可行;吃掉后服务器产生一条定时消息,并设置通知时间;服务器收到消息恢复对应的道具就 ok 了。减少定时器对性能的消耗。
imherer
2017-12-06 13:02:34 +08:00
@janxin 嗯。就想#1 说的,全局一个 setinterval,因为我这里的最小单位是 1s,那就让这个 setinterval 1s 执行一次,每次执行的时候 我就去处理是否有相应的逻辑触发?
imherer
2017-12-06 13:03:26 +08:00
@solee 对消息队列这块比较陌生,有相应的资料吗?
solee
2017-12-06 13:08:50 +08:00
@imherer 这是阿里云的定时消息和延时消息 https://help.aliyun.com/document_detail/43349.html?spm=5176.doc29532.6.565.h0vG6B

应用角度讲现成方案已经很成熟,当然如果是想研究的话 kafka 等都是消息队列的实现。我也没深入研究过。
Nitromethane
2017-12-06 13:13:02 +08:00
是否考虑单独一个 node 进程服务所有的定时事件,这样子即使定时服务倒了,也不至于影响主要业务
wxsm
2017-12-06 13:47:54 +08:00
node-schedule 确实是存在上述问题,我司已被坑过了。解决方案是另起进程跑 schedules,定时重启。
fds
2017-12-06 13:51:11 +08:00
说的是下面这个库?结尾没有 d 呀?
https://github.com/node-schedule/node-schedule
看起来这个库主要是运行那种定期执行的任务,可能设计时没太注意大量使用的情况?不过看代码 cancel 以后应该删除了,没看出什么问题……

其实几秒钟的时间用 setTimeout 更方便呀,不知道为什么舍近求远用这个库呢?
imherer
2017-12-06 13:56:08 +08:00
@fds 嗯,不好意思,文中多打了一个 d
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 的大小打出来看看?

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/412402

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX