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

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