V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
imherer
V2EX  ›  Node.js

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

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

    最近用 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   ·   实用小工具   ·   1072 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 23:17 · PVG 07:17 · LAX 15:17 · JFK 18:17
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.