NSQ的重塑之路

李文 · · 10477 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

在 Gopherchina 2017 中,杭州有赞科技的李文带来了题为《NSQ 重塑之路》的分享,以下是对他演讲内容的整理。



今天我会分享有赞 NSQ 重塑的一个过程,主要分为以下几个部分:第一,回顾旧的架构;第二,在使用的过程中遇到的一些问题,以及为什么要做重构;第三,重构具体的细节和架构;第四,测试的整个流程。第五,与其他产品进行对比;第六,有赞的使用场景。

Original Architecture Overview

首先,什么是 MQ?MQ 大多数在微服务或者一些业务之间的消息投递过程。一般情况下,如果直接调用它的接口会导致两个业务之间有很多耦合,业务之间依赖比较重。所以在微服务改造过程中,MQ 是非常重要的组件。NSQ 是其中主要的一个产品,类似 kafka。我们当时用 NSQ 是为这个微服务的改造提供一个基础组件。

图 1

如图 1 所示是它原来的架构图。首先有比较简单的两个部分,一是数据储存,另外一个是查询服务。所有的生产者都只和数据节点打交道。生产者直接发送到它比较临近的一个数据节点,这个数据节点会定时将自己的数据上报给查询服务,消费者消费时通过这个查询服务,可以知道数据到底在哪一个节点上,然后从中拉取数据。


图右是数据通道的过程。因为它本身是一个比较简化的 MQ,所以架构比较简单。它首先使用 Golang 的 Channel,channel 本身有一个派发过程,所有客户端的链接全部在这个 Channel 上等待。如果 Channel 是满的,它会落盘。如果不满,则一直在 Channel 中流通。所以在整个消费能力不错,没有积压的情况下,整个数据只会在 Channel 中,不会落盘,效率较高。但这个架构本身存在一些问题。

Missing Features and the Damand of Youzan

接下来,我们聊聊大部分业务的 6 个需求:

第一,Replication。需要有两个手段保证数据不丢,一个是副本,一个是落盘。

第二,HA。即原先机器挂了,能不能继续消费服务,原来数据是否可以继续访问。

第三,Auto-Balance。数据均衡,一台老机器掉了,如何将数据平均分配到其他节点。

第四,Deliverry in order。即顺序投递的过程。

第五,Tracing。即寻找消息在什么状态,是否丢失,是否被消费,消费的客户端是什么。

第六,Consumer history messages。即消费历史消息。

Redesigned Architecture

Redesigned Topic Queue

如何让数据可以定位消息是重点。用 Go channel 储存数据无法对某条消息进行定位,它只能从 Channel 中拿出来,但它不能搜索,也不能储存。它本身有一个机制,即 Channel 满了就存到磁盘。但是大部分情况下如果 Channel 不满,则数据就是在 Channel 里走一遍,挂了全部丢失。所以,我们第一步就是改造 Channel,Channel 不应该用于储存重要数据,它是一个用来沟通协调的通道。如果是用于储存数据,则第一步就是要改造这个 Channel,将数据落盘。

Topic Write Flow on NSQD

图 2

整体的架构如图 2 所示。首先我们讲一下单机的情况,数据写入的流程如图 3、4、5 所示。本身我们要有一个 WriteBuffer,因为所有落盘的数据是一个比较慢的过程,比如同步、刷盘等过程比较慢。那么,如何提高它写入的效率?先写到 WriteBuffer,等上一次同步结束,或者刷盘结束后,我们一次性提交。但是,由于它还要通过副本同步,因此会通知副本将数据写到磁盘。副本会判断 leader 是否合法,如果同意这个数据可以提交,就可以刷盘,然后返回给客户端。只要他返回写入成功给客户端,这个数据就不会丢,这是一个磁盘的写入过程。

图 3

图 4

图 5

Improve Write Performance

刚刚说到写入过程,原来是写 Channel ,现在改成写磁盘,那效率和性能会不会有所下降?其实我们改造之后,通过测试可以发现,它本身就是一个队列,是顺序 IO。所以我们改造之后的性能跟原来的性能相比,没有很大差别。而且通过我们对其他代码的优化,反而比原来的性能更高。

Group Conmmit In Go

图 6

Group Conmmit In Go 整个流程如图 6 所示。首先每个 Topic 有一个 buffer,这个 buffer 其实是一个用于临时存储的 Channel。写到 Topic Channel 后,它会在另外一个 Channel 等候,所有的客户端会等待上一次的刷盘结束,这是客户端的等待写入返回的流程。

图 7

接着,我们看 Topic 本身的写入流程,如图 7 所示。每一次,它都会在同一个循环中等待上一次刷盘,或者上一次副本同步完成。整个流程全部同步后,一次性批量提交,俗称主提交。主提交后,会把客户端等待的 Channel 关闭。客户端上所有 wait 的 Channel 就会全部返回。

Redesigned Consumer Channel

接着聊聊消费端,原来的架构是把 Topic Channel 所有的数据拷贝到 Consume Channel,所以整个过程是一个数据拷贝过程。如果有落盘的情况,也会把磁盘上的 topic 队列数据拷贝到 Channel 所属的磁盘队列。如果在某个 Topic 有十几个 Channel 的情况下,会导致数据被拷贝十几份,整个磁盘空间占用和写入性能都会有比较大的损失。因为我们已经改造成落盘的数据了,Channel 只是一个标记,即消费到哪里的标记。此时,你有再多的 Channel 也只是一个游标数据而已,不是真正的消息。

图 8

如图 8 所示是每个 Channel 上的标记,可以记录消费到哪里,非常轻量级。Channel 改造后,我们消费历史数据则非常简单,只要把这个位置移动到历史位置。如果你不想消费一部分数据,就可以移动到前面。移动的位置可以支持按时间,按位移。移动的开销非常小,这个是基于落盘的这个设计带来的优点, 可以让其他改造变得简化,改造会方便很多。

图 9

整个消费的流程如图 9 所示,和原来差不多。因为消费的改造只是把Channel 的数据拷贝流程这一块的数据改掉了,其他的 Channel 的消费流程没有改,大部分的情况和原来老的架构差不多。


我简单讲一下。首先是从磁盘读出来,然后 buffer 到 Channel 里面,消费客户端连接后,在 Go Channel 上 select,Dispatcher 时,每个 select 是一个调度的过程,可以均衡调度到客户端连接,一旦选择某个客户端发出,就会在 InFlight 中记录一个消息状态,即这条消息正被处理。客户端如果 Finished 过来,代表这个消息已经被 confirm。如果是超时或者它处理不了,那么它可能重新写入磁盘,或者可能走优化流程, 直接放到内存进行重新投递,整个过程和原来的架构差不多,所以这一块的改造较小。

