消息队列 & Kafka
😉 本文共6000字,阅读时间约20min
消息队列
消息队列作用
- 异步处理降低响应时间
- 削峰/限流
- 解耦,降低系统耦合
异步处理减少响应时间
将用户的请求数据存储到消息队列之后就立即返回结果。随后,系统再对消息进行消费。
因为用户请求数据写入消息队列之后就立即返回给用户了,但是请求数据在后续的业务校验、写数据库等操作中可能失败。因此,使用消息队列进行异步处理之后,需要适当修改业务流程进行配合,比如用户在提交订单之后,订单数据写入消息队列,不能立即返回用户订单提交成功,需要在消息队列的订单消费者进程真正处理完该订单之后,甚至出库后,再通过电子邮件或短信通知用户订单成功,以免交易纠纷。这就类似我们平时手机订火车票和电影票。
削峰/限流
先将短时间高并发产生的事务消息存储在消息队列中,然后后端服务再慢慢根据自己的能力去消费这些消息,这样就避免直接把后端服务打垮掉。
举例:在电子商务一些秒杀、促销活动中,合理使用消息队列可以有效抵御促销活动刚开始大量订单涌入对系统的冲击。
解耦
使用消息队列还可以降低系统耦合性。如果模块之间不存在直接调用,那么新增模块或者修改模块就对其他模块影响较小,这样系统的可扩展性更好一些。
消息队列的两种模式
点对点模式
发布 / 订阅模式
常见消息队列特点
对比方向 | 概要 |
---|---|
吞吐量 | 万级的 RabbitMQ 吞吐量比十万甚至百万级的 RocketMQ 和 Kafka 低一个数量级。 |
时效性 | RabbitMQ 延时很低,达到微秒级,其他几个都是 ms 级。 |
消息丢失 | RabbitMQ 丢失的可能性非常低, Kafka、RocketMQ 理论上可以做到 0 丢失。 |
RabbitMQ 在吞吐量方面虽然稍逊于 Kafka 、RocketMQ , 但是性能极其好,延时很低,达到微秒级。如果业务场景对并发量要求不是太高(十万级、百万级),那这几种消息队列中,RabbitMQ 或许是你的首选。
Kafka提供超高的吞吐量和可靠性,延时在毫秒级别。唯一的一点劣势是有可能消息重复消费,那么对数据准确性会造成极其轻微的影响,在大数据领域中以及日志采集中,这点轻微影响可以忽略这个特性天然适合大数据实时计算以及日志收集。这已经是事实上的规范。
Kafka基础架构
Kafka基础架构
\(Producer \stackrel{生产者发送}{\rightarrow} Broker(Topic、Partition、Replica) \stackrel{消费者拉取}{\leftarrow} Consumer\)
- Topic创建时可以指定分区数和副本数。
- 分区:
- 一个 Topic 可以有多个 Partition ,可以分布在不同的 Broker 上。
- 目的在于提供吞吐量和存储的水平扩展能力。
- 副本:一个topic的每个分区都有若干个副本,一个Leader和若干个Follower。
- Leader:生产者和消费者只与 leader 副本交互。
- Follower:提供故障恢复能力,保证消息存储的安全性。不过也增加了存储成本。
- Follower和Leader保持同步,我们发送的消息会被发送到 leader 副本,然后 follower 副本才能从 leader 副本中拉取消息进行同步。
- Leader发生故障时,某个Follower会成为新的Leader。
- 分区:
- 消费者组:消费者组内每个消费者负责消费不同分区数据,一个分区只能由一个组内的一个消费者消费,避免之后还要做消息去重。多个消费者组互补干扰。
多分区机制和多副本机制
分区好处
从性能考虑,如果topic消息只存于一个broker,那么这个broker就会成为吞吐量和存储的瓶颈,无法做到水平扩展。吞吐量和存储的水平扩展。
- 并发生产/并发消费:生产者并行生产,消费者组内每个消费者并行消费,提高吞吐量。
- 海量存储 / 可扩展性:通过增加broker来增加系统的存储量
副本好处
提供故障恢复能力,保证消息存储的安全性。
副本:一个topic的每个分区都有若干个副本,一个Leader和若干个Follower。
- Leader:生产者和消费者只与 leader 副本交互。
- Follower:提供故障恢复能力,保证消息存储的安全性。不过也增加了存储成本。
- Follower和Leader保持同步,我们发送的消息会被发送到 leader 副本,然后 follower 副本才能从 leader 副本中拉取消息进行同步。
- Leader发生故障时,某个Follower会成为新的Leader。
Zookeeper 在 Kafka 中的作用
ZooKeeper 主要为 Kafka 提供元数据管理的功能。
元数据管理
Broker注册
/kafka/broker/ids
:[0,1,2]
。记录有哪些服务器。
每个Broker注册时,会到该路径下创建节点,并在节点中记录自己的IP和端口等信息。
Topic注册
/kafka/brokers/topics/first/partitions/0/state
:{"leader":1 ,"isr":[1,0,2] }
记录了某个topic的某个分区的leader是哪个broker上的副本,以及可用的副本。
辅助选举Leader
/kafka/controller
:{“brokerid”:0}
记录辅助选举副本Leader的broker
Replica Leader选举
Replica副本概念
- Kafka中副本分为:Leader和Follower。Kafka生产者只会把数据发往Leader,然后Follower找Leader进行同步数据。
- Kafka分区中的所有副本统称为AR(Assigned Repllicas)。 AR = ISR + OSR。ISR,表示和Leader保持同步的Follower集合。如果Follower长时间未向Leader发送通信请求或同步数据,则该Follower将被踢出ISR。
Leader选举流程
-
Kafka集群中有一个broker的Controller会被选举为Controller Leader,负责管理集群broker的上下线,所有topic的分区副本分配和Leader选举等工作。
-
Controller的信息同步工作是依赖于Zookeeper的。
分区Rebalance机制
触发时机
分区rebalance指kafka在一些情况下会重新分配消费者消费分区的关系。触发时机:
- 消费组里的consumer增加或减少了
- 动态给topic增加了分区
- 消费者消费超时,一直没有提交offset
不良影响
- 不良影响:rebalance过程中,消费者无法从kafka消费消息,这对kafka的TPS会有影响,如果kafka集群内节点较多,比如数百个,那重平衡可能会耗时极多,所以应尽量避免在系统高峰期的重平衡。
rebalance分区策略:
-
range():按照分区序号排序(范围分配)。假设一个主题有10个分区(0-9),现在有三个consumer消费,比如分区03给一个consumer,分区46给一个consumer,分区7~9给一个consumer。
-
round-robin(轮询):比如分区0、3、6、9给一个consumer,分区1、4、7给一个consumer,分区2、5、8给一个consumer
-
sticky(粘性):初始时分配策略与round-robin类似,但是在rebalance的时候,需要保证如下两个原则。
- 分区的分配要尽可能均匀 。分区的分配尽可能与上次分配的保持相同。
- 当两者发生冲突时,第一个目标优先于第二个目标 。这样可以最大程度维持原来的分区分配的策略。
- 比如对于第二种情况的分配,如果第三个consumer挂了,那么重新用sticky策略分配的结果如下:consumer1除了原有的03,会再分配一个2,consumer2除了原有的46,会再分配5、8。
Kafka如何提高吞吐量
生产者提高吞吐量
生产者重要参数列表 | 描述 |
---|---|
batch.size | 缓冲区一批数据最大值,默认16k。适当增加该值,可以提高吞吐量,但是如果该值设置太大,会导致数据传输延迟增加。 |
linger.ms | 如果数据迟迟未达到batch.size,sender等待linger.time之后就会发送数据。单位ms,默认值是0ms,表示没有延迟。生产环境建议该值大小为5-100ms之间。 |
- 生产者生产消息时机
- batch满了。(默认16k)
- 等待时间到了(lingerMS默认是0ms,这个真要设置,不然来一个发一个,吞吐差)
- RecordAccumulator满了。默认32M。
- RecordAccumulator关闭了
总结:提高batch大小,提高等待时间,提高缓冲区大小,消息压缩
消费者提高吞吐量 / 如何解决数据积压
Kafka消费者消费方式
Kafka 可靠性、唯一性、有序性保证
Kafka消息 可靠性/不丢失 保证
kafka架构由Producer、Broker、Consumer组成,所以保证消息不丢失从这三个方面考虑。
ACK机制 & Retries重试
Producer发送一个消息,Broker必须在规定时间ACK应答。如果未应答,Producer会进行N次Retry。
acks参数
- acks参数表示的是生产者生产消息时,写入到副本的严格程度,决定了生产者如何在性能和可靠性之间做取舍。
ack | 描述 |
---|---|
0 | 生产者无需等待任何确认 |
1 | leader数据落盘后,不等follower完全确认,就做出响应;如果leader宕机,会造成数据丢失。 |
all(-1) | 生产者发送过来的数据,Leader+和isr队列里面的所有节点收齐数据后应答。 |
ISR:In-Sync Replica,防止有followe迟迟不拉取数据,造成阻塞,直接将其踢出ISR。
Retries重试
默认ACK超时时间为30s , retries 次数为Integer.MAX_VALUE,一直重发。如果retry设置为3,可以通过异步回调写日志,之后进行补偿。
注意:acks = all 和 retries同时使用,会导致某些数据重复。因为可能因为网络抖动,生产者收不到ACK,可能发送多次,这样就会导致消息重复。可以用幂等性解决。
Producer侧
如何判断消息是否发送成功?
-
同步(不推荐):
send
方法实际上是异步的操作,可以通过get()
方法获取调用结果,但是这样也让它变为了同步操作。kafkaTemplate.send(topic, o).get(); if (ex != null) // log
-
异步回调:可以采用为其添加回调函数的形式,如果消息发送失败的话,我们检查失败的原因之后记录日志补偿发送即可!
- 回调函数会在producer收到ack时调用,为异步调用。如果Exception为null,说明消息发送成功,如果Exception不为null,说明消息发送失败。
- 注意:如果retry很大,消息发送失败会自动重试,不需要我们在回调函数中手动重试。
future = kafkaTemplate.send(topic, o); future.addCallback(result -> // log);
Broker侧
数据完全可靠条件为 acks = -1,分区副本>=2 ,ISR最小应答副本数>=2
若后两者为1,跟ack = 1也没区别。
为了保证整个 Kafka 服务的高可用性,你需要确保 replication.factor > min.insync.replicas 。为什么呢?设想一下假如两者相等的话,只要是有一个副本挂掉,整个分区就无法正常工作了。这明显违反高可用性!一般推荐设置成 replication.factor = min.insync.replicas + 1。
Consumer侧
-
原因:Consumer不能一拉取到消息,就自动提交offset。否则,刚拉到就挂掉,offset被提交,下次重启也消费不到这条消息了。
-
正确的做法:拉取消息 --- 业务处理 ---- 提交消费位移。也就是关闭自动提交
enable.auto.commit
,消费完消息再手动提交offset。 -
带来的另一个问题:
- Consumer消费完消息之后,还没提交 offset,挂掉了。这个消息理论上就会被消费两次。
- 解决方案:允许拉取重复消息,但是消费端自己做幂等性控制。保证只成功消费一次。
- 关于幂等技术方案很多,我们可以采用数据表或Redis缓存存储处理标识,每次拉取到消息,处理前先校验处理状态,再决定是处理还是丢弃消息。
Kafka消息 唯一性/去重 保证
Exactly Once
Exactly Once
= 幂等性 + 至少一次(acks = -1,分区副本>=2 ,ISR最小应答副本数>=2)
Producer幂等性
- 幂等性就是执行多次操作与执行一次操作的影响是一致的。生产者通过
enable.idempotence
开启幂等性,默认为true - 重复数据的判断标准:具有<PID,Partition,Seq>相同主键的消息提交时,Broker只会持久化一条。
- PID生产者ID,每次重启都会分配一个新的。Partition分区号,Seq Number单调递增。
- 如果新消息的seq正好是broker端维护的<PID,Partition> -> sequence number大1,说broker会接受处理这条消息。小于等于,重复丢弃。大于等于2,说明中间丢数据了。
- 所以幂等性只能保证单次会话内不重复。
- 如何保证多次会话内幂等性:kafka事务,引入事务ID,一个事务ID只对应一个pid,有新的producer初始化时,可以根据事务ID返回之前的PID。
Consumer幂等性
-
kafka出现消息重复消费的原因:
- 服务端侧已经消费的数据没有成功提交 offset(根本原因)。
- 消费者消费超时或网络原因导致分区rebalance,没有成功提交 offset。
-
消息表,来去重。利用比如 Redis 的set、MySQL 的主键等天然的幂等功能。
Kafka消息 有序性 保证
单分区有序性
- Kafka保证单分区内数据有序(有条件)
- 多分区,分区与分区间数据无序
消息在被追加到 Partition(分区)的时候都会分配一个特定的偏移量(offset)。Kafka 通过偏移量(offset)来保证消息在分区内的顺序性。
如何保证全局消费的有序性
尽量让需要业务有序的数据,发送到同一个分区
1 个 Topic 只对应一个 Partition
不推荐,违反Kafka的设计初衷
(推荐)生产者发送消息的时候指定 key/Partition
Kafka 中发送 1 条消息的时候,可以指定 topic, partition, key,data(数据) 4 个参数。如果你发送消息的时候指定了 Partition 的话,所有消息都会被发送到指定的 Partition。并且,同一个 key 的消息可以保证只发送到同一个 partition,这个我们可以采用表/对象的 id 来作为 key 。
Kafka 高性能原理 / 为啥快?
发:内存池复用
存:分区、磁盘顺序写、page cache
收:稀疏索引、零拷贝
批发批收带压缩