Kafka复习计划 - 客户端实践及原理(分区/压缩/消息丢失)

一. 生产者消息分区机制原理

背景:一般Kafka用来收集应用服务器的日志数据,但是这个数据量又是非常庞大的,基本上每分钟产生的日志大小都能以GB为单位。

问题:如何将庞大的数据量均匀地分配到Kafka的各个Broker上?

回答:靠的是Kafka的分区机制。

上一章Kafka基础知识我们提到了Kafka的三级消息结构:主题-分区-消息。

官网图:
在这里插入图片描述
使用分区的目的:提供负载均衡的能力。实现系统的高伸缩性。

  1. 因为不同的分区可以放到不同的机器上,因此数据的读写操作也是针对分区来进行的。
  2. 自然而然的每个节点的机器都能独立地执行读写请求。
  3. 同时我们也可以通过添加新节点的方式来增加系统的吞吐量。

首先来说下如何自定义分区,在代码编写层面,实现自定义的Partitioner接口,实现partition()方法即可:

package org.apache.kafka.clients.producer;

public interface Partitioner extends Configurable, Closeable {
    int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster);

    void close();

    @Deprecated
    default void onNewBatch(String topic, Cluster cluster, int prevPartition) {
    }
}

自定义完自己的分区实体类后,通过设置partitioner.class参数即可实现自定义的分区。

接下来来说下几种常见的分区策略。

1.1 轮询策略

轮询策略(Round-robin):顺序分配,也是Kafka默认的分区策略的其中一种(为什么说一种,在下文会有所提及)。我们假设某个主题下有3个分区,然后消息分配的过程如图:
在这里插入图片描述

轮询策略拥有最好的负载均衡表现,因为它总能保证消息最大限度地被平均分配到所有分区上。 因此默认情况下它是最合理也是最常用的分区策略。

1.2 随机策略

从字面上理解也就是随机的将消息放置到任意一个分区上,如图:
在这里插入图片描述
实质上操作并不复杂:

  1. 计算出该主题的分区数M
  2. 随机返回一个小于M的正整数。

1.3 按消息保序策略

Kafka允许为每条消息定义消息建Key这样就可以保证同一个Key下的所有消息都进入到相同的分区。并且每个分区下的消息处理又是有序的。因此叫保序策略。如图:
在这里插入图片描述
上面提到,轮询策略作为Kafka默认分区策略的其中一种,这里做出补充说明,Kafka的默认分区策略同时实现了两种策略:

  1. 当指定了Key的时候,默认实现为按消息保序策略。
  2. 否则默认实现为轮询策略。

分区的使用是非常重要的,它是实现负载均衡以及高吞吐量的一个关键,倘若使用不当,就会造成数据倾斜,使某一些分区有性能瓶颈。

二. 关于Kafka压缩那些事

首先有个背景需要了解,Kafka目前有两大类消息格式,分别为版本V1和版本V2我们只需要记住两个版本之间的消息格式是不一样的!后文会提到。

不过,无论那个版本的消息格式,Kafka的消息都分为两层:

  1. 消息。
  2. 消息集合:包含若干条日志项(真正封装消息的地方)。

Kafka底层的消息日志就由一系列的消息集合构成,Kafka对于信息的操作也都是在消息集合这一个层面上进行的。

2.2 消息格式V1和V2版本的区别

区别一:概念名称有所区分。

  • 消息:V1messageV2record
  • 消息都是分批次去读写的。就好像V1以集合message set为单位。V2就对应的record batch
  • V1message set包含多条messageV2record batch可包含多条record

区别二:在原本的V1版本中,每条消息都需要执行CRC校验但是在有些情况下这种机制就会造成CPU的浪费。 例如:

  1. Broker端可能会对消息的时间戳字段进行更新的时候,那么重新计算之后的CRC值也会进行更新。
  2. Broker端执行消息格式转换时(V2V1版本之间的兼容),也会造成CRC值的变化。