图 10

Dispatcher 利用了 Golang Channel 特别好的特性,即每个 Channel 可以在多个 worker 上 select,由 Channel 自行调度,因此,客户端的负载均衡就是基于 Channel 这个机制来做的。它整个结果的响应也会返回到 Channel。Goroutine 可以监听多个 Channel,两边可以同时写入到另外一个 Channel,这个是比较常用的模式。

图 11

另外,Channel timeout 也是利用 Channel 上面一个典型的设计模式。Golang 有一个问题,其实它在大量超时处理上面比较耗 CPU。如果一个进程几千个超时,它的 CPU 消耗会非常厉害,所以如何减少 CPU 消耗?将所有 timer 都简化成一个 timer,降低精度。比如原来是一百个一秒钟的 timer,你可以改成 10 个几十毫秒的 timer。当整个 timer 的数量下降,CPU 就下降,这是 golang 里经常用到的一个优化方式。


由少量的 timer 去派发 timeout 事件,通过事件的方式写到 Channel,所有的 goroutine 在超时的事件 Channel 上等待,则 timer 的数量会大量减少。通过 goroutine 调度去处理所有的 timeout 事件,而 goroutine 本身就是一个 goroutine pool,如果 timeout 非常多,可能处理不过来,可以动态的增加 pool 中的 goroutine。虽然 goroutine 在 golang 里面非常轻量级,但是也存在资源消耗。所以使用 pool 来减少它的资源消耗非常重要。因为在业务场景中会有非常多的客户端和 Channel 连上来,节约一个也是节约,最后可以看到整个流程,goroutine 减少了,timer 也减少了,整个调度消耗也减少了。这是一个比较常用的一个优化方式。

Redesigned Replication & HA

再看一下很重要的一部分。副本和 HA 的过程对数据的可靠性要求比较高。

图 12

如图 12 所示为整体架构图,与原来架构的两个模块差不多,但是我们引入了 ETCD 做元数据存储,这个元数据,比如 Topic 现在的一个参数,比如参数是三个副本,那么它这边会有三个可用的节点信息写在 ETCD。如果有任何变化,或者任何数据迁移,都是通过 ETCD 的元数据去协调的,这样在整个集群中就不会看到不一致的现象,从 ETCD 的视角看,整个集群的分布的情况,leader 的使用情况最终都是一致的。 ETCD 扮演了很重要的元数据存储角色。


另外 lookup 多了分配数据节点的功能。因为所有的节点数据分配都经过协调。而 ETCD 本身不具备这个功能,但是我们可以利用 ETCD 储存元数据,由 lookup 去计算如何分配。比如 根据 CPU 的使用率,最近写入的磁盘 IO这些情况去判断一个节点数据是否太多,或者新加入的时候一个数据是否太少。这些全部由 lookup 来分配。


接下来讲分布式,ETCD 主要是元数据,lookup 主要是分配查询和分配服务,然后 NSQD 是数据存储的服务。

图 13

如图 13 所示是 ETCD 中元数据的关系。首先每个有一个唯一的集群 ID,集群 ID里面有 Topic 元数据。下面两个,一个是当前可用的数据储存节点,是用来分配的一个可靠依据。另外一个是 lookup 的节点,lookup 本身也有主备。因为本身 loopup 也会挂。

Replication and HA

图 14

然后我们再看一下节点自动切换的过程。 首先看到绿色节点 ISR,ISR 是已经同步的副本。ISR 绿色节点的所有数据全部一样,黄色节点是正在 catch up 的节点,它的数据可能有一部分还未同步。


那么 lookup 如何选节点?首先所有的绿色节点是同步的才能选,另外 lookup 选了之后,会再次判断现在的数据哪一个是最新的,只有最新的数据才能成为新的 leader。如果 leader 因为网络发生了闪断等各种情况,以至于连不上 ETCD,它会触发其他的副本节点leader失效事件。然后副本节点就会开始 try lock,这是 ETCD 的一个cas接口,Try lock 一旦成功了,节点就会通过 lookup 告诉整个协调集群它已经成为新 leader,然后派发给所有副本节点,副本节点判断这是不是合法事件。如果合法,所有的 follower 就会和新的 leader 同步工作,这个过程就是整个 leader 的切换过程。新的 leader 同步之后,剩下 follower 同步完成,则会变成绿色的同步节点,即 ISR 节点。

Dynamic ISR

如果发生 leader 切换,或者副本节点不可用,或者网络闪断的情况,我们要怎样避免同步的整个流程导致整个写入被堵塞?因为写入要同步,如果有一个节点太慢,是不是可能导致整个写入被堵塞住?所以我们需要优化这个情况,避免一个节点导致写入被堵塞。


图 15

另外我们调整负载均衡也会用到动态 ISR。动态 ISR 整个流程如图 15 所示,分为左边红色部分和右边绿色部分,往红色那边都是失败的流程,往绿色这边都是可能成功的流程。


成功流程很简单,就是写入后到 leader 提交,再发到所有副本节点再返回,成功后提交,然后在本地提交,返回客户端,即成功。这个过程比较简单,副本节点只需要判断 leader 的 lease 是否合法,以及它在本地是否提交过,由于网络原因可能会收到重复写入请求, 所以一定要记住上次是否提交过这个数据。


失败的流程:发给 ISR 时有很多种情况,可能有一部分节点返回,有一部分节点没有返回,这时要看是不是大于副本数的一半,譬如 3 个副本,至少 2 个节点返回成功才算成功。如果大于一半的副本已经成功,那慢的节点可能有问题,我们可以通知他,下线或者离开这个集群。如果他主动离开是最好的,离开后,我们刷新正在同步的副本再判断,如果已经全部写入成功,就可以返回给客户端。如果没有主动离开,我们会通过 lookup 的节点去协调,lookup 会判断这个 leader 是否合法,是不是可以强迫一个节点离开集群。Lookup 相当于仲裁功能,仲裁慢节点是否应该离开集群,如果应该离开,leader 就可以成功提交写入。如果不行,就要重试。这个过程中, leader 也会判决自己是不是一个合法的 leader。如果出现网络断掉等情况,可能有新的 leader 在写,那么,他这个就不合法了。所以,它要快速判断自己是否合法,如果不合法就自己退出集群,让剩下的节点可以完成写入操作。这是一个快速失败的流程。


