今天主要講的是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