今天主要讲的是NSQ Channel 的代码实现,Channel 作为Topic的重要组成部分,主要的作用是通过队列的形式传递消息,并等待订阅者消费。
主要代码文件:
1.nsqd/channel.go
channel结构体
type Channel struct {
requeueCount uint64 //重入队列累计
messageCount uint64 //消息累计
timeoutCount uint64 //超时消息累计
sync.RWMutex
topicName string //主题名称
name string
ctx *context //包装了NSQD的上下文
backend BackendQueue //磁盘消息队列
memoryMsgChan chan *Message //内存消息队列
exitFlag int32 //退出标志
exitMutex sync.RWMutex //锁
// state tracking
clients map[int64]Consumer //消费者集合
paused int32 //停止channel
ephemeral bool
deleteCallback func(*Channel) // channel删除回调
deleter sync.Once
// Stats tracking
e2eProcessingLatencyStream *quantile.Quantile
deferredMessages map[MessageID]*pqueue.Item //消息延时
deferredPQ pqueue.PriorityQueue //延时优先级队列
deferredMutex sync.Mutex
inFlightMessages map[MessageID]*Message //等待消费确认的消息
inFlightPQ inFlightPqueue //优先级队列
inFlightMutex sync.Mutex
}
NewChannel 主要实现Channel的实例化 和 通知 NSQD 有新的 topic创建,让 nsqd 上报 lookupd。
func NewChannel(topicName string, channelName string, ctx *context,
deleteCallback func(*Channel)) *Channel {
c := &Channel{
...
}
// 创建内存队列
if ctx.nsqd.getOpts().MemQueueSize > 0 {
c.memoryMsgChan = make(chan *Message, ctx.nsqd.getOpts().MemQueueSize)
}
//?????
if len(ctx.nsqd.getOpts().E2EProcessingLatencyPercentiles) > 0 {
...
}
//初始化优先级队列(延时队列,消费确认队列)
c.initPQ()
if strings.HasSuffix(channelName, "#ephemeral") {
c.ephemeral = true
c.backend = newDummyBackendQueue()
} else {
//磁盘队列初始化
....
}
//通知nsqd
c.ctx.nsqd.Notify(c)
return c
}
PutMessage/put 函数用于发布消息
// PutMessage writes a Message to the queue
func (c *Channel) PutMessage(m *Message) error {
c.RLock()
defer c.RUnlock()
if c.Exiting() { //判断是否channel可用
return errors.New("exiting")
}
err := c.put(m) //发布消息
if err != nil {
return err
}
//增加消息累计
atomic.AddUint64(&c.messageCount, 1)
return nil
}
func (c *Channel) put(m *Message) error {
select {
case c.memoryMsgChan <- m: //如果内存足够,将消息放入内存队列,否则放入磁盘
default:
b := bufferPoolGet()
err := writeMessageToBackend(b, m, c.backend)
bufferPoolPut(b)
c.ctx.nsqd.SetHealth(err)
if err != nil {
c.ctx.nsqd.logf(LOG_ERROR, "CHANNEL(%s): failed to write message to backend - %s",
c.name, err)
return err
}
}
return nil
}
PutMessageDeferred/StartDeferredTimeout/putDeferredMessage/addToDeferredPQ 四个函数实现消息的延时, 这个队列在NSQD中,会有专门的goroutine 维护,间隔时间扫描,如果最小堆的根元素小于当前时间,重新加入消费队列。
func (c *Channel) PutMessageDeferred(msg *Message, timeout time.Duration) {
atomic.AddUint64(&c.messageCount, 1) //累计消息总数
c.StartDeferredTimeout(msg, timeout)
}
func (c *Channel) StartDeferredTimeout(msg *Message, timeout time.Duration) error {
absTs := time.Now().Add(timeout).UnixNano()
item := &pqueue.Item{Value: msg, Priority: absTs}
err := c.pushDeferredMessage(item) //记录延时消息到map
if err != nil {
return err
}
c.addToDeferredPQ(item) //加入延时优先级队列(根据time 的早晚,实现的最小堆(完全二叉树))
return nil
}
func (c *Channel) pushDeferredMessage(item *pqueue.Item) error {
c.deferredMutex.Lock()
// TODO: these map lookups are costly
id := item.Value.(*Message).ID
_, ok := c.deferredMessages[id]
if ok {
c.deferredMutex.Unlock()
return errors.New("ID already deferred")
}
c.deferredMessages[id] = item //记录消息
c.deferredMutex.Unlock()
return nil
}
func (c *Channel) addToDeferredPQ(item *pqueue.Item) {
c.deferredMutex.Lock()
/*
heap:
堆有大根堆和小根堆, 分别是说: 对应的二叉树的树根结点的键值是所有堆节点键值中最大/小者。
heap 与 pqueue 公共实现优先级队列
pqueue 中的Less 决定实现最大堆还是最小堆, heap.Push 中的 up 和 down 操作 会使用Less 函数来移动数组
qpueue的具体实现文件 internal/pqueue.go
*/
heap.Push(&c.deferredPQ, item)
c.deferredMutex.Unlock()
}
processDeferredQueue 函数的作用是,处理延时队列中哪些消息可以加入消费队列中进行消费(NSQD维护)
func (c *Channel) processDeferredQueue(t int64) bool {
...
dirty := false
for {
c.deferredMutex.Lock()
item, _ := c.deferredPQ.PeekAndShift(t) //弹出延时时间
StartInFlightTimeout/pushInFlightMessage/addToInFlightPQ 三个函数作用是将消息发送给消费者的同时记录这个消息,并等待消费确认。这个队列在NSQD中,会有专门的goroutine 维护,间隔时间扫描,如果最小堆的根元素小于当前时间,重新加入消费队列。
func (c *Channel) StartInFlightTimeout(msg *Message, clientID int64, timeout time.Duration) error {
now := time.Now()
msg.clientID = clientID
msg.deliveryTS = now
msg.pri = now.Add(timeout).UnixNano()
err := c.pushInFlightMessage(msg) //记录等待消费确认的消息
if err != nil {
return err
}
c.addToInFlightPQ(msg) //加入优先级队列(最小堆)
return nil
}
func (c *Channel) pushInFlightMessage(msg *Message) error {
...
}
func (c *Channel) addToInFlightPQ(msg *Message) {
c.inFlightMutex.Lock()
c.inFlightPQ.Push(msg) //最小堆实现参考 nsqd/in_flight_pqueue.go
c.inFlightMutex.Unlock()
}
processInFlightQueue 函数的作用是,处理消费确认队列中哪些消息已超过消费时间需要重新加入消费队列中进行消费(NSQD维护)
func (c *Channel) processInFlightQueue(t int64) bool {
...
dirty := false
for {
c.inFlightMutex.Lock()
msg, _ := c.inFlightPQ.PeekAndShift(t) //弹出 消费超时时间
FinishMessage 函数实现消费确认
func (c *Channel) FinishMessage(clientID int64, id MessageID) error {
msg, err := c.popInFlightMessage(clientID, id) //删除记录
...
c.removeFromInFlightPQ(msg) //移除Filght队列中的消息
...
return nil
}
其他函数说明:
TouchMessage:主要用于更新消费超时时间,延迟重新进入队列的时间
RequeueMessage:把在等待消费确认的消息,重新加入队列 或者 加入延时队列,而不是等待时间到来
popInFlightMessage:弹出等待消费确认的消息
removeFromInFlightPQ:移除等待消费确认的消息
popDeferredMessage:弹出延时队列中的消息
总结:
channel主要实现三个队列,一个消费队列(磁盘和内存队列),另一个是等待消费确认的队列(InFlight),以及延时消息队列(Deffer)。 其中后面两个队列通过NSQD 调用 processInFlightQueue 和 processDeferredQueue 维护,且它们实现优先级队列的方式都是通过完全二叉树实现最小堆。
下次分享:NSQD对 等待消费确认队列 和 延时队列 的维护实现 queueScanLoop