剖析nsq消息队列-目录

上两篇帖子主要说了一下nsq的拓扑结构,如何进行故障处理和横向扩展,保证了客户端和服务端的长连接,连接保持了,就要传输数据了,nsq如何保证消息被订阅者消费,如何保证消息不丢失,就是今天要阐述的内容。

nsq topic、channel、和消费我客户端的结构如上图,一个topic下有多个channel每个channel可以被多个客户端订阅。

消息处理的大概流程:当一个消息被nsq接收后,传给相应的topic,topic把消息传递给所有的channel ,channel根据算法选择一个订阅客户端,把消息发送给客户端进行处理。

看上去这个流程是没有问题的,我们来思考几个问题

  • 网络传输的不确定性,比如超时;客户端处理消息时崩溃等,消息如何重传;
  • 如何标识消息被客户端成功处理完毕;
  • 消息的持久化,nsq服务端重新启动时消息不丢失;

服务端对发送中的消息处理逻辑

之前的帖子说过客户端和服务端进行连接后,会启动一个gorouting来发送信息给客户端

	go p.messagePump(client, messagePumpStartedChan)

然后会监听客户端发过来的命令client.Reader.ReadSlice('\n')

服务端会定时检查client端的连接状态,读取客户端发过来的各种命令,发送心跳等。每一个连接最终的目的就是监听channel的消息,发送给客户端进行消费。

当有消息发送给订阅客户端的时候,当然选择哪个client也是有无则的,这个以后讲,

func (p *protocolV2) messagePump(client *clientV2, startedChan chan bool) {
// ...
for {
// ...
case b := <-backendMsgChan:
if sampleRate > 0 && rand.Int31n(100) > sampleRate {
continue
} msg, err := decodeMessage(b)
if err != nil {
p.ctx.nsqd.logf(LOG_ERROR, "failed to decode message - %s", err)
continue
}
msg.Attempts++ subChannel.StartInFlightTimeout(msg, client.ID, msgTimeout)
client.SendingMessage()
err = p.SendMessage(client, msg)
if err != nil {
goto exit
}
flushed = false
case msg := <-memoryMsgChan:
if sampleRate > 0 && rand.Int31n(100) > sampleRate {
continue
}
msg.Attempts++ subChannel.StartInFlightTimeout(msg, client.ID, msgTimeout)
client.SendingMessage()
err = p.SendMessage(client, msg)
if err != nil {
goto exit
}
flushed = false
case <-client.ExitChan:
goto exit
}
} // ...
}

看一下这个方法调用subChannel.StartInFlightTimeout(msg, client.ID, msgTimeout),在发送给客户端之前,把这个消息设置为在飞翔中,

// pushInFlightMessage atomically adds a message to the in-flight dictionary
func (c *Channel) pushInFlightMessage(msg *Message) error {
c.inFlightMutex.Lock()
_, ok := c.inFlightMessages[msg.ID]
if ok {
c.inFlightMutex.Unlock()
return errors.New("ID already in flight")
}
c.inFlightMessages[msg.ID] = msg
c.inFlightMutex.Unlock()
return nil
}

然后发送给客户端进行处理。

在发送中的数据,存在的各种不确定性,nsq的处理方式是:对发送给客户端信息设置为在飞翔中,如果在如果处理成功就把这个消息从飞翔中的状态中去掉,如果在规定的时间内没有收到客户端的反馈,则认为这个消息超时,然后重新归队,两次进行处理。所以无论是哪种特殊情况,nsq统一认为消息为超时。

服务端处理超时消息

nsq对超时消息的处理,借鉴了redis的过期算法,但也不太一样redis的更复杂一些,因为redis是单线程的,还要处理占用cpu时间等等,nsq因为gorouting的存在要很简单很多。

简单来说,就是在nsq启动的时候启动协程去处理channel的过期数据

func (n *NSQD) Main() error {
// ...
// 启动协程去处理channel的过期数据
n.waitGroup.Wrap(n.queueScanLoop)
n.waitGroup.Wrap(n.lookupLoop)
if n.getOpts().StatsdAddress != "" {
n.waitGroup.Wrap(n.statsdLoop)
} err := <-exitCh
return err
}

