1.分区策略
什么是分区策略
所谓分区策略是决定生产者将消息发送到哪个分区的算法(可以理解为负载均衡算法)。
在Java中如何自定义分区策略
- 写一个类实现这个接口org.apache.kafka.clients.producer.Partitioner
- 实现partition方法
- 构建Properties的时候添加分区策略
分区策略——轮询策略(默认的)
轮询策略是 Kafka Java 生产者API 默认提供的分区策略。如果你未指定partitioner.class参数,那么你的生产者程序会按照轮询的方式在主题的所有分区间均匀地“码放”消息,如下面这张图所示:
分区策略——随机策略(老版本默认的)
Java实现方式
List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
return ThreadLocalRandom.current().nextInt(partitions.size());
我们随意地将消息放置到任意一个分区上,如下面这张图所示:
分区策略——按消息键保序策略(可以保证消息顺序性)
Kafka 允许为每条消息定义消息键,简称为 Key。这个 Key的作用非常大,它可以是一个有着明确业务含义的字符串,比如客户代码、部门编号或是业务 ID 等;也可以用来表征消息元数据。一旦消息被定义了 Key,那么你就可以保证同一个Key 的所有消息都进入到相同的分区里面,由于每个分区下的消息处理都是有顺序的,故这个策略被称为按消息键保序策略,如下图所示:
Java实现方式:
List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
return Math.abs(key.hashCode()) % partitions.size();
2.消息压缩
Java代码
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 压缩
//压缩算法 前三个是2.1.0版本之前有的,最后一个2.1.0之后才支持:GZIP,Snappy,LZ4,Zstandard(zstd)
props.put("compression.type", "gzip");
Producer<String, String> producer = new KafkaProducer<>(props);
对比
在吞吐量方面:LZ4 > Snappy > zstd 和 GZIP;而在压缩比方面,zstd > LZ4 > GZIP > Snappy。具体到物理资源,使用 Snappy 算法占用的网络带宽最多,zstd 最少,这是合理的,毕竟 zstd 就是要提供超高的压缩比;在 CPU使用率方面,各个算法表现得差不多,只是在压缩时 Snappy 算法使用的 CPU 较多一些,而在解压缩时 GZIP 算法则可能使用更多的 CPU
最佳实践
CPU充足时开启压缩
如果你的环境中带宽资源有限,那么我也建议你开启压缩。事实上我见过的很多 Kafka 生产环境都遭遇过带宽被打满的情况。这年头,带宽可是比CPU 和内存还要珍贵的稀缺资源,毕竟万兆网络还不是普通公司的标配,因此千兆网络中Kafka 集群带宽资源耗尽这件事情就特别容易出现。如果你的客户端机器 CPU 资源有很多富余,我强烈建议你开启 zstd 压缩,这样能极大地节省网络资源消耗
3.保证消息不丢失
一句话概括,Kafka 只对“已提交”的消息(committed message)做有限度的持久化保证,也就是说如果该消息没有已提交则不会被持久化,所谓的已提交指的是Kafka 的若干个 Broker 成功地接收到一条消息并写入到日志文件后,它们会告诉生产者程序这条消息已成功提交
- 生产者在发送消息的时候请使用带回调的重载方法
- 消费端要先消费再更新Consumer Offset
- 设置 acks = all。acks 是 Producer 的一个参数,代表了你对“已提交”消息的定义。如果设置成 all,则表明所有副本 Broker 都要接收到消息,该消息才算是“已提交
- 设置 retries 为一个较大的值。这里的 retries 同样是 Producer 的参数,对应前面提到的 Producer 自动重试。当出现网络的瞬时抖动时,消息发送可能会失败,此时配置了retries > 0 的 Producer 能够自动重试消息发送,避免消息丢失
- 设置 unclean.leader.election.enable = false
- 设置 min.insync.replicas > 1。这依然是 Broker 端参数,控制的是消息至少要被写入到多少个副本才算是“已提交”。设置成大于 1 可以提升消息持久性。在实际环境中千万不要使用默认值 1
- 确保消息消费完成再提交。Consumer 端有个参数 enable.auto.commit,最好把它设置成 false,并采用手动提交位移的方式
4.拦截器
可以在消息发送前或者在消息被消费后进行拦截,Kafka的拦截器分为生产者拦截器和消费者拦截器,生产者拦截器允许你在发送消息前以及消息提交成功后拦截,而消费者拦截器支持在消费消息以前和更新偏移量之后拦截,这两种拦截器都支持链式
Java代码
//生产者 实现org.apache.kafka.clients.producer.ProducerInterceptor这个接口,onSend方法指的是消息发送前,onAcknowledgement指的是消息成功提交或发送失败之后,onAcknowledgement这个拦截的执行要早于发送的callback,这个拦截和Callback不是一个线程如果使用了共享对象要保证线程安全
//消费者实现实现org.apache.kafka.clients.consumer.ConsumerInterceptor接口,onConsume方法是消费前调用,onCommit方法是提交偏移量之前调用
Properties props = new Properties();
List<String> interceptors = new ArrayList<>();
// 拦截器 1
interceptors.add("com.yourcompany.kafkaproject.interceptors.AddTimestampInterceptor");
// 拦截器 2
interceptors.add("com.yourcompany.kafkaproject.interceptors.UpdateCounterInterceptor");
props.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, interceptors);