四个案例
案例一:系统崩溃
此时会在相关环节卡住
解决方案:解耦(利用消息队列)
案例二:服务能力有限
解决方案:削峰
案例三:链路耗时长尾
解决方案:异步
案例四:日志存储
什么是消息队列?
消息队列(MQ),指保存消息的一个容器,本质是个队列。但这个队列呢,需要支持 高吞吐,高并发,并且高可用。
- 解耦
- 削峰
- 异步
- 日志处理
消息队列前世今生
消息队列发展历程
业界消息队列对比
- Kafka:分布式的、分区的、多副本的日志提交服务,在 高吞吐场景下 发挥较为出色
- RocketMQ:低延迟、强一致、高性能、高可靠、万亿级容量和灵活的可扩展性,在一些 实时场景中 运用较广
- Pulsar:是下一代云原生分布式消息流平台,集消息、存储、轻量化函数式计算为一体、采用存算分离的架构设计
- **BMQ:**和Pulsar架构类似,存算分离,初期定位是承接高吞吐的离线业务场景,逐步替换掉对应的Kafka集群
消息队列-Kafka
使用场景
- 离线信息,如日志
- Metrics 数据(程序运行时采集的数据)
- 用户行为数据
如何使用 Kafka
- 创建集群
- 新增Topic
- 编写生产者逻辑
- 编写消费者逻辑
基本概念
- Topic:逻辑队列,不同Topic可以建立不同的Topic
- Cluster:物理集群,每个集群中可以建立多个不同的Topic
- Producer:生产者,负责将业务消息发送到Topic中
- Consumer:消费者,负责消费Topic中的消息
- ConsumerGroup:消费者组,不同组 Consumer消费进度互不千涉
Offset
Offset:消息在 partition内的相对位置信息,可以理解为唯一ID,在 partition内部严格递增。
Replica
每个分片有多个Replica,Leader Replica将会从ISR中选出。
Leader 是主要负责读和写的,其它 Follower 不断将 Leader 中的消息同步,有点类似 Redis 集群
在 ISR 中,假如消息相差过远,Follower 是会被踢出的,上古版本使用 offset 判断,但是很明显这样做有极大缺陷,所以现在都通过写入时间判断
当 Leader 宕机则换其它 Follower,保证高可用
数据复制
controller 担当了集群大脑的职责,计算分区分配状态并告知集群
Kafka 架构
ZooKeeper:负责存储集群元信息,包括分区分配信息等
Cluster:Kafka 集群,包含多个 Broker
Producer:消息的生产者
Consumer:消费者,一般被分成若干个消费者组
Producer
批量发送
如果发送一条消息并确认之后在发送下一条,不能充分利用 Kafka 的高吞吐,所以考虑将消息打包进 Batch,最后批量发送
这样可以减少 IO 次数加强发送能力
但是由于消息量较大,可能会导致要传输的数据量大,耗尽带宽怎么办?
数据压缩
通过压缩,减少消息大小,目前支持 Snappy、Gzip、LZ4、ZSTD 压缩算法
ZSTD 在性能和压缩率上都比较好,推荐
Broker
数据的存储
Broker 将压缩后数据如何存储到本地磁盘?
Topic——>多个 Partition——>多个 Replica——>多个Log——>切分为LogSegment
LogSegment:
- .log(日志文件)
- .index(偏移量索引文件)
- .timeindex(时间戳索引文件)
- 其他文件
磁盘结构
移动磁头找到对应磁道,磁盘转动,找到对应扇区,最后写入。寻道成本比较高,因此顺序写可以减少寻道所带来的时间成本,提高写入效率。
如何找到消息
Consumer通过发送FetchRequest请求消息数据,Broker 会将指定Offset 处的消息,按照时间窗口和消息大小窗口发送给Consumer,寻找数据这个细节是如何做到的呢?
偏移量索引文件
采用二分法找到小于目标 Offset 的最大文件
然后二分找到小于目标 offset的最大索引位置(batch 中存有一组信息,所以索引是稀疏的)
时间戳索引文件
在文件和offset之间加入了一级时间戳索引
二分找到小于目标时间戳最大的索引位置,在通过寻找 offset的方式找到最终数据。
数据拷贝
传统拷贝需要经由 Application Buffer 和 Socket Buffer 才能传给网卡发送
利用 零拷贝 可以跨过这两个环节,直接从 Read Buffer 发给网卡
Consumer
主要需要解决Partition在Consumer Group中的分配问题
Low Level
通过手动进行分配,哪一个 Consumer消费哪一个Partition完全由业务来决定。
缺点:
- 不能自动容灾(Consumer宕机后,部分分区数据流断掉)
- 加入新消费者时需要启停较复杂
优点:
提前写好,速度快
High Level
Coordinator(协调者),可以进行 Rebalance 操作:
感知 Consumer 状态,当出现异常时及时剔除,出现新消费者及时加入,并重新分配分区
Rebalance
-
消费者此时手上没有相关信息,会向 Broker(找负载最小的)发送 FindCoordinatorRequest,确认 Coordinator 位置
-
当多个 Consumer 达成一致时,发送 JoinGroupRequest 请求加入分组,此时协调者收到请求后会在 Consumer 中选取一个 Leader
-
协调者返回响应,并在指定 Leader 的响应中通知 isLeader=true
-
此时,Consumer 算加入了组内,但是对组内的分配策略还一无所知,因此还需要一次请求 SyncGroupRequest 来获取相关方案,这一次,Leader 发送的请求中会携带其分配方案
-
之后,会定期发送心跳,确保 Consumer 处于活动状态,一旦超时无响应则判定为死亡
数据复制问题
Kafka 为了保证高可用,Follower 会从 Leader 上同步数据
假如由于业务需要,我们重启了 Leader
-
关闭、重启:
重启后,Leader 会转到一台 Follower 上
-
Leader切换,追赶数据:
直到相差不大才会认为同步完成
-
数据同步完成
-
Leader 回切
这里有一个回切操作,也就是将 Leader 换回原来的节点,这是为了避免我们依次重启节点后所有 Leader 都归到最后一个节点造成过大压力
一台节点重启花费一些时间,所有节点依次重启整个周期将会非常长
而且不能够并发重启,如果在一个只有两分片(一Leader一Follower)的情况下可能造成该 Topic 的使用问题
替换、扩容、缩容
这三种操作都会涉及节点变更操作,不可避免会有数据的复制同步,也会带来时间成本问题
负载不均衡
当某一 Broker 上一个 Partiton 相当大之后,我们为了负载均衡,考虑将别的较小的 Partiton 移动至其它 Broker,但是这样的移动又会造成数据读写带来的负载问题,恶性循环了属于是
总结
有哪些帮助Kafka提高吞吐或者稳定性的功能:
- Producer:批量发送、数据压缩
- Broker:顺序写,消息索引,零拷贝
- Consumer:Rebalance
问题:
- 运维成本高
- 对于负载不均衡的场景,解决方案复杂
- 没有自己的缓存,完全依赖Page Cache
- Controller 和 Coordinator和Broker在同一进程中,大量IO会造成其性能下降