Zinx框架学习 - 消息队列及多任务

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()
		...
		}
	}()
}

猜你喜欢

转载自blog.csdn.net/qq_47431008/article/details/131034318
今日推荐