编辑推荐: |
本文来自于腾讯云,主要介绍了转转ZZMQ在架构、存储系统、保证消息可靠存储,可靠消费,以及netty的使用等。
|
|
背景
互联网公司使用最频繁的服务调用组件是RPC框架,RPC同步调用有些场景并不是很适用,而这些场景刚好是一个可靠MQ的适用场景。
我们看看RPC调用的场景。服务A调用如图所示服务。在正常情况下,一般都不会有问题。但是在以下情况,服务A调用会遇到问题。
问题一:如果有流量高峰,服务B响应超时,会发生什么情况?
整个RPC调用链路都会受到影响,甚至发生雪崩。
问题二:服务A逻辑复杂,逻辑耦合严重,怎么做拆分?
把一些调用链路中可以异步调用的逻辑调整为消费MQ消息。
问题三:RPC调用,jar依赖问题?服务B升级,调用B的相关服务是否需要升级?
RPC服务需要依赖生成的接口描述jar,服务接口升级一般很难做到向前兼容,所以相关调用方也需要升级。
MQ是以消息为载体的可靠异步调用的框架,能很好的应对上面三个问题。流量削峰,MQ是天然支持的,因为MQ有可靠存储,可以落地。解耦合,交给MQ也很合适。因为MQ的接入方处理的是消息,做到向前兼容也是比较容易的。
使用MQ之后,服务A,B通过MQ做到松耦合,也能很好的应对流量高峰。
MQ很好很强大,是RPC的有效补充,那问题来了怎么实现一个可靠MQ?本文结合二手交易平台的特点,详细介绍转转ZZMQ架构设计实践。
转转消息队列(ZZMQ)架构设计
设计ZZMQ,我们考虑的重点是:可靠,高性能,运维友好,接入方便,可以支持大量堆积,有效缓冲业务高峰,针对这些需求,我们做了一些设计上的考虑和取舍,最终形成了如下的架构方案。
RegisterCenter 作为注册中心,负责路由服务,无状态,每个NameNode都是对等的,NameNode
可以任意水平扩展。NameNode 与broker和Client都建立了长连接。Broker 内是主从两台机器,slave从master拉取消息Log和消费Offset,做HA。Broker也可以方便地水平扩展,加入新机器,更新topic的路由信息,client会定时更新路由信息。Consumer
和Producer 都需要到注册中心注册,同时拉取Topic的路由信息。Management Protal
是用来管理集群,维护Topic的相关联系人信息。
存储设计
一个高性能ZZMQ的瓶颈和难点就是存储系统,存储系统关乎到性能,数据可靠存储实现是否简单,数据备份控制,消息状态的表示。
目前最常见的是以Kafka为代表的Log-append-file,文件顺序写,追求吞吐量,消息消费状态通过offset来控制,还有以各种存储引擎实现的,比如leveldb(activemq),rocksdb等。
从这张图(http://queue.acm.org/detail.cfm?id=1563874),我们能看到磁盘顺序读写的性能甚至超过了内存随机访问的性能,能达到50多M/s。并且,相对来说,Log-append-file简单一些。最后选择了类似kafka的log-append-file。
Kafkatopic分成partition,顺序读写,partition有一个小的问题,单机不能支撑大量的topic,单机能支持几百分区,随着分区数量的增加,性能有所下降。而我们的场景的是:业务大部分topic的qps并不像kafka处理的日志文件那么高,希望能单机支撑更多的topic数量。
针对这一点,我们做了一些调整,topic消息本身不再分区,统一存储,使用更加轻量的Consumer
Queue实现partition的功能。Consumer Queue存储的只是位置信息,更加轻量,能做到支持更多的topic。
消息写入pageCache,再Dispatch到对应的Consumer
Queue中去,Consumer Queue只有消息Log的offset信息,以及大小和其他的一些元数据,占用很小的空间。这样设计,经过验证,单机能够支持几千队列。
这样的设计也带来了一个副作用,因为消息本身是统一存储,topic数量多的时候,消息的读取是随机读,性能有些许下降,目前,我们通过优化linuxpageCache和IO调度,开启系统预读,性能相对顺序读没有太大下降。
ZZMQ消息订阅模型
Consumergroup A 有两个消费者实例,B也有两个消费者实例,同时订阅了Topic-A,他们之间的消费进度是独立的。Group处理相同topic,消费逻辑一致的一个整体,包含不同的消费实例。
ConsumerGroup基本上是跟kafka的Group一致,由不同的ConsumerGroup来区分不同的消费组,维护Group对应的消费状态,能很方便的实现。
网络
基于netty实现了网络层的协议。Netty对nio,和reactor做了很好的封装,netty4.x
对direct buffer的利用,以及高效的buffer分配和回收算法,netty也解决了TCP协议的半包问题,使用方不需要自己组TCP包。用netty实现网络层协议,网络层的可靠由netty来做,减少了复杂度。
Push 还是Pull
这个设计的考虑主要是两个方面:慢消费,以及消费延迟。主动Push能做到低的消费延迟,但是对于慢消费,不能很好的应对,主动Push需要感知消费者的速率,不至于push
太快,把消费者压垮了。Pull模式,由消费者控制拉取的速度,能很好的应对慢消费的问题,但是,Pull模式对消费延迟不敏感,拉取的频率不好控制,处理不好有可能造成CPU使用率飙升。参照kafka的pull,实现了longpull。Consumer
与Broker建立长连接,Consumer发起拉取请求,如果有消息,Broker 返回消息,如果没有消息,broker
hold住这个请求一段时间,等有消息再notify这个请求,返回给客户端。基于long pull,消费延迟能降到跟push差不多,同时又能由Consumer
控制拉取速度。
重复消息和顺序消息
消息重复在一个复杂网络条件下很难避免的,如果由MQ本身来做去重,代价太大,所以我们要求接入MQ的逻辑做好幂等(状态机或者版本号的一些机制)。顺序消息,跟kafka一样,Consumer
queue跟partition类似,一个Queue内部,消费是有序的。如果要做到完全有序,只能一个Producer,一个Consumer。
高可用
Broker组由主从两台机器构成,可以配置的策略有同步双写和异步复制。默认情况,采用的异步复制,由slave去拉取master上面的消息Log和offset。Broker
接入了内部的监控系统,每分钟上报topic的消费情况和broker状态,能做到分钟级别发现异常消费。消息写入PageCache之后,默认是异步刷盘。也可以配置为同步刷盘,只有刷盘成功才会返回。
Broker的默认配置异步刷盘,Master-Slave异步复制。Client消费消费消息的可靠是通过Consume
queue的offset来保证的,Client会定时上报已经消费成功的Offset信息。如果Client被异常kill掉,没有确认的消息会被Client
重新拉取,消费。
一条消息的生命过程
Proudcer通过与NameNode的路由找到topic的broker地址,producer发消息,netty序列化,通过TCP传输到对应的broker,broker写log的pageCache,返回。Broker
Dispatch消息到对应的Consumer queue,broker唤醒刷盘线程,broker唤醒slave同步线程,slave拉取消息。
NotifyConsumer 拉取请求,经过netty序列化,通过tcp到达Consumer,Consumer消费成功,Client上报消费成功offset。
Broker持久化Offset,slave同步offset。三天之后,broker删除消息。
总结
本文主要介绍了转转ZZMQ在架构、存储系统、保证消息可靠存储,可靠消费,以及netty的使用等经验 |