1. 分区和副本机制
1.1 生产者分区写入策略
生产者写入消息到topic,Kafka将依据不同的策略将数据分配到不同的分区中
轮询分区策略随机分区策略按key分区分配策略自定义分区策略
1.1.1 轮询策略
默认的策略,也是使用最多的策略,可以最大限度保证所有消息平均分配到一个分区。如果在生产消息时,key为null,则使用轮询算法均衡地分配分区1.1.2 随机策略(不用)
随机策略,每次都随机地将消息分配到每个分区。在较早的版本,默认的分区策略就是随机策略,也是为了将消息均衡地写入到每个分区。但后续轮询策略表现更佳,所以基本上很少会使用随机策略。
1.1.3 按key分配策略
按key分配策略,有可能会出现「数据倾斜」,例如:某个key包含了大量的数据,因为key值一样,所有所有的数据将都分配到一个分区中,造成该分区的消息数量远大于其他的分区。
1.1.4 乱序问题
轮询策略、随机策略都会导致一个问题,生产到Kafka中的数据是乱序存储的。而按key分区可以一定程度上实现数据有序存储——也就是局部有序,但这又可能会导致数据倾斜,所以在实际生产环境中要结合实际情况来做取舍。
总结
在Kafka中生产者是有写入策略,如果topic有多个分区,就会将数据分散在不同的partition中存储当partition数量大于1的时候,数据(消息)会打散分布在不同的partition中如果只有一个分区,消息是有序的
1.1.5 自定义分区策略
实现步骤:
1.创建自定义分区器
public class KeyWithRandomPartitioner implements Partitioner {private Random r;@Overridepublic void configure(Map<String, ?> configs) {r = new Random();}@Overridepublic int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {// cluster.partitionCountForTopic 表示获取指定topic的分区数量return r.nextInt(1000) % cluster.partitionCountForTopic(topic);}@Overridepublic void close() {}}
2.在Kafka生产者配置中,自定使用自定义分区器的类名
props.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, KeyWithRandomPartitioner.class.getName());
1.3 消费者分区分配策略
1.3.1 Range范围分配策略
Range范围分配策略是Kafka默认的分配策略,它可以确保每个消费者消费的分区数量是均衡的。
注意:Rangle范围分配策略是针对每个Topic的。
配置
配置消费者的partition.assignment.strategy为org.apache.kafka.clients.consumer.RangeAssignor。
算法公式
n = 分区数量 / 消费者数量
m = 分区数量 % 消费者数量
前m个消费者消费n+1个
剩余消费者消费n个
1.3.2 RoundRobin轮询策略
RoundRobinAssignor轮询策略是将消费组内所有消费者以及消费者所订阅的所有topic的partition按照字典序排序(topic和分区的hashcode进行排序),然后通过轮询方式逐个将分区以此分配给每个消费者。
配置
配置消费者的partition.assignment.strategy为org.apache.kafka.clients.consumer.RoundRobinAssignor。
1.3.3 Stricky粘性分配策略
从Kafka 0.11.x开始,引入此类分配策略。主要目的:
分区分配尽可能均匀在发生rebalance的时候,分区的分配尽可能与上一次分配保持相同
没有发生rebalance时,Striky粘性分配策略和RoundRobin分配策略类似。
上面如果consumer2崩溃了,此时需要进行rebalance。如果是Range分配和轮询分配都会重新进行分配,例如:
通过上图,我们发现,consumer0和consumer1原来消费的分区大多发生了改变。接下来我们再来看下粘性分配策略。
我们发现,Striky粘性分配策略,保留rebalance之前的分配结果。这样,只是将原先consumer2负责的两个分区再均匀分配给consumer0、consumer1。这样可以明显减少系统资源的浪费,例如:之前consumer0、consumer1之前正在消费某几个分区,但由于rebalance发生,导致consumer0、consumer1需要重新消费之前正在处理的分区,导致不必要的系统开销。(例如:某个事务正在进行就必须要取消了)
总结:
分区分配策略:保障每个消费者尽量能够均衡地消费分区的数据,不能出现某个消费者消费分区的数量特别多,某个消费者消费的分区特别少
Range分配策略(范围分配策略):Kafka默认的分配策略 n:分区的数量 / 消费者数量m:分区的数量 % 消费者数量前m个消费者消费n+1个分区剩余的消费者消费n个分区 RoundRobin分配策略(轮询分配策略) 消费者挨个分配消费的分区 Striky粘性分配策略 在没有发生rebalance跟轮询分配策略是一致的发生了rebalance,轮询分配策略,重新走一遍轮询分配的过程。而粘性会保证跟上一次的尽量一致,只是将新的需要分配的分区,均匀的分配到现有可用的消费者中即可减少上下文的切换
1.4 副本机制
副本的目的就是冗余备份,当某个Broker上的分区数据丢失时,依然可以保障数据可用。因为在其他的Broker上的副本是可用的。
1.4.1 producer的ACKs参数
对副本关系较大的就是,producer配置的acks参数了,acks参数表示当生产者生产消息的时候,写入到副本的要求严格程度。当producer不断地往Kafka中写入数据,写入数据会有一个返回结果,表示是否写入成功。它决定了生产者如何在性能和可靠性之间做取舍。
配置:
props.put("acks", "all");
acks = 0:生产者只管写入,不管是否写入成功,可能会数据丢失。性能是最好的acks = 1:生产者会等到leader分区写入成功后,返回成功,接着发送下一条acks = -1/all:确保消息写入到leader分区、还确保消息写入到对应副本都成功后,接着发送下一条,性能是最差的
根据业务情况来选择ack机制,是要求性能最高,一部分数据丢失影响不大,可以选择0/1。如果要求数据一定不能丢失,就得配置为-1/all。
分区中是有leader和follower的概念,为了确保消费者消费的数据是一致的,只能从分区leader去读写消息,follower做的事情就是同步数据,Backup,即follower不提供读写服务。
那这样一来是否就不具有分布式性质了?
还是具有的。leader不是broker集群的leader,而是相对于一个分区而言的,即一个分区只有一个leader。不同的分区leader在不同的broker上这就实现了分布式。
关于副本是否提供服务,详情请看/weixin_46055693/article/details/124824889?spm=1001..3001.5501
1.5 高级API(High-Level API)、低级API(Low-Level API)
1.5.1 高级API
高级API的优点
不需要执行去管理offset,直接通过ZK管理;也不需要管理分区、副本,由Kafka统一管理消费者会自动根据上一次在ZK中保存的offset去接着获取数据在ZK中,不同的消费者组(group)同一个topic记录不同的offset,这样不同程序读取同一个topic,不会受offset的影响
高级API的缺点
不能控制offset,例如:想从指定的位置读取不能细化控制分区、副本、ZK等
1.5.2 低级API
通过使用低级API,我们可以自己来控制offset,想从哪儿读,就可以从哪儿读。而且,可以自己控制连接分区,对分区自定义负载均衡。而且,之前offset是自动保存在ZK中,使用低级API,我们可以将offset不一定要使用ZK存储,我们可以自己来存储offset。例如:存储在文件、MySQL、或者内存中。但是低级API,比较复杂,需要执行控制offset,连接到哪个分区,并找到分区的leader。
1.5.3 手动消费分区数据
之前的代码,我们让Kafka根据消费组中的消费者动态地为topic分配要消费的分区。但在某些时候,我们需要指定要消费的分区,例如:
如果某个程序将某个指定分区的数据保存到外部存储中,例如:Redis、MySQL,那么保存数据的时候,只需要消费该指定的分区数据即可如果某个程序是高可用的,在程序出现故障时将自动重启(例如:后面我们将学习的Flink、Spark程序)。这种情况下,程序将从指定的分区重新开始消费数据。
如何进行手动消费分区中的数据呢?
不再使用之前的 subscribe 方法订阅主题,而使用 「assign」方法指定想要消费的消息
String topic = "test";TopicPartition partition0 = new TopicPartition(topic, 0);TopicPartition partition1 = new TopicPartition(topic, 1);consumer.assign(Arrays.asList(partition0, partition1));
一旦指定了分区,就可以就像前面的示例一样,在循环中调用「poll」方法消费消息
注意
当手动管理消费分区时,即使GroupID是一样的,Kafka的组协调器都将不再起作用如果消费者失败,也将不再自动进行分区重新分配