NSQ系列(二):nsqd原理和实现

本文详述了nsqd的工作原理和实现,包括nsqd与Producer、Consumer的交互,延迟发送处理,以及与nsqlookupd的同步。内容涵盖数据结构设计,如NSQD、Topic和Channel,以及启动过程、TCP Server的IOLoop、messagePump和Exec命令处理。此外,还讨论了queueScanLoop和lookupLoop的作用,如消息确认超时和延迟队列的管理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在前一篇文章,我们大概了解了NSQ的集群架构和交互流程,这里我们将结合代码详细了解下nsqd的一些特性是如何实现,主要有,

  • nsqd与Producer的交互。nsqd如何处理Producer投递的消息,如果消息是延迟发送怎么处理
  • nsqd与Consumer的交互。nsqd如何处理Consumer Client的连接,如何把消息投递给Consumer,如果消息ack超时怎么处理
  • nsqd与nsqlookupd的交互。nsqd与nsqlookupd定期ping以及获取Topic和Channel的变更信息

数据结构

先看下数据结构的设计,主要三部分:NSQD、Topic和Channel

NSQD

nsq的数据结构定义如下,

type NSQD struct {
   
	// 64bit atomic vars need to be first for proper alignment on 32bit platforms
	clientIDSequence int64

	sync.RWMutex

	opts atomic.Value

	dl        *dirlock.DirLock
	isLoading int32
	errValue  atomic.Value
	startTime time.Time

	topicMap map[string]*Topic

	clientLock sync.RWMutex
	clients    map[int64]Client

	lookupPeers atomic.Value

	tcpListener   net.Listener
	httpListener  net.Listener
	httpsListener net.Listener
	tlsConfig     *tls.Config

	poolSize int

	notifyChan           chan interface{
   }
	optsNotificationChan chan struct{
   }
	exitChan             chan int
	waitGroup            util.WaitGroupWrapper

	ci *clusterinfo.ClusterInfo
}

NSQD的一些主要的字段,

topicMap map[string]*Topic

存储所有的Topic信息,key为topic name,

clients    map[int64]Client

存储当前所有与nsqd建立连接的Consumer,key为ClientID(nsqd会为每个Consumer Client在建立连接时分配一个ID)。

Topic

type Topic struct {
   
	// 64bit atomic vars need to be first for proper alignment on 32bit platforms
	messageCount uint64
	messageBytes uint64

	sync.RWMutex

	name              string
	// 记录Topic对应的Channel
	channelMap        map[string]*Channel 
	// 消息后端存储(目前主要是磁盘存储)
	backend           BackendQueue
	// 消息内存存储
	memoryMsgChan     chan *Message
	startChan         chan int
	exitChan          chan int
	// 监听Topic下的Channel信息是否更新
	channelUpdateChan chan int
	waitGroup         util.WaitGroupWrapper
	exitFlag          int32
	idFactory         *guidFactory
	
	// 是否是临时Topic
	ephemeral      bool
	deleteCallback func(*Topic)
	deleter        sync.Once

	paused    int32
	pauseChan chan int

	ctx *context
}

Channel

NSQ中,消息从Producer投递到Topic,Topic再投递到Channel,和Topic一样,Channel对消息的存储也分内存和后端,其中,后端存储目前也主要是磁盘存储。

type Channel struct {
   
	// 64bit atomic vars need to be first for proper alignment on 32bit platforms
	requeueCount uint64
	messageCount uint64
	timeoutCount uint64

	sync.RWMutex

	topicName string
	name      string
	ctx       *context
	
	// 后端存储
	backend BackendQueue
	// 内存存储
	memoryMsgChan chan *Message
	exitFlag      int32
	exitMutex     sync.RWMutex

	// state tracking
	clients        map[int64]Consumer
	paused         int32
	// 是否是临时Channel
	ephemeral      bool
	deleteCallback func(*Channel)
	deleter        sync.Once

	// Stats tracking
	e2eProcessingLatencyStream *quantile.Quantile

	// TODO: these can be DRYd up
	// NSQ支持消息延迟发送,Channel中会存储消息的延迟发送信息
	// map存储所有需要延迟发送的消息
	deferredMessages map[MessageID]*pqueue.Item
	// 延迟发送队列, 基于延迟时间进行堆排序
	deferredPQ       pqueue.PriorityQueue
	deferredMutex    sync.Mutex
	// map存储处于发送中的消息
	inFlightMessages map[MessageID]*Message
	// 同样用队列来记录消息的超时时间
	inFlightPQ       inFlightPqueue
	inFlightMutex    sync.Mutex
}

