nsqd解析:nsqd-topic

Topic 解析

  上一篇我们对 nsqd 的整体框架有了大概的了解,但是光看整个运行的流程是十分空洞的,本篇文章来对这个框架进行填充,对 topic 这一组成部分进行详尽的解析.
  topic 管理着多个 channel 通过从 client 中获取消息,然后将消息发送到 channel 中传递给客户端.在 channel 初始化时会加载原有的 topic 并在最后统一执行 topic.Start(),新创建的 topic 会同步给 lookupd 后开始运行.
  nsqd 中通过创建创建多个 topic 来管理不同类别的频道.

topic struct

// topic 结构体
type Topic struct {
	// 这两个字段仅作统计信息,保证 32 位对其操作
	messageCount uint64 // topic 接受的消息数
	messageBytes uint64 // 所有接收消息结构体的长度

	sync.RWMutex // 在 channel 的操作需要加锁,包括 putMessage

	name              string                // topic 的 name
	// 存储这个 topic 的所有 channel
	channelMap        map[string]*Channel   
	// 当消息数过多,消息队列中存不下会将消息存入 backend(消息落地)
	backend           BackendQueue          
	// 内存中存放的消息队列,默认长度是 10000
	memoryMsgChan     chan *Message         
	// 接收开始信号的 channel,调用 start 开始 topic 消息循环
	startChan         chan int              
	// 判断 topic 是否退出
	exitChan          chan int              
	// 在 select 的地方都要添加 exitChan
	// 除非使用 default 或者保证程序不会永远阻塞在 select 处,即可以退出循环
	// channel 更新时用来通知并更新消息循环中的 chan 数组
	channelUpdateChan chan int              
	// 用来等待所有的子 goroutine
	waitGroup         util.WaitGroupWrapper 
	
	exitFlag          int32                 // topic 退出标识符
	idFactory         *guidFactory          // 生成 guid 的工厂方法

	ephemeral      bool         // 该 topic 是否是临时 topic
	deleteCallback func(*Topic) // topic 删除时的回调函数
	deleter        sync.Once    // 确保 deleteCallback 仅执行一次

	paused    int32    // topic 是否暂停
	pauseChan chan int // 改变 topic 暂停/运行状态的通道

	ctx *context // topic 的上下文
}

  nsqd 是所有 topic 的管理者,topic 是它自己所有 channel 的管理者,同样也是使用了 map+RWMutex 来存储管理 channel.

NewTopic 操作

下面就是 topic 的创建流程,传入的参数参数包括,topicName,上下文环境,删除回调函数:

func NewTopic(topicName string, ctx *context, 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),
		ctx:               ctx,
		paused:            0,
		pauseChan:         make(chan int),
		deleteCallback:    deleteCallback,
		// 所有 topic 使用同一个 guidFactory,因为都是用的 nsqd 的 ctx.nsqd.getOpts().ID 为基础生成的
		idFactory:         NewGUIDFactory(ctx.nsqd.getOpts().ID),
	}
	// 根据消息队列生成消息 chan,default size = 10000
	if ctx.nsqd.getOpts().MemQueueSize > 0 {
		// 初始化一个消息队列
		t.memoryMsgChan = make(chan *Message, ctx.nsqd.getOpts().MemQueueSize)
	}
	// 判断这个 topic 是不是暂时的,暂时的 topic 消息仅仅存储在内存中
	// DummyBackendQueue 和 diskqueue 均实现了 backend 接口
	if strings.HasSuffix(topicName, "#ephemeral") {
		// 临时的 topic,设置标志并使用 newDummyBackendQueue 初始化 backend
		t.ephemeral = true
		t.backend = newDummyBackendQueue()   // 实现了 backend 但是并没有逻辑,所有操作仅仅返回 nil
	} else {
		dqLogf := func(level diskqueue.LogLevel, f string, args ...interface{}) {
			opts := ctx.nsqd.getOpts()
			lg.Logf(opts.Logger, opts.LogLevel, lg.LogLevel(level), f, args...)
		}
		// 使用 diskqueue 初始化 backend 队列
		t.backend = diskqueue.New(
			topicName,
			ctx.nsqd.getOpts().DataPath,
			ctx.nsqd.getOpts().MaxBytesPerFile,
			int32(minValidMsgLength),
			int32(ctx.nsqd.getOpts().MaxMsgSize)+minValidMsgLength,
			ctx.nsqd.getOpts().SyncEvery,
			ctx.nsqd.getOpts().SyncTimeout,
			dqLogf,
		)
	}
	// 使用一个新的协程来执行 messagePump
	t.waitGroup.Wrap(t.messagePump)
	// 调用 Notify
	t.ctx.nsqd.Notify(t)

	return t
}

  每个 topic 都有一个消息队列缓冲区,默认大小为 10000.并且多余的消息会放到 backend 中进行落地保存.保存的方式有两种,如果这是一个临时的 topic 会直接进行丢弃,否则会使用 diskqueue 保存到磁盘中.在 NewTopic 的最后启动了一个 goroutine 来执行 t.messagePump().
  看到这里会好奇,topic 是怎么和 nsqd 联系起来的呢?当 nsqd 初始化时,会加载已经存在的 topic 然后执行 Start() 函数开始运行.