那么针对上述情况,再针对每条消息都执行CRC校验就没有必要了,而V2版本中,消息的CRC工作就不再是针对每条消息了,而是针对的某一个消息集合了。即粒度变大了。

CRC(Cyclic Redundancy Check)

是一种根据网络数据包或计算机文件等数据产生简短固定位数校验码的一种信道编码技术,主要用来检测或校验数据传输或者保存后可能出现的错误。


区别三:保存压缩消息的方法有所改变。

  • V1:将多条消息进行压缩,再保存到外层消息的消息体字段中,其实也就是一条条单个压缩
  • V2对整个消息集合进行压缩。

区别四:V2版本无论是否启用压缩功能,都比V1版本要节省磁盘空间,如图:
在这里插入图片描述

2.2 Kafka压缩功能需要注意的点

首先:Kafka中的压缩可能发生在两个地方。

  1. 生产者端:生产者端只需要指定 compression.type = xxx 即可开启压缩功能,例如gzip压缩。
  2. Broker端。

Broker端需要单独拿出来细说,首先明确一点:Broker端在大部分情况下会对生产者端接收到的消息进行原封不动地保存,即不存在解压缩,也就不存在重新压缩的操作了。 但是有以下两种例外情况:

  • 情况一:默认情况下,Broker端也有一个参数配置compression.type=producer意思是和生产者端的压缩算法保持一致。 但倘若Broker端重新配置了该参数,并且和生产者端不一致,此时就会发生解压缩和压缩操作。而这样通常会导致Broker端的CPU使用率飙升。
  • 情况二:Broker端发生了消息格式转换。 也就是V2版本的消息会向上兼容,向V1版本进行格式转换,而这个过程中也会涉及到消息的解压缩和重新压缩。

其次:Kafka中的解压缩可能发生在两个地方。

  1. 消费者端:毫无疑问的,生产者端如果指定了压缩算法,那么消费的地方自然而然要跟着走。
  2. Broker端:这里就和压缩不太一样了,Broker端的解压缩操作在 生产者端开启了压缩功能的情况下 是百分百发生的。

这里依旧是需要将Broker端拿出来细说。每个压缩过的消息集合在Broker端写入的时候,都要发生解压缩操作,目的是为了对消息执行验证。 那么问题来了。

问题一:为什么要对消息执行验证?

回答:

  1. 验证分为很多种,其中就有CRC校验,目的是防止在网络传输的过程中出现数据丢失的情况。消息的完整性校验。
  2. 其次还有消息级别的校验,即消息的合规性校验: 主要是检验offset是否是单调增长的,以及MessageSize是超过了最大值。

问题二:能否把消息校验移到生产者端来做?这样Broker端就可以避免进行解压缩操作。Broker 直接读取校验结果即可。

回答:不可以,理由如下:

  1. 在生产者端做校验的意义不大,以消息的完整性方面来考虑,倘若先校验,而在后续的网络传输过程中发生了数据的丢失,造成了消息的不完整性,那么校验的意义也就自然而然的没有了。

注意:Broker端虽然发生了解压缩操作,但是他不会对保存的数据格式进行修改,解压缩只是为了校验里面的数据,对于数据的保存,生产者端传过来是咋样的,Broker端保存也就是咋样的。

一般情况下,记住这一点:生产者端压缩,Broker端保持不动,消费者端解压缩。

三. 如何配置Kafka无消息丢失

首先,Kafka只会对 已提交 的消息做 有限度的持久化保证。

  1. 已提交:就好比Mysql中事务的一次commit操作。当Kafka中的若干个Broker成功接收到一条消息并写入到日志文件中后,会返回一个Ack,代表此消息已经提交。
  2. 有限度的持久化:Kafka不丢失消息(已经保存下来的)的前提是,NBroker上至少有一个是存活的。

3.1 消息丢失的案例

案例一:生产者程序丢失数据。

这种情况的发生一般在于生产者程序中,对于消息的发送API,使用的是:

producer.send(message);

