目录
消费者与消费组
分区分配策略
消费逻辑
参数说明
消费位置
位移提交方式
参考资料
消费组是一个逻辑上的概念,它将旗下的消费者归为一类,每一个消费者只隶属于一个消费组。每一个消费组都会有一个固定的名称,消费者在进行消费前需要指定其所属消费组的名称,这个可以通过消费者客户端参数group.id 来配置,默认值为空宇符串。
消费者(Consumer)负责订阅Kafka中的主题(Topic),并且从订阅的主题上拉取消息。每个消费者都有一个对应的消费组。当消息发布到主题后,只会被投递给订阅它的每个消费组中的一个消费者,假若这个主题有多个分区,那么每一个分区只能被一个消费组中的一个消费者所消费。
分区分配策略可以通过消费者客户端参数partition.assignment.strategy 来设置消费者与订阅主题之间的分区分配策略。常见的分区分配策略有:
RangeAssignor 分配策略(默认):
按照消费者总数和分区总数进行整除运算来获得一个跨度,然后将分区按照跨度进行平均分配, 以保证分区尽可能均匀地分配给所有的消费者。对于每一个主题,该策略会将消费组内所有订阅这个主题的消费者按照名称的字典序排序, 然后为每个消费者划分固定的分区范围,如果不够平均分配,那么字典序靠前的消费者会被多分配一个分区。
假设n =分区数/消费者数量, m = 分区数%消费者数量,那么前m 个消费者每个分配n + l 个
分区,后面的(消费者数量- m )个消费者每个分配n 个分区。
RoundRobinAssignor 分配策略:
是将消费组内所有消费者及消费者订阅的所有主题的分区按照字典序排序,然后通过轮询方式逐个将分区依次分配给每个消费者。如果同一个消费组内所有的消费者的订阅信息都是相同的,那么RoundRobinAssignor 分配策略的分区分配会是均匀的。 举个例子,假设消费组中有2 个消费者c0和c1,都订阅了主题t0和t1,并且每个主题都有3个分区, 那么订阅的所有分区可以标识为: t0p0 、t0p1 、t0p2 、t1p0 、t1p1 、t1p2 。最终的分配结果为:
消费者C0 : t0p0 、t0p2 、 t1p1
消费者C1: t0p1、 t1p0 、t1p2
如果同一个消费组内的消费者订阅的信息是不相同的,那么在执行分区分配的时候就不是完全的轮询分配,有可能导致分区分配得不均匀。举个例子,假设消费组内有3 个消费者C0 、C1 和C2,它们共订阅了3 个主题( t0 、t1 、t2,这3 个主题分别有1 、2 、3 个分区,即整个消费组订阅了t0p0 、t1p0 、t1p1、t2p0 、t2p1、t2p2 这6 个分区。具体而言,消费者C0 订阅的是主题t0 ,消费者C1 订阅的是主题t0 和t1,消费者C2 订阅的是主题t0 、t1和t2 , 那么最终的分配结果为:
消费者C0: t0p0
消费者C1: tlp0
消费者C2: tlp1,t2p0 、t2p1,t2p2
StickyAssignor 分配策略:
从0.11.x 版本开始引入这种分配策略,它主要有两个目的:(1)分区的分配要尽可能均匀;(2)分区的分配尽可能与上次分配的保持相同。当两者发生冲突时,第一个目标优先于第二个目标。
假设消费组内有3个消费者(C0、C1 和C2 ),它们都订阅了4 个主题( t0 、t1 、t2 、t3 ) ,并且每个主题有2 个分区。也就是说,整个消费组订阅了t0p0 、t0p1 、t1p0 、t1p1 、t2p0 、t2p1 、t3p0 、t3p1 这8 个分区。最终的分配结果如下:
消费者C0 : t0p0 、t1p1、t3p0
消费者Cl : t0p1、t2p0、t3p1
消费者C2 : t1p0 、t2p1
假设此时消费者C1脱离了消费组,那么消费组就会执行再均衡操作,进而消费分区会重新分配,
消费者C0: t0p0 、t1p1、t3p0 、t2p0
消费者C2 : t1p0 、t2p1, t0p1, t3p1
可以看到分配结果中保留了上一次分配中对消费者C0和C2 的所有分配结果,并将原来消费者C1的“负担”分配给了剩余的两个消费者C0和C2,最终C0 和C2 的分配还保持了均衡。
订阅信息不同的情况下的处理。
举个例子,同样消费组内有3 个消费者C0 、C1和C2 ), 集群中有3 个主题t0 、t1 和t2),这3 个主题分别有1 、2 、3 个分区。也就是说, 集群中有t0p0 、t1p0 、t1p1 、t2p0 、t2p1 、t2p2 这6 个分区。消费者C0 订阅了主题t0 ,消费者C1订阅了主题t0 和t1 ,消费者C2 订阅了主题t0 、t1 和t2 。
消费者C0: t0p0
消费者C1 : t1p0 、t1p1
消费者C2 : t2p0 、t2p1、t2p2
如前所述,使用StickyAssignor 分配策略的一个优点就是可以使分区重分配具备“黏性”,减少不必要的分区移动(即一个分区剥离之前的消费者,转而分配给另一个新的消费者) 。
自定义分配策略:
该策略必须要实现org.apache.kafka.clients.consumer.intemals.PartitionAssignor接口。
1)配置消费者客户端参数及创建相应的消费者实例;
2)订阅主题;
3)拉取消息并消费;
4)提交消费位移;
5)关闭消费者实例。
1)bootstrap.servers:该参数用来指定生产者客户端连接Kafka集群所需的broker地址清单,具体的内容格式为host1:port1,host2:port2,可以设置一个或多个地址,中间以逗号隔开,此参数的默认值为""。
2)group.id:消费者隶属的消费组的名称,默认值为“”。如果设置为空,则会报出异常。
3)client.id:这个参数用来设定KafkaConsumer 对应的客户端id , 默认值也为""。如果客户端不设置, 则KafkaConsumer会自动生成一个非空字符串,内容形式如"consumer-1""consumer-2",即字符串"consumer-"与数字的拼接。
4)key.deserializer:消息中key所对应的反序列化类,需要实现org.apache.kafka.common.serialization.Deserializer接口,默认值为空。
5)value.deserializer:消息中value所对应的反序列化类,需要实现org.apache.kafka.common.serialization.Deserializer接口,默认值为空。
6)auto.offset.reset:参数值为字符串类型,有效值为"earliest""latest""none",配置为其余值会报出异常。默认值为"latest"。
7)enable.auto.commit:参数为boolean 类型, 配置是否开启自动提交消费位移的功能,默认开启。
8)fetch.min.bytes:该参数用来配置Consumer在一次拉取请求(调用poll()方法)中能从Kafka中拉取的最小数据量,默认值为1(B)。Kafka 在收到Consumer的拉取请求时,如果返回给Consumer的数据量小于这个参数所配置的值,那么它就需要进行等待,直到数据量满足这个参数的配置大小。可以适当调大这个参数的值以提高一定的吞吐量,不过也会造成额外的延迟(latency),对于延迟敏感的应用可能就不可取了。
9)fetch.max.bytes:该参数用来配置Consumer 在一次拉取请求中从Kafka中拉取的最大数据量,默认值为52428800(B),也就是50MB 。如果这个参数设置的值比任何一条写入Kafka中的消息要小,那么会不会造成无法消费呢?该参数设定的不是绝对的最大值,如果在第一个非空分区中拉取的第一条消息大于该值,那么该消息将仍然返回,以确保消费者继续工作。也就是说,上面问题的答案是可以正常消费。
与此相关的,Kafka中所能接收的最大消息的大小通过服务端参数message.max.bytes(对应于主题端参数max.message.bytes)来设置。
10)max.poll.records:这个参数用来配置Consumer在一次拉取请求中拉取的最大消息数,默认值为500(条)。如果消息的大小都比较小,则可以适当调大这个参数值来提升一定的消费速度。
在Kafka中每当消费者查找不到所记录的消费位移时,就会根据消费者客户端参数auto.offset.reset的配置来决定从何处开始进行消费,这个参数的默认值为“ latest ”,表示从分区末尾开始消费消息。auto.offset.reset 参数也只能在找不到消费位移或位移越界的情况下粗粒度地从开头或末尾开始消费。有些时候,我们需要一种更细粒度的掌控,可以让我们从特定的位移处开始拉取消息,而KafkaConsumer 中的seek()方法正好提供了这个功能,让我们得以追前消费或回溯消费,同时也为我们提供了将消费位移保存在外部存储介质中的能力, 还可以配合再均衡监听器来提供更加精准的消费能力。
注意:在执行seek()方法之前需要先执行一次poll()方法,等到分配到分区之后才可以重置消费位置。
一个分区的起始位置起初是0,但并不代表每时每刻都为0 , 因为日志清理的动作会清理旧的数据,所以分区的起始位置会自然而然地增加。
这里的位移指消费者消费到的位置offset。位移提交方式有自动提交和手动提交两种方式。 1)自动提交:消费者客户端参数enable.auto.commit需要为true。消费者每隔auto.commit.interval.ms设定的时长会将拉取到的每个分区中最大的消息位移进行提交。自动位移提交的动作是在poll()方法的逻辑里完成的,在每次真正向服务端发起拉取请求之前会检查是否可以进行位移提交,如果可以,那么就会提交上一次轮询的位移。 该方式存在重复消费和消息丢失的问题。假设刚刚提交完一次消费位移,然后拉取一批消息进行消费,在下一次自动提交消费位移之前,消费者崩溃了,那么又得从上一次位移提交的地方重新开始消费,这样便发生了重复消费的现象; 假设消费存在两个线程,一个线程A不断地拉取消息并存入本地缓存 ,一个线程B从缓存中读取消息并进行相应的逻辑处理, 当线程A超前于线程B且在线程B处发生了异常,待线程B的异常解决后,会从线程A当前的位移处进行消息读取并处理,就发生了消息丢失。
2)手动提交:消费者客户端参数enable.auto.commit需要为false。手动提交可以细分为同步提交和异步提交,对应于KafkaConsumer中的commitSync()和commitAsync()两种类型的方法。
commitSync()方法会根据poll()方法拉取的最新位移来进行提交,只要没有发生不可恢复的错误( Unrecoverable Eηor ),它就会阻塞消费者线程直至位移提交完成。对于不可恢复的错误,比如CommitFailedException 、WakeupException 、
InterruptException 、AuthenticationException 、AuthorizationException 等,我们可以将其捕获并做针对性的处理。commitAsync()在执行的时候消费者线程不会被阻塞, 可能在提交消费位移的结果还未返回之前就开始了新一次的拉取操作。
《深入理解Kafka:核心设计与实践原理》