这两个流程可以快速的发现,终止写入过程,然后构建一个新的可用的ISR列表。避免一个节点意外的影响整个写入的流程,处理各种网络异常情况,脑裂的情况。

Auto Balance

Balance 集群也用到上面讲的整个动态 ISR 流程,Balance 是一个节点加入集群和退出集群的整个过程。即刚刚讲的那个动态过程。

图 16

我们简单的讲一下它的流程,如图 16 所示,它有三个 Topic,每个 Topic 有一个 leader,一个副本。新加入一个节点后开始同步,这时 lookup 判断新的节点没有任何数据,就把一部分数据移过去,这就是同步。同步后变成绿色节点,绿色节点即拥有完整数据。当发现一边有两个 leader,另一边没有 leader,就切换 leader,保证每边节点各一个 leader,达到均衡。但是还有两个副本是多余的,此时就要把副本剔除掉,所以每个节点就变成两个数据,一个是 leader,一个是副本。

Smart Client

我们可以看到上面整个流程,Topic 随时有可能迁移到另外一个节点。此时客户端如何感知这个过程呢?这是一个 smartClient,首先它会定时更新 Topic leader 和 partitions 的信息,哪个节点的主备都在哪些机器,这样所有的请求都在对应的 topic leader上面处理。


如果发到不是 leader 的节点,它就会返回一个错误,这个错误就是你请求的已经被迁移走了,或者我这个已经不是 leader 时,客户端就可以感知到。肯定发生了集群变更,所以客户端写入的时候一旦发现这种情况,就会重新拉一遍 leader,整个过程在几毫秒之内完成,所以再重试一次,就可以完成切换写入到一个新的 leader。整个流程对业务方没有任何的感知。所以整个流程是:写入失败,重新获取,再重新写入。


写入策略。比如现在有 3,4 个 partitions,我可以随机写入一个分区,或者按 hash key 写入一个,包括后面顺序要求时,要求把同样的分区数据写入同样的分区情况下,也是由 smartClient 处理。

Delievery In Order

图 17

顺序消费需求是由有赞的一个业务催生的。当时有一个业务叫拼团,特点是有很多人参与同一个订单,这就必然发生订单状态的不确定性,它不是一条线走到底的订单流程。可以看到这个流程非常复杂如图 17 所示,每个状态都有可能回到另外一个状态,所以如何保证状态同步到另外一个系统时,它还是原来的状态呢?如果它不是原来的状态,最后一个拼团成功的订单可能变成退款订单,这两边是不一致的。所以,我们为了保证在这么多状态间同步的过程中最终的状态是一样的,就需要用到顺序消费功能。


那么顺序消费有什么要求?首先 Topic 有多个分区,同一个 Topic 数据可能写到不同的分区,但是在同一个分区下面是有序的。如果你是订单 ID,或者用户 ID,同一个订单发到同一个分区,就不可能出现两个分区有同一个订单的数据的情况,所以你只需要保证,同一个分区里面是有序的即可。


另外一个要求就是分区客户端要处理好,根据分片 ID 算出要写入到哪个分区,并且写入过程中,一个 ID 全部是由同一个线程处理,每一个都是按顺序去投递。如果前面的写入流程是顺序的,我们可以看一下消费过程。首先乱序的情况下会发生什么情况?乱序的时候 A 投到一个节点,B 投到另一个节点,这两个是同时处理的,你无法知道处理的先后。此时,不知道消费顺序如何,所以在有序的情况下,我们如何保证数据消费是按写入顺序的呢?我们一定等上一条消息 ACK 回来,再投递下一条消息。这样整个性能会下降,但是性能是通过分区提高的,比如一个分区的性能如此,但是如果建十个分区,二十个分区,就可以二十个同时消费。因为分区之间没有顺序关系。所有的顺序的订单号全部写到同一个 Topic 分区下,这是一个顺序投递的过程。

Tracing

那么为什么要 tracing?你可以发现整个集群中,消息的流转非常多状态,它可能刚刚被写入,可能是被消费了,可能是各种中间状态的情况。那我们这个系统要如何去判断这个消息是被谁消费,或者有没有消费,或者到底是不是顺序投递的?我们要把 NSQ 本身的消息 ID 和外部的一个业务 ID 关联起来,所以我们把 NSQ 本身的消息 ID 分成两部分,内部 ID 和跟踪 ID。NSQ 原来的消息 ID 是 16 个字节,刚好可以分成两部分,因此,改造比较方便。

图 18

内部 ID 是一个自增 ID,外部 ID 是传过来的,譬如可以传订单号,可以传用户 ID,也可以传商品 ID,传进来后,这两条就关联在一起了。消息有正在生产,读,正在处理,已经完成等各种状态,我们如何将这个数据进行索引呢?就是发到 Log 搜索系统(通过 ElasticSearch 建立的),数据全部发到这边之后,就可以根据消息 ID,即内部 ID 和外部 ID 去搜索。可以搜内部 ID,也可以搜外部 ID,但大部分情况下,如果是业务用,肯定是拿一个订单号进行搜索,看这个订单号中间有哪几个状态在变迁,或者看用户有哪几个中间态在变迁。

Jepsen test in new NSQ

分布式系统本身的整个测试是比较复杂的。所以如何用一个好的工具保证系统比较稳定,没有比较明显的 bug,或者如何发现难以发现的 bug?就是通过 Jepsen 测试,Jepsen 测试在很多分布式系统里面有用到过,比如像 Kafka 等都用 Jepsen 测过,都用 Jepsen 发现过可能的分布式问题,然后用这个测试工具解决了潜在的 bug。我们也用到这个工具,去保证我们的系统没有明显的 bug,没有分布式系统中常见的异常问题,或者网络分区导致的异常情况。

图 19

整个测试流程如图 19 所示。它需要把某些节点从网络里面剔除,这些节点就是网络不可达,或者网络经常丢包,此时,它和集群是失联的。它这个过程是随机的,所以不会出现,固定的某一个可能的连接状态影响整个测试结果。所以他用随机的方式去不停的去切换网络。然后外部这个使用者,即写入者和消费者都是在不停的写和读。

图 20

