Zinx - V0.8 消息队列及多任务
- 之前zinxV0.7我们已经实现了读写分离,对应每个client,我们有3个go程,分别是reader、writer、DoMsgHandle
- 假设服务器有10W个client请求,那么server就会有10W个reader的go、10W个writer的go程,10W个DoMsgHandler的go程;其中10W个reader的go程和10W个writer的go程处于阻塞状态并不会抢占CPU资源,CPU仍然会在10W个DoMsgHandler的go程中来回切换,这个切换成本还是很高的;我们希望有固定的go程数量来处理DoMsgHandler的业务
- 接下来我们就需要给Zinx添加消息队列和多任务Worker机制了。我们可以通过worker的数量来限定处理业务的固定goroutine数量,⽽不是⽆限制的开辟Goroutine,虽然我们知道go的调度算法已经做的很极致了,但是⼤数量的Goroutine依然会带来一些不必要的环境切换成本,这些本应该是服务器应该节省掉的成本。我们可以⽤消息队列来缓冲worker⼯作的数据
创建消息队列
- msgHandler.go
MsgHandle增加消息队列和worker池
//消息处理模块的实现
type MsgHandle struct {
//存放每个MsgID 所对应的处理方法
Apis map[uint32]ziface.IRouter
//负责Worker取任务的消息队列
TaskQueue []chan ziface.IRequest
//业务工作Worker池的worker数量
WorkerPoolSize uint32
}
//初始化/创建MsgHandle方法
func NewMsgHandle() *MsgHandle {
return &MsgHandle{
Apis: make(map[uint32]ziface.IRouter),
WorkerPoolSize: utils.GlobalObject.WorkerPoolSize, //从全局配置中获取
TaskQueue: make([]chan ziface.IRequest, utils.GlobalObject.MaxWorkerTaskLen),
}
}
- globalobj.go
将消息队列和worker数量配置化
package utils
import (
"encoding/json"
"io/ioutil"
"zinx/ziface"
)
//存储一切有关Zinx框架的全局参数, 供其他模块使用
//一些参数是可以通过zinx.json由用户进行配置
type GlobalObj struct {
//Server
TcpServer ziface.IServer //当前Zinx全局的Server对象
Host string //当前服务器主机监听的IP
TcpPort int //当前服务器主机监听的端口号
Name string //当前服务器的名称
//Zinx
Version string //当前Zinx的版本号
MaxConn int //当前服务器主机允许的最大链接数
MaxPackageSize uint32 //当前Zinx框架数据包的最大值
WorkerPoolSize uint32 //当前业务工作Worker池的Goroutine数量
MaxWorkerTaskLen uint32 //Zinx框架允许用户最多开辟多少个Worker(限定条件)
}
//定义一个全局的对外Globalobj
var GlobalObject *GlobalObj
//从 zinx.json去加载用于自定义的参数
func (g *GlobalObj) Reload() {
data, err := ioutil.ReadFile("conf/zinx.json")
if err != nil {
panic(err)
}
//将json文件数据解析到struct中
err = json.Unmarshal(data, &GlobalObject)
if err != nil {
panic(err)
}
}
//提供一个init方法,初始化当前的GlobalObject
func init() {
//如果配置文件没有加载,默认的值
GlobalObject = &GlobalObj{
Name: "ZinxServerApp",
Version: "V0.8",
TcpPort: 8999,
Host: "0.0.0.0",
MaxConn: 1000,
MaxPackageSize: 4096,
WorkerPoolSize: 10, //Worker工作池的队列的个数
MaxWorkerTaskLen: 1024, //每个worker对应的消息队列的任务的数量最大值
}
//应该尝试从conf/zinx.json去加载一些用户自定义的参数
GlobalObject.Reload()
}
创建及启动Worker工作池
- StartWorkerPool() ⽅法是启动Worker⼯作池,这⾥根据⽤户配置好的 WorkerPoolSize 的数量来启动,然后分别给每个Worker分配⼀个 TaskQueue ,然后⽤⼀个goroutine来承载⼀个Worker的⼯作业务
- StartOneWorker() ⽅法就是⼀个Worker的⼯作业务,每个worker是不会退出的(⽬前没有设定worker的停⽌⼯作机制),会不断的阻塞从对应的TaskQueue中等待消息,并处理
//启动一个Worker工作池(开启工作池的动作只能发生一次,一个zinx框架只能有一个worker工作池)
func (mh *MsgHandle) StartWorkerPool() {
//根据workerPoolSize 分别开启Worker,每个Worker用一个go来承载
for i := 0; i < int(mh.WorkerPoolSize); i++ {
//一个worker被启动
// 1 当前的worker对应的channel消息队列 开辟空间 第0个worker 就用第0个channel ...
mh.TaskQueue[i] = make(chan ziface.IRequest, utils.GlobalObject.MaxWorkerTaskLen)
//2 启动当前的Worker, 阻塞等待消息从channel传递进来
go mh.StartOneWorker(i, mh.TaskQueue[i])
}
}
//启动一个Worker工作流程
func (mh *MsgHandle) StartOneWorker(workerID int, taskQueue chan ziface.IRequest) {
fmt.Println("Worker ID = ", workerID, " is started ...")
//不断的阻塞等待对应消息队列的消息
for {
select {
//如果有消息过来,出列的就是一个客户端的Request, 执行当前Request所绑定业务
case request := <-taskQueue:
mh.DoMsgHandler(request)
}
}
}
发送消息给消息队列
//将消息交给TaskQueue, 由Worker进行处理
func (mh *MsgHandle) SendMsgToTaskQueue(request ziface.IRequest) {
//1 将消息平均分配给不通过的worker
//根据客户端建立的ConnID来进行分配
workerID := request.GetConnection().GetConnID() % mh.WorkerPoolSize
fmt.Println("Add ConnID = ", request.GetConnection().GetConnID(),
" reqeust MsgID = ", request.GetMsgID(),
" to WorkerID = ", workerID)
//2 将消息发送给对应的worker的TaskQueue即可
mh.TaskQueue[workerID] <- request
}
SendMsgToTaskQueue() 作为⼯作池的数据⼊⼝,接收传入的request消息,这⾥⾯采⽤的是轮询的分配机制,因为不同链接信息都会调⽤这个⼊⼝,那么到底应该由哪个worker处理该链接的请求处理,整理⽤的是⼀个简单的求模运算。⽤ConnID取余和workerID的匹配来进⾏分配
Zinx集成消息队列及工作池机制
- 将消息交给消息队列处理
//链接的读业务方法
func (c *Connection) StartReader() {
...
//得到当前conn数据的Request请求数据
req := Request{
conn: c,
msg: msg,
}
if utils.GlobalObject.WorkerPoolSize > 0 {
//已经开启了工作池机制,将消息发送给Worker工作池处理即可
c.MsgHandler.SendMsgToTaskQueue(&req)
} else {
//从路由中,找到注册绑定的Conn对应的router调用
//根据绑定好的MsgID 找到对应处理api业务 执行
go c.MsgHandler.DoMsgHandler(&req)
}
}
}
这⾥并没有强制使⽤多任务Worker机制,⽽是判断⽤户配置 WorkerPoolSize 的个数,如果⼤于0,那么我就启动多任务机制处理链接请求消息,如果=0或者<0那么,我们依然只是之前的开启⼀个临时的Goroutine处理客户端请求消息
- 工作池开启
//启动服务器
func (s *Server) Start() {
fmt.Printf("[Zinx] Server Name : %s, listenner at IP : %s, Port:%d is starting\n",
utils.GlobalObject.Name, utils.GlobalObject.Host, utils.GlobalObject.TcpPort)
fmt.Printf("[Zinx] Version %s, MaxConn:%d, MaxPackeetSize:%d\n",
utils.GlobalObject.Version,
utils.GlobalObject.MaxConn,
utils.GlobalObject.MaxPackageSize)
go func() {
//0 开启消息队列及Worker工作池
s.MsgHandler.StartWorkerPool()
...
}
}()
}