在使用Apache Kafka生产和消费消息的时候,肯定是希望能够将数据均匀地分配到所有服务器上。比如很多公司使用Kafka收集应用服务器的日志数据,这种数据都是很多的,特别是对于那种大批量机器组成的集群环境,每分钟产生的日志量都能以GB数,因此如何将这么大的数据量均匀地分配到Kafka的各个Broker上,就成为一个非常重要的问题。
分区
消息组织方式:主题 - 分区- 消息
主题下的每条消息只会保存在某一个分区中,而不会在多个分区中被保存多份。
分区是为了提高负载均衡的能力,为了实现系统的高伸缩性。(也可以实现业务级别的消息顺序)
不同的分区能够被放置到不同节点的机器上,而数据的读写操作也都是针对分区这个粒度而进行的,这样每个节点的机器都能独立地执行各自分区的读写请求处理。并且,我们还可以通过添加新的节点机器来增加整体系统的吞吐量。
分区应该是实现 org.apache.kafka.clients.producer.Partitioner 接口
分区策略
决定生产者将消息发送到哪个分区的算法,kafka提供了默认的分区策略,也可以自定义。
Kafka默认分区策略实际上同时实现了两种策略:如果指定了Key,那么默认实现按消息键保序策略;如果没有指定Key,则使用轮询策略。
1、轮询策略
即顺序分配,依次放在分区中,是默认的分区策略。
轮询策略有非常优秀的负载均衡表现,它总是能保证消息最大限度地被平均分配到所有分区上,故默认情况下它是最合理的分区策略,也是我们最常用的分区策略之一。
2、随机策略
是随意地将消息放置到任意一个分区上。
不如轮询策略好,需要实现 partition方法:先计算出该主题总的分区数,然后随机地返回一个小于它的正整数。
List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
return ThreadLocalRandom.current().nextInt(partitions.size());
3、 Key-ordering策略
Kafka允许为每条消息定义消息键,简称为Key。这个Key的作用非常大,它可以是一个有着明确业务含义的字符串,比如客户代码、部门编号或是业务ID等;也可以用来表征消息元数据。特别是在Kafka不支持时间戳的年代,在一些场景中,工程师们都是直接将消息创建时间封装进Key里面的。一旦消息被定义了Key,那么你就可以保证同一个Key的所有消息都进入到相同的分区里面,由于每个分区下的消息处理都是有顺序的,故这个策略被称为按消息键保序策略,如下图所示。
List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
return Math.abs(key.hashCode()) % partitions.size();
如何实现消息的顺序问题?
kafka同一个topic是无法保证数据的顺序性的,但是同一个partition中的数据是有顺序的。
可以修改为根据某些特定的key、地理ip、汽车key自定义分区策略
List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
return partitions.stream().filter(p -> isSouth(p.leader().host())).map(PartitionInfo::partition).findAny().get();
如何通过水平扩展甚至是线性扩展来进一步提升吞吐量呢? Kafka就是使用了分区(partition),通过将topic的消息打散到多个分区并分布保存在不同的broker上实现了消息处理(不管是producer还是consumer)的高吞吐量。
其他知识点:
-
Java客户端默认的生产者分区策略的实现类为org.apache.kafka.clients.producer.internals.DefaultPartitioner。
-
默认策略为:如果指定了partition就直接发送到该分区;如果没有指定分区但是指定了key,就按照key的hash值选择分区;如果partition和key都没有指定就使用轮询策略。而且如果key不为null,那么计算得到的分区号会是所有分区中的任意一个;如果key为null并且有可用分区时,那么计算得到的分区号仅为可用分区中的任意一个
-
消息重试只是简单地将消息重新发送到之前的分区
-
我们公司之前也有一个业务是单分区,要保证全局顺序。后来发现其实使用key+多分区也可以实现。反正保证同一批因果依赖的消息分到一个分区就可以
-
consumer.assign()直接消息指定分区
-
总结:首先判断ProducerRecord中的partition字段是否有值,即是否在创建消息记录的时候直接指定了分区;如果指定了分区,则直接将该消息发送到指定的分区,否则调用分区器的partition方法,执行分区策略;如果用户配置了自己写的分区器,且在生产者配置是指定了,则使用用户指定的分区器,否则使用默认的分区器,即DefaultPartitioner;如果指定了key,则使用该key进行hash操作,并转为正数,然后对topic对应的分区数量进行取模操作并返回一个分区;如果没有指定key,则通过先产生随机数,之后在该数上自增的方式产生一个数,并转为正数之后进行取余操作。
1. 防止乱序可以通过设置max.in.flight.requests.per.connection=1来保证
2. 两个生产者生产的消息无法保证顺序,因为它们本身就没有前后之分,它们是并发的关系。
生产者压缩算法
Kafka的消息层次都分为两层:消息集合和消息。
一个消息集合中包含若干条日志项(record item),而日志项才是真正封装消息的地方。Kafka底层的消息日志由一系列消息集合日志项组成。Kafka通常不会直接操作具体的一条条消息,它总是在消息集合这个层面上进行写入操作。
这样消息层次有两层:外层是消息批次(或消息集合);里层是消息(或日志项)。
V2版本是Kafka 0.11.0.0中正式引入的,V2版本对V1版本进行了修正。
1、就是把消息的公共部分抽取出来放到外层消息集合里面,这样就不用每条消息都保存这些信息了。比如,在V2版本中,消息的CRC校验工作就被移到了消息集合这一层。
2、保存压缩消息的方法不同: V1版本是把多条消息进行压缩然后保存到外层消息的消息体字段中;V2版本是对整个消息集合进行压缩。
压缩
在Kafka中,压缩可能发生在两个地方:生产者端和Broker端。
compression.type = gzip
在生产者端启用压缩是很自然的想法,那为什么我说在Broker端也可能进行压缩呢?其实大部分情况下Broker从Producer端接收到消息后仅仅是原封不动地保存而不会对其进行任何修改,但这里的“大部分情况”也是要满足一定条件的。有两种例外情况就可能让Broker重新压缩消息。
情况一:Broker端指定了和Producer端不同的压缩算法。
情况二:Broker端发生了消息格式转换。 为了兼容老版本的格式,Broker端会对新版本消息执行向老版本格式的转换。这个过程中会涉及消息的解压缩和重新压缩。
Consumer怎么知道这些消息是用何种压缩算法压缩的呢? Kafka会将启用了哪种压缩算法封装进消息集合中,这样当Consumer读取到消息集合时,它自然就知道了这些消息使用的是哪种压缩算法。
总结:
Producer端压缩、Broker端保持、Consumer端解压缩。
但针对于broker的操作,在两种特殊情况下需要重新压缩(1.两端是不同的压缩算法;2.兼容新老版本进行消息格式转换,即需要解压再压缩)
在Kafka 2.1.0版本之前,Kafka支持3种压缩算法:GZIP、Snappy和LZ4
从2.1.0开始,Kafka正式支持Zstandard算法(简写为zstd)。它是Facebook开源的一个压缩算法,能够提供超高的压缩比(compression ratio)。
一个压缩算法的优劣,有两个重要的指标:一个指标是压缩比,原先占100份空间的东西经压缩之后变成了占20份空间,那么压缩比就是5,显然压缩比越高越好;另一个指标就是压缩/解压缩吞吐量,比如每秒能压缩或解压缩多少MB的数据。同样地,吞吐量也是越高越好。
对于Kafka而言,它们的性能测试结果却出奇得一致,即在吞吐量方面:LZ4 > Snappy > zstd和GZIP;而在压缩比方面,zstd > LZ4 > GZIP > Snappy。具体到物理资源,使用Snappy算法占用的网络带宽最多,zstd最少,这是合理的,毕竟zstd就是要提供超高的压缩比;在CPU使用率方面,各个算法表现得差不多,只是在压缩时Snappy算法使用的CPU较多一些,而在解压缩时GZIP算法则可能使用更多的CPU。
解压缩:
除了在Consumer端解压缩,Broker端也会进行解压缩。
注意了,这和前面提到消息格式转换时发生的解压缩是不同的场景。每个压缩过的消息集合在Broker端写入时都要发生解压缩操作,目的就是为了对消息执行各种验证。
我们必须承认这种解压缩对Broker端性能是有一定影响的,特别是对CPU的使用率而言。
其他解答:
-
前面我们提到了Broker要对压缩消息集合执行解压缩操作,然后逐条对消息进行校验,有人提出了一个方案:把这种消息校验移到Producer端来做,Broker直接读取校验结果即可,这样就可以避免在Broker端执行解压缩操作。你认同这种方案吗?
-
刚刚看到4天前京东提的那个jira已经修复了,看来规避了broker端为执行校验而做的解压缩操作,代码也merge进了2.4版本。有兴趣的同学可以看一下:https://issues.apache.org/jira/browse/KAFKA-8106
-
不认同,因为网络传输也会造成丢失,但是我建议可以在消息里面使用一种消耗较小的签名方式sign,比如多使用位移等方式,broke端也这么操纵,如果签名不一致证明有数据丢失,同时签名的方式可以避免CPU大量消耗
-
总结:
怎么压缩:
1、新版本改进将每个消息公共部分取出放在外层消息集合,例如消息的 CRC 值
2、新老版本的保存压缩消息的方法变化,新版本是对整个消息集合进行压缩
何时压缩:
1、正常情况下都是producer压缩,节省带宽,磁盘存储
2、例外情况 a、broker端和producer端使用的压缩方法不同 b、broker与client交互,消息版本不同
何时解压缩:
1、consumer端解压缩
2、broker端解压缩,用来对消息执行验证
优化:选择适合自己的压缩算法,是更看重吞吐量还是压缩率。其次尽量server和client保持一致,这样不会损失kafka的zero copy优势
如果多条消息组成消息集合发送,那是什么条件控制消息发送,如果是一条又是什么条件控制触发发送的
主要是这两个参数:batch.size和linger.ms。如果是生产了一条消息且linger.ms=0,通常producer就会立即发送者一条消息了。
把Kafka官网通读几遍然后再实现一个实时日志收集系统(比如把服务器日志实时放入Kafka)
Kafka 用户日志上报实时统计之分析与设计