什么是Topic
Topic作为nsqd的重要组成部分,里面存在一些有趣的设计,单独开一篇文章进行学习。
每个nsqd实例旨在一次处理多个数据流。这些数据流称为“topics”,一个topic具有1个或多个“channels”。每个channel都会收到topic所有消息的副本,实际上下游的服务是通过对应的channel来消费topic消息。
我的理解,topic就是类似一个键值对的键,不同于数据结构或者redis中的键值对,它的值是多个数据流,并且存放在channel当中。需要获取这个数据流的方法就是,创建一个consumer去订阅该topic下的某个channel。添加数据流的方法就是向这个topic发布数据。
源码分析
数据结构
type Topic struct {
// 64bit atomic vars need to be first for proper alignment on 32bit platforms
messageCount uint64
messageBytes uint64
// topic实例可以作为读写锁,用于并发安全
sync.RWMutex
// topic的名称
name string
// 利用字典存放这个topic对应的所有channel
channelMap map[string]*Channel
// 负责向磁盘文件中写入消息、从磁盘文件中读取消息,是NSQ实现数据持久化的最重要结构
backend BackendQueue
// 内存消息通道,用于外部nsqd控制该topic的运行流程,包括内存消息,启停以及通道更新
memoryMsgChan chan *Message
startChan chan int
exitChan chan int
channelUpdateChan chan int
// 多线程时的同步等待组
waitGroup util.WaitGroupWrapper
// 退出标记
exitFlag int32
// 生产消息ID的工厂对象
idFactory *guidFactory
// 是否为临时topic
ephemeral bool
// 删除topic的方法指针
deleteCallback func(*Topic)
// 用于调用上面的删除回调函数,保证并发请客下,只调用一次
deleter sync.Once
// 暂停标记
paused int32
pauseChan chan int
// 对应的nsqd指针
nsqd *NSQD
}
实例化
// 传入参数包括topic的名称,nsqd实例以及topic删除回调函数指针
func NewTopic(topicName string, nsqd *NSQD, deleteCallback func(*Topic)) *Topic {
t := &Topic{
name: topicName,
channelMap: make(map[string]*Channel),
memoryMsgChan: nil,
startChan: make(chan int, 1),
exitChan: make(chan int),
channelUpdateChan: make(chan int),
nsqd: nsqd,
paused: 0,
pauseChan: make(chan int),
deleteCallback: deleteCallback,
idFactory: NewGUIDFactory(nsqd.getOpts().ID),
}
// create mem-queue only if size > 0 (do not use unbuffered chan)
// 分配内存消息通道大小
if nsqd.getOpts().MemQueueSize > 0 {
t.memoryMsgChan = make(chan *Message, nsqd.getOpts().MemQueueSize)
}
// 如果topic的名称结尾是#ephemeral,那么说明是个临时topic,那么就不需要将数据存放到硬盘当中
if strings.HasSuffix(topicName, "#ephemeral") {
t.ephemeral = true
// 用一个基于内存的队列
t.backend = newDummyBackendQueue()
} else {
dqLogf := func(level diskqueue.LogLevel, f string, args ...interface{}) {
opts := nsqd.getOpts()
lg.Logf(opts.Logger, opts.LogLevel, lg.LogLevel(level), f, args...)
}
// 用基于硬盘文件的方式存储内存信息
t.backend = diskqueue.New(
topicName,
nsqd.getOpts().DataPath,
nsqd.getOpts().MaxBytesPerFile,
int32(minValidMsgLength),
int32(nsqd.getOpts().MaxMsgSize)+minValidMsgLength,
nsqd.getOpts().SyncEvery,
nsqd.getOpts().SyncTimeout,
dqLogf,
)
}
// 启动消息处理线程
t.waitGroup.Wrap(t.messagePump)
t.nsqd.Notify(t, !t.ephemeral)
return t
}
channel操作
// GetChannel performs a thread safe operation
// to return a pointer to a Channel object (potentially new)
// for the given Topic
// 返回topic下一个指定名称的channel,如果名称不存在,则创建一个新的channel
func (t *Topic) GetChannel(channelName string) *Channel {
t.Lock()
// 获取该channel
channel, isNew := t.getOrCreateChannel(channelName)
t.Unlock()
// 如果是一个新channel,那么通知消息处理线程更新本地channel
if isNew {
// update messagePump state
select {
case t.channelUpdateChan <- 1:
case <-t.exitChan:
}
}
return channel
}
// this expects the caller to handle locking
// 获取该channel或者创建
func (t *Topic) getOrCreateChannel(channelName string) (*Channel, bool) {
channel, ok := t.channelMap[channelName]
if !ok {
deleteCallback := func(c *Channel) {
t.DeleteExistingChannel(c.name)
}
// 创建channel
channel = NewChannel(t.name, channelName, t.nsqd, deleteCallback)
t.channelMap[channelName] = channel
t.nsqd.logf(LOG_INFO, "TOPIC(%s): new channel(%s)", t.name, channel.name)
return channel, true
}
return channel, false
}
// 获取存在的channel
func (t *Topic) GetExistingChannel(channelName string) (*Channel, error) {
t.RLock()
defer t.RUnlock()
channel, ok := t.channelMap[channelName]
if !ok {
return nil, errors.New("channel does not exist")
}
return channel, nil
}
// DeleteExistingChannel removes a channel from the topic only if it exists
// 删除存在的channel
func (t *Topic) DeleteExistingChannel(channelName string) error {
t.RLock()
channel, ok := t.channelMap[channelName]
t.RUnlock()
if !ok {
return errors.New("channel does not exist")
}
t.nsqd.logf(LOG_INFO, "TOPIC(%s): deleting channel %s", t.name, channel.name)
// delete empties the channel before closing
// (so that we dont leave any messages around)
//
// we do this before removing the channel from map below (with no lock)
// so that any incoming subs will error and not create a new channel
// to enforce ordering
channel.Delete()
t.Lock()
// 删除topic字典中的channel
delete(t.channelMap, channelName)
numChannels := len(t.channelMap)
t.Unlock()
// update messagePump state
// 通知消息处理线程,channel状态发生改变
select {
case t.channelUpdateChan <- 1:
case <-t.exitChan:
}
// 如果该topic的通道数为空,同时该topic为一个临时topic,那么直接删除该topic
if numChannels == 0 && t.ephemeral == true {
go t.deleter.Do(func() { t.deleteCallback(t) })
}
return nil
}
topic消息发布
// PutMessage writes a Message to the queue
// 向topic塞入一个消息
func (t *Topic) PutMessage(m *Message) error {
// 线程安全操作
t.RLock()
defer t.RUnlock()
// 如果topic以及退出,直接返回错误
if atomic.LoadInt32(&t.exitFlag) == 1 {
return errors.New("exiting")
}
err := t.put(m)
if err != nil {
return err
}
// 原子操作,修改消息数量和总消息大小
atomic.AddUint64(&t.messageCount, 1)
atomic.AddUint64(&t.messageBytes, uint64(len(m.Body)))
return nil
}
// PutMessages writes multiple Messages to the queue
// 塞入多条消息到topic
func (t *Topic) PutMessages(msgs []*Message) error {
t.RLock()
defer t.RUnlock()
if atomic.LoadInt32(&t.exitFlag) == 1 {
return errors.New("exiting")
}
messageTotalBytes := 0
for i, m := range msgs {
err := t.put(m)
if err != nil {
atomic.AddUint64(&t.messageCount, uint64(i))
atomic.AddUint64(&t.messageBytes, uint64(messageTotalBytes))
return err
}
messageTotalBytes += len(m.Body)
}
atomic.AddUint64(&t.messageBytes, uint64(messageTotalBytes))
atomic.AddUint64(&t.messageCount, uint64(len(msgs)))
return nil
}
// 最终的消息添加函数,没有加锁
func (t *Topic) put(m *Message) error {
select {
// 将消息压入内存消息通道
case t.memoryMsgChan <- m:
default:
// 如果消息通道缓存已经满了,那么就将消息压入硬盘中
err := writeMessageToBackend(m, t.backend)
t.nsqd.SetHealth(err)
if err != nil {
t.nsqd.logf(LOG_ERROR,
"TOPIC(%s) ERROR: failed to write message to backend - %s",
t.name, err)
return err
}
}
return nil
}
消息处理线程
负责将消息发布者的数据推送给每一个channel。
// messagePump selects over the in-memory and backend queue and
// writes messages to every channel for this topic
func (t *Topic) messagePump() {
var msg *Message
var buf []byte
var err error
var chans []*Channel
var memoryMsgChan chan *Message
var backendChan <-chan []byte
// do not pass messages before Start(), but avoid blocking Pause() or GetChannel()
// 如果处于channel更新或者暂停状态,则不进入下面的消息处理
for {
select {
case <-t.channelUpdateChan:
continue
case <-t.pauseChan:
continue
case <-t.exitChan:
goto exit
case <-t.startChan:
}
break
}
// 将topic所有的channel复制到局部变量,保证数据安全
t.RLock()
for _, c := range t.channelMap {
chans = append(chans, c)
}
t.RUnlock()
if len(chans) > 0 && !t.IsPaused() {
memoryMsgChan = t.memoryMsgChan
backendChan = t.backend.ReadChan()
}
// main message loop
for {
select {
// 如果内存中有数据,那么先从内存中读取一个消息
case msg = <-memoryMsgChan:
// 从缓存从读取数据
case buf = <-backendChan:
// 数据节码
msg, err = decodeMessage(buf)
if err != nil {
t.nsqd.logf(LOG_ERROR, "failed to decode message - %s", err)
continue
}
// 通道状态发生变化
case <-t.channelUpdateChan:
chans = chans[:0]
t.RLock()
// 重新从topic中复制channel到本地
for _, c := range t.channelMap {
chans = append(chans, c)
}
t.RUnlock()
if len(chans) == 0 || t.IsPaused() {
memoryMsgChan = nil
backendChan = nil
} else {
memoryMsgChan = t.memoryMsgChan
backendChan = t.backend.ReadChan()
}
continue
// topic暂停工作
case <-t.pauseChan:
if len(chans) == 0 || t.IsPaused() {
memoryMsgChan = nil
backendChan = nil
} else {
memoryMsgChan = t.memoryMsgChan
backendChan = t.backend.ReadChan()
}
continue
// topic退出
case <-t.exitChan:
goto exit
}
// 将所有的数据塞入每一个通道当中
for i, channel := range chans {
chanMsg := msg
// copy the message because each channel
// needs a unique instance but...
// fastpath to avoid copy if its the first channel
// (the topic already created the first copy)
if i > 0 {
chanMsg = NewMessage(msg.ID, msg.Body)
chanMsg.Timestamp = msg.Timestamp
chanMsg.deferred = msg.deferred
}
if chanMsg.deferred != 0 {
channel.PutMessageDeferred(chanMsg, chanMsg.deferred)
continue
}
err := channel.PutMessage(chanMsg)
if err != nil {
t.nsqd.logf(LOG_ERROR,
"TOPIC(%s) ERROR: failed to put msg(%s) to channel(%s) - %s",
t.name, msg.ID, channel.name, err)
}
}
}
exit:
t.nsqd.logf(LOG_INFO, "TOPIC(%s): closing ... messagePump", t.name)
}
内存清理
当系统内存不足,可以将内存中的缓存消息转移到硬盘中,从而防止溢出。
func (t *Topic) flush() error {
if len(t.memoryMsgChan) > 0 {
t.nsqd.logf(LOG_INFO,
"TOPIC(%s): flushing %d memory messages to backend",
t.name, len(t.memoryMsgChan))
}
for {
select {
case msg := <-t.memoryMsgChan:
// 将所有的消息逐一塞入硬盘队列中
err := writeMessageToBackend(msg, t.backend)
if err != nil {
t.nsqd.logf(LOG_ERROR,
"ERROR: failed to write message to backend - %s", err)
}
default:
goto finish
}
}
finish:
return nil
}
总结
topic源码心得要点如下:
- topic为了提高消息读取的的性能,做了两层缓存,上层是基于内存的缓存memoryMsgChan,利用golang的有缓冲通道实现,外部发布数据给topic时优先将数据放到memoryMsgChan,但是当消息积压太多,内存会耗尽,因此,还设计了基于硬盘的缓存backendChan,当memoryMsgChan满了的时候,就会将数据存储到backendChan,将消息转发channel的时候,优先从memoryMsgChan读取消息,然后从这样,backendChan,这样提高了数据读写效率的同时,又节约内存;
- 当然,如果为了进一步提高读写性能,并不考虑内存的使用情况,可以将Topic的名称以“#ephemeral”结尾,将Topic设置为临时Topic,那么backendChan同样是使用计算机内存,临时topic的通道数为0的时候,将会被删除;
- 在对channel做写操作的时候,需要加锁保证线程安全。