NSQ 源码分析之NSQD--Channel

今天主要讲的是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) //弹出延时时间<t 的元素
		c.deferredMutex.Unlock()
        ...
		msg := item.Value.(*Message)
		_, err := c.popDeferredMessage(msg.ID) //移除记录
		...
		c.put(msg) //发送消息到消费队列
	}

exit:
	return dirty
}

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) //弹出 消费超时时间 <t
		c.inFlightMutex.Unlock()
        ...
		_, err := c.popInFlightMessage(msg.clientID, msg.ID) //删除记录
	     //累计超时记录
		atomic.AddUint64(&c.timeoutCount, 1)
	    ...
		c.put(msg) //重新发送
	}

exit:
	return dirty
}

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章