TT Lab
开始
学习 学习路径 课程

订单重复到达,又消失了一次

Kafka 的保证——分区内顺序与整数表示的位置

在 TT Lab 中继续学习

一句话总结

Kafka 的主题(topic)是按分区切分的、只追加的日志,相同键的事件会按顺序堆积在同一个分区里, 而消费者组的“读到哪里了”,是每个分区一个整数(偏移量)。消费之后并不会被删除——只有保留设置才会删除。 这四句话,是解读后面“两次”和“消失”的钥匙。

为什么需要它

用过队列的人,会把 Kafka 误认为队列。队列中的消息一旦取出就消失了,broker 会为 每条消息记住“谁收到了什么”。设计文档 的“Consumer Position”一节,写下了这种方式的代价——如果一交给消费者就标记为已消费, 消费者在处理过程中崩溃,消息就丢了;如果等待确认(ack), 处理完了却没能发出确认,就会被消费两次;而且 broker 还得为每条消息保存 多个状态。

Kafka 选择了另一条路。把主题切分成全序(totally ordered)的分区, 让每个分区在一个消费者组内只由一个消费者读取,这样消费者的 位置就只是“下一个要读取的偏移量”这一个整数。定期为这个整数做检查点, 就是确认的全部,成本非常低,而副产品是倒回重读——如果代码里 有 bug,可以回到旧的偏移量重新读取。文档写道,这违背了队列的 契约,但对很多消费者来说是必不可少的功能。

工作原理

事件与主题。 按介绍文档所述,事件带有 键、值、时间戳(以及可选的头部),主题就是这些事件堆积的地方。 主题可以同时接纳多个生产者和多个消费者,事件在被消费之后也 不会被删除。要保留多久,由每个主题的保留设置决定(第 4 个模块)。

分区与键。 主题被切分成多个分区(“桶”),分布在多个 broker 上。 新事件会追加到其中某个分区的末尾,而相同键(例如订单号)的事件 会进入同一个分区。生产者配置文档 中的 partitioner.class 条目写明了默认行为——有键时按键的哈希选择分区,没有键时 采用 sticky 方式:在攒满 batch.size 之前,一直追加到同一个 分区。所以不带键发送的少量事件会挤在一个分区里,给了键,则每个订单排成一条线。

Kafka 承诺的顺序只在分区内部。同一个订单的 created → paid → shipped 因为在同一个分区,会按这个顺序被读到,但不同订单之间,哪个分区 先被读取,是没有规定的。这就是需要保证顺序的单位必须成为键的原因。

orders (3 파티션)              컨슈머 그룹 order-svc 의 위치
 P0: order-2 created, order-3 created, order-2 paid   → 오프셋 3
 P1: order-1 created, order-1 paid, order-1 shipped   → 오프셋 3
 P2: order-4 created, order-5 created                 → 오프셋 2

消费者组与偏移量。 组是用 group.id 绑在一起的消费者,组的位置就是 每个分区上提交给 broker 的偏移量。不同的组有不同的位置—— 即使 order-svc 已经读到了末尾,analytics 仍会从头开始重新读取。日志末尾 偏移量与已提交偏移量之间的差,就是延迟(lag),也是运维中最先要看的 数字。kafka-consumer-groups.sh --describe 会按分区显示 CURRENT-OFFSET、 LOG-END-OFFSET 和 LAG。

分区数只能增加。 运维文档 的“Modifying topics”一节列出了增加分区时的三个副作用。数据按 hash(key) % 파티션 수(占位符为分区数)划分,所以数量一变,相同的键就可能进入另一个分区, 已有的数据也不会被重新分配(键的顺序保证可能被破坏); auto.offset.reset=latest 的现有消费者,可能会漏掉在它发现新分区之前 进来的消息;元数据的传播存在延迟。此外,不支持缩减。

在现场相遇的样子

“订单状态看起来是颠倒的”这类反馈,常见的原因是键。要么订单服务没有带键 发送,要么用事件类型而不是订单号作了键,要么在增加分区数之后,同一个订单的 旧事件和新事件被分散在不同的分区里。这三种情况下,broker 是正常的,日志也是 正常的——是对它没有承诺过的东西抱了期待。

延迟曲线突然上升,无非两种情况之一:生产者发送了很多,或者消费者停了。 对组做 describe,如果 CONSUMER-ID 一列是空的,就是后者。 即使消费者挂了,已提交的偏移量仍保留在 broker 上,所以等它重新启动,就会从那个位置 继续读取——那时什么会来两次、什么会消失,就是第 3 个模块的内容。

下一项实验要做什么

往有 3 个分区的主题里放入带键的事件,观察相同的键按顺序堆积在同一个分区里, 读取组的偏移量和延迟,确认两个组互相独立,然后 把分区增加到 6 个,亲自观察键的分布发生变化。