// 根据 topic 名称获取或创建一个 topic 指针
func (n *NSQD) GetTopic(topicName string) *Topic {
	// Fast path:该 topic 已经存在,加读锁进行获取
	n.RLock()
	t, ok := n.topicMap[topicName]
	n.RUnlock()
	if ok {
		return t
	}
	// Slow path
	n.Lock()
	// 再进行一次判断,dobule check
	t, ok = n.topicMap[topicName]
	if ok {
		n.Unlock()
		return t
	}
	// topic 删除时回调,仅执行一次(Once 保证)
	deleteCallback := func(t *Topic) {
		n.DeleteExistingTopic(t.name)
	}
	// 创建一个新的 Topic 传入名字、context、删除回调函数
	t = NewTopic(topicName, &context{n}, deleteCallback)
	// 插入到 map 中
	n.topicMap[topicName] = t

	n.Unlock()

	// topic 已经创建,但是 messagePump 还没有开始运行
	// 如果在启动时加载元数据,尚无 nsqdlookupd 连接,则在加载后启动 topic,由 LoadMetadata 自己启动即可
	if atomic.LoadInt32(&n.isLoading) == 1 {
		return t
	}
	// 从下面开始就不再使用 NSQD 的全局锁转而使用 topic 的细粒度锁
	// 如果使用了 lookupd 就需要阻塞获取该 topic 的所有 channel,并立即进行创建
	// 这样可以确保将收到的任何消息缓冲到正确的通道
	lookupdHTTPAddrs := n.lookupdHTTPAddrs()
	if len(lookupdHTTPAddrs) > 0 {
		// 在所有 lookupd 中获取这个 topic 所有的 channels,这一步同样不需要在 LoadMetadata 时执行
		channelNames, err := n.ci.GetLookupdTopicChannels(t.name, lookupdHTTPAddrs)
		if err != nil {
			...
		}
		for _, channelName := range channelNames {
			// 临时的就不管了
			if strings.HasSuffix(channelName, "#ephemeral") {
				continue // do not create ephemeral channel with no consumer client
			}
			// 否则需要创建 channel
			t.GetChannel(channelName)
		}
	} else if len(n.getOpts().NSQLookupdTCPAddresses) > 0 {
		// 没有 lookupd 地址但是配置文件中却有。。。
		...
	}

	// 已经添加了所有 channel 可以开始 topic 下的消息分发
	// 向 startChan 发送通知 messagePump 开始运行
	t.Start()
	return t
}

  如果 nsqd 中没有这个 topic 会进行创建,如果是从 nsqd.dat 加载文件时创建 topic 直接返回不执行 Start(),如果是新的 topic,会尝试从 lookupd 中获取对应的所有 channels,保证 channel 列表的正确性,最后执行 topic.Start()启动 topic.

Start && messagePump 操作

topic 启动的代码很简单,就是发送一个 startChan:

// 开始消息循环
func (t *Topic) Start() {
	select {
	case t.startChan <- 1:
	default:
	}
}

  那么这个 startChan 发给哪里了呢?还记得 NewTopic 最后执行了 t.messagePump(),startChan 就发送给了它,messagePump 函数负责分发整个 topic 接收到的消息给该 topic 下的 channels.