等它恢复之后,再去检查数据。他检查数据中,比较重要的一个就是最终的检查结果。比如如何判断你的数据有没有丢?即写入了多少,收到了多少,看有没有没收到的,没收到就属于丢。可能超时发的一些数据,我认为我没有收到,但其实已经收到了,这种情况就属于recovered。当我因为不停的重试,发了多次这种情况就属于重复的情况。这一块,在大部分的 MQ 系统基本上是不需要去避免的,只需要尽量的减少它的重复。因为在这种随机网络的情况下,重试的概率比较大,但实际生产中,重复的数据比较少。因此,我们主要是保证不丢数据,一旦有这个保证,我们就可以知道这个系统是比较稳定的。至少从 Jepsen 测试来看,没有任何明显的潜在问题。

Performance and Data in Youzan

图 21

接着,我们再看一下性能测试。 kafka 的数据没有详细的官方测试,所以我从网上找了一个数据对比如图 21 所示(阿里巴巴技术团队做的测试)。我们之前在测试四台节点集群时,机器配置大概如此,32个分区,两个副本的情况下,大概有 500K 消息写入。只有一个副本的情况下,大概有 900K 写入。Kafka 官方说有百万个写入,那它是如何测出来的呢?他是客户端写入后,没有等待反馈结果,相当于批量提交,可能一次提交一千条消息。但是这样测,实际则没有百万,单机的情况下,业界测 Kafka 的数据大概 100K 左右,分布式情况下还要看同步的流程。


不到一年,我们在有赞的生产集群(包括在线,离线处理)总共有 1500 亿数据,而这些消息的处理证明了整个系统的稳定性。在使用的过程中,我们很少发现有数据丢失的情况。如果有数据丢失,基本上是业务的处理流程本身已经处理了,但是他自己不知道已经处理了,此时他认为丢失了,但其实在 NSQ 系统本身并没有丢失。我们使用以来,没有确认过任何一个服务端数据丢失的情况。

Compare with others

图 22

接着,我们简单对比一下开源的其他 MQ系统的特点。如图 22 所示,我对比了三个常用的 MQ系统,绿色部分是我们改造之后的特性,原本没有。开源生态 NSQ 做得还可以,虽然没有 Kafka 那么强大,但是它本身的文档和客户端比较丰富。可靠性这一块,Kafka 本身是没有强保证,它可能在各种测试情况下丢数据,所以一般用于日志系统,日志系统丢几条数据是没有关系的。Kafka 最新的版本会有一些改进了,但是毕竟还没有发布,没办法说它的可靠性有多高。


Kafka 批量写入的性能非常高,但是实际业务大部分情况下,一个客户端不可能批量写入,大部分都是几百个客户端同时写入,这种情况下每个客户端只有几条消息。所以这种情况下的性能比较,Kafka 和其它两个产品还是有一定差距。


RocketMQ 本身的灵活性比较小,因为它不能按照 Topic 配置参数,比如 A Topic 是 2 个副本,B Topic 是 4 个副本。不能这样配,只能大家都是三个副本,参数也都是一样的,它的灵活性比较低。


RocketMQ 有提供严格顺序的功能,但是在我们测试过程中,可能出现乱序的情况。当然,我们用的开源版,和生产版本可能不太一样。堆积能力和 Topic 设计有关系的,官方也说它堆积能力没有那么好。消息跟踪就是我们做的 Tracing 系统,RocketMQ 和 NSQ 都做得还不错。

Migrate data in Youzan

如何迁移数据?首先 NSQ 本身支持多个 lookup 查找数据节点,所以有两个集群就有两个 lookup,两个 lookup 可以同时从两个集群消费数据,这样两个集群的数据都可以消费掉。但是,你新写入的数据全部都写入新的集群,所以老的数据越来越少,新的集群数据越来越多,整个数据迁移的过程比较平滑。为了减少业务方使用两个 lookup 配置的情况,我们还做了一个简化的配置服务,让它更容易的迁移。我们可以在界面上直接控制 topic 消费哪几个集群,不需要业务方改配置、以及重启等。地址的变更都是通过界面控制,迁移过程不会产生数据丢失,过程更方便。因此,我们的工作就是减少他们的迁移过程,让系统自动做迁移。

Usage in Youzan

图 23

另外,有赞整个系统怎么样去做到微服务?微服务是各个服务之间不能有直接的耦合。如果有直接的耦合,进行直接调用,则相当于一个整体的大钢板,无法拆。所以为了做到微服务,需要把所有的小组件全部拆开,然后通过 NSQ 通信。他可以发一条信息,另外一个 App 异步收到这个消息后,处理消息,整个流程只和消息有关,和任何接口无关,包括导入到搜索系统、离线系统、spark、跟踪系统,全部通过这个技术组件去做。

图 24

如图 24 是两个典型的场景。一,搜索系统,商品的修改变更如何同步到搜索系统?一个商品变更发到 NSQ 上面,然后把它的消息同步到搜索系统;二,消息通道,当某个业务要推送一条消息都要走消息通道的,他会放到 NSQ 里面,然后等消息通道一个个拉下来,再推到另外一个下放的渠道。上面渠道和下面渠道两个是分离的,通过中间的 NSQ 进行消息流转。


发放奖励,付款同步,发货同步,维权等等都是异步的通知,都非常适合使用 NSQ。包括高峰时期,突然一个大流量过来,你处理不了,就只能用 NSQ 暂时承担一些,然后异步慢慢的下单,这也是一个很重要的削峰功能。


目前很多组件都有开源的,我们这边的 Golang、Java、PHP 的客户端都是开源的,服务端也开源, 地址是 https://github.com/absolute8511/nsq (后面将会迁移到 youzan 公司组织下)。我们正在打造一个生态,让 NSQ 可以对接各个开源的产品,比如可以对接 Spark、对接 Flume,可以对接 ES等等,整个对接的过程要做到一个比较完善的生态。最好可以做到像 Kafka 那样可以对接主流的分析场景。开源是我们公司的重要文化,不光是要把代码放到 Github ,而是把整个文档留存, 开发的过程公开,跟其他的系统对接全部开放出来,这样才是真正的开放心态做这个开源。后面还有很多的开源的组件,都会在有赞开源出来。

最后,我们再回顾一下今天讲的内容,首先第一点是我们需要把 Channel 改造成磁盘储存,第二个我们要避免消费者的数据拷贝,第三个从没有副本到有副本并且 HA 可以自动均衡。最后还有几个特性,顺序消费,历史回溯,跟踪,测试,最重要的是我们保持大部分协议兼容。原来 NSQ 老的开源的框架,在新的 NSQ 上面都可以用,只是有一些高级功能不能用。而如何让业务方没有感知的使用新集群,协议兼容则非常重要。



在 Gopherchina 2017 中,杭州有赞科技的李文带来了题为《NSQ 重塑之路》的分享,以下是对他演讲内容的整理。



