什么是消息队列?
消息队列(MQ),指保存消息的一个容器,本质是个队列。但这个队列呢,需要支持高吞吐,高并发,并且高可用。
前世今生
业界消息队列对比:
- Kafka: 分布式的、分区的、多副本的日志提交服务,在高吞吐场景下发挥较为出色
- RocketMQ: 低延迟、强一致、高性能、高可靠、万亿级容量和灵活的可扩展性,在一些实时场景中运用较广
- Pulsar: 是下一代云原生分布式消息流平台,集消息、存储、轻量化函数式计算为一体、采用存算分离的架构设计
- BMQ: 和Pulsar架构类似,存算分离,初期定位是承接高吞吐的离线业务场景,逐步替换掉对应的Kafka集群
消息队列-Kafka
使用场景
- 日志信息
- 用户行为信息
基本概念
Topic:Kakfa中的逻辑队列,可以理解成每一个不同的业务场景就是一个不同的topic,对于这个业务来说,所有的数据都存储在这个topic中
Cluster:Kafka的物理集群,每个集群中可以新建多个不同的topic
Producer:顾名思义,也就是消息的生产端,负责将业务消息发送到Topic当中Consumer:消息的消费端,负责消费已经发送到topic中的消息
Partition:通常topic会有多个分片,不同分片直接消息是可以并发来处理的,这样提高单个Topic的吞吐
- Offset
消息在partition内的相对位置信息,可以理解为唯一ID,在 partition内部严格递增。
- Replica
每个分片有多个Replica,Leader Replica将会从ISR中选出。
数据复制
例如,某个kafka集群包含了4个Broker机器节点,有两个Topic,分别是Topic1和Topic2,Topic1有两个分片,Topic2有1个分片,每个分片都是三副本的状态。
这里中间有一个Broker同时也扮演了Controller的角色,Controller是整个集群的大脑,负责对副本和Broker进行分配。副本中的数据会从主副本拉取数据进行数据复制。
为什么快
- producer消息批量发送,减少网络io次数
- producer支持消息压缩,减少消息大小,目前支持Snappy、Gzip、LZ4、ZSTD压缩算法
- broker采用mmap文件映射的方式顺序写磁盘,提高写效率
- broker发送数据时通过零拷贝,减少数据复制次数
- Consumer通过reblance机制保证消费速率
当前缺陷
- Brocker重启、替换、扩容、缩容存在数据复制问题,运维成本高
- 当数据量分布不均时,数据复制会引起负载不均衡的问题,对于负载不均衡的场景,解决方案复杂
- 没有自己的缓存,完全依赖Page Cache
- Controller和Coordinator和Broker在同一进程中,大量IO会造成其性能下降
消息队列-BMQ
基本概念
BMQ兼容Kafka协议,存算分离,云原生消息队列,初期定位是承接高吞吐的离线业务场景,逐步替换掉对应的Kaka集群。
BMQ架构如下:
运维操作
| 操作 | Kafka | BMQ |
|---|---|---|
| 重启 | 需要数据复制,分钟级重启 | 重启后可直接对外服务,秒级完成 |
| 替换 | 需要数据复制,分钟级替换,甚至天级别 | 替换后可直接对外服务,秒级完成 |
| 扩容 | 需要数据复制,分钟级扩容,甚至天级别 | 扩容后可直接对外服务,秒级完成 |
| 缩容 | 需要数据复制,分钟级缩容,甚至天级别 | 缩容后可直接对外服务,秒级完成 |
读写流程
HDFS写文件流程:
同一个副本是由多个segment组成,我们来看看BMQ对于单个文件写入的机制是怎么样的,首先客户端写入前会选择一定数量的DataNode,这个数量是副本数,然后将一个文件写入到这三个节点上,切换到下一个segment之后,又会重新选择三个节点进行写入。这样一来,对于单个副本的所有segment来讲,会随机的分配到分布式文件系统的整个集群中。
Kafka与BMQ持久化存储对比:
对于Kafka分片数据的写入,是通过先在Leader上面写好文件,然后同步到Follwer上,所以对于同一个副本的所有Segment都在同一台机器上面。就会存在单分片过大导致负载不均衡的问题,但在BMQ集群中,因为对于单个副本来讲,是随机分配到不同的节点上面的,因此不会存在Kafka的负载不均问题。
对于写入的逻辑来说,还有一个状态机的机制,用来保证不会出现同一个分片在两个Broker上同时启动的情况,另外也能够保证一个分片的正常运行。首先,Controller做好分片的分配之后,如果在该Broker分配到了分片,首先会start这个分片,然后进入Recover状态,这个状态主要有两个目的:
- 获取分片写入权利,也就是说,对于hdfs来讲,只会允许一个分片进行写入,只有拿到这个权利的分片才能写入。
- 如果上次分片是异常中断的,没有进行save checkpoint,这里会重新进行一次save checkpoint。
然后就进入了正常的写流程状态,创建文件,写入数据,到一定大小之后又开始建立新的文件进行写入。
Broker写文件流程:
- CRC数据校验
- 参数是否合法校验完成后,会把数据放入Buffer中
- 通过一个异步的Write Thread线程将数据最终写入到底层的存储系统当中
对于业务的写入来说,可以配置数据返回的方式,可以在写完缓存之后直接返回,也可以数据真正写入存储系统后再返回,前者损失了数据的可靠性,带来了吞吐性能的优势,因为只写入内存是比较快的,但如果在下一次flush前发生宕机了,这个时候数据就有可能丢失了;后者因为数据已经写入了存储系统,这个时候也不需要担心数据丢失,但相应的来说吞吐就会小—些。
我们再来看看Thread的具体逻辑,首先会将Buffer中的数据取出来,调用底层写入逻辑,在一定的时间周期flush,flush完成后开始建立Index,也就是offset和timestamp对于消息具体位置的映射关系Index建立好以后,会save一次checkpoint,也就表示,checkpoint后的数据是可以被消费的。如果没有checkpoint的情况下会发生什么问题,如果flush完成之后宕机,index还没有建立,这个数据是不应该被消费的。最后当文件到达一定大小之后,需要建立—个新的segment文件来写入数据。
Broker写文件失败如何处理?
当建立一个新的文件,会随机挑选与副本数量相当的数据节点进行写入,那如果此时挑选的节点中有一个出现了问题,导致不能正常写入了,应该怎么处理?Broker具有Failover机制,Broker可以重新找正常的DataNode节点创建新的文件进行写入,这样也就保证了我们写入的可用性。
Proxy请求wait机制:
Consumer发送一个Fetch Request,会有一个Wait流程,那么他的作用是什么呢?想象一个Topic,如果一直没有数据写入,那么consumer就会一直发送Fetch Request。如果Consumer数量过多,BMQ的server端是扛不住这个请求的。因此,proxy设置了一个等待机制,如果没有fetch到指定大小的数据,那么proxy会等待一定的时间,再返回给用户侧,这样也就降低了fetch请求的IO次数。
经过wait流程后,首先会到Cache里面去找到是否有存在我们想要的数据,如果有直接返回。如果没有,再开始去存储系统当中寻找,首先会Open这个文件,然后通过index找到数据所在的具体位置,从这个位置开始读取数据,最后返回。
多机房部署:
对于一个高可用的服务,除了要防止单机故障所带来的的影响以外,也要防止机房级故障所带来的影响,比如机房断点,机房之间网络故障等等。
BMQ的多机房部署如下图所示:
高级特性
泳道
一般正式开发流程如下:
BOE:Bytedance Offline Environment,是一套完全独立的线下机房环境,数据与流量和线上都是隔离的。
PPE:Product Preview Environment,即产品预览环境,流量与线上是隔离的。
Databus
直接使用原生SDK会有什么问题?
- 客户端配置较为复杂
- 不支持动态配置,更改配置需要停掉服务
- 对于latency不是很敏感的业务,batch效果不佳
使用Databus:
- 简化消息队列客户端复杂度
- 解耦业务与Topic
- 缓解集群压力,提高吞吐
Mirror
Index
如果希望通过写入的 Logld、Userld或者其他的业务字段进行消息的查询,应该怎么做?
Parquest
Apache Parquet是Hadoop生态圈中一种新型列式存储格式,它可以兼容Hadoop 生态圈中多数计算框架(Hadoop、Spark等),被多种查询引擎支持(Hive、Impala、Drill等)。
消息队列-RocketMQ
使用场景
针对电商业务线,其业务涉及广泛,如注册、订单、库存、物流等;同时,也会涉及许多业务峰值时刻,如秒杀活动、周年庆、定期特惠等
基本概念
RocketMQ与Kafka对比:
| 名称 | Kafka | RocketMQ |
|---|---|---|
| 逻辑队列 | Topic | Topic |
| 消息体 | Message | Message |
| 标签 | 无 | Tag |
| 分区 | Partition | ConsumerQueue |
| 生产者 | Producer | Producer |
| 生成者集群 | 无 | Producer Group |
| 消费者 | Consumer | Consumer |
| 消费者集群 | Consumer Group | Consumer Group |
| 集群控制器 | Controller | Nameserver |
RockerMQ架构图:
RockerMQ存储模型如下图:
对于一个Broker来说所有的消息的会append到一个CommitLog上面,然后按照不同的Queue,重新Dispatch到不同的Consumer中,这样Consumer就可以按照Queue进行拉取消费,但需要注意的是,这里的ConsumerQueue所存储的并不是真实的数据,真实的数据其实只存在CommitLog中,这里存的仅仅是这个Queue所有消息在CommitLog上面的位置,相当于是这个Queue的一个密集索引。
高级特性
事务处理
考虑以下场景:
正常情况下,下单的流程应该是这个样子,首先保证库存足够能够顺利-1,这个时候再让消息队列通知其他系统来处理,比如订单系统和商家系统,但这里有个比较重要的点,库存服务和消息队列必须要是在同—个事务内处理。这里库存记录和往消息队列里面发的消息这两个事情,是需要有事务保证的,这样不至于发生库存已经-1了,但我的订单没有增加,或者商家也没有收到通知要发货。因此RocketMQ提供事务消息来保证类似的场景,我们来看看其原理是怎么样的:
延迟消息
延迟消息应用场景:
延迟消息投递流程:
失败处理
RocketMQ的失败处理是通过消费重试和死信队列来完成的:
课后作业
- 手动搭建一个Kafka集群。
- 完成Hello World的发送与接收。
- 关闭其中一个 Broker,观察发送与接收的情况,并写出在关闭一个Broker后,Kafka集群会做哪些事情?