概要
Kiteq是一个分布式消息队列
它的实现是一种自组织形式,没有中心控制节点负责topic和consumer的管理,每个server自己将能处理的topic上报给zk,同时在zk上拉取topic与下游group的映射关系。
git地址与架构图
https://github.com/shineit/kiteq
流程如下
1. KiteQ启动会将自己可以接受和投递的Topics列表给到zookeeper
2. KiteQ拉取Zookeeper上的Topics下的订阅关系(Bingding:订阅方推送上来的订阅消息信息)。
3. Consumer推送自己需要订阅的Topic+messageType的消息的订阅关系(Binding)到Zookeeper
4. Consumer拉取当前提供推送Topics消息的KiteQ地址列表,并发起TCP长连接
5. Producer推送自己可以发布消息Topics列表到Zookeeper
6. Producer拉取当前提供接受Topics消息的KiteQ地址列表,并发起TCP长连接
注:
Kiteq server提供config变化的api,这样如果有新topic需要该server关注,或者取消,可以通过这个api发起,由server自己上报给zk。
Zk的数据组织如下
KiteServer : /kiteq/server/${topic}/ip:port
Producer : /kiteq/pub/${topic}/${groupId}/ip:port
Consumer : /kiteq/sub/${topic}/${groupId}-bind/#$data(bind)
注:
Producer_client和consumer client均是自己去发现并主动(长)连接kiteq server。通常一个topic会由多个kiteq server负责管理,这样Producer_client可以随机将msg发放进任意一个server里;同理会有多个consumer ip消费同一个group id,kiteq server可以随机将msg下发给任意的消费服务。
源码分析
Kiteq_server_monitor.go
消息队列的监控指标
Goroutine -- 服务所启动的协程数
DeliverGo --
DeliverCount -- 发送消息的数据量
RecieveCount -- 接收消息的数据量
MessageCount -- 消息存储的数据量
TopicsDeliver – 按照topic区分后,发送消息的数据量
TopicsRecieve -- 按照topic区分后,接收消息的数据量
Groups
KiteServerLimter -- 消息吞吐量限制
HandleStat函数返回的是统计的信息,它的实现比较有意思,是通过一个只有两个数的数组切换得到的。当前的请求会通过时间戳的方式获取上一秒填写的状态值,这样只要每次写入时间不超过一秒,是不会有问题的。(不过这个场景下写和读都不是高频操作,意义不大)。另外除了上述信息,该函数还返回了客户端的连接数。
HandleBindings函数返回的是topic的映射关系。一个topic会被多个消费组所消费。另外每个topic还会绑定一个限流信息。
startFlow函数其实是往之前提到的只有两个数的数组赋值,每秒写一次。
Recover_manager.go
这个模块的作用是将之前投递失败的消息(存储到db中了),通过一定的方式,再次投递给下游服务。
具体的方式包括:
时间间隔,通过recoverPeriod参数决定;
并行处理,在存储过程中就需要考虑hash,由kitestore.RecoverNum()控制;
一次性从db中查询msg条数的控制,由kitestore.PageQueryEntity(hashKey, self.serverName, preTimestamp, startIdx, 200)决定;
发送的限流控制,由recoveLimiter决定;
Kiteq_server.go
NewKiteQServer函数初始化依赖的组件,包括数据库,客户端连接管理器,exchanger,消息投递注册器,流控组件,消息重发服务,最核心的是初始化pipeline组件。
Start函数启动服务,当收到一条消息后,触发pipeline,把当前的绑定的topics通过exchanger推送到远端,这样客户端就知道哪些topic在什么服务器上。然后启动流量统计,启动recoverManager,启动DLQ的时间(它的作用是产生一个定时触发事件,清楚数据库中过期的数据)。提供/stat,/binds和/reload三个外部接口,分别提供数据统计,绑定关系展示和重启配置文件。
HandleReloadConf函数执行具体的重新加载配置文件的功能。
Bind_exchanger.go
和zk打交道,主要的作用是发布本服务的能接受的topic列表和建立topic与下游消费group绑定关系。对于前者,producer client在发送一条msg前,会先查看该topic能被哪些server所接受;对于后者,一条msg需要发送给绑定的consumer。
Topic2Limiters和Topic2Groups函数主要给monitor使用,便于查询状态信息;PushQServer将负责的topics推送到zk,删除之前关注但现在取消的,同时对于新关注的topic订阅数据变动,其实现在subscribeBinds函数中;FindBinds函数根据topic和messageType 类型获取订阅关系。
Event Handler
Accept event
对于ack和heartbeat类型的消息,直接响应客户端。对于消息类型的msg,先过流控,再封装成persistentEvent类型的消息交由下游handler进行处理;
Check event
先判断消息所属的topic是否在本server的处理范畴,不在则直接返回给客户端;再判断是否已经处理过,此处会有一个lru进行存储已经处理过的消息,处理过则直接返回给客户端;再对消息的部分字段进行赋值,如CreateTime、DeliverLimit、ExpiredTime等等。NewCheckMessageHandler实现了topics的变更通知,方式比较简洁,值得借鉴。
Persistent msg event
主要作用是将msg存储下来,以备后续重试使用,同时响应客户端(消息已经存储好了,producer相关逻辑已经走完)。它又细分成先投递和先存储这两种模式可供选择,从消息发送的整体性能来讲先投递肯定消息延迟更低(写mysql或文件还是需要话一些时间的),但是producer client等待的时间会更长(异步接口除外)。
Deliever pre event
先进行消息注册,防止重复处理。再进行流控,实现方式通过atomic原子比较,好处是避免了线程切换带来的开销,重试三次失败后丢掉消息(该实现带来的问题,因为client通常是进行异步发送的,即使有回调多数情况下也不一定会关注,作为基础主件需要考虑到用户的使用情况)。然后判断消息重发的次数和有效时间。如果没有消息体需要去store中获取。最后将之前未成功投递消息组重新进行赋值后,交给deliverEvent进行处理。
Deliever qos event
主要作用是1、对consumer group中的每一个接收方,进行限流判断;2、将msg发送给consumer group。
Deliever result event
等待consumer group的响应,如果都投递成功,则将该消息从store中删除,否则会安排重新投放策略。