当然不是每一个channel启动一个协程来处理过期数据,而是有一些规定,我们看一下一些默认值,然后再展开讲算法

	return &Options{
// ... HTTPClientConnectTimeout: 2 * time.Second,
HTTPClientRequestTimeout: 5 * time.Second,
// 内存最大队列数
MemQueueSize: 10000,
MaxBytesPerFile: 100 * 1024 * 1024,
SyncEvery: 2500,
SyncTimeout: 2 * time.Second, // 扫描channel的时间间隔
QueueScanInterval: 100 * time.Millisecond,
// 刷新扫描的时间间隔
QueueScanRefreshInterval: 5 * time.Second,
QueueScanSelectionCount: 20,
// 最大的扫描池数量
QueueScanWorkerPoolMax: 4,
// 标识百分比
QueueScanDirtyPercent: 0.25,
// 消息超时
MsgTimeout: 60 * time.Second,
MaxMsgTimeout: 15 * time.Minute,
MaxMsgSize: 1024 * 1024,
MaxBodySize: 5 * 1024 * 1024,
MaxReqTimeout: 1 * time.Hour,
ClientTimeout: 60 * time.Second, // ...
}

这些参数都可以在启动nsq的时候根据自己需要来指定,我们主要说一下这几个:

  • QueueScanWorkerPoolMax就是最大协程数,默认是4,这个数是扫描所有channel的最大协程数,当然channel的数量小于这个参数的话,就调整协程的数量,以最小的为准,比如channel的数量为2个,而默认的是4个,那就调扫描的数量为2
  • QueueScanSelectionCount 每次扫描最大的channel数量,默认是20,如果channel的数量小于这个值,则以channel的数量为准。
  • QueueScanDirtyPercent 标识脏数据 channel的百分比,默认为0.25,eg: channel数量为10,则一次最多扫描10个,查看每个channel是否有过期的数据,如果有,则标记为这个channel是有脏数据的,如果有脏数据的channel的数量 占这次扫描的10个channel的比例超过这个百分比,则直接再次进行扫描一次,而不用等到下一次时间点。
  • QueueScanInterval 扫描channel的时间间隔,默认的是每100毫秒扫描一次。
  • QueueScanRefreshInterval 刷新扫描的时间间隔 目前的处理方式是调整channel的协程数量。

    这也就是nsq处理过期数据的算法,总结一下就是,使用协程定时去扫描随机的channel里是否有过期数据。
func (n *NSQD) queueScanLoop() {
workCh := make(chan *Channel, n.getOpts().QueueScanSelectionCount)
responseCh := make(chan bool, n.getOpts().QueueScanSelectionCount)
closeCh := make(chan int) workTicker := time.NewTicker(n.getOpts().QueueScanInterval)
refreshTicker := time.NewTicker(n.getOpts().QueueScanRefreshInterval) channels := n.channels()
n.resizePool(len(channels), workCh, responseCh, closeCh) for {
select {
case <-workTicker.C:
if len(channels) == 0 {
continue
}
case <-refreshTicker.C:
channels = n.channels()
n.resizePool(len(channels), workCh, responseCh, closeCh)
continue
case <-n.exitChan:
goto exit
} num := n.getOpts().QueueScanSelectionCount
if num > len(channels) {
num = len(channels)
} loop:
// 随机channel
for _, i := range util.UniqRands(num, len(channels)) {
workCh <- channels[i]
} numDirty := 0
for i := 0; i < num; i++ {
if <-responseCh {
numDirty++
}
} if float64(numDirty)/float64(num) > n.getOpts().QueueScanDirtyPercent {
goto loop
}
} exit:
n.logf(LOG_INFO, "QUEUESCAN: closing")
close(closeCh)
workTicker.Stop()
refreshTicker.Stop()
}

在扫描channel的时候,如果发现有过期数据后,会重新放回到队列,进行重发操作。