今天我会分享有赞 NSQ 重塑的一个过程,主要分为以下几个部分:第一,回顾旧的架构;第二,在使用的过程中遇到的一些问题,以及为什么要做重构;第三,重构具体的细节和架构;第四,测试的整个流程。第五,与其他产品进行对比;第六,有赞的使用场景。

Original Architecture Overview

首先,什么是 MQ?MQ 大多数在微服务或者一些业务之间的消息投递过程。一般情况下,如果直接调用它的接口会导致两个业务之间有很多耦合,业务之间依赖比较重。所以在微服务改造过程中,MQ 是非常重要的组件。NSQ 是其中主要的一个产品,类似 kafka。我们当时用 NSQ 是为这个微服务的改造提供一个基础组件。

图 1

如图 1 所示是它原来的架构图。首先有比较简单的两个部分,一是数据储存,另外一个是查询服务。所有的生产者都只和数据节点打交道。生产者直接发送到它比较临近的一个数据节点,这个数据节点会定时将自己的数据上报给查询服务,消费者消费时通过这个查询服务,可以知道数据到底在哪一个节点上,然后从中拉取数据。


图右是数据通道的过程。因为它本身是一个比较简化的 MQ,所以架构比较简单。它首先使用 Golang 的 Channel,channel 本身有一个派发过程,所有客户端的链接全部在这个 Channel 上等待。如果 Channel 是满的,它会落盘。如果不满,则一直在 Channel 中流通。所以在整个消费能力不错,没有积压的情况下,整个数据只会在 Channel 中,不会落盘,效率较高。但这个架构本身存在一些问题。

Missing Features and the Damand of Youzan

接下来,我们聊聊大部分业务的 6 个需求:

第一,Replication。需要有两个手段保证数据不丢,一个是副本,一个是落盘。

第二,HA。即原先机器挂了,能不能继续消费服务,原来数据是否可以继续访问。

第三,Auto-Balance。数据均衡,一台老机器掉了,如何将数据平均分配到其他节点。

第四,Deliverry in order。即顺序投递的过程。

第五,Tracing。即寻找消息在什么状态,是否丢失,是否被消费,消费的客户端是什么。

第六,Consumer history messages。即消费历史消息。

Redesigned Architecture

Redesigned Topic Queue

如何让数据可以定位消息是重点。用 Go channel 储存数据无法对某条消息进行定位,它只能从 Channel 中拿出来,但它不能搜索,也不能储存。它本身有一个机制,即 Channel 满了就存到磁盘。但是大部分情况下如果 Channel 不满,则数据就是在 Channel 里走一遍,挂了全部丢失。所以,我们第一步就是改造 Channel,Channel 不应该用于储存重要数据,它是一个用来沟通协调的通道。如果是用于储存数据,则第一步就是要改造这个 Channel,将数据落盘。

Topic Write Flow on NSQD

图 2

整体的架构如图 2 所示。首先我们讲一下单机的情况,数据写入的流程如图 3、4、5 所示。本身我们要有一个 WriteBuffer,因为所有落盘的数据是一个比较慢的过程,比如同步、刷盘等过程比较慢。那么,如何提高它写入的效率?先写到 WriteBuffer,等上一次同步结束,或者刷盘结束后,我们一次性提交。但是,由于它还要通过副本同步,因此会通知副本将数据写到磁盘。副本会判断 leader 是否合法,如果同意这个数据可以提交,就可以刷盘,然后返回给客户端。只要他返回写入成功给客户端,这个数据就不会丢,这是一个磁盘的写入过程。

图 3

图 4

图 5

Improve Write Performance

刚刚说到写入过程,原来是写 Channel ,现在改成写磁盘,那效率和性能会不会有所下降?其实我们改造之后,通过测试可以发现,它本身就是一个队列,是顺序 IO。所以我们改造之后的性能跟原来的性能相比,没有很大差别。而且通过我们对其他代码的优化,反而比原来的性能更高。

Group Conmmit In Go

图 6

Group Conmmit In Go 整个流程如图 6 所示。首先每个 Topic 有一个 buffer,这个 buffer 其实是一个用于临时存储的 Channel。写到 Topic  Channel 后,它会在另外一个 Channel 等候,所有的客户端会等待上一次的刷盘结束,这是客户端的等待写入返回的流程。

图 7 

接着,我们看 Topic 本身的写入流程,如图 7 所示。每一次,它都会在同一个循环中等待上一次刷盘,或者上一次副本同步完成。整个流程全部同步后,一次性批量提交,俗称主提交。主提交后,会把客户端等待的 Channel 关闭。客户端上所有 wait 的 Channel 就会全部返回。

Redesigned Consumer Channel

接着聊聊消费端,原来的架构是把 Topic Channel 所有的数据拷贝到 Consume Channel,所以整个过程是一个数据拷贝过程。如果有落盘的情况,也会把磁盘上的 topic 队列数据拷贝到 Channel 所属的磁盘队列。如果在某个 Topic 有十几个 Channel 的情况下,会导致数据被拷贝十几份,整个磁盘空间占用和写入性能都会有比较大的损失。因为我们已经改造成落盘的数据了,Channel 只是一个标记,即消费到哪里的标记。此时,你有再多的 Channel 也只是一个游标数据而已,不是真正的消息。

图 8

如图 8 所示是每个 Channel 上的标记,可以记录消费到哪里,非常轻量级。Channel 改造后,我们消费历史数据则非常简单,只要把这个位置移动到历史位置。如果你不想消费一部分数据,就可以移动到前面。移动的位置可以支持按时间,按位移。移动的开销非常小,这个是基于落盘的这个设计带来的优点, 可以让其他改造变得简化,改造会方便很多。  

图 9

整个消费的流程如图 9 所示,和原来差不多。因为消费的改造只是把Channel  的数据拷贝流程这一块的数据改掉了,其他的 Channel 的消费流程没有改,大部分的情况和原来老的架构差不多。


我简单讲一下。首先是从磁盘读出来,然后 buffer 到 Channel 里面,消费客户端连接后,在 Go Channel 上 select,Dispatcher 时,每个 select 是一个调度的过程,可以均衡调度到客户端连接,一旦选择某个客户端发出,就会在 InFlight 中记录一个消息状态,即这条消息正被处理。客户端如果 Finished 过来,代表这个消息已经被 confirm。如果是超时或者它处理不了,那么它可能重新写入磁盘,或者可能走优化流程, 直接放到内存进行重新投递,整个过程和原来的架构差不多,所以这一块的改造较小。