功能实现

nsqd启动

先看下nsqd的启动过程,

func (p *program) Start() error {
   
	// 加载 and 验证 启动配置
	opts := nsqd.NewOptions()
	...
	
	// 加载元数据
	err = p.nsqd.LoadMetadata()
	// 持久化元数据
	err = p.nsqd.PersistMetadata()
	// 异步启动 nsqd 实例
	go func() {
   
		err := p.nsqd.Main()
		if err != nil {
   
			p.Stop()
			os.Exit(1)
		}
	}()
}

上面代码涉及到了MetaData的加载和持久化,这里MetaData主要存储Topic和Channel信息,这样nsqd在崩溃重启时,可以像崩溃前一样继续服务。

func (n *NSQD) Main() error {
   
	// 监听错误退出
	exitCh := make(chan error)
	var once sync.Once
	exitFunc := func(err error) {
   
		once.Do(func() {
   
			if err != nil {
   
				n.logf(LOG_FATAL, "%s", err)
			}
			exitCh <- err
		})
	}

	tcpServer := &tcpServer{
   ctx: ctx}
	n.waitGroup.Wrap(func() {
   
		exitFunc(protocol.TCPServer(n.tcpListener, tcpServer, n.logf))
	})
	httpServer := newHTTPServer(ctx, false, n.getOpts().TLSRequired == TLSRequired)
	n.waitGroup.Wrap(func() {
   
		exitFunc(http_api.Serve(n.httpListener, httpServer, "HTTP", n.logf))
	})
	if n.tlsConfig != nil && n.getOpts().HTTPSAddress != "" {
   
		httpsServer := newHTTPServer(ctx, true, true)
		n.waitGroup.Wrap(func() {
   
			exitFunc(http_api.Serve(n.httpsListener, httpsServer, "HTTPS", n.logf))
		})
	}

	n.waitGroup.Wrap(n.queueScanLoop)
	n.waitGroup.Wrap(n.lookupLoop)
	if n.getOpts().StatsdAddress != "" {
   
		n.waitGroup.Wrap(n.statsdLoop)
	}
}

nsqd的启动,除了启动TCP Server和HTTP Server(nsq支持TCP和HTTP两种方式发布消息,消费信息只支持TCP),还启动了三个Goroutine loop,queueScanLoop、lookupLoop 和 statsdLoop。

提供服务

在启动模块我看看到了nsqd启动了TCP Server和HTTP Server,以及三个goroutine,这里详细介绍下这些服务和loop的作用,

TCP Server

nsq支持基于TCP连接来发送、消费消息,创建、删除Topic和Channel等。TCP Server的Handle方法负责处理每个与nsqd建立的TCP连接。nsq制定了一套简单的通讯协议(详细可以参考https://nsq.io/clients/tcp_protocol_spec.html),代码中的实现就是protocolV2的IOLoop方法,

IOLoop
func (p *protocolV2) IOLoop(conn net.Conn) error {
   
	// 注册Client信息。每个与nsqd建立连接的Client都会被分配一个ID
	clientID := atomic.AddInt64(&p.ctx.nsqd.clientIDSequence, 1)
	client := newClientV2(clientID, conn, p.ctx)
	p.ctx.nsqd.AddClient(client.ID, client)

	// 启动messagePump goroutine,主要进行
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值