mirror of
https://github.com/CyC2018/Interview-Notebook.git
synced 2026-10-31 11:05:36 +00:00
Update 消息队列.md
补充幂等自动关闭的条件、重平衡的分配策略、RocketMQ 5.x 定时消息的上限与同步复制的配置、min.insync.replicas 的默认值;拆分较长的段落,调整配图的说明。
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 192 KiB After Width: | Height: | Size: 151 KiB |
+9
-5
@@ -119,6 +119,8 @@ Kafka、RocketMQ 等通过消费者组把两种模型结合起来:一条消息
|
||||
|
||||
需要注意的是,RabbitMQ 对无法路由到任何队列的消息同样返回 ack,只开启确认而不设置 mandatory 时,发错路由键的消息会被直接丢弃;设置了 mandatory,这类消息会先退回给生产者。
|
||||
|
||||
RocketMQ 5.x 的主从部署中(未开启 Controller 模式,其它复制参数保持默认),只把 brokerRole 设为 SYNC_MASTER 不会等待从节点:需要确认的副本数由 inSyncReplicas 决定,默认为 1,即主节点写入后就返回。一主一从要同步复制时,还要把 totalReplicas 和 inSyncReplicas 都设为 2。
|
||||
|
||||
即使这些配置都打开,消息仍可能因为程序缺陷、误操作等原因丢失或者处理失败,关键业务还要定期核对上下游的数据(对账),发现缺失时补发。
|
||||
|
||||
### 发送端的可靠性
|
||||
@@ -162,7 +164,7 @@ COMMIT;
|
||||
|
||||
要保证同一业务的消息有序,需要让发送端把这些消息按业务 Key(例如订单号)路由到同一个队列,并且接收端对同一个队列只用一个线程顺序消费。
|
||||
|
||||
发送端也要按业务顺序发送。生产者的重试也可能打乱顺序:多个批次同时在途时,“已支付”的批次失败重试,后面“已发货”的批次先写入成功。Kafka 默认开启的幂等生产者用序号避免了这种乱序,关闭幂等时要把 max.in.flight.requests.per.connection 设为 1。
|
||||
发送端也要按业务顺序发送。生产者的重试也可能打乱顺序:多个批次同时在途时,“已支付”的批次失败重试,后面“已发货”的批次先写入成功。Kafka 默认开启的幂等生产者用序号避免了这种乱序,关闭幂等时要把 max.in.flight.requests.per.connection 设为 1。需要注意的是,没有显式设置 enable.idempotence 时,只要把 acks 设为 all 以外的值(例如 acks=1),幂等就会被自动关闭(只在 INFO 日志中提示一句),而 max.in.flight.requests.per.connection 仍是默认的 5,多个批次同时在途时重试乱序又会出现;retries=0 同样会关闭幂等,但这时不再重试。显式设为 true 时,冲突的配置会在启动时报错。
|
||||
|
||||
<div align="center"> <img src="pics/mq-order-by-key.png" width="760px"/> </div><br>
|
||||
|
||||
@@ -216,7 +218,7 @@ RabbitMQ 的队列默认没有长度上限,积压的消息占用内存和磁
|
||||
各消息队列的支持情况:
|
||||
|
||||
- RocketMQ 4.x 只支持 18 个固定的延迟级别:1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h,消息先存入内部的定时主题,每个级别一个队列,同一队列中先写入的先到期,只需检查队头;
|
||||
- RocketMQ 5.x 改用时间轮,支持指定任意的投递时间,默认精度为 1 秒;最长时长由配置决定,开源 5.5.1 中经 Proxy 发送的默认 1 天、Broker 默认 3 天,超出时拒绝写入(以官方最新文档为准);
|
||||
- RocketMQ 5.x 改用时间轮,支持指定任意的投递时间,默认精度为 1 秒;最长时长由配置决定,开源 5.x 中 gRPC 客户端经 Proxy 发送时默认最长 1 天(Proxy 的 maxDelayTimeMills),Remoting 客户端默认最长 3 天(Broker 的 timerMaxDelaySec),超出时发送失败,不会立即投递(官方文档写的是最长 24 小时、超出立即投递,与开源实现不同);
|
||||
- RabbitMQ 常用的做法是给消息设置过期时间(TTL),过期后转入死信交换机,由死信队列的消费者处理。过期的消息到达队头才会被转出,前面 30 分钟的消息会挡住后面已到期的 1 分钟的消息,所以每种延迟时长单独建一个队列;4.4 起仲裁队列也支持延迟投递(以官方最新文档为准);
|
||||
- Kafka 不支持延迟消息,需要自己实现,例如按延迟时长分成几个主题,消费者读到未到期的消息就暂停等待。
|
||||
|
||||
@@ -291,13 +293,15 @@ Kafka 中的消息按主题(Topic)分类,每个主题分成多个分区(
|
||||
|
||||
Kafka 4.2 起提供共享组(Share Group):一个分区的消息可以分给多个消费者,消费者数可以超过分区数,每条消息单独确认,超时未确认就重新投递,代价是不再保证顺序。例如 1 个分区的主题,3 个共享消费者各处理了 40 条消息(Kafka 4.4 实测,见[附录 A6](#a6-共享组))。
|
||||
|
||||
消费者加入、退出或者分区数变化时,组内会重新分配分区,称为重平衡(Rebalance)。默认的 classic 协议下所有消费者先交出全部分区,整个组暂停;Kafka 4.0 起的新协议(group.protocol=consumer)只暂停需要移动的分区。例如 6 个分区的组加入第二个消费者时,第一个消费者在两种协议下分别交出 6 个和 3 个分区(Kafka 4.4 实测,见[附录 A7](#a7-重平衡时交出的分区))。
|
||||
消费者加入、退出或者分区数变化时,组内会重新分配分区,称为重平衡(Rebalance)。默认的 classic 协议使用 RangeAssignor 分配,所有消费者先交出全部分区,整个组暂停;classic 下改用 CooperativeStickyAssignor,或者使用 Kafka 4.0 起的新协议(group.protocol=consumer,由 broker 计算分配),都只暂停需要移动的分区。例如 6 个分区的组加入第二个消费者时,第一个消费者在两种协议下分别交出 6 个和 3 个分区(Kafka 4.4 实测,见[附录 A7](#a7-重平衡时交出的分区))。
|
||||
|
||||
重平衡还可能造成重复消费。一个常见的原因是处理一批消息的时间超过了 max.poll.interval.ms(默认 5 分钟),消费者被认为已经失效,它的分区被分给其他消费者,这时它再提交偏移量会失败。可以调小 max.poll.records 或调大 max.poll.interval.ms;正常的重平衡可以在 ConsumerRebalanceListener 的 onPartitionsRevoked 中先同步提交已处理完的偏移量,再交出分区。
|
||||
|
||||
### 副本与可靠性
|
||||
|
||||
每个分区有多个副本,分布在不同的 broker 上,其中一个是 Leader,负责处理读写,其余的 Follower 不断从 Leader 拉取数据。与 Leader 保持同步的副本集合称为 ISR(In-Sync Replicas),Follower 超过 replica.lag.time.max.ms(默认 30 秒)没有追上 Leader 就会被移出 ISR。ISR 中所有副本都已写入的位置称为高水位(High Watermark, HW),消费者只能读到高水位之前的消息。Kafka 4.0 起完全移除了 ZooKeeper,Leader 选举等元数据由内置的 KRaft 控制器管理,它基于 [Raft](分布式.md#六raft) 协议实现。分区数据的复制没有用 Raft 的多数派:多数派要有 2f+1 个副本,才能在 f 个副本故障后保住已提交的消息并选出新 Leader;Kafka 的消息要写入 ISR 中的全部副本才算提交,提交时 ISR 有 f+1 个副本,损失其中 f 个仍能保住这条消息。代价是提交要等 ISR 中最慢的副本;故障后能否继续写入还取决于下文的 min.insync.replicas。
|
||||
每个分区有多个副本,分布在不同的 broker 上,其中一个是 Leader,负责处理读写,其余的 Follower 不断从 Leader 拉取数据。与 Leader 保持同步的副本集合称为 ISR(In-Sync Replicas),Follower 超过 replica.lag.time.max.ms(默认 30 秒)没有追上 Leader 就会被移出 ISR。ISR 中所有副本都已写入的位置称为高水位(High Watermark, HW),消费者只能读到高水位之前的消息。
|
||||
|
||||
Kafka 4.0 起完全移除了 ZooKeeper,Leader 选举等元数据由内置的 KRaft 控制器管理,它基于 [Raft](分布式.md#六raft) 协议实现。分区数据的复制没有用 Raft 的多数派:多数派要有 2f+1 个副本,才能在 f 个副本故障后保住已提交的消息并选出新 Leader;Kafka 的消息要写入 ISR 中的全部副本才算提交,提交时 ISR 有 f+1 个副本,损失其中 f 个仍能保住这条消息。代价是提交要等 ISR 中最慢的副本;故障后能否继续写入还取决于下文的 min.insync.replicas。
|
||||
|
||||
例如创建 3 个分区、3 个副本的主题,然后用 kill -9 停掉 broker 3(3 个节点的 KRaft 集群实测):
|
||||
|
||||
@@ -324,7 +328,7 @@ Follower 落后几条消息时仍在 ISR 中,Leader 宕机后它可以成为
|
||||
|
||||
<div align="center"> <img src="pics/kafka-acks1-loss.png" width="760px"/> </div><br>
|
||||
|
||||
acks=all 要配合 min.insync.replicas 使用,否则 ISR 只剩 Leader 一个时,acks=all 就退化成了 acks=1。min.insync.replicas 设为 2 时,ISR 少于 2 个副本,broker 就拒绝 acks=all 的写入。上面停掉 broker 3 之后 ISR 还有 2 个副本,写入正常;把 min.insync.replicas 改为 3 再写入,生产者重试几次后报错 NotEnoughReplicasException;改用 acks=1 时写入成功,但高水位不前进,停掉的 broker 回到 ISR 后消费者才能读到这条消息(Kafka 4.4 实测,见[附录 A8](#a8-mininsyncreplicas-与-acks))。常用的配置是 3 个副本、min.insync.replicas=2、acks=all,一个副本故障时既不丢消息,也不停止写入。
|
||||
acks=all 要配合 min.insync.replicas(默认为 1)使用,否则 ISR 只剩 Leader 一个时,acks=all 就退化成了 acks=1。min.insync.replicas 设为 2 时,ISR 少于 2 个副本,broker 就拒绝 acks=all 的写入。上面停掉 broker 3 之后 ISR 还有 2 个副本,写入正常;把 min.insync.replicas 改为 3 再写入,生产者重试几次后报错 NotEnoughReplicasException;改用 acks=1 时写入成功,但高水位不前进,停掉的 broker 回到 ISR 后消费者才能读到这条消息(Kafka 4.4 实测,见[附录 A8](#a8-mininsyncreplicas-与-acks))。常用的配置是 3 个副本、min.insync.replicas=2、acks=all,一个副本故障时既不丢消息,也不停止写入。
|
||||
|
||||
生产者没有收到确认就会重试,这可能让同一条消息写入两次。Kafka 3.0 起默认开启幂等生产者(3.0.0、3.1.0 因缺陷未生效):broker 给每个生产者分配一个 ID,生产者给发往每个分区的消息编上序号,broker 发现序号重复就丢弃。后文查看 .log 文件时,每个批次都带有 producerId: 1001 和 baseSequence 字段。幂等只保证单个分区内、生产者自身重试时不重复,应用自己重发的消息不能去重;要让写入多个分区的消息同时成功或失败,需要使用事务,消费者设置 isolation.level=read_committed 后只读取已提交事务中的消息。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user