图 10

Dispatcher 利用了 Golang  Channel 特别好的特性,即每个 Channel 可以在多个 worker 上 select,由 Channel 自行调度,因此,客户端的负载均衡就是基于 Channel 这个机制来做的。它整个结果的响应也会返回到 Channel。Goroutine 可以监听多个 Channel,两边可以同时写入到另外一个 Channel,这个是比较常用的模式。

图 11

另外,Channel timeout 也是利用 Channel 上面一个典型的设计模式。Golang 有一个问题,其实它在大量超时处理上面比较耗 CPU。如果一个进程几千个超时,它的 CPU 消耗会非常厉害,所以如何减少 CPU 消耗?将所有 timer 都简化成一个 timer,降低精度。比如原来是一百个一秒钟的 timer,你可以改成 10 个几十毫秒的 timer。当整个 timer 的数量下降,CPU 就下降,这是 golang 里经常用到的一个优化方式。


由少量的 timer 去派发 timeout 事件,通过事件的方式写到 Channel,所有的 goroutine 在超时的事件 Channel 上等待,则 timer 的数量会大量减少。通过 goroutine 调度去处理所有的 timeout 事件,而 goroutine 本身就是一个 goroutine pool,如果 timeout 非常多,可能处理不过来,可以动态的增加 pool 中的 goroutine。虽然 goroutine 在 golang 里面非常轻量级,但是也存在资源消耗。所以使用 pool 来减少它的资源消耗非常重要。因为在业务场景中会有非常多的客户端和 Channel 连上来,节约一个也是节约,最后可以看到整个流程,goroutine 减少了,timer 也减少了,整个调度消耗也减少了。这是一个比较常用的一个优化方式。    

Redesigned Replication & HA

再看一下很重要的一部分。副本和 HA 的过程对数据的可靠性要求比较高。

图 12

如图 12 所示为整体架构图,与原来架构的两个模块差不多,但是我们引入了 ETCD 做元数据存储,这个元数据,比如 Topic 现在的一个参数,比如参数是三个副本,那么它这边会有三个可用的节点信息写在 ETCD。如果有任何变化,或者任何数据迁移,都是通过 ETCD 的元数据去协调的,这样在整个集群中就不会看到不一致的现象,从 ETCD 的视角看,整个集群的分布的情况,leader 的使用情况最终都是一致的。 ETCD 扮演了很重要的元数据存储角色。


另外 lookup 多了分配数据节点的功能。因为所有的节点数据分配都经过协调。而 ETCD 本身不具备这个功能,但是我们可以利用 ETCD 储存元数据,由 lookup 去计算如何分配。比如 根据 CPU 的使用率,最近写入的磁盘 IO这些情况去判断一个节点数据是否太多,或者新加入的时候一个数据是否太少。这些全部由 lookup 来分配。    


接下来讲分布式,ETCD 主要是元数据,lookup 主要是分配查询和分配服务,然后 NSQD 是数据存储的服务。

图 13

如图 13 所示是 ETCD 中元数据的关系。首先每个有一个唯一的集群 ID,集群 ID里面有 Topic 元数据。下面两个,一个是当前可用的数据储存节点,是用来分配的一个可靠依据。另外一个是 lookup 的节点,lookup 本身也有主备。因为本身 loopup 也会挂。

Replication and HA

图 14

然后我们再看一下节点自动切换的过程。 首先看到绿色节点 ISR,ISR 是已经同步的副本。ISR 绿色节点的所有数据全部一样,黄色节点是正在 catch up 的节点,它的数据可能有一部分还未同步。


那么 lookup 如何选节点?首先所有的绿色节点是同步的才能选,另外 lookup 选了之后,会再次判断现在的数据哪一个是最新的,只有最新的数据才能成为新的 leader。如果 leader 因为网络发生了闪断等各种情况,以至于连不上 ETCD,它会触发其他的副本节点leader失效事件。然后副本节点就会开始 try lock,这是 ETCD 的一个cas接口,Try lock 一旦成功了,节点就会通过 lookup 告诉整个协调集群它已经成为新 leader,然后派发给所有副本节点,副本节点判断这是不是合法事件。如果合法,所有的 follower 就会和新的 leader 同步工作,这个过程就是整个 leader 的切换过程。新的 leader 同步之后,剩下 follower 同步完成,则会变成绿色的同步节点,即 ISR 节点。

Dynamic ISR 

如果发生 leader 切换,或者副本节点不可用,或者网络闪断的情况,我们要怎样避免同步的整个流程导致整个写入被堵塞?因为写入要同步,如果有一个节点太慢,是不是可能导致整个写入被堵塞住?所以我们需要优化这个情况,避免一个节点导致写入被堵塞。


图 15

另外我们调整负载均衡也会用到动态 ISR。动态 ISR 整个流程如图 15 所示,分为左边红色部分和右边绿色部分,往红色那边都是失败的流程,往绿色这边都是可能成功的流程。


成功流程很简单,就是写入后到 leader 提交,再发到所有副本节点再返回,成功后提交,然后在本地提交,返回客户端,即成功。这个过程比较简单,副本节点只需要判断 leader 的 lease 是否合法,以及它在本地是否提交过,由于网络原因可能会收到重复写入请求, 所以一定要记住上次是否提交过这个数据。


失败的流程:发给 ISR 时有很多种情况,可能有一部分节点返回,有一部分节点没有返回,这时要看是不是大于副本数的一半,譬如 3 个副本,至少 2 个节点返回成功才算成功。如果大于一半的副本已经成功,那慢的节点可能有问题,我们可以通知他,下线或者离开这个集群。如果他主动离开是最好的,离开后,我们刷新正在同步的副本再判断,如果已经全部写入成功,就可以返回给客户端。如果没有主动离开,我们会通过 lookup 的节点去协调,lookup 会判断这个 leader 是否合法,是不是可以强迫一个节点离开集群。Lookup 相当于仲裁功能,仲裁慢节点是否应该离开集群,如果应该离开,leader 就可以成功提交写入。如果不行,就要重试。这个过程中, leader 也会判决自己是不是一个合法的 leader。如果出现网络断掉等情况,可能有新的 leader 在写,那么,他这个就不合法了。所以,它要快速判断自己是否合法,如果不合法就自己退出集群,让剩下的节点可以完成写入操作。这是一个快速失败的流程。


这两个流程可以快速的发现,终止写入过程,然后构建一个新的可用的ISR列表。避免一个节点意外的影响整个写入的流程,处理各种网络异常情况,脑裂的情况。

