本博客与RayXXZhang的博客保持同步更新,转载请注明来自RayXXZhang的博客-nsq源码阅读笔记之nsqd(四)——Channel
与Channel相关的代码主要位于nsqd/channel.go
, nsqd/nsqd.go
中。
Channel与Topic的关系
Channel是消费者订阅特定Topic的一种抽象。对于发往Topic的消息,nsqd向该Topic下的所有Channel投递消息,而同一个Channel只投递一次,Channel下如果存在多个消费者,则随机选择一个消费者做投递。这种投递方式可以被用作消费者负载均衡。
Channel从属于特定Topic,可以认为是Topic的下一级。在同一个Topic之下可以有零个或多个Channel。
和Topic一样,Channel同样有永久和临时之分,永久的Channel只能通过显式删除销毁,临时的Channel在最后一个消费者断开连接的时候被销毁。
与服务于生产者的Topic不同,Channel直接面向消费者。
生产者 -> 消息 -> Topic -> Channel -> 消费者1
-> Channel2 -> 消费者2
-> Channel3 -> 消费者3
在代码上Channel和Topic有许多相似之处,对于和Topic相同或者相似的部分,以下不再赘述,可以参考Topic相关博文。
Channel的创建
func NewChannel(topicName string, channelName string, ctx *context,
deleteCallback func(*Channel)) *Channel {
c := &Channel{
topicName: topicName,
name: channelName,
memoryMsgChan: make(chan *Message, ctx.nsqd.getOpts().MemQueueSize),
clientMsgChan: make(chan *Message),
exitChan: make(chan int),
clients: make(map[int64]Consumer),
deleteCallback: deleteCallback,
ctx: ctx,
}
if len(ctx.nsqd.getOpts().E2EProcessingLatencyPercentiles) > 0 {
c.e2eProcessingLatencyStream = quantile.New(
ctx.nsqd.getOpts().E2EProcessingLatencyWindowTime,
ctx.nsqd.getOpts().E2EProcessingLatencyPercentiles,
)
}
c.initPQ()
if strings.HasSuffix(channelName, "#ephemeral") {
c.ephemeral = true
c.backend = newDummyBackendQueue()
} else {
// backend names, for uniqueness, automatically include the topic...
backendName := getBackendName(topicName, channelName)
c.backend = newDiskQueue(backendName,
ctx.nsqd.getOpts().DataPath,
ctx.nsqd.getOpts().MaxBytesPerFile,
int32(minValidMsgLength),
int32(ctx.nsqd.getOpts().MaxMsgSize)+minValidMsgLength,
ctx.nsqd.getOpts().SyncEvery,
ctx.nsqd.getOpts().SyncTimeout,
ctx.nsqd.getOpts().Logger)
}
go c.messagePump()
c.ctx.nsqd.Notify(c)
return c
}
Channel和Topic在创建的时候都会初始化结构,初始化backend,创建消息循环,不同的是Channel在创建时多了给e2eProcessingLatencyStream
赋值的以及initPQ
部分。
其中e2eProcessingLatencyStream
主要用于统计消息投递的延迟等,将在以后的博文中叙述。
func (c *Channel) initPQ() {
pqSize := int(math.Max(1, float64(c.ctx.nsqd.getOpts().MemQueueSize)/10))
c.inFlightMessages = make(map[MessageID]*Message)
c.deferredMessages = make(map[MessageID]*pqueue.Item)
c.inFlightMutex.Lock()
c.inFlightPQ = newInFlightPqueue(pqSize)
c.inFlightMutex.Unlock()
c.deferredMutex.Lock()
c.deferredPQ = pqueue.New(pqSize)
c.deferredMutex.Unlock()
}
initPQ
函数创建了两个字典inFlightMessages
、deferredMessages
和两个队列inFlightPQ
、deferredPQ
。在nsq中inFlight指的是正在投递但还没确认投递成功的消息,defferred指的是投递失败,等待重新投递的消息。initPQ
创建的字典和队列主要用于索引和存放这两类消息。其中两个字典使用消息ID作索引。
inFlightPQ
使用newInFlightPqueue
初始化,InFlightPqueue
位于nsqd\in_flight_pqueue.go
。nsqd\in_flight_pqueue.go
是nsq实现的一个优先级队列,提供了常用的队列操作,值得学习。
deferredPQ
使用pqueue.New
初始化,pqueue
位于nsqd\pqueue.go
,也是一个优先级队列。
待投递消息进入Channel
在分析Topic时提到,消息进入Topic的消息循环后会被投递到该Topic下所有的Channel,由Channel的PutMessage
函数进行处理。
// PutMessage writes a Message to the queue
func (c *Channel) PutMessage(m *Message) error {
c.RLock()
defer c.RUnlock()
if atomic.LoadInt32(&c.exitFlag) == 1 {
return errors.New("exiting")
}
err := c.put(m)
if err != nil {
return err
}
atomic.AddUint64(&c.messageCount, 1)