NSQ源码分析之Topic

什么是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源码心得要点如下:

  1. topic为了提高消息读取的的性能,做了两层缓存,上层是基于内存的缓存memoryMsgChan,利用golang的有缓冲通道实现,外部发布数据给topic时优先将数据放到memoryMsgChan,但是当消息积压太多,内存会耗尽,因此,还设计了基于硬盘的缓存backendChan,当memoryMsgChan满了的时候,就会将数据存储到backendChan,将消息转发channel的时候,优先从memoryMsgChan读取消息,然后从这样,backendChan,这样提高了数据读写效率的同时,又节约内存;
  2. 当然,如果为了进一步提高读写性能,并不考虑内存的使用情况,可以将Topic的名称以“#ephemeral”结尾,将Topic设置为临时Topic,那么backendChan同样是使用计算机内存,临时topic的通道数为0的时候,将会被删除;
  3. 在对channel做写操作的时候,需要加锁保证线程安全。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值