实际上,生产者端发送消息API还有一个重载方法:

producer.send(message,callback);

可见后者只是比前者多了一个回调函数,但是它的重要性就很大了。

  1. 首先,Kafka发送消息的操作是异步的,通常调用producer.send(message);方法会立刻返回,但是它并不代表此时消息已经发送完成。
  2. 也就是说,该API只负责消息的发送,并不关心消息是否发送成功。因此倘若出现消息丢失,例如:消息超过了Broker规定的阈值,网络抖动等因素。我们也是无法知晓的。

相反,带有回调函数的API它能够准确地告诉你消息是否真的提交成功了。 因此面对这种情况,我们不要去使用带一个参数的send()方法,应该去使用producer.send(message,callback); 这样,即使消息失败,我们也可以做出对应的程序处理了。


案例二:消费者程序丢失数据

消费者端丢失数据主要体现于其需要消费的消息不见了。而这个消费的过程,离不开位移(offset)的概念。它代表着某个Consumer当前消费到的某个主题的某个分区的具体位置。 如图:
在这里插入图片描述

Kafka消费者端消费消息一般有两个动作,而且有先后顺序:

  1. 读取位移值。
  2. 消费完后,更新自己的一个位移值。

那么试想一下,倘若这个过程颠倒过来,先更新,再读取,会发生什么事情:

  1. 倘若原本的offset是10,本次Consumer可能处理的消息需要到offset为20的位置。
  2. 倘若我先更新offset为20,然后进行消息的消费,当程序读到offset为15位置的时候,发生了宕机,
  3. 那么下一次程序读取的时候,将会从offset为20的位置开始消费。
  4. 也就是说期间offset为15-20之间的消息就发生了丢失。

KafkaConsumer端的消息丢失同理。倘若要对抗这种问题,就需要让程序维持先读取再更新的一个操作顺序。 能最大限度地保证消息不丢失。


案例三:开启多线程异步消费消息。

过程如下:

  1. Consumer程序接收消息后,开启多个线程同时处理消息。
  2. 倘若此时的机制是自动更新位移,那么当某个线程运行失败了,那么自然而然的,它所负责的消息就没有被成功地处理。但此时此刻,位移已经被更新了。
  3. 那么这条消息对于Consumer而言就是丢失的。

因此对于多线程处理消息的这种情况:我们应该禁止自动提交位移的机制,而是让应用程序手动提交位移。

3.2 配置建议

  • 不要使用producer.send(message); 使用producer.send(message,callback);避免消息发送失败的时候却不知道结果。
  • 设置ack = all,代表当所有副本Broker都接收到消息后,该消息才算是已提交,它是最高级别的 ”已提交“定义。但与此同时,该级别的吞吐量也是最低的。详细可以参考深入理解Kafka系列(五)–Kafka可靠的数据传递
  • Producer端的参数,retries设置的值可以稍微大一点。目的是:发生网络波动的时候消息可能会发送失败,此时Producer能够自动重试消息的发送,避免消息的丢失。
  • 设置unclean.leader.election.enable = false。目的:避免那些 落后了大量数据的副本 竞选为新的Leader副本,倘若允许这种情况的发生,也会造成数据的丢失。毕竟和外界交互都是交给Leader副本来执行的。
  • Broker端的参数,设置replication.factor >= 3,将消息多保存几份(具体看你的硬件设施的条件),因为副本机制,即冗余,是防止消息丢失的一个重要机制。
  • Broker端的参数,设置min.insync.replicas > 1,这个参数的作用是:控制消息至少要被写入到多少个副本才算是“已提交”。
  • 确保 replication.factor > min.insync.replicas。原因:倘若两者相等,那么只要有一个副本挂掉了,那么整个分区就无法正常工作了,因为此时的消息都是被认定为提交失败的。
  • 确保消息消费完成再提交,相关参数:enable.auto.commit = false,即禁止自动提交位移。对于单Consumer下的多线程处理场景是非常重要的一个参数。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Zong_0915

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值