Kafka 核心技术与实战
客户端实践及原理剖析
11 | 无消息丢失配置怎么实现?
Kafka 只对“已提交”的消息(committed message)做有限度的持久化保证。
第一个核心要素是“已提交的消息”。
当 Kafka 的若干个 Broker 成功地接收到一条消息并写入到日志文件后,它们会告诉生产者程序这条消息已成功提交。此时,这条消息在 Kafka 看来就正式变为“已提交”消息了。
向 Kafka 发送数据并不是真要等数据被写入磁盘才会认为成功,而是只要数据被写入到操作系统的页缓存(Page Cache)上就可以了,随后操作系统根据 LRU 算法会定期将页缓存上的“脏”数据落盘到物理磁盘上。这个定期就是由提交时间来确定的,默认是 5 秒。
为什么是若干个 Broker 呢?这取决于对“已提交”的定义。既可以只有一个 Broker 成功保存该消息就算是已提交,也可以是所有 Broker 都成功保存该消息才算是已提交。不论哪种情况,Kafka 只对已提交的消息做持久化保证这件事情是不变的。
第二个核心要素就是“有限度的持久化保证”,也就是说 Kafka 不可能保证在任何情况下都做到不丢失消息。
假如消息保存在 N 个 Kafka Broker 上,那么这个前提条件就是这 N 个 Broker 中至少有 1 个存活。只要这个条件成立,Kafka 就能保证这条消息永远不会丢失。
“消息丢失”案例
案例 1:生产者程序丢失数据
目前 Kafka Producer 是异步发送消息的,也就是说如果调用的是 producer.send(msg) 这个 API,那么它通常会立即返回,但此时不能认为消息发送已成功完成。
如果用这个方式,可能会有哪些因素导致消息没有发送成功呢?其实原因有很多,例如网络抖动,导致消息压根就没有发送到 Broker 端;或者消息本身不合格导致 Broker 拒绝接收(比如消息太大了,超过了 Broker 的承受能力)等。
解决此问题的方法:Producer 永远要使用带有回调通知的发送 API,也就是说不要使用 producer.send(msg),而要使用 producer.send(msg, callback)。通过 callback(回调)能确认消息是否真的提交成功了。一旦出现消息提交失败的情况,可以有针对性地进行处理。
如果是因为那些瞬时错误,那么仅仅让 Producer 重试就可以了;如果是消息不合格造成的,那么可以调整消息格式后再次发送。总之,处理发送失败的责任在 Producer 端而非 Broker 端。
案例 2:消费者程序丢失数据
Consumer 端丢失数据主要体现在 Consumer 端要消费的消息不见了。Consumer 程序有个“位移”的概念,表示的是这个 Consumer 当前消费到的 Topic 分区的位置。下面这张图来自于官网,它清晰地展示了 Consumer 端的位移数据。
比如对于 Consumer A 而言,它当前的位移值就是 9;Consumer B 的位移值是 11。
这里的“位移”类似于看书时使用的书签,它会标记当前阅读了多少页,下次翻书的时候能直接跳到书签页继续阅读。
正确使用书签有两个步骤:第一步是读书,第二步是更新书签页。如果这两步的顺序颠倒了,就可能出现这样的场景:当前的书签页是第 90 页,先将书签放到第 100 页上,之后开始读书。当阅读到第 95 页时,临时有事中止了阅读。当下次直接跳到书签页阅读时,就丢失了第 96~99 页的内容,即这些消息就丢失了。
Kafka 中 Consumer 端的消息丢失就是这么一回事。要对抗这种消息丢失,办法很简单:维持先消费消息(阅读),再更新位移(书签)的顺序即可。这样就能最大限度地保证消息不丢失。这种处理方式可能带来的问题是消息的重复处理,类似于同一页书被读了很多遍,但这不属于消息丢失的情形。
隐蔽的消息丢失场景:
Consumer 程序从 Kafka 获取到消息后开启了多个线程异步处理消息,而 Consumer 程序自动地向前更新位移。假如其中某个线程运行失败了,它负责的消息没有被成功处理,但位移已经被更新了,因此这条消息对于 Consumer 而言实际上是丢失了。
这个问题的解决方案:如果是多线程异步处理消费消息,Consumer 程序不要开启自动提交位移,而是要应用程序手动提交位移。
最佳实践
- 不要使用 producer.send(msg),而要使用 producer.send(msg, callback)。
- 设置 acks = all。 acks 是 Producer 的一个参数,代表了对“已提交”消息的定义。如果设置成 all,则表明所有同步副本 Broker 都要接收到消息,该消息才算是“已提交”。 这是最高等级的“已提交”定义。
- 设置 retries 为一个较大的值。当出现网络的瞬时抖动时,消息发送可能会失败,此时配置了 retries > 0 的 Producer 能够自动重试消息发送,避免消息丢失。
- 设置 unclean.leader.election.enable = false。 Broker 端的参数,它控制的是哪些 Broker 有资格竞选分区的 Leader。如果一个 Broker 落后原先的 Leader 太多,那么它一旦成为新的 Leader,必然会造成消息的丢失。故一般都要将该参数设置成 false,即不允许这种情况的发生。
- 设置 replication.factor >= 3。 Broker 端的参数,将消息多保存几份,目前防止消息丢失的主要机制就是冗余。
- 设置 min.insync.replicas > 1。 Broker 端参数,控制的是消息至少要被写入到多少个同步副本才算是“已提交”。 设置成大于 1 可以提升消息持久性。在生产环境中千万不要使用默认值 1。
- 确保 replication.factor > min.insync.replicas。 如果两者相等,那么只要有一个副本挂机,整个分区就无法正常工作了。不仅要改善消息的持久性,防止数据丢失,还要保证可用性。推荐设置成 replication.factor = min.insync.replicas + 1。
- 确保消息消费完成再提交。 Consumer 端有个参数 enable.auto.commit,把它设置成 false,并采用手动提交位移的方式。
acks = all 与 min.insync.replicas 的关系
acks = all 是表示所有同步副本(ISR)都接收到消息才算是“已提交”。
但如果同步副本中只有 Leader 副本,那不等于设置 acks=1 吗?
min.insync.replicas 表示允许 acks=all 请求所需存在的最小同步副本数。也就是说,如果同步副本的数量低于配置的最小数量,所有带有 acks=all 的请求都不会被处理并收到错误响应。它充当一种守门员,以确保不会发生上述情况。(详细内容请参考 Kafka Acks Explained)
举例说明:
所有的副本为 0、1、2、3、4,min.insync.replicas = 4。如果 ISR = [0,1,2,3],则 acks = all 有效;如果 ISR = [0,1,2],则所有带有 acks=all 的请求都不会被处理并收到错误响应。