timer icon indicating copy to clipboard operation
timer copied to clipboard

目标和参考资料

Open guonaihong opened this issue 4 years ago • 9 comments

目标

  • 用go使用5级时间轮实现定时器
  • 如有可能效率追求fast fast fast

参考资料

  • https://blog.csdn.net/musicml/article/details/103878367 (good)
  • https://www.zhihu.com/question/38427301 (good)
  • https://www.cnblogs.com/newbeeyu/p/9022623.html(good)

论文

http://www.cs.columbia.edu/~nahum/w6998/papers/ton97-timing-wheels.pdf

参考项目

  • linux kernel https://github.com/torvalds/linux/blob/v4.7/kernel/time/timer.c (linux 2.6内核引入时间轮)
  • skynet https://github.com/cloudwu/skynet/blob/master/skynet-src/skynet_timer.c
  • timingwheel (借鉴API名称) https://github.com/RussellLuo/timingwheel

参考书本

  • 书名 深入linux内核架构(Professional Linux Kernel Architecture)(第15章时间管理) tv1 0-255

  • 范围 tv2 2的8次方-2的14次方-1 tv3 2的14次方-2的20次方-1 tv4 2的20次方-2的26次方-1 tv5 2的26次方-2的32次方-1

  • 移动 第一组的内容在最多256个时间周期之后就会耗尽,必须将后续各组的定时器依次前推,重新补足第一组。在第一组的索引位置恢复到初始位置0之后,会将第二组中一个数组项的所有定时器补充到第一组。这种做做法,解释了为什么各组选择了不同的时间间隔。因为第一组的各数组项可能有256个不同的到期时间,而第二组中一个数组项的数据就足以填充整个第一组的整个数组。该道理同样适用于后续各组。第三组的一个数组项的数据同样足以填充整修第二个组,第四组的一个数组项也足以填充整修第三组,而第五组的一个数组项也足以填充整个第四组。

后续各组的数组位置并非随机选择的,其中的索引项仍然发挥了作用。但索引项的值不再是每个时钟周期加1,而是第256的i-1次方

  • cascade函数用于从指定组取得定时器补充到前一组

guonaihong avatar May 28 '20 10:05 guonaihong

感谢。、

kingname avatar Jun 23 '20 11:06 kingname

@kingname :blush:

guonaihong avatar Jun 24 '20 01:06 guonaihong

我的使用场景是这样,面对很多网络连接,先声明一个NewTimer ,然后用一个协程去Run,而后每一个连接打开的时候,使用TM.ScheduleFunc() 对每个连接进行不同时间的心跳,测试的时候是每个连接到来,每5秒下发一次心跳,一个连接没问题,超过一个连接,其他的连接发送了几次心跳后会莫名停止,就是TM.ScheduleFunc中执行的函数不执行了,众多连接只有一个连接能发送心跳,其他的都不能,请赐教,是不是我用的方式不对。

zhendliu avatar Jul 14 '20 03:07 zhendliu

@jungeshidai go.mod是v0.0.4吧?可否发下调用代码,我测试下。

guonaihong avatar Jul 14 '20 03:07 guonaihong

@jungeshidai go.mod是v0.0.4吧?可否发下调用代码,我测试下。我使用的版本是1.14 ,随便写了个测试代码。调试工具是(以太网调试助手SocketTool_NoAD),很多连接时,每个连接大约都能跑1分钟所有,过了一分钟,只剩下一个连接能发送心跳了。

package main

import (
	"encoding/hex"

	"flag"
	"fmt"
	"github.com/antlabs/timer"
	"net/http"
	_ "net/http/pprof"

	"log"
	"time"

	"github.com/panjf2000/gnet"
)





var (
	SetBytes, _ = hex.DecodeString("01030000001445C5")
	TM          = timer.NewTimer()
)

type iotServer struct {
	*gnet.EventServer
}

func (es *iotServer) OnInitComplete(srv gnet.Server) (action gnet.Action) {
	log.Printf("Iot server is listening on %s (multi-cores: %t, loops: %d)\n", srv.Addr.String(), srv.Multicore, srv.NumEventLoop)
	go ProfStart()
	//启动时间轮
	go TimeStart()
	return
}

/*
 消息处理函数
*/
func (es *iotServer) React(data []byte, c gnet.Conn) (out []byte, action gnet.Action) {
	return
}

/*
	连接打开时
*/
func (es *iotServer) OnOpened(c gnet.Conn) (out []byte, action gnet.Action) {
	TM.ScheduleFunc(5*time.Second, func() {
		fmt.Printf("开始下发指令:[%s]\n", c.RemoteAddr())
		if c == nil {
			return
		}
		err := c.AsyncWrite(SetBytes)
		fmt.Printf("时间[%s]:[%s]:下发指令:%x\n", time.Now().Format("2006-01-02 15:04:05"), c.RemoteAddr(), SetBytes)
		if err != nil {
			c.Close()
		}
	})

	return
}
/*
	连接退出时
*/
func (es *iotServer) OnClosed(c gnet.Conn, err error) (action gnet.Action) {
	return
}
/*
	程序性能监控模块启动
*/
func ProfStart() {
	http.ListenAndServe("0.0.0.0:6060", nil)
}
/*
	时间轮模块启动
*/
func TimeStart() {
	TM.Run()
}

func main() {
	var port int
	var multicore bool

	flag.IntVar(&port, "port", 501, "--port 9000")
	flag.BoolVar(&multicore, "multicore", true, "--multicore true")
	flag.Parse()
	iot := new(iotServer)
	log.Fatal(gnet.Serve(iot, fmt.Sprintf("tcp://:%d", port), gnet.WithMulticore(multicore)))

}

zhendliu avatar Jul 14 '20 03:07 zhendliu

ok,我看下

guonaihong avatar Jul 14 '20 05:07 guonaihong

@jungeshidai 下载v0.0.5版本看下呢,刚刚处理了下周期性定时器边界问题。

guonaihong avatar Jul 14 '20 11:07 guonaihong

@guonaihong 目前测试看,问题已经修复,之前的0.4版本我已经再测试环境测试了性能问题,长期运行CPU占用0.7左右的。马上发布0.5的版本到测试服务器上测试,有问题再向您反馈,感谢。

zhendliu avatar Jul 15 '20 00:07 zhendliu

@jungeshidai ok,客气了。 对于上面的代码有个建议,在tcp连接关闭时,可以把这个tcp绑定的time wheel node也关闭。ScheduleFunc会返回time wheel节点对象,调用Stop()就是关闭。

提问题可以建新的issue。一个问题一个issue,这样以后也可以帮助到别人检索问题和帮助。

guonaihong avatar Jul 15 '20 06:07 guonaihong