nsq源码分析

432 阅读7分钟

摘要

  • nsq是一个由golang编写的开源实时分布式消息系统,nsq支持集群部署和水平扩展,所以没有单点故障, 并提供可靠的消息交付,是一个高可用的消息系统。一个nsq集群主要由nsqd,nsqlookup,以及nsqdadmin三部分核心的组件组成。生产者可以通过http或rpc的方式直接向nsqd发送消息到对应topic,nsqd在启动时需指定其所注册的nsqlookup,并会将节点信息以及挂载的topic等相关内容注册到nsqlookup该,消费者消费数据时,通过向nsqlookup获取到相应的nsqd节点信息,然后直连nsqd订阅相关topic,当有消息过来时由nsqd主动向消费者推送消息。
  • 这篇文章我就从源码角度,带大家剖析一下nsq的各大特性,以及nsq各核心模块,各组件之间的关系。相信大家看完这篇文章之后会对nsq的原理有一个更清晰的认知,为日后在业务中使用nsq时能做到更加的得心应手(nsq源码)

nsqd

nsqd是nsq最核心的模块,nsqd下主要挂载了一堆topic和Channel,以及提供http,https,以及rpc服务。

初始化

nsqd在启动前,会去nsqd.dat文件中加载之前注册的topic,以及topic下的Channel 1680701013070.png

topic

下又挂载了一堆Channel,以及一个backend队列和一个存放消息的go chan。当nsqd接收到对应topic的消息时会先往memoryMsgChan中放,当这里放不下时就会暂存在backend队列中(从这里其实就能看出为什么说nsq是一个内存队列)。同时topic会启动一个messagePump协程来监听memoryMsgChan中的消息,并往topic下的所有Channel中发送 1680701321642.png

topic被创建之后,就会启动一个协程来监听发往memoryMsgChan或backend队列中的消息,然后转发给topic下面的各个Channel 1680701798683.png 1680701885703.png

Channel

再来看看Channel,Channel同样的也包含backend和emoryMsgChan用于存放正常的消息,clients下挂载了订阅了次Channel的消费者,deferredPQ用于实现延迟队列,inFlightPQ存放的是已发往消费者但还未被确认已消费成功的消息 1680702049148.png

nsqd正式启动

以上就是nsqd在构建之前会初始化的一系列动作。然后nsq就正式启动http,https以及rpc服务接受生产发送的消息。如下图所示!!! 1680702780476.png

queueScanLoop协程

其中queueScanLoop也是非常重要的一块实现,它其实是一个管理协程,这个协程管理着一个协程池子,这些协程用来处理nsqd中所有Channel下的inflight队列以及deferred队列。从这里其实也可以看出,当消费者的延迟较大或协程池太小,会在一定程度上影响到延迟队列的,导致延迟时间出现偏差。 1680703214773.png 1680703246343.png

lookupLoop协程

而lookupLoop主要用于监听topic和Channel创建的消息以及让nsqd和lookup保持心跳。当有Topic或Channel创建时会主动向lookupLoop监听的chan中发送消息,收到消息后lookupLoop便会向nsq中注册的所有lookup中发送消息。以便消费者后续能从lookup中获取。 1680703860302.png

tcpServer

再来看看nsqd的tcpSrver在被创建后会做些啥,可以看到tcpServer用于监听每个消费者的连接,当有接收到新的消费者连接时,便会启动一个Handle协程,协程的核心处理逻辑在protocolV2的IOLoop中。在这里会启动一个messagePump协程,用于消费消息然后发往对应的消费者。然后Exec则处理具体的请求 1680704264121.png 1680704366830.png

messagePump协程

用过nsq的同学应该知道,消费者需要指定消费的topic和channel。在messagePump中,就会获取到当前消费者消费的Channel,然后监听Channel中memoryMsgChan和backenQueue的消息,然后主动推送到对应的消费者。从这里我们也可以看出,当有多个消费者都监听了同一个Topic下相同Channel的消息时,nsqd是如何负载均衡的将消息发送给其中的一个消费者的 1680704669014.png

httpServer和httpsServer