func (c *Channel) processInFlightQueue(t int64) bool {
// ...
for {
c.inFlightMutex.Lock()
msg, _ := c.inFlightPQ.PeekAndShift(t)
c.inFlightMutex.Unlock() if msg == nil {
goto exit
}
dirty = true _, err := c.popInFlightMessage(msg.clientID, msg.ID)
if err != nil {
goto exit
}
atomic.AddUint64(&c.timeoutCount, 1)
c.RLock()
client, ok := c.clients[msg.clientID]
c.RUnlock()
if ok {
client.TimedOutMessage()
}
//重新放回队列进行消费处理。
c.put(msg)
} exit:
return dirty
}

客户端对消息的处理和响应

之前的帖子中的例子中有说过,客户端要消费消息,需要实现接口

type Handler interface {
HandleMessage(message *Message) error
}

在服务端发送消息给客户端后,如果在处理业务逻辑时,如果发生错误则给服务器发送Requeue命令告诉服务器,重新发送消息进处理。如果处理成功,则发送Finish命令

func (r *Consumer) handlerLoop(handler Handler) {
r.log(LogLevelDebug, "starting Handler") for {
message, ok := <-r.incomingMessages
if !ok {
goto exit
} if r.shouldFailMessage(message, handler) {
message.Finish()
continue
} err := handler.HandleMessage(message)
if err != nil {
r.log(LogLevelError, "Handler returned error (%s) for msg %s", err, message.ID)
if !message.IsAutoResponseDisabled() {
message.Requeue(-1)
}
continue
} if !message.IsAutoResponseDisabled() {
message.Finish()
}
} exit:
r.log(LogLevelDebug, "stopping Handler")
if atomic.AddInt32(&r.runningHandlers, -1) == 0 {
r.exit()
}
}

服务端收到命令后,对飞翔中的消息进行处理,如果成功则去掉,如果是Requeue则执行归队和重发操作,或者进行defer队列处理。

消息的持久化

默认的情况下,只有内存队列不足时MemQueueSize:10000时,才会把数据保存到文件内进行持久到硬盘。

	select {
case c.memoryMsgChan <- m:
default:
b := bufferPoolGet()
err := writeMessageToBackend(b, m, c.backend)
bufferPoolPut(b)
c.ctx.nsqd.SetHealth(err)
if err != nil {
c.ctx.nsqd.logf(LOG_ERROR, "CHANNEL(%s): failed to write message to backend - %s",
c.name, err)
return err
}
}
return nil

如果将 --mem-queue-size 设置为 0,所有的消息将会存储到磁盘。我们不用担心消息会丢失,nsq 内部机制保证在程序关闭时将队列中的数据持久化到硬盘,重启后就会恢复。

nsq自己开发了一个库go-diskqueue来持久会消息到内存。这个库的代码量不多,理解起来也不难,代码逻辑我想下一篇再讲。

看一下保存在硬盘后的样子:

