Case:前端采集的数据通过Kafka Topic回传至后端分析程序,但是单个分析线程(Consumer)很有可能忙不过来。
既然单个消费者忙不过来,那就多来几个消费者。此时,这些消费者就构成了一个小团队。
Topic中的消息在被分割、摊派后,每个成员(消费者)就自顾自地忙自己那部分。
在Kafka中,这个团队称为”Consumer Group”。
至于同一个Topic中的Message如何摊派呢?
一个Partition给一个消费者。 一对一的形式,易于Partition offset的管理。
当然,Consumer可以同时处理多个Partition。
But there is no point in adding more consumers than the partitions in a topic. : )
Another Case:前端采集的数据,可能不仅仅传给分析系统A,还可能传给B,传给C...
在这种场景中,每套系统都要有一份完整的数据,而不是割分出的一个子集。
当然,单套系统内部可以有多个消费者协同工作。
此时,分析程序A就对应一个Consumer Group,而分析程序B对应另一个Consumer Group...
在实现上,Kafka不需要将原始消息流拷贝一份,每个消费者只需要记住自己当前的offset即可。
Case 1: consumer crash Or new consumer becomes online
Case 2: new partitions are added to the topic
无论是消费者个数有变动,还是分区的个数有变动,消费者-分区之间的平衡都已被打破。
此时,分区需要在消费者之间重新分配,这一过程就叫做Partition Reblance。
对应每个consumer group,会有一个broker(Kafka服务器)作为协调者(group coordinator),负责Partition分配等事务。
每次reblance时触发下述流程:
当一个consumer被关闭时,consumer会立即通知group coordinator,主动触发rebalance操作。
此外,由于crash、网络等原因,consumer可能会静悄悄地挂了。
为此,Group Coordinator通过消费者定时发送的心跳包来监测消费者的存活情况。
当等待超过指定时间时,Group Coordinator将会将其标注为down,并触发reblance。
通过上述reblance的流程可见,reblance过程中整个Group的工作都将stall(毕竟不知道要干啥)
设置项 | 含义 |
fetch.min.bytes | 限定消费者单次拉取的最小数据 (降低频率),减少网络、Broker、消费者三方的压力 |
fetch.max.wait.ms | 避免fetch.min.bytes造成的长时间等待,限定等待时间(默认500ms) |
max.partition.fetch.bytes | 消费者从单个partition拉取的最大数据量 下界:大于单条消息 上界:避免消息处理时间过长。consumer需要及时调用poll(),更新心跳等 |
session.timeout.ms | 多久没发送心跳,可被视为down |
auto.offset.reset | 当消费者缺乏有效的offset时,从何处开始读取。 选项:latest or earlist |
enable.auto.commit | true: 消费者自动提交offset false: 人为控制offset提交时机 |
partition.assignment.strategy | Topic 1: P0, P1, P2 Topic 2: P0, P1, P2 Group: C1, C2
Range分配策略: C1: T1P0, T1P1 -> T2P0, T2P1 C2: T1P2 -> T2P2
RoundRobin分配策略: C1: T1P0 -> T1P2 -> T2P1 C2: T1P1 -> T2P0 -> T2P2 |
client.id | 任意字符串,用于区分Kafka Client |
max.poll.records | 单次poll()操作能返回的最多记录个数 |
consumer通过offset来追踪分区当前的处理进度。
如果consumer永远不挂(或者Partition没有变动),其实就是没有触发Reblance操作,
offset在consumer内部自行维护即可,我自己知道就行。
但实际情况就是,reblance总是有可能发生的。
为了能让其他consumer能够接手,就需要把该分区的进度(offset)存在一个大家都能访问的地方。
一种比较简单的方式就是,往__consumer_offsets topic这个特殊Topic上,发送offset消息。
当发生reblance时,其他consumer只需要根据保存的offset开始处理即可。
但是,这里涉及两种边界情况: 重复处理和漏处理。
Case 1: 重复处理
如果我们处理了一部分消息,但还没来得及提交这部分的offset。
那么下一个consumer在接手时,就会从已经处理过的部分开始处理,使得数据被处理两次。
无论我们提交的多么频繁,总是有几率发生“重复处理”的情形,只是说重复处理所涉及的消息个数可能更少。
Case 2: 漏处理
上面的case,不是因为不及时的commit造成“重复处理”嘛?
那么我们在处理完之前就提前commit呢?
这种方法虽然避免了“重复处理”,但是可能造成一部分消息"压根没处理"。
如果enable.auto.commit为true,那么Kafka客户端每隔5秒就会提交一次offset,把上一次Poll()拉取的消息都给committed掉。
commit的逻辑,被放在poll()函数内部。每次poll()调用,都会检查距离上次提交是否过去了5s。
老生常谈的一点,就是如果消息处理逻辑和poll()放在一个循环时,就需要保证消息处理逻辑不能消耗太多时间。
当auto.commit.offset为false时,调用commitSync()函数将会提交上次poll()拉取的消息(提交成功返回,否则抛出异常)
同步提交的缺点就是,系统的处理速度将很大程度上受RTT影响。
优点就是,同步API会自动进行retry,直到认为无法补救了才抛异常。
异步提交,在提交完commit请求之后就返回,不会阻塞业务逻辑。
缺点就是,异步API不会进行retry。其背后的设计逻辑认为,后续的提交可以弥补单个commit的缺失。