nodejs中大量短时间定时器实现方案?
发布于 7 年前 作者 im-here 10244 次浏览 来自 问答

最近用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

25 回复

node-schedule 的场景和你这个发道具的的确不太合适,虽然我赶脚是使用姿势问题 其实我赶脚settimeout都不需要,你说到的这个场景,领取道具和得知下一个的冷却都是主动出发的,除了第一次以外都可以在领道具的时候返回 另外你这个架构我感觉可能会有一些小问题哇,比如单核,比如使用内存存储各种道具。。 有需要的话可以加个qq沟通一下

@aojiaotage 非常感谢,加你了

看起来还是像使用姿势不对, 不过每一个道具都绑这么复杂的东西没必要,用个集合维护下道具,处理下吃道具的event感觉就完事了啊.

为啥不在数据库或缓存里记验证的时间戳,用客户端来算倒计时,服务端只在使用的时候做验证.

@178220709 @baka397 实现上却是有点问题,我是接盘侠。听了各位的意见,正在修改中。。。

(nodejs)游戏服务器能不用定时器尽量不用定时器,能用时间戳尽量用时间戳

可以看看这个时间轮库

这两天看到的文章,就实现了一个库出来

https://github.com/axetroy/wheel-timer.js

好处就是,用一个定时器,不断的tick(假设一秒tick一次)。

然后这个定时器转一周之后,在tick另一个定时器。

例如:

  • 秒定时器
  • 分定时器
  • 时定时器

每秒转动一下秒定时器,秒定时器转动一周(60),则转动一下分定时器…一次类推。

每个定时器,每次转动,就代表有数据要过期…

可以在回调里面监听哪些数据过期,然后对数据进行处理…

1.jpg

举个例子,道具冷却时间。这个道具可以无限使用,冷却时间为60秒.

const HashWheelTimer = require('wheel-timer');

// 定义秒定时器
class SecondTimer extends HashWheelTimer {
  constructor() {
    super(60);
  }
  tick() {
    const beforeRound = this.round;
    super.tick();
    const afterRound = this.round;

    if (afterRound > beforeRound) {
	  // 如果秒针走了一圈,则让分针进入到下一刻度
      minute.tick();
    }
  }
}

// 定义分定时器
class MinuteTimer extends HashWheelTimer {
  constructor() {
    super(60);
  }
}

const second = new SecondTimer();
const minute = new MinuteTimer();

const item = {name: "道具名"}

// 这里调用使用物品
function userItem(){
  // 判断物品是否在冷却
  if(minute.map.has(item)){
    // 如果是在冷却,说明不能使用物品
	return
  }
  
  // 在这里使用物品
  // 比如增加xxx属性之类
  
  minute.add(item) // 把物品调用放在分表里面,等待冷却
}

minute.on('tick', (data) => {
  // 分针被推动了,说明前一分钟的数据(道具)已过期
  // 这些数据已经从时间转盘中删除,然后调用这个回调函数
});

setInterval(() => {
  second.tick(); // 每间隔1秒,推动一下秒针
}, 1000);

@imhered 恭喜楼主有进展了,你可以看看我的这个文,一种因闭包引发的内存泄露 希望对你有帮助

查了一下 node-schedule 底层还是 settimeout实现的哦·

@ipengyo 嗯。 下午4点做了一次修改,bug应该修了。 跑到现在目前还没出现问题

@imhered 老哥求指教 具体是哪里出了毛病 让我也学习一下, 我最近也准备使用nodejs做游戏服务端~~

@ipengyo 问题很简单,每次执行一个task后cancel掉就行了,我创建了好多只执行一次的task我以为执行完了就不用管了。看了源码,就算是执行一次的task也必须cancel,不然这个task一直会存在于cache中

不过建议能不用它的还是不用,感觉我目前的需求用这个有点浪费,我是接手别人的项目才写成这样的

@nobody 时间戳是定时器的完美取代品!

@baka397老哥, 时间戳怎么代替node-scheduled 能稍微多说一点吗

@axetroy 我在自己项目中也造了个tick ,原因是很多地方用到setTimeout ,想找个地方统一管理一下,结果写成:

 co(function *(resume){
    while(0 == 0){
	   yiled setTimeout(resume,1000);
	   // check the job list
	}
 })()

这样的形式,后来想想有点傻逼,setTimeout 实质也是个大的loop ,而且还是C++的,感觉有点多此一举

@yyrdl 我说的方案,是适用于大量的延迟操作。

这样不用开N个定时器,只需要一定定时器,不断的推其他定时器,就可以了。

本质上,只有一个setInterval

回到顶部