对于nsqd的httpServer和httpsServer我就不再描述了,这两个就比较简单,就是主要接受生产者到的请求然后处理请求,一般业务中正式环境也不会用http的方式来推送或数据。

nsqd启动概述

关于nsqd核心的内容大致就是我上面点出来的这些,从这里就能看出nsqd中topic和Channel的关系,nsqd和nsqlookup之间的交互,以及nsqd和消费者生产者之间是如何交互的。

nsq消息生产者

我们再来看看生产推送一条消息的源码流程大概是什么样的。消费者一般通过rpc与其中一台nsqd建立连接并发送消息(注意:如果数据量较大需要往多个nsqd节点发送同样的业务消息,则需自己实现负载均衡策略,或者考虑换其他消息队列)。

消息发往topic

生产者与nsqd建立连接之后其实就会走到我们上面说的protocolV2.IOLoop中的Exec方法中,在这里如果是发送消息,则会找到对应的topic,并往Topic的memoryMsgChan中或backendQu中发送。如果是新创建的topic,则也会通知到nsqlookup(可以看出topic是在消息首次发送时才创建) 1680706052448.png

topic将消息分发给所有Channel

然后topic的messagePump协程会监听到这条消息,并往topic下的所有Channel中发送,Channel接收到消息后其实也是往自己的memoryMsgChan中发送,如果有多个消费者监听了此Channel的消息,每个消费者对应的的messagePump协程(在protocolV2.IOLoop中创建)就会去竞争Channel中memoryMsgChan中的消息,竞争到了则发往对应的消费者节点。 1680706428566.png

消费者订阅消息

建立连接

消费者订阅消息时需要指定topic和channel,然后先从nsqlookup中查询保存了此消息的nsqd节点,然后和nsqd节点建立链接。 1680706930485.png 建立链接之后,nsqd也会给该消费者开启一个协程。并走到了protocolV2.IOLoop中创建messagePump协程。再次贴一下这块代码。在这里再监听对应Channel的memoryMsgChan和backendQueue中的消息。 1680706871654.png

订阅Channel

在上面可能看过源码的同学会有些疑问,此时的client.SubEventChan是nil,那它是在哪初始化的,其实消费者连接上nsqd之后,就会立刻发送一个SUB指令,表明订阅哪个topic下的channel消息,如下所示源码。订阅成功就会竞争此Channel下memoryMsgChan下的消息(又回到前面) 1680707193247.png 至此,一条消息从发送到,到最后被接收的所有流程我们就从源码角度梳理完了。后面等有时间我再把流程图更新上来吧~~~

延迟队列

最后再给大家讲讲nsqd中关于延迟队列的实现,这里我觉得是非常值得学习的一点。看完这个对于实现一个本地延迟队列我想是收到擒来的。对于延迟消息,其实从发送到topic,这一步和上面正常的消息是一样的。关键在于topic发送到Channel中的实现,当topic判断到这条消息的defer时间大于0时,就会往Channel的deferQueue中发,deferQueue其实就是一个小根堆。deferQueue中的消息由nsqd中queueScanLoop中维持的Worker来消费(发往本Channel中的memoryMsgChane中),如下基于时间的小根堆 1680707956709.png

消息超时和重试以及消费报错

消费报错

消息消费报错之后,go-nsq客户端会将消息重新推送回队列中(也即相应Channel的momeryMsChannel中),并消费者产生backoff,即延迟一段时间再重新消费消息(防止重复报错),如果不想让消费者产生backoff,则需手动设置RequeueWithNoBackoff。

消息超时或消费者宕机

消息被推往消费者之后会,会被暂存在inflightPQ中。如果消息迟迟未收到消费者消费确认的回复,或者消费者挂机了,就会由queueScanLoop协程消费进行处理。远端消费者被标记为暂时不可用状态,然后消息重新推送回Channel的momoryMsgChan中,如下图所示: image.png

总结

关于nsq的核心源码,就先分析到这。如果能通读上面核心源码的话,对于更多的细节以及nsqlookup的相关实现,就不难分析了。关于源码上面文字描述居多可能很多人没耐心看下去部分,后面有时间我会再整理几张uml图!