// 负责消息循环,将进入 topic 中的消息投递到 channel 中
// 仅在 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

	// 仅在触发 startChan 开始运行,否则会阻塞住,直到退出
	for {
		select {
		case <-t.channelUpdateChan:
			continue
		case <-t.pauseChan:
			continue
		case <-t.exitChan:
			goto exit
		case <-t.startChan:
			// 只有接收到 startChan 才触发开始运行
		}
		break
	}
	t.RLock()
	// 获取当前 topic 的所有 channel
	for _, c := range t.channelMap {
		chans = append(chans, c)
	}
	t.RUnlock()
	// 设置消息循环接收的通道
	if len(chans) > 0 && !t.IsPaused() {
		memoryMsgChan = t.memoryMsgChan      // 消息缓冲区
		backendChan = t.backend.ReadChan()   // 落地通道
	}

	// 主消息循环
	for {
		select {
		case msg = <-memoryMsgChan:   // 消息队列收到了新消息,开始运行后面的代码
		case buf = <-backendChan:
			// 内存满了后放到磁盘中的消息读取出来,需要解数据才能使用
			// 解消息的消息结构见 writeMessageToBackend
			msg, err = decodeMessage(buf)
			if err != nil {
				t.ctx.nsqd.logf(LOG_ERROR, "failed to decode message - %s", err)
				continue
			}
		case <-t.channelUpdateChan:
			// channel 的更新,清空后重新获取即可
			chans = chans[:0]
			t.RLock()
			// 新的 channels
			for _, c := range t.channelMap {
				chans = append(chans, c)
			}
			t.RUnlock()
			// 更新一下监听 channel 即可,没有 channel 就暂停
			if len(chans) == 0 || t.IsPaused() {
				memoryMsgChan = nil
				backendChan = nil
			} else {
				memoryMsgChan = t.memoryMsgChan
				backendChan = t.backend.ReadChan()
			}
			continue
		case <-t.pauseChan:
			// 可能是暂停 topic 也可能是开启 topic
			if len(chans) == 0 || t.IsPaused() {
				// 暂停 topic
				memoryMsgChan = nil
				backendChan = nil
			} else {
				// 恢复 topic
				memoryMsgChan = t.memoryMsgChan
				backendChan = t.backend.ReadChan()
			}
			continue
		case <-t.exitChan:
			// 退出 topic 消息循环
			goto exit
		}
		// 遍历所有 channel,在 memoryMsgChan 触发和 backendChan 解消息成功时执行
		for i, channel := range chans {
			// msg 就是那个消息
			chanMsg := msg
			// 每个 channel 都需要唯一的实例,第一个 channel 不需要复制,已经有一份了(msg),小优化
			if i > 0 {
				// 复制新的 message
				chanMsg = NewMessage(msg.ID, msg.Body)
				chanMsg.Timestamp = msg.Timestamp
				chanMsg.deferred = msg.deferred
			}
			// 延迟消息,使用 PutMessageDeferred 进行投递
			if chanMsg.deferred != 0 {
				channel.PutMessageDeferred(chanMsg, chanMsg.deferred)
				continue
			}
			// 即时消息直接投递到所有 channel 即可
			err := channel.PutMessage(chanMsg)
			if err != nil {
				...
			}
		}
	}

exit:
	t.ctx.nsqd.logf(LOG_INFO, "TOPIC(%s): closing ... messagePump", t.name)
}

  整个 topic 的消息循环还是很简洁的,通过一个 for 循环来监听 channel 的修改,topic 的暂停恢复,以及来自消息缓冲区的新消息和落地队列中读取出来的旧消息,再将收到的消息给每个 channel 复制一份进行投递

Topic 上的 channel 相关操作

因为 topic 管理着多个 channel,下面来看看 topic 对 channel 的管理操作

