nodejs 中大量短时间定时器实现方案? - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
imherer
V2EX    Node.js

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

  •  1
     
  •   imherer 2017-12-06 11:12:27 +08:00 7753 次点击
    这是一个创建于 2948 天前的主题,其中的信息可能已经有所发展或是发生改变。

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

    heapdump 分析

    程序刚启动打一个 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

    28 条回复    2017-12-06 15:46:43 +08:00
    bramblex
        1
    bramblex  
       2017-12-06 11:24:33 +08:00   1
    建议用 setinterval , 全局共用一个 setinterval.

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

    我认为你用 settimeout,setinverval 可能都还是有问题。
    建议:
    1.先看下有多少用户量,如果用户量少,那还是代码问题,
    2.首先利用多核,前面另个负载后面起两个进程就行了,PM2 的利用多核具体没用过。
    3.其次建议你用 redis 的通知机制来实现定时器,让 redis 通知你的 node
    imherer
        6
    imherer  
    OP
       2017-12-06 11:41:05 +08:00
    @haozes
    1.用户量很少的,因为是小范围的测试,同时在线就 100 多点。所以是代码问题
    2.按道理单个正常情况下支持个上百人应该没问题吧,所以就暂时没考虑负载均衡了(正式上线肯定是需要的)
    3.redis 通知这个一开始有考虑过,后来想到一个道具就 10 多秒就恢复了,就没用 redis 这一层
    janxin
        7
    janxin  
       2017-12-06 11:43:46 +08:00   1
    @imherer 逻辑放在客户端的话也一样的,定时器放在客户端去做了而已。如果是联机游戏,因为吃掉道具是一定需要通知服务器更新其他用户信息的,只是需要你服务端来验证这次吃掉是不是真吃掉了,本地作弊是没有用的。
    imherer
        8
    imherer  
    OP
       2017-12-06 11:44:59 +08:00
    @janxin 嗯,感谢。 是联机游戏,我和客户端讨论下。
    keenwon
        9
    keenwon  
       2017-12-06 12:27:53 +08:00   1
    和 #1 类似,之前做过一个秒杀系统,页面上一大堆倒计时,统一用一个 setInterval 处理 https://github.com/keenwon/Tictac
    jysperm
        10
    jysperm  
       2017-12-06 12:47:50 +08:00   2
    给事件维护一个有序列表(按事件的触发时间排序),然后循环处理列表最前的事件(应该最先被触发的事件),如果该触发的事件已经处理完了,就按照列表里接下来第一个事件来设置一个 setInterval。可以直接用 Redis 的 ZSET,很容易拓展。
    janxin
        11
    janxin  
       2017-12-06 12:56:33 +08:00
    @imherer 只是一种实现思路,不过现阶段直接用 setinterval 可以试一下
    solee
        12
    solee  
       2017-12-06 13:00:03 +08:00
    换个思路,因为是消费过段时间通知的模式,感觉采用定时消息队列的方式也可行;吃掉后服务器产生一条定时消息,并设置通知时间;服务器收到消息恢复对应的道具就 ok 了。减少定时器对性能的消耗。
    imherer
        13
    imherer  
    OP
       2017-12-06 13:02:34 +08:00
    @janxin 嗯。就想#1 说的,全局一个 setinterval,因为我这里的最小单位是 1s,那就让这个 setinterval 1s 执行一次,每次执行的时候 我就去处理是否有相应的逻辑触发?
    imherer
        14
    imherer  
    OP
       2017-12-06 13:03:26 +08:00
    @solee 对消息队列这块比较陌生,有相应的资料吗?
    solee
        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 等都是消息队列的实现。我也没深入研究过。
    Nitromethane
        16
    Nitromethane  
       2017-12-06 13:13:02 +08:00   1
    是否考虑单独一个 node 进程服务所有的定时事件,这样子即使定时服务倒了,也不至于影响主要业务
    wxsm
        17
    wxsm  
       2017-12-06 13:47:54 +08:00   1
    node-schedule 确实是存在上述问题,我司已被坑过了。解决方案是另起进程跑 schedules,定时重启。
    fds
        18
    fds  
       2017-12-06 13:51:11 +08:00   1
    说的是下面这个库?结尾没有 d 呀?
    https://github.com/node-schedule/node-schedule
    看起来这个库主要是运行那种定期执行的任务,可能设计时没太注意大量使用的情况?不过看代码 cancel 以后应该删除了,没看出什么问题……

    其实几秒钟的时间用 setTimeout 更方便呀,不知道为什么舍近求远用这个库呢?
    imherer
        19
    imherer  
    OP
       2017-12-06 13:56:08 +08:00
    @fds 嗯,不好意思,文中多打了一个 d
    fds
        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 的大小打出来看看?
    imherer
        21
    imherer  
    OP
       2017-12-06 14:24:51 +08:00
    @fds 旧的 task 没有 cancel,因为我这里的 task 都是只执行一次的,即在某个时间点执行一次,执行之后才会有新的 task 产生,我产生新的 task 是直接把新旧 taks 覆盖掉。不知道这里有没有问题。 因为都是只执行一次而且都是已经执行过了,task 再 cancel 已经没意义了吧?
    chairuosen
        22
    chairuosen  
       2017-12-06 14:31:39 +08:00
    @imherer 也许这就是问题,你知道只执行一次,但是 node 不知道呀,闭包一直留着呢
    imherer
        23
    imherer  
    OP
       2017-12-06 14:35:22 +08:00
    @chairuosen 听你这么一说好像有点道理。 也没深入了解过这个库,我以为这种只执行一次的任务是不需要 cancel 的,我先把这部分修改了
    fds
        24
    fds  
       2017-12-06 14:38:33 +08:00
    @imherer 建议你看下源码,只有调用 cancel,才会从 scheduledJobs 这个字典里删除,否则就一直缓存在里面了。

    这个设计勉强也可以理解,因为这个库就主要是为了那些会重复执行的 task 设计的。当然在新语法下,这个 scheduledJobs 应该用 WeakMap 比较好。

    总之,你那需求应该用 setTimeout ……当然我觉得更好的做法是与客户端同步服务器时间,然后是直接告知在某某时间点某某道具开始有效,不必用定时器。生效前客户端就消耗了道具应该视为无效。正确消耗了,通知下一个时间点即可。
    imherer
        25
    imherer  
    OP
       2017-12-06 14:45:30 +08:00
    @fds 嗯。让客户端来做倒计时服务端验证应该是最好的。 我看了下 snapshot 确实是好多 scheduledJob 都在_cache 里,应该就是没 cancel 掉。 我先本地测试下,thanks。
    123s
        26
    123s  
       2017-12-06 15:35:49 +08:00
    node-scheduled 以前用来做定时爬虫,跑一段时间就挂,我就猜是它
    123s
        27
    123s  
       2017-12-06 15:40:52 +08:00
    好吧,看错了,不是这个
    jyf
        28
    jyf  
       2017-12-06 15:46:43 +08:00
    可以考虑用 libev 定制个这种外部服务 比自己维护好点 到点了来触发下
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     801 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 29ms UTC 20:49 PVG 04:49 LAX 12:49 JFK 15:49
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86