Auto Balance

Balance 集群也用到上面讲的整个动态 ISR 流程,Balance 是一个节点加入集群和退出集群的整个过程。即刚刚讲的那个动态过程。

图 16

我们简单的讲一下它的流程,如图 16 所示,它有三个 Topic,每个 Topic 有一个 leader,一个副本。新加入一个节点后开始同步,这时 lookup 判断新的节点没有任何数据,就把一部分数据移过去,这就是同步。同步后变成绿色节点,绿色节点即拥有完整数据。当发现一边有两个 leader,另一边没有 leader,就切换 leader,保证每边节点各一个 leader,达到均衡。但是还有两个副本是多余的,此时就要把副本剔除掉,所以每个节点就变成两个数据,一个是 leader,一个是副本。   

Smart Client

我们可以看到上面整个流程,Topic 随时有可能迁移到另外一个节点。此时客户端如何感知这个过程呢?这是一个 smartClient,首先它会定时更新 Topic leader 和 partitions 的信息,哪个节点的主备都在哪些机器,这样所有的请求都在对应的 topic leader上面处理。


如果发到不是 leader 的节点,它就会返回一个错误,这个错误就是你请求的已经被迁移走了,或者我这个已经不是 leader 时,客户端就可以感知到。肯定发生了集群变更,所以客户端写入的时候一旦发现这种情况,就会重新拉一遍 leader,整个过程在几毫秒之内完成,所以再重试一次,就可以完成切换写入到一个新的 leader。整个流程对业务方没有任何的感知。所以整个流程是:写入失败,重新获取,再重新写入。


写入策略。比如现在有 3,4 个 partitions,我可以随机写入一个分区,或者按 hash key 写入一个,包括后面顺序要求时,要求把同样的分区数据写入同样的分区情况下,也是由 smartClient 处理。

Delievery In Order

图 17

顺序消费需求是由有赞的一个业务催生的。当时有一个业务叫拼团,特点是有很多人参与同一个订单,这就必然发生订单状态的不确定性,它不是一条线走到底的订单流程。可以看到这个流程非常复杂如图 17 所示,每个状态都有可能回到另外一个状态,所以如何保证状态同步到另外一个系统时,它还是原来的状态呢?如果它不是原来的状态,最后一个拼团成功的订单可能变成退款订单,这两边是不一致的。所以,我们为了保证在这么多状态间同步的过程中最终的状态是一样的,就需要用到顺序消费功能。


那么顺序消费有什么要求?首先 Topic 有多个分区,同一个 Topic 数据可能写到不同的分区,但是在同一个分区下面是有序的。如果你是订单 ID,或者用户 ID,同一个订单发到同一个分区,就不可能出现两个分区有同一个订单的数据的情况,所以你只需要保证,同一个分区里面是有序的即可。


另外一个要求就是分区客户端要处理好,根据分片 ID 算出要写入到哪个分区,并且写入过程中,一个 ID 全部是由同一个线程处理,每一个都是按顺序去投递。如果前面的写入流程是顺序的,我们可以看一下消费过程。首先乱序的情况下会发生什么情况?乱序的时候 A 投到一个节点,B 投到另一个节点,这两个是同时处理的,你无法知道处理的先后。此时,不知道消费顺序如何,所以在有序的情况下,我们如何保证数据消费是按写入顺序的呢?我们一定等上一条消息 ACK 回来,再投递下一条消息。这样整个性能会下降,但是性能是通过分区提高的,比如一个分区的性能如此,但是如果建十个分区,二十个分区,就可以二十个同时消费。因为分区之间没有顺序关系。所有的顺序的订单号全部写到同一个 Topic 分区下,这是一个顺序投递的过程。

Tracing

那么为什么要 tracing?你可以发现整个集群中,消息的流转非常多状态,它可能刚刚被写入,可能是被消费了,可能是各种中间状态的情况。那我们这个系统要如何去判断这个消息是被谁消费,或者有没有消费,或者到底是不是顺序投递的?我们要把 NSQ 本身的消息 ID 和外部的一个业务 ID 关联起来,所以我们把 NSQ 本身的消息 ID 分成两部分,内部 ID 和跟踪 ID。NSQ 原来的消息 ID 是 16 个字节,刚好可以分成两部分,因此,改造比较方便。

图 18

内部 ID 是一个自增 ID,外部 ID 是传过来的,譬如可以传订单号,可以传用户 ID,也可以传商品 ID,传进来后,这两条就关联在一起了。消息有正在生产,读,正在处理,已经完成等各种状态,我们如何将这个数据进行索引呢?就是发到 Log 搜索系统(通过 ElasticSearch 建立的),数据全部发到这边之后,就可以根据消息 ID,即内部 ID 和外部 ID 去搜索。可以搜内部 ID,也可以搜外部 ID,但大部分情况下,如果是业务用,肯定是拿一个订单号进行搜索,看这个订单号中间有哪几个状态在变迁,或者看用户有哪几个中间态在变迁。

Jepsen test in new NSQ

分布式系统本身的整个测试是比较复杂的。所以如何用一个好的工具保证系统比较稳定,没有比较明显的 bug,或者如何发现难以发现的 bug?就是通过 Jepsen 测试,Jepsen 测试在很多分布式系统里面有用到过,比如像 Kafka 等都用 Jepsen 测过,都用 Jepsen 发现过可能的分布式问题,然后用这个测试工具解决了潜在的 bug。我们也用到这个工具,去保证我们的系统没有明显的 bug,没有分布式系统中常见的异常问题,或者网络分区导致的异常情况。   

图 19

整个测试流程如图 19 所示。它需要把某些节点从网络里面剔除,这些节点就是网络不可达,或者网络经常丢包,此时,它和集群是失联的。它这个过程是随机的,所以不会出现,固定的某一个可能的连接状态影响整个测试结果。所以他用随机的方式去不停的去切换网络。然后外部这个使用者,即写入者和消费者都是在不停的写和读。

图 20

等它恢复之后,再去检查数据。他检查数据中,比较重要的一个就是最终的检查结果。比如如何判断你的数据有没有丢?即写入了多少,收到了多少,看有没有没收到的,没收到就属于丢。可能超时发的一些数据,我认为我没有收到,但其实已经收到了,这种情况就属于recovered。当我因为不停的重试,发了多次这种情况就属于重复的情况。这一块,在大部分的 MQ 系统基本上是不需要去避免的,只需要尽量的减少它的重复。因为在这种随机网络的情况下,重试的概率比较大,但实际生产中,重复的数据比较少。因此,我们主要是保证不丢数据,一旦有这个保证,我们就可以知道这个系统是比较稳定的。至少从 Jepsen 测试来看,没有任何明显的潜在问题。