// 获取一个 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
}
// 获取一个 channel 没有就创建一个
func (t *Topic) GetChannel(channelName string) *Channel {
	t.Lock()
	channel, isNew := t.getOrCreateChannel(channelName)
	t.Unlock()

	// 这里监听 exitChan 是一个值得探讨的点,通过监听 exitChan 可以获取 channelUpdateChan 是否关闭
	// 从而控制是否向 channelUpdateChan 中发送消息
	// 否则可能,channelUpdateChan 的对端已经不再监听,但是程序会阻塞在向 channelUpdateChan 发送消息不能释放
	if isNew {
		// 更新 messagePump 状态
		select {
		// 创建新的 channel 就更新 topic
		case t.channelUpdateChan <- 1:
		case <-t.exitChan:
		}
	}

	return channel
}
// 获取或创建一个 Channel 这个函数期望调用者加锁
// 这个函数仅由内部进行调用,例如 GetChannel 调用
func (t *Topic) getOrCreateChannel(channelName string) (*Channel, bool) {
	// 获取 channel
	channel, ok := t.channelMap[channelName]
	if !ok {
		// 没有就创建
		deleteCallback := func(c *Channel) {
			// channel 删除函数,用回调的方式添加到 channel 中
			t.DeleteExistingChannel(c.name)
		}
		// 创建新的 channel
		channel = NewChannel(t.name, channelName, t.ctx, deleteCallback)
		// 加入 channel map
		t.channelMap[channelName] = channel
		return channel, true
	}
	// 存在直接返回
	return channel, false
}

  这里 getChannel 类似 getTopic 都是不存在就创建新的,需要注意的是 channel 的删除回调函数:

// channel 删除的回调函数,仅在 channel 存在时才进行删除
func (t *Topic) DeleteExistingChannel(channelName string) error {
	t.Lock()
	channel, ok := t.channelMap[channelName]
	if !ok {
		t.Unlock()
		return errors.New("channel does not exist")
	}
	// 从 channelMap 中删除这个 channel
	delete(t.channelMap, channelName)
	// not defered so that we can continue while the channel async closes
	numChannels := len(t.channelMap)
	t.Unlock()

	// 删除 channel 前,会先清空 channel 中的 msg 再删除
	channel.Delete()

	// 更新 messagePump 状态
	select {
	case t.channelUpdateChan <- 1:
	case <-t.exitChan:
	}
	// 当临时 topic 中不存在 channel 时,将其删除
	if numChannels == 0 && t.ephemeral == true {
		go t.deleter.Do(func() { t.deleteCallback(t) })
	}
	return nil
}

PutMessage 操作

  topic 里的消息从哪里来?怎么放进来的?首先 topic 里的消息来自于客户端,暂时先不需要了解这部分,只需要知道 topic 怎么去接收一条或多条消息即可,代码如下:

// 向 topic 中写入一条消息
func (t *Topic) PutMessage(m *Message) error {
	t.RLock()
	defer t.RUnlock()
	// topic 已经退出了
	if atomic.LoadInt32(&t.exitFlag) == 1 {
		return errors.New("exiting")
	}
	// 调用 put 放进一条消息
	err := t.put(m)
	if err != nil {
		return err
	}
	// 增加消息计数和数据传输大小
	atomic.AddUint64(&t.messageCount, 1)
	atomic.AddUint64(&t.messageBytes, uint64(len(m.Body)))
	return nil
}

