学习极客时间 《Kafka核心技术与实战》笔记-客户端实践及原理剖析09-11
作者 胡夕,Apache Kafka 的一名代码贡献者,目前在社区的 Patch 提交总数位列第 22 位,应该说算是国内比较活跃的贡献者了。
https://time.geekbang.org/column/intro/191?utm_campaign=guanwang&utm_source=baidu-ad&utm_medium=ppzq-pc&utm_content=title&utm_term=baidu-ad-ppzq-title
胡夕老师 赠言: 聪明人也要下死功夫 。
生产者消息分区机制原理剖析
Kafka消息的组织方式是三级结构:主题-分区-消息。
其中分区是Kafka实现负载均衡、高吞吐量的一个核心因素。官网如图展示:
不同的分区能够放置到不同节点的机器上,数据的读写操作也是针对分区这个细粒度进行,这样每个节点的机器都能独立的执行各自分区的读写请求,故通过添加新的节点机器,可增加整体系统的吞吐量,实现系统的高伸缩性、负载均衡等能力。
分区策略
Kafka分区策略如下:
-
** 轮询策略**
按照分区数量进行轮询投放消息,每个分区会被按照顺序投放消息,为Kafka默认 分区策略。如下图所示:
轮序策略是大家最容易想到的负载均衡方法,同时也是负载均衡表现非常优秀的方法。 -
** 随机策略**
随机策略,顾名思义就是在多个分区中通过随机选择的方式将消息投放进去,如下图所示。随机策略是老版本Kafka默认分区策略,从其实际表现上要逊于轮询方式。
如果需要实现随机策略partition 方法,代码如下:
List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
return ThreadLocalRandom.current().nextInt(partitions.size());
- ** 按消息键保序策略**
Kafka允许为每条消息定义消息键key,按照key值可将相同key值的消息投到一起,如下图:
其应用场景往往是要求消息按照顺序执行,同时消息中又有明确的业务含义的字符串,比如客户代码、部门编号或者业务ID等。
要实现这个策略的partition方法,方法如下:
List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
return Math.abs(key.hashCode()) % partitions.size();
刚才提到Kafka是默认分区策略为 轮序策略,但如果指定了Key,那么默认策略即会变为按照消息键保序策略。
- 自定义分区策略
除了以上分区策略外,Kafka还支持我们按照实际业务需求自定义分区策略。在编写生产者程序时,我们可以自定义类,实现 org.apache.kafka.clients.producer接口,实现两个方法:partition()和 close(),通常只需要实现最重要的partition方法即可。
int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster);
topic、key、keyBytes、value、valueBytes:消息数据。cluster :集群信息(当前Kafka集群有多少主题,多少broker等)。
partition方法提供了这么参数信息,可以让你能够充分的利用这些信息对消息进行分区,计算出他要被发送到哪个分区中。实现了partition方法,设置partitioner.class参数为你自己实现类的Full Qualified Name 那么生产者程序就会找你自定义的代码逻辑对消息分区了。
生产者压缩算法
为了减少磁盘占用和IO ,通常情况下我们会把较大的文件进行压缩后进行传输,已获取更好的性能表现。
- 那么Kafka如何压缩消息呢?
要弄清楚这个问题之前要先了解Kafka的消息格式,其主要分为两大类,社区分别称之为V1 版本 和 V2 版本。
不论哪个版本Kafka的消息格式都分为两层:消息集合(message set)和 消息(message)。一个消息集合中包含若干条日志项(record item)。Kafka底层的消息日志由一系列消息集合日志项组成。Kafka通常不会直接操作具体消息,而是在消息集合层面上进行写入操作。
之前V1版本中保存压缩消息的方法是把多余消息进行压缩后保存到外层消息的消息体字段中;而V2版本是将整个消息集合进行压缩,所以V2的压缩效果比V1更好。
- 压缩时机:生产者端和Broker端
生产者端:生产者通过配置compression.type参数即可启用指定类型的压缩算法,如开启GZIP压缩:
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("acks", "all");
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
// 开启GZIP压缩
props.put("compression.type", "gzip");
Producer<String, String> producer = new KafkaProducer<>(props);
Broker端: 一般情况下Broker从Producer端接收到消息后不会进行修改,但如果满足以下两个条件,就会触发Broker端压缩:
1.Broke端和Producer端指定的压缩方法不同
2.Broker端发生了消息格式转换(兼容V1老版本,性能影响较大)
-
解压时机 :Consumer端解压、Borker端
生产者端进行压缩了,消费者端肯定要进行解压才可读取消息。同样,Borker端也会解压,每个压缩过的消息集合在Broker端写入时都要进行解压缩,以便对小消息进行各种验证。
总体来说:Producer端压缩、Broker端保持、Consumer端解压。 -
压缩方法
吞吐量方面:LZ4>Snappy>zstd/GZIP
压缩比方面:zstd>LZ4>GZIP>Snappy
保证消息不丢失配置
-
什么情况下擦能保证消息不丢失?
一句话:Kafka只对“已提交”的消息(committed message)做有限度的持久化保证。 -
为甚么会丢失消息?
生产者消息丢失:
producer.send(msg) 有丢失消息风险,因为其只管发送,调用后立即返回“成功”,仅仅的调用方法成功,消息是否发送到,只有天知道。
避免此类消息丢失方法即 使用带有回调通知的发送API producer.send(msg,callback),callback在消息提交失败的情况下,可做针对性的处理,如消息重发、调整格式从发等。
消费者消息丢失
- 消费者位移操作顺序问题:先更新位移再读取消息.
以读书为例,我们读到第10页时计划再读2页,这时直接先将书签放到第12页,然后再进行读书,在读书的过程中临时有事不读了。下次再按照书签读书时,即10、11页的内容可能就没有读到,即丢失了。
**解决方式:先读取消息(阅读),再更新位移(书签)**
-
多线程消费(自动提交位移)
解决方式:多线程异步处理消费消息,关闭自动提交位移 -
无消息丢失最佳实践
1.使用producer.send(msg,callback)
2.设置acks=all ,acks 是producer 的一个参数,代表“已提交”,设置all,表示所有副本broker都要接收到消息才算“已提交”(最高等级提交定义)
3.设置retries 为一个较大的值,消息重试避免丢失
4.设置unclean.leader.election.enable = false
Broker端参数,如果一个Broker落后leader太多不允许其竞选leader,避免消息丢失
5.设置replication.factor >=3
Broker端参数,消息保存多分,冗余保存防止消息丢失
6.设置min.insync.replicas>1
Broker端参数,控制消息至少写入多个副本才算是“已提交”,设置为大于1可以提升消息持久性,实际环境中不要使用默认值1.
7.确保replication.factor > min.insync.relpicas
如果两者相等,那么一个副本挂了,整个分区无法正常工作了;放置消息丢失不能影响其可用性,推荐设置成 replication.factor = min.insync.replicas + 1 ;- 确保 消息消费完再提交。Consumer 端参数enable.auto.commit = false,采取手动提交位移方式。