Performance and Data in Youzan

图 21

接着,我们再看一下性能测试。 kafka 的数据没有详细的官方测试,所以我从网上找了一个数据对比如图 21 所示(阿里巴巴技术团队做的测试)。我们之前在测试四台节点集群时,机器配置大概如此,32个分区,两个副本的情况下,大概有 500K 消息写入。只有一个副本的情况下,大概有 900K 写入。Kafka 官方说有百万个写入,那它是如何测出来的呢?他是客户端写入后,没有等待反馈结果,相当于批量提交,可能一次提交一千条消息。但是这样测,实际则没有百万,单机的情况下,业界测 Kafka 的数据大概 100K 左右,分布式情况下还要看同步的流程。


不到一年,我们在有赞的生产集群(包括在线,离线处理)总共有 1500 亿数据,而这些消息的处理证明了整个系统的稳定性。在使用的过程中,我们很少发现有数据丢失的情况。如果有数据丢失,基本上是业务的处理流程本身已经处理了,但是他自己不知道已经处理了,此时他认为丢失了,但其实在 NSQ 系统本身并没有丢失。我们使用以来,没有确认过任何一个服务端数据丢失的情况。

Compare with others

图 22

接着,我们简单对比一下开源的其他 MQ系统的特点。如图 22 所示,我对比了三个常用的 MQ系统,绿色部分是我们改造之后的特性,原本没有。开源生态 NSQ 做得还可以,虽然没有 Kafka 那么强大,但是它本身的文档和客户端比较丰富。可靠性这一块,Kafka 本身是没有强保证,它可能在各种测试情况下丢数据,所以一般用于日志系统,日志系统丢几条数据是没有关系的。Kafka 最新的版本会有一些改进了,但是毕竟还没有发布,没办法说它的可靠性有多高。


Kafka 批量写入的性能非常高,但是实际业务大部分情况下,一个客户端不可能批量写入,大部分都是几百个客户端同时写入,这种情况下每个客户端只有几条消息。所以这种情况下的性能比较,Kafka 和其它两个产品还是有一定差距。


 RocketMQ 本身的灵活性比较小,因为它不能按照 Topic 配置参数,比如 A  Topic 是 2 个副本,B Topic 是 4 个副本。不能这样配,只能大家都是三个副本,参数也都是一样的,它的灵活性比较低。


RocketMQ 有提供严格顺序的功能,但是在我们测试过程中,可能出现乱序的情况。当然,我们用的开源版,和生产版本可能不太一样。堆积能力和 Topic 设计有关系的,官方也说它堆积能力没有那么好。消息跟踪就是我们做的 Tracing 系统,RocketMQ 和 NSQ 都做得还不错。

Migrate data in Youzan

如何迁移数据?首先 NSQ 本身支持多个 lookup 查找数据节点,所以有两个集群就有两个 lookup,两个 lookup 可以同时从两个集群消费数据,这样两个集群的数据都可以消费掉。但是,你新写入的数据全部都写入新的集群,所以老的数据越来越少,新的集群数据越来越多,整个数据迁移的过程比较平滑。为了减少业务方使用两个 lookup 配置的情况,我们还做了一个简化的配置服务,让它更容易的迁移。我们可以在界面上直接控制 topic 消费哪几个集群,不需要业务方改配置、以及重启等。地址的变更都是通过界面控制,迁移过程不会产生数据丢失,过程更方便。因此,我们的工作就是减少他们的迁移过程,让系统自动做迁移。

Usage in Youzan

图 23

另外,有赞整个系统怎么样去做到微服务?微服务是各个服务之间不能有直接的耦合。如果有直接的耦合,进行直接调用,则相当于一个整体的大钢板,无法拆。所以为了做到微服务,需要把所有的小组件全部拆开,然后通过 NSQ 通信。他可以发一条信息,另外一个 App 异步收到这个消息后,处理消息,整个流程只和消息有关,和任何接口无关,包括导入到搜索系统、离线系统、spark、跟踪系统,全部通过这个技术组件去做。    

图 24

如图 24 是两个典型的场景。一,搜索系统,商品的修改变更如何同步到搜索系统?一个商品变更发到 NSQ 上面,然后把它的消息同步到搜索系统;二,消息通道,当某个业务要推送一条消息都要走消息通道的,他会放到 NSQ 里面,然后等消息通道一个个拉下来,再推到另外一个下放的渠道。上面渠道和下面渠道两个是分离的,通过中间的 NSQ 进行消息流转。


发放奖励,付款同步,发货同步,维权等等都是异步的通知,都非常适合使用 NSQ。包括高峰时期,突然一个大流量过来,你处理不了,就只能用 NSQ 暂时承担一些,然后异步慢慢的下单,这也是一个很重要的削峰功能。


目前很多组件都有开源的,我们这边的 Golang、Java、PHP 的客户端都是开源的,服务端也开源, 地址是 https://github.com/absolute8511/nsq (后面将会迁移到 youzan 公司组织下)。我们正在打造一个生态,让 NSQ 可以对接各个开源的产品,比如可以对接 Spark、对接 Flume,可以对接 ES等等,整个对接的过程要做到一个比较完善的生态。最好可以做到像 Kafka 那样可以对接主流的分析场景。开源是我们公司的重要文化,不光是要把代码放到 Github ,而是把整个文档留存, 开发的过程公开,跟其他的系统对接全部开放出来,这样才是真正的开放心态做这个开源。后面还有很多的开源的组件,都会在有赞开源出来。  

 

最后,我们再回顾一下今天讲的内容,首先第一点是我们需要把 Channel 改造成磁盘储存,第二个我们要避免消费者的数据拷贝,第三个从没有副本到有副本并且 HA 可以自动均衡。最后还有几个特性,顺序消费,历史回溯,跟踪,测试,最重要的是我们保持大部分协议兼容。原来 NSQ 老的开源的框架,在新的 NSQ 上面都可以用,只是有一些高级功能不能用。而如何让业务方没有感知的使用新集群,协议兼容则非常重要。




有疑问加站长微信联系(非本文作者)

本文来自:微信公众平台

感谢作者:李文

查看原文:NSQ的重塑之路

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

10477 次点击  ∙  1 赞  
加入收藏 微博
被以下专栏收入,发现更多相似内容
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传