// 向 topic 中写入多条消息
func (t *Topic) PutMessages(msgs []*Message) error {
	t.RLock()
	defer t.RUnlock()
	// topic 已经退出了
	if atomic.LoadInt32(&t.exitFlag) == 1 {
		return errors.New("exiting")
	}

	messageTotalBytes := 0

	for i, m := range msgs {
		// 调用 put 添加 msg
		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
}

// 投递一条消息到 channel 中
func (t *Topic) put(m *Message) error {
	select {
	case t.memoryMsgChan <- m:   // 内存中的消息队列还可以继续存放,直接放入 memoryMsgChan 交给 messagePump 处理即可
	default:
		// 内存中消息队列存放满了,需要存放到 backend 中
		// 临时的 topic 会直接进行丢弃,永久的则会存放到磁盘中
		b := bufferPoolGet() // 从 buffer pool 中获取一个 buffer
		// 调用 writeMessageToBackend 将消息写入磁盘中
		err := writeMessageToBackend(b, m, t.backend)
		// 归还 buffer 对象
		bufferPoolPut(b)
		// 将错误存储在 nsqd 中
		t.ctx.nsqd.SetHealth(err)
		if err != nil {
			...
			return err
		}
	}
	return nil
}

  向 topic 放入消息流程很简单,在放入的同时做一些记录,然后使用 put 先尝试将消息存入 memoryMsgChan,存不进去就会存入 backend 中,然后 topic 的 messagePump 会从 memoryMsgChan 和 backend 中获取消息进行投递.

client->topic->allChannel->clients

  在一个 select 中 case 的优先级高于 default,put 中的 select 其实会被编译为 if{}else{}类型,所以会先判断 memoryMsgChan 能不能存消息,不能存的话,才进行消息的落地

exit 操作

讲完了 topic 的运行流程,最后是如何关闭或删除一个 topic:

// 删除 topic 并关闭所有 channel
func (t *Topic) Delete() error {
	return t.exit(true)
}
// 关闭 topic 存储所有未完成的 topic data 并关闭所有 channel
func (t *Topic) Close() error {
	return t.exit(false)
}
// topic 删除和关闭均调用这个函数
func (t *Topic) exit(deleted bool) error {
	// 打开退出标志
	if !atomic.CompareAndSwapInt32(&t.exitFlag, 0, 1) {
		return errors.New("exiting")
	}
	// 是否删除
	if deleted {
		...
		// 需要在 lookupd 中删除这个 topic
		t.ctx.nsqd.Notify(t)
	} else {
		t.ctx.nsqd.logf(LOG_INFO, "TOPIC(%s): closing", t.name)
	}
	// 关闭 topic 所有 channel,真好用
	close(t.exitChan)

	// 同步等待 messagePump 关闭
	t.waitGroup.Wait()

	if deleted {
		// 删除的话,先清空 channelMap
		t.Lock()
		for _, channel := range t.channelMap {
			// 手动释放了
			delete(t.channelMap, channel.name)
			channel.Delete()
		}
		t.Unlock()

		// 删除所有未投递的消息
		t.Empty()
		return t.backend.Delete()
	}

	// 关闭所有 channel,并不删除消息
	for _, channel := range t.channelMap {
		err := channel.Close()
		if err != nil {
			t.ctx.nsqd.logf(LOG_ERROR, "channel(%s) close - %s", channel.name, err)
		}
	}

	// 将 channel 中未投递的数据全部保存至磁盘中
	t.flush()
	return t.backend.Close()
}
// 清空未投递的消息
func (t *Topic) Empty() error {
	for {
		select {
		case <-t.memoryMsgChan:
			// 把所有消息队列中的消息删除,接受但不处理
		default:
			goto finish
		}
	}
finish:
	// 最后清除 backend 中的消息
	return t.backend.Empty()
}

// 将内存中的未投递消息保存至磁盘中
func (t *Topic) flush() error {
	var msgBuf bytes.Buffer

	if len(t.memoryMsgChan) > 0 {
		...log...
	}
	for {
		select {
		case msg := <-t.memoryMsgChan:
			// 使用 writeMessageToBackend 写入磁盘即可
			err := writeMessageToBackend(&msgBuf, msg, t.backend)
			if err != nil {
				...log...
			}
		default:
			goto finish
		}
	}

finish:
	return nil
}

  topic 的删除和关闭使用的都是 exit 函数,但是二者的区别在于删除会删除所有包括内存和磁盘的消息,但是关闭会把内存中未投递的消息保存至磁盘中.

小结

  topic 下的源码基本就看完了,虽然还没有别的部分完整的完整的串联起来,但是也可以了解到,多个 topic 在初始化时就开启了消息循环 goroutine,执行完 Start 后开始消息分发,将收到的消息分发到管理的 channel 中.每个 topic 运行的 goroutine 比较简单,只有一个消息分发 goroutine: messagePump.
最后附上 github 的 nsqd 的源码解析地址:https://github.com/nercoeus/nsq_source_note

发布了5 篇原创文章 · 获赞 0 · 访问量 89
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 游动-白 设计师: 上身试试

分享到微信朋友圈

×

扫一扫,手机浏览