摘要
- 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
topic
下又挂载了一堆Channel,以及一个backend队列和一个存放消息的go chan。当nsqd接收到对应topic的消息时会先往memoryMsgChan中放,当这里放不下时就会暂存在backend队列中(从这里其实就能看出为什么说nsq是一个内存队列)。同时topic会启动一个messagePump协程来监听memoryMsgChan中的消息,并往topic下的所有Channel中发送
topic被创建之后,就会启动一个协程来监听发往memoryMsgChan或backend队列中的消息,然后转发给topic下面的各个Channel
Channel
再来看看Channel,Channel同样的也包含backend和emoryMsgChan用于存放正常的消息,clients下挂载了订阅了次Channel的消费者,deferredPQ用于实现延迟队列,inFlightPQ存放的是已发往消费者但还未被确认已消费成功的消息
nsqd正式启动
以上就是nsqd在构建之前会初始化的一系列动作。然后nsq就正式启动http,https以及rpc服务接受生产发送的消息。如下图所示!!!
queueScanLoop协程
其中queueScanLoop也是非常重要的一块实现,它其实是一个管理协程,这个协程管理着一个协程池子,这些协程用来处理nsqd中所有Channel下的inflight队列以及deferred队列。从这里其实也可以看出,当消费者的延迟较大或协程池太小,会在一定程度上影响到延迟队列的,导致延迟时间出现偏差。
lookupLoop协程
而lookupLoop主要用于监听topic和Channel创建的消息以及让nsqd和lookup保持心跳。当有Topic或Channel创建时会主动向lookupLoop监听的chan中发送消息,收到消息后lookupLoop便会向nsq中注册的所有lookup中发送消息。以便消费者后续能从lookup中获取。
tcpServer
再来看看nsqd的tcpSrver在被创建后会做些啥,可以看到tcpServer用于监听每个消费者的连接,当有接收到新的消费者连接时,便会启动一个Handle协程,协程的核心处理逻辑在protocolV2的IOLoop中。在这里会启动一个messagePump协程,用于消费消息然后发往对应的消费者。然后Exec则处理具体的请求
messagePump协程
用过nsq的同学应该知道,消费者需要指定消费的topic和channel。在messagePump中,就会获取到当前消费者消费的Channel,然后监听Channel中memoryMsgChan和backenQueue的消息,然后主动推送到对应的消费者。从这里我们也可以看出,当有多个消费者都监听了同一个Topic下相同Channel的消息时,nsqd是如何负载均衡的将消息发送给其中的一个消费者的
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是在消息首次发送时才创建)
topic将消息分发给所有Channel
然后topic的messagePump协程会监听到这条消息,并往topic下的所有Channel中发送,Channel接收到消息后其实也是往自己的memoryMsgChan中发送,如果有多个消费者监听了此Channel的消息,每个消费者对应的的messagePump协程(在protocolV2.IOLoop中创建)就会去竞争Channel中memoryMsgChan中的消息,竞争到了则发往对应的消费者节点。
消费者订阅消息
建立连接
消费者订阅消息时需要指定topic和channel,然后先从nsqlookup中查询保存了此消息的nsqd节点,然后和nsqd节点建立链接。
建立链接之后,nsqd也会给该消费者开启一个协程。并走到了protocolV2.IOLoop中创建messagePump协程。再次贴一下这块代码。在这里再监听对应Channel的memoryMsgChan和backendQueue中的消息。
订阅Channel
在上面可能看过源码的同学会有些疑问,此时的client.SubEventChan是nil,那它是在哪初始化的,其实消费者连接上nsqd之后,就会立刻发送一个SUB指令,表明订阅哪个topic下的channel消息,如下所示源码。订阅成功就会竞争此Channel下memoryMsgChan下的消息(又回到前面)
至此,一条消息从发送到,到最后被接收的所有流程我们就从源码角度梳理完了。后面等有时间我再把流程图更新上来吧~~~
延迟队列
最后再给大家讲讲nsqd中关于延迟队列的实现,这里我觉得是非常值得学习的一点。看完这个对于实现一个本地延迟队列我想是收到擒来的。对于延迟消息,其实从发送到topic,这一步和上面正常的消息是一样的。关键在于topic发送到Channel中的实现,当topic判断到这条消息的defer时间大于0时,就会往Channel的deferQueue中发,deferQueue其实就是一个小根堆。deferQueue中的消息由nsqd中queueScanLoop中维持的Worker来消费(发往本Channel中的memoryMsgChane中),如下基于时间的小根堆
消息超时和重试以及消费报错
消费报错
消息消费报错之后,go-nsq客户端会将消息重新推送回队列中(也即相应Channel的momeryMsChannel中),并消费者产生backoff,即延迟一段时间再重新消费消息(防止重复报错),如果不想让消费者产生backoff,则需手动设置RequeueWithNoBackoff。
消息超时或消费者宕机
消息被推往消费者之后会,会被暂存在inflightPQ中。如果消息迟迟未收到消费者消费确认的回复,或者消费者挂机了,就会由queueScanLoop协程消费进行处理。远端消费者被标记为暂时不可用状态,然后消息重新推送回Channel的momoryMsgChan中,如下图所示:
总结
关于nsq的核心源码,就先分析到这。如果能通读上面核心源码的话,对于更多的细节以及nsqlookup的相关实现,就不难分析了。关于源码上面文字描述居多可能很多人没耐心看下去部分,后面有时间我会再整理几张uml图!