NSQ(6)-nsq相关策略
1:nsq的流量控制 RDY
消息中间件的实现无非两种套路,一种让客户端pull,典型的比如kafka便是如此,而另一种则是push,也就是让客户端不需要做任何操作,只需要做好conn便可以源源不断收到服务端的推送,典型的代表就是我们今天介绍的nsq。
pull的优势在于客户端可以自己做流控,比如客户端想什么时候pull就什么时候pull,不会因为服务端的强迫而接受,但劣势也很明显,如果服务端的生产速度很慢,客户端需要不断的轮询会让cpu处于繁忙且无用的状态。
push的优势则在于能够不受限于客户端的速度,可以让服务端更快的、批量的把数据push给客户端,因此大部分push实现的消息中间件都是属于内存型,而nsq比较特殊,它实际上是内存+磁盘的一个消息中间件。上面也说了,pull流的优势在于可以让客户端自由控制消息的速度,但是push流不一样,push流不管客户端是否多繁忙都会推送消息,如果没有一个流控机制,很容易让客户端最终因为消费速度跟不上导致产生各种性能问题。nsq其实也考虑到这一点,于是采用了一个
RDY的状态字段来表示流控。简单来说,就是客户端连接上nsqd之后,会告诉nsqd它的可接受的消息数量是多少,每当nsqd给客户端推送一条消息这个RDY就会减一,而客户端消费完毕并且发送一个FIN之后,这个RDY又会加一(其实这个设计有点类似tcp中的用来控制流量的窗口机制)。
// ConnectToNSQD 部分代码
// pre-emptive signal to existing connections to lower their RDY count
r.connections[addr] = conn
for _, c := range r.conns() {
r.maybeUpdateRDY(c)
}
- 可以看到 go-nsq 的Consumer 结构体中有一个字段 connections,它是一个 map,key是nsqd addr,value是连接成功的Conn。
- 上面的代码表示会遍历这个Customer的所有 nsqd conn(customer可以同时连接多个nsqd),然后调用
maybeUpdateRDY方法。
func (r *Consumer) maybeUpdateRDY(conn *Conn) {
inBackoff := r.inBackoff()
inBackoffTimeout := r.inBackoffTimeout()
if inBackoff || inBackoffTimeout {
r.log(LogLevelDebug, "(%s) skip sending RDY inBackoff:%v || inBackoffTimeout:%v",
conn, inBackoff, inBackoffTimeout)
return
}
count := r.perConnMaxInFlight()
r.log(LogLevelDebug, "(%s) sending RDY %d", conn, count)
r.updateRDY(conn, count)
}
- 计算平均每一个连接的最大 InFlight 数量。
func (r *Consumer) updateRDY(c *Conn, count int64) error {
if c.IsClosing() {
return ErrClosing
}
// never exceed the nsqd's configured max RDY count
if count > c.MaxRDY() {
count = c.MaxRDY()
}
// stop any pending retry of an old RDY update
r.rdyRetryMtx.Lock()
if timer, ok := r.rdyRetryTimers[c.String()]; ok {
timer.Stop()
delete(r.rdyRetryTimers, c.String())
}
r.rdyRetryMtx.Unlock()
// never exceed our global max in flight. truncate if possible.
// this could help a new connection get partial max-in-flight
rdyCount := c.RDY()
maxPossibleRdy := int64(r.getMaxInFlight()) - atomic.LoadInt64(&r.totalRdyCount) + rdyCount
if maxPossibleRdy > 0 && maxPossibleRdy < count {
count = maxPossibleRdy
}
if maxPossibleRdy <= 0 && count > 0 {
if rdyCount == 0 {
// we wanted to exit a zero RDY count but we couldn't send it...
// in order to prevent eternal starvation we reschedule this attempt
// (if any other RDY update succeeds this timer will be stopped)
r.rdyRetryMtx.Lock()
r.rdyRetryTimers[c.String()] = time.AfterFunc(5*time.Second,
func() {
r.updateRDY(c, count)
})
r.rdyRetryMtx.Unlock()
}
return ErrOverMaxInFlight
}
return r.sendRDY(c, count)
}
- 此处主要是对 rdy计数count 进行了预处理。
func (r *Consumer) sendRDY(c *Conn, count int64) error {
if count == 0 && c.LastRDY() == 0 {
// no need to send. It's already that RDY count
return nil
}
atomic.AddInt64(&r.totalRdyCount, count-c.RDY())
c.SetRDY(count)
err := c.WriteCommand(Ready(int(count)))
if err != nil {
r.log(LogLevelError, "(%s) error sending RDY %d - %s", c.String(), count, err)
return err
}
return nil
}
- 组装成
RDY命令,通过当前连接的WriteCommand方法告诉给 nsqd。
// NewConsumer
func (r *Consumer) rdyLoop() {
redistributeTicker := time.NewTicker(r.config.RDYRedistributeInterval)
for {
select {
case <-redistributeTicker.C:
r.redistributeRDY()
case <-r.exitChan:
goto exit
}
}
exit:
redistributeTicker.Stop()
r.log(LogLevelInfo, "rdyLoop exiting")
r.wg.Done()
}
- 在连接成功之后,也会单独开启一个 goroutine 在后台不断去调整这个
rdycount。
2.nsq消息传输的可靠性与持久化
消息传输的可靠性
思考下面的问题:
- 网络传输的不确定性,比如超时。
- 客户端处理消息时崩溃,消息如何重传。
- 如何标识消息被客户端成功处理完毕。
- 消息的持久化,nsq服务端重新启动时消息不丢失。
func (p *protocolV2) messagePump(client *clientV2, startedChan chan bool)
- 客户端处理消息的逻辑:
- 在上述函数中,服务端会定时检查client端的连接状态,读取客户端发过来的各种命令,发送心跳等。
- 每一个连接最终的目的就是监听
channel的消息,发送给客户端进行消费。 - 在发送给客户端之前,把这个消息设置为在飞翔中。
- 对发送给客户端信息设置为在飞翔中,如果在如果处理成功就把这个消息从飞翔中的状态中去掉,如果在规定的时间内没有收到客户端的反馈,则认为这个消息超时,然后重新归队。
func (n *NSQD) queueScanLoop()
- 服务端处理消息的逻辑:
- 使用协程定时去扫描随机的
channel里是否有过期数据。 - 在扫描
channel的时候,如果发现有过期数据后,会重新放回到队列,进行重发操作。
- 使用协程定时去扫描随机的
客户端对消息的处理和响应
func (r *Consumer) handlerLoop(handler Handler)
- 在服务端发送消息给客户端后,如果在处理业务逻辑时,如果发生错误则给服务器发送
Requeue命令告诉服务器,重新发送消息进处理。 - 如果处理成功,则发送
Finish命令。 - 服务端收到命令后,对飞翔中的消息进行处理,如果成功则去掉,如果是
Requeue则执行归队和重发操作,或者进行defer队列处理。
消息的持久化
- 虽然系统支持消息持久化存储在磁盘中(通过
--mem-queue-size),不过默认情况下消息都在内存中。 - 如果将
--mem-queue-size(一个channel的容量) 设置为 0,所有的消息将会存储到磁盘。我们不用担心消息会丢失,nsq 内部机制保证在程序关闭时将队列中的数据持久化到硬盘,重启后就会恢复。 - NSQ 没有内置的复制机制,却有各种各样的方法管理这种权衡,比如部署拓扑结构和技术,在容错的时候从属并持久化内容到磁盘。
diskqueue是nsq自己实现的一个先进先出的消息文件队列,go-diskqueue是把消息爆出到本地文件内。
3.消息的负载处理
实际应用中,一部分服务集群可能会同时订阅同一个
topic,并且处于同一个channel下。当nsqd有消息需要发送给订阅客户端去处理时,发给哪个客户端是需要考虑的,也就是消息的负载。 如果不考虑负载情况,把随机的把消息发送到某一个客服端去处理消息,如果机器的性能不同,可能发生的情况就是某一个或几个客户端处理速度慢,但还有大量新的消息需要处理,其他的客户端处于空闲状态。理想的状态是,找到当前相对空闲的客户端去处理消息。
nsq的处理方式是客户端主动向nsqd报告自已的可处理消息数量(也就是RDY命令)。nsqd根据每个连接的客户端的可处理消息的状态来随机把消息发送到可用的客户端,来进行消息处理。MaxInFlight用来设置最大的处理中的消息数量,会根据这个变量计算是否需要更新RDY。- 初始化的时候 客户端会向连接的nsqd服务端来发送updateRDY来设置最大处理数。
func (r *Consumer) onConnMessage(c *Conn, msg *Message) {
atomic.AddInt64(&r.totalRdyCount, -1)
atomic.AddUint64(&r.messagesReceived, 1)
r.incomingMessages <- msg
r.maybeUpdateRDY(c)
}
当有消息从
nsqd发送过来后也会调用maybeUpdateRDY方法,计算是否需要发送RDY命令。上面就是主要的处理逻辑,但还有一些逻辑,就是当消息处理发生错误时,
nsq有自己的退避算法backoff也会更新RDY简单来说就是当发现有处理错误时,来进行重试和指数退避,在退避期间RDY会为0,重试时会先放尝试RDY为1看有没有错误,如果没有错误则全部放开,具体处理逻辑这里就不细说了。
参考资料:
https://studygolang.com/articles/21065?fr=sidebar
https://www.cnblogs.com/li-peng/p/11868123.html
https://github.com/nsqio/go-diskqueue
NSQ(6)-nsq相关策略的更多相关文章
- Redis-设置Key的过期时间及相关策略
Redis-设置Key的过期时间及相关策略 1.设置key的过期时间 1.1expire key second:设置key的过期时间(秒) 1.2ttl key:查看key的有效期 1.3persis ...
- Go操作NSQ
NSQ是目前比较流行的一个分布式的消息队列,本文主要介绍了NSQ及Go语言如何操作NSQ. NSQ NSQ介绍 NSQ是Go语言编写的一个开源的实时分布式内存消息队列,其性能十分优异. NSQ的优势有 ...
- Go之NSQ
文章引用自 NSQ NSQ是目前比较流行的一个分布式的消息队列,本文主要介绍了NSQ及Go语言如何操作NSQ. NSQ介绍 NSQ是Go语言编写的一个开源的实时分布式内存消息队列,其性能十分优异. N ...
- Go之NSQ简介,原理和使用
NSQ简介 NSQ是Go语言编写的一个开源的实时分布式内存消息队列,其性能十分优异. NSQ 是实时的分布式消息处理平台,其设计的目的是用来大规模地处理每天数以十亿计级别的消息.它具有分布式和去中心化 ...
- GO学习-(27) Go语言操作NSQ
Go语言操作NSQ NSQ是目前比较流行的一个分布式的消息队列,本文主要介绍了NSQ及Go语言如何操作NSQ. NSQ NSQ介绍 NSQ是Go语言编写的一个开源的实时分布式内存消息队列,其性能十分优 ...
- NSQ源码剖析之nsqd
NSQ简介 NSQ 是实时的分布式消息处理平台,其设计的目的是用来大规模地处理每天数以十亿计级别的消息.NSQ 具有分布式和去中心化拓扑结构,该结构具有无单点故障.故障容错.高可用性以及能够保证消息的 ...
- Golang入门教程(十七)Linux/Windows下快速搭建和配置NSQ
前言 NSQ是一个基于Go语言的分布式实时消息平台,它基于MIT开源协议发布,代码托管在GitHub,其当前最新版本是0.3.1版.NSQ可用于大规模系统中的实时消息服务,并且每天能够处理数亿级别的消 ...
- nsq多播分发和负载均衡实验
什么是nsq?请参考实时分布式消息平台nsq. 本地如何搭建nsq?请参考本地搭建nsq经验分享. 从NSQ的设计文档中得知,单个nsqd被设计为一次能够处理多个流数据,NSQ中的数据流模型是由str ...
- 12.1 Go nsq
12.1 Go nsq 1.nsq是Go语言编写的,开源的内存分布式消息队列中间件 2.可以大规模的处理每天数以十亿级别的消息 3.分布式和去中心化拓扑结构,无单点故障 4.地址https://git ...
- Spring之SpringMVC(源码)初始化DispatcherServlet策略配置
1.从上一篇文章中可以SpringMVC初始化的过程中完成的其中一件事就是DispatcherServlet的相关策略的配置,如下所示 protected void initStrategies(Ap ...
随机推荐
- CF825F - String Compression
题意:压缩字符串,把字符串分成若干个子串,每个子串可以被压缩成"循环次数 \(+\) 循环节"的形式,求最小长度. dp 求 lcp 先 \(O(n^2)\) dp 求出所有后缀对 ...
- 基于ArcGIS的三维路网可视化
1. 引言 ArcGIS作为GIS的集大成者,对于三维可视化方面也有集成,参考自:3D 折线 (polyline) 要素-ArcMap | 文档 (arcgis.com),可以使用ArcGIS来构造与 ...
- postgresql VACUUM 不会从表中删除死行的三个原因
一.为什么是VACUUM? 每当更新或删除PostgreSQL表中的行时,都会留下死元组.VACUUM摆脱了它们,以便空间可以重复使用.如果一个表没有被清理,它就会变得臃肿,这会浪费磁盘空间并减慢表的 ...
- 利用自定义ref实现防抖
1. debounce.js import { customRef } from 'vue'; export function debounceRef(value, delay = 1000) { l ...
- GmSSL3.0密码算法库
GmSSL3.0密码算法库 一.开发背景 GmSSL 3.0版本具有更快.更小.更安全的特点,相比于GmSSL 2.0我们主要从以下方向进行改进: 采用CMake替代目前基于Perl的构建系统 支持L ...
- 测开-面试题-OS、Linux、算法、其他
1 OS 1.1 进程.线程.协程区别? 答: 1.进程是资源分配的单位:2.线程是CPU调度的单位:3.协程是一种比线程更加轻量级的存在,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在 ...
- 洛谷P4726 【模板】多项式指数函数(多项式 exp)
题目 https://www.luogu.com.cn/problem/P4726 思路 (略) 是个板题,但是包含了很多多项式的基础板子,适合用来练手. 据说递归版的好写(好抄),但是我猜测和fft ...
- javaWeb学习一
web开发(web就是网页): 静态web html.css 提供给所有人看到数据不会改变 动态web 提供给所有人看到数据会改变,不同用户.不同时间和地点都会不同 技术栈:Servlet/JSP,A ...
- lg8935 [JRKSJ R7] 茎 题解
由于标签内包含背包和树形dp,所以考虑用背包dp和树形dp求解. 考虑每次合并2个连通块(一个包含根节点),设\(f_{i,j}\)表示\(i\)子树内,操作\(j\)次的方案数(未合并连通块),设\ ...
- K8s集群安全机制
安全机制说明 k8s作为一个分布式集群管理的工具,保证集群的安全性是其一个重要的任务.API Server是集群内部各个组件通信的中介,也是外部控制的入口,,所以K8s的安全机制就是围绕保护API S ...