nsq (三) 消息传输的可靠性和持久化[一]的更多相关文章

  1. nsq (三) 消息传输的可靠性和持久化[二]diskqueue

    上一篇主要说了一下nsq是如何保证消息被消费端成功消费,大概提了一下消息的持久化,--mem-queue-size 设置为 0,所有的消息将会存储到磁盘. 总有人说nsq的持久化问题,消除疑虑的方法就 ...

  2. 深入浅出 JMS(三) - ActiveMQ 消息传输

    深入浅出 JMS(三) - ActiveMQ 消息传输 一.消息协商器(Message Broker) broke:消息的交换器,就是对消息进行管理的容器.ActiveMQ 可以创建多个 Broker ...

  3. RabbitMQ系列(四)--消息如何保证可靠性传输以及幂等性

    一.消息如何保证可靠性传输 1.1.可能出现消息丢失的情况 1.Producer在把Message发送Broker的过程中,因为网络问题等发生丢失,或者Message到了Broker,但是出了问题,没 ...

  4. RabbitMQ原理与相关操作(三)消息持久化

    现在聊一下RabbitMQ消息持久化: 问题及方案描述 1.当有多个消费者同时收取消息,且每个消费者在接收消息的同时,还要处理其它的事情,且会消耗很长的时间.在此过程中可能会出现一些意外,比如消息接收 ...

  5. RabbitMQ(三):消息持久化策略

    原文:RabbitMQ(三):消息持久化策略 一.前言 在正常的服务器运行过程中,时常会面临服务器宕机重启的情况,那么我们的消息此时会如何呢?很不幸的事情就是,我们的消息可能会消失,这肯定不是我们希望 ...

  6. RabbitMQ的消息传输保障三个层级

    这里只简单介绍一下三个层级,笔记摘录自<RabbitMQ实战指南>朱忠华作者 消息可靠传输一般是业务系统接入消息中间件时候首要考虑的问题,一般消息中间件的消息传输保障分为三个层级 1  A ...

  7. 转:TCP为什么要3次握手和4次挥手时等待2MSL、 TCP如何保证消息顺序以及可靠性到达

    关于tcp三次握手.四次挥手可以看这里:TCP与UDP的差别以及TCP三次握手.四次挥手 1.TCP为甚要3次握手? 在谢希仁著<计算机网络>第四版中讲“三次握手”的目的是“为了防止已失效 ...

  8. Rsyslog的三种传输协议简要介绍

    rsyslog的三种传输协议 rsyslog 可以理解为多线程增强版的syslog. rsyslog提供了三种远程传输协议,分别是: 1. UDP 传输协议 基于传统UDP协议进行远程日志传输,也是传 ...

  9. 实时消息传输协议 RTMP(Real Time Messaging Protocol)

    实时消息传输协议(RTMP)最初是由 Macromedia 为互联网上 Flash player 和服务器之间传输音频.视频以及数据流而开发的一个私有协议.Adobe 收购 Macromedia 购以 ...

随机推荐

  1. 神奇的互换身体术--java的类型擦除

    故事背景 <互换身体>是由环球影业发行的喜剧电影,于2011年8月5日在美国上映.该片由大卫·道金执导,瑞安·雷诺兹.杰森·贝特曼.奥利维亚·王尔德等主演.该片讲述了一位居家好男人和一位蜂 ...

  2. 转:怎么用Sql语句获取一个数据库中的所有表的名字

    用sql获取数据库中所有的表名的方法:1.oracle下:select table_name from all_tables;2.MySQL下:select table_name from infor ...

  3. meta标签设置(移动端)

    一.首先出结论:移动端meta标签一般设置为: <meta content="width=device-width,initial-scale=1.0,maxinmum-scale=1 ...

  4. 构建之法——homework1:问题思考

    1.我看了第一章概论,1.2.4 软件工程的目标——创造“足够好”的软件,其中提到了什么是好的软件?  软件工程的一个要素就是把软件的Bug都消灭掉的过程. 提问:我们知道Bug是不可能完全消灭掉的, ...

  5. java异常类的妙用

    异常类的妙用   以往在使用异常时,只是知道通过异常类的构造方法设置一些出错信息,此外最多就是把引起该异常的原因通过Throwable类的子类一同设置进去.今天在分析springSecurity3.0 ...

  6. 【ADO.NET基础-Login】带验证码验证的登录界面(用于简单的基础学习)

    以下代码如果有不对或者不妥之处,还望大神们指点一二 或者有同学者有问题或建议,一定要提出来,共同探讨 小弟在此感谢! 前台代码: <!DOCTYPE html> <html xmln ...

  7. Angular Cli 升级到最新版本

    1. 卸载当前版本 npm uninstall -g angular-cli 2. 清除未卸载干净的angular-cli缓存 npm cache clean -f 3. 到安装目录查看是否卸载干净 ...

  8. win10 cnpm安装完之后一直说不是内部命令的原因

    找到cnpm的默认安装路径 一般默认的是 D:\Program Files\nodejs\node_modules 然后添加环境变量中 win10是在系统环境变量中切记不是在用户变量中.保存之后,重新 ...

  9. 文本数据IO操作--字符流

    一.Reader和Writer 1. 字符流原理 Reader是所有字符输入流的父类而Writer是所有字符输出流的父类.字符流是以字符(char)为单位读写数据的.一次处理一个unicode.字符流 ...

  10. Android的各大框架整理

    OpenDanmaku  :一个Android的弹幕控件 地址:https://github.com/linsea/OpenDanmaku AndroidViewAnimations:Andorid视 ...