本章纯属理论篇,其中涉及到面试常问的理论知识点。
Broker知识点
Zookeeper 存储的 Kafka 信息目录结构
- /{根目录}/admin/delete_topics:记录被删除的主题。
- /{根目录}/brokers/ids:记录kafka节点,集群多个节点用 , 隔开。
- /{根目录}/brokers/topics:记录有哪些主题。
- /{根目录}/brokers/topics/{主题}/partitions/{分区}/state:记录当前分区的Leader和ISR。
- /{根目录}/cluster/id:记录当前kafka的集群ID,该ID和kafka
log.dirs
目录下的meta.properties文件记录的cluster.id必须保持一致。否则启动会报 The Cluster ID Vdg7_xx_TlmDDU32F0DAewa doesn’t match stored clusterId Some(Vdg7_xx_TlmDDU32F0DAew) in meta.properties. The broker is trying to join the wrong cluster. Configured zookeeper.connect may be wrong. 错误。 - /{根目录}/consumers:0.9版本之前用于保存消费者offset信息,因为kafka需要经常连接zookeeper同步offset信息,极大影响性能,0.9版本之后offset存储在kafka主题中。
- /{根目录}/controller:记录辅助选举的节点。kafka服务没启动时,该节点不存在,kafka集群启动后,由最先启动的节点创建该路径,并由该节点辅助选举。
zookeeper存储kafka信息默认根路径为 /,根路径配置项为zookeeper.connect
,我的配置为:zookeeper.connect=localhost:2181,localhost:2182,localhost:2183/kafka,所以根路径为/kafka。kafka相关信息都存储在了配置的根路径下。
Kafka与Zookeeper协同工作流程
客户端连接kafka集群流程
consumer或者producer客户端连接kafka集群,然后通过kafka集群到zookeeper集群寻找 /{根目录}/topics/{topic}/partitions/{partition}/state 的leader节点id,如下:
// /kafka/brokers/topics/topic_1/partitions/0/state
{
"controller_epoch" : 56,
"leader" : 2,
"version" : 1,
"leader_epoch" : 30,
"isr" : [ 2, 0 ]
}
在通过leader.id到/{根目录}/brokers/ids/{leader.id}获取endpoints的链接地址(即发布到ZK中的advertised.listeners配置),告诉kafka客户端要通过什么协议访问指定主机名和端口开放的Kafka服务。
// /kafka/brokers/ids/2
{
"listener_security_protocol_map" : {
"PLAINTEXT" : "PLAINTEXT"
},
"endpoints" : [ "PLAINTEXT://10.211.55.5:9094" ],
"jmx_port" : -1,
"features" : { },
"host" : "10.211.55.5",
"timestamp" : "1656759314778",
"port" : 9094,
"version" : 5
}
Kafka故障处理机制
假设Leader有10条数据,ISR中的副本Follower1同步了9条数据,副本Follower2同步了8条数据,当Leader挂了之后,如果选举Follower1为新的Leader,则从消费者角度来看,能消费到9条数据,如果选举Follower2为新的Leader,则从消费者角度来看,能消费到8条数据。从消费者角度来说能消费能消费多少数据完全取决于选举了哪个Follower为Leader,这就会造成了消费端消费数据的一致性问题。
其实以上都是个人假设,其实对于消费者来说,能消费到的数据是HW标志的数据(木桶效应)。这就解决了不管选举哪个Follower为新的Leader,对于消费端来说能消费到的数据都是一样的。
LEO(Log End Offset):日志末端位移,代表日志文件中下一条待写入消息的offset,这个offset上实际是没有消息的。不管是leader副本还是follower副本,都有这个值。当leader副本收到生产者的一条消息,LEO通常会自增1,而follower副本需要从leader副本fetch到数据后,才会增加它的LEO。
HW(high watermark):值所有副本中最小的LEO ,即水桶中最短的那块木板。
Follower故障处理
- Follower发生故障后会被临时踢出ISR。
- 这个期间Leader和其他Follower继续接收数据。
- 待故障Follower恢复后,该Follower会读取本地磁盘记录的上次的HW(故障前的HW),并将log文件高于该HW的部分截取掉(一般指的是故障前高出HW部分),从HW开始向Leader进行同步。
- 等待故障Follower的LEO大于等于该分区的HW,即Follower追上Leader之后,就可以重新加入ISR了。
Leader故障处理
- Leader发生故障之后,会从ISR中选出一个新的Leader。
- 为保证多个副本之间的数据一致性,其余的Follower会先将各自的log文件高于HW的部分截掉,然后从新的Leader同步数据。
- 待旧的Leader恢复后,会成为新Leader的Follower,并将高出HW的数据截取掉。
注意:这只能保证副本之间的数据一致性,并不能保证数据不丢失或者不重复。
Leader Partition 负载平衡
正常情况下,Kafka本身会自动把Leader Partition均匀分散在各个机器上,来保证每台机器的读写吞吐量都是均匀的。但是如果某些broker宕机,会导致Leader Partition过于集中在其他少部分几台broker上,这会导致少数几台broker的读写请求压力过高,其他宕机的broker重启之后都是follower partition,读写请求很低,造成集群负载不均衡。
自动平衡配置
- auto.leader.rebalance.enable:开启自动平衡开关,默认是true。
- leader.imbalance.per.broker.percentage:默认是10%。每个broker允许的不平衡的leader的比率。如果每个broker超过了这个值,控制器会触发leader的自动平衡。
- leader.imbalance.check.interval.seconds:默认值300秒。检查leader负载是否平衡的间隔时间。
不平衡率计算方法
公式:不平衡率 = 不平衡数 / 分区数。
平衡的情况下,leader和副本的第一个节点是一致的。
当leader发生过宕机后,kafka投票重新选举新的leader,这种情况下就会导致leader和第一个副本节点不一致,则不平衡数+1。根据公式,平衡率1/3 = 33%,大于默认的平衡率10%,会自动触发自动平衡。
Kafka数据存储机制
Topic是逻辑上的概念,而partition是物理上的概念,每个partition对应于一个log文件,该log文件中存储的就是Producer生产的数据。Producer生产的数据会被不断追加到该log文件末端,为防止log文件过大导致数据定位效率低下,Kafka采取了分片和索引机制,将每个partition分为多个segment。每个segment包括:.index文件、.log文件和 .timeindex 等文件。这些文件位于一个文件夹下,该文件夹的命名规则为:topic名称+ “-” +分区序号,例如:test_topic-0。
数据文件和索引文件
每个segment包含 .log文件、.index文件 和 .timeindex文件,以当前 segment 的第一条消息的 offset 命名,数值最大为64位long大小,20位数字字符长度,长度不够用0填充。
- .log文件:存储大量的数据,索引文件中的元数据指向对应数据文件中 message 的物理偏移地址。
- .index文件:用来建立消息偏移量(offset)到物理地址之间的映射关系,方便快速定位消息所在的物理文件位置。
- .timeindex文件:则根据指定的时间戳(timestamp)来查找对应的偏移量信息。
文件数据格式
.index索引文件数据格式如下:
字段名 | 描述 |
---|---|
offset | 此批消息相对当前segment偏移量。绝对偏移量 = .index 文件名 + 相对偏移量。比如相对偏移量为159,当前 .index文件完整名称为 00000000000000000522.index,则绝对偏移量为:522 + 159 = 681。 |
position | 数据的物理地址。与 .log文件的position匹配索引到数据文件的对应记录。 |
.timeindex索引文件数据格式如下:
字段名 | 描述 |
---|---|
timestamp | 此批消息的发送时间戳。与 .log文件的CreateTime匹配索引到数据文件的对应记录。 |
offset | 此批消息相对当前segment偏移量。绝对偏移量 = .index 文件名 + 相对偏移量。此批消息索引最后偏移量。 |
.log数据文件数据格式如下:
字段名 | 描述 |
---|---|
baseOffset | 此批消息索引开始偏移量(该分区数据的绝对偏移量)。 |
lastOffset | 此批消息索引最后偏移量(该分区数据的绝对偏移量)。 |
count | 此批消息总数,(lastOffset - baseOffset) + 1。 |
producerId | PID。 |
isTransactional | 是否开启事务。 |
position | 数据的物理地址,用于 .index 文件的position索引到该条数据。 |
CreateTime | 此批消息的发送时间戳,用于 .timeindex文件的timestamp索引到该条数据。 |
size | 此批消息数据大小。 |
magic | 魔数字段,可以用于拓展存储一些信息,当前3.0版本的magic是2。 |
compresscodec | 数据压缩类型。可选值有:none、gzip、snappy、lz4或zstd |
crc | crc校验码。 |
稀疏索引
Kafka 中的 .index索引文件,以 稀疏索引(sparse index) 的方式构造消息的索引,它并不保证每个消息在索引文件中都有对应的索引项。每当写入一定量(由 broker 端参数 log.index.interval.bytes
指定,默认值为 4096,即 4KB)的消息时,偏移量索引文件 和 时间戳索引文件 分别增加一个偏移量索引项和时间戳索引项,增大或减小 log.index.interval.bytes
的值,对应地可以缩小或增加索引项的密度。
偏移量索引文件中的偏移量是单调递增的,查询指定偏移量时,使用二分查找法来快速定位偏移量的位置,如果指定的偏移量不在索引文件中,则会返回小于指定偏移量的最大偏移量。
时间戳索引文件中的时间戳也保持严格的单调递增,查询指定时间戳时,也根据二分查找法来查找不大于该时间戳的最大偏移量,至于要找到对应的物理文件位置还需要根据偏移量索引文件来进行再次定位。
稠密索引
稠密索引,一个索引对应着数据文件一条message;稀疏索引,一个索引对应着数据文件多条message。kafka默认采用稀疏存储的方式,它不会为每一条message都建立索引,而是每隔4k左右,建立一条索引,避免索引文件占用过多的空间。缺点是没有建立索引的offset不能一次定位到message的位置,需要做一次顺序扫描,但是扫描的范围很小。
索引数据步骤
如何找到offset为600在 .log文件中的数据?(绝对offset列在 .index 真实不存在的,为了讲解需要)
- 定位segment文件。offset为600介于[522, 1004]之间,所以定位到Segement-1文件。
- 定位 .index 索引文件记录(.timeindex 文件同理)。offset为600介于绝对偏移量[587, 639]之间,所以往上一条记录取绝对偏移量为587这条记录。(因为这条记录的数据的绝对偏移量范围为[587, 638],而600介于这个区间。)
- 定位 .log 记录。上一步骤找到记录的position与 .log 文件的值为6410的position匹配到图中第五条记录。
- 遍历查找数据。从上一步定位到的 .log 记录向下遍历找到offset为600介于[baseOffset, lastOffset]之间的记录。此处找到的是图中第六条数据。该条记录对应的Record数据即为offset为600的数据。
文件清理策略
Kafka 中提供的日志清理策略有 delete(删除) 和 compact (压缩)两种,Kafka中的数据超过了指定存留时间和文件大小 称为过期数据,过期数据将采用配置的清理策略。
存留时间和文件大小配置如下:
- log.cleanup.policy:文件清理策略,默认:delete。
- log.retention.hours:数据存留时间,单位:时,默认 7 天。
- log.retention.minutes:数据存留时间,单位:分,默认未配置。启用该配置,则
log.retention.hours
失效。 - log.retention.ms:数据存留时间,单位:毫秒,默认未配置。启用该配置,则
log.retention.minutes
失效,配置 -1 则表示无时间限制。 - log.retention.bytes:启动清理日志前的最大文件大小。默认:-1,表示无穷大。
- log.retention.check.interval.ms:负责设置检查周期,默认 5 分钟。
delete
删除策略将过期数据进行删除。
如果一个 segment 中有一部分数据过期,一部分没有过期,怎么处理?
Kafka只会删除Segment中完全过期的数据,对应一部分过期一部分没有过期的数据,kafka将等待其完全过期才删除。
compact
压缩策略将过期数据进行压缩。 对于过期数据相同key的不同value值,只保留最后一个版本。
压缩后的offset可能是不连续的,比如上图中没有6,当从这些offset消费消息时,将会拿到比这个offset大的offset对应的消息,实际上会拿到offset为7的消息,并从这个位置开始消费。
压缩策略只适合特殊场景,比如消息的key是用户ID,value是用户的资料,通过这种压缩策略,整个消息集里就保存了所有用户最新的资料。、
高效读写数据实现
- Kafka 本身是分布式集群,可以采用分区技术,并行度高。
- 读数据采用稀疏索引,可以快速定位要消费的数据。
- 顺序写磁盘
Kafka 的 producer 生产数据,要写入到 log 文件中,写的过程是一直追加到文件末端,为顺序写。官网有数据表明,同样的磁盘,顺序写能到 600M/s,而随机写只有100K/s。这与磁盘的机械机构有关,顺序写之所以快,是因为其省去了大量磁头寻址的时间。 - 页缓存 + 零拷贝技术
零拷贝:Kafka的数据加工处理操作交由Kafka生产者和Kafka消费者处理。Kafka Broker应用层不关心存储的数据,所以就不用走应用层,传输效率高。
PageCache页缓存:Kafka重度依赖底层操作系统提供的PageCache功 能。当上层有写操作时,操作系统只是将数据写入PageCache。当读操作发生时,先从PageCache中查找,如果找不到,再去磁盘中读取。实际上PageCache是把尽可能多的空闲内存都当做了磁盘缓存来使用。
kafka配置列表
位于config/server.properties文件配置信息:
配置项 | 描述 |
---|---|
replica.lag.time.max.ms | ISR 中,如果 Follower 长时间未向Leader 发送通信请求或同步数据,则该 Follower将被踢出 ISR。该时间阈值,默认 30s。 |
auto.leader.rebalance.enable | 默认是 true。 自动 Leader Partition 平衡。 |
leader.imbalance.per.broker.percentage | 默认是 10%。每个 broker 允许的不平衡的 leader 的比率。如果每个 broker 超过了这个值,控制器会触发 leader 的自动平衡。 |
leader.imbalance.check.interval.seconds | 默认值 300 秒。检查 leader 负载是否平衡的间隔时间。 |
log.segment.bytes | Kafka 中 log 日志是分成一块块存储的,此配置是指 log 日志划分 成块的大小,默认值 1G。 |
log.index.interval.bytes | 默认4kb,kafka 里面每当写入了指定大小的日志(.log),然后就往 index 文件里面记录一个索引。 |
log.retention.hours | Kafka 中数据存留的时间,单位:时,默认 7 天。 |
log.retention.minutes | Kafka 中数据存留的时间,单位:分,默认关闭。优先级高于log.retention.hours |
log.retention.ms | Kafka 中数据存留的时间,单位:毫秒,默认关闭。优先级高于log.retention.minutes。 |
log.retention.check.interval.ms | 检查数据是否保存超时的间隔,默认是 5分钟。 |
log.retention.bytes | 默认等于-1,表示无穷大。超过设置的所有日志总大小,删除最早的 segment。 |
log.cleanup.policy | 默认是 delete,表示所有数据启用删除策略;如果设置值为 compact,表示所有数据启用压缩策略。 |
num.io.threads | 默认是 8。负责写磁盘的线程数。整个参数值要占总核数的 50%。 |
num.replica.fetchers | 副本拉取线程数,这个参数占总核数的50%的 1/3。 |
num.network.threads | 默认是 3。数据传输线程数,这个参数占总核数的50%的 2/3 。 |
log.flush.interval.messages | 强制页缓存刷写到磁盘的条数,默认是 long 的最大值,9223372036854775807。一般不建议修改,交给系统自己管理。 |
log.flush.interval.ms | 每隔多久刷数据到磁盘,默认是 null。一般不建议修改,交给系统自己管理。 |
exclude.internal.topics | 订阅主题是否排除系统内部主题,默认是 true,即不订阅。 |
生产者知识点
生产者消息发送流程
Producer的main线程将外部数据封装成ProducerRecord对象,然后发送给拦截器(Interceptors),再经过序列化器(Serializer)和分区器(Partitioner),分区器再将数据发送给消息累加器(RecordAccumulator,默认大小32m,buffer.memory
配置),消息累加器内部结构有一个以分区为纬度的存储队列(默认大小16k,batch.size
配置),当队列存储大小达到了batch.size
(默认16k) 大小或者在队列存储时间达到了 linger.ms
(默认0ms,没等待),则sender线程使用网络客户端(NetworkClient)读取队列中的消息,并封装成一个个数据请求(Request),并发送给Broker Leader,Broker Leader收到请求后根据配置的应答策略(ack
,默认1)给网络客户端以应答。如果发送成功网络客户端则将队列的数据清理掉,如果没有收到Broker的应答,则将数据请求缓存(max.in.flight.requests.per.connection
,默认5)下来以继续重发,超过了重发次数(retries
,默认为Integer.MAX_VALUE)将抛出异常。
生产者分区分配策略
分区原因
方便在集群中扩展
。每个 Partition 可以通过调整以适应它所在的机器,而一个 topic又可以有多个 Partition 组成,因此整个集群就可以适应任意大小的数据了。可以提高并发
。因为可以以 Partition 为单位读写了。- 分区原则
默认分区策略
在Java代码中,我们需要将 producer 发送的数据封装成一个 ProducerRecord 对象。
- 指明 partition 的情况下,直接将指明的值直接作为 partiton 值;
- 没有指明 partition 值但有 key 的情况下,将 key 的 hash 值与 topic 的 partition 数进行取余得到partition 值;
- 既没有partition值又没有key值的情况下,Kafka采用Sticky Partition(黏性分区器),会随机选择一个分区,并尽可能一直使用该分区,待该分区的batch已满或者已完成,Kafka再随机一个分区进行使用(和上一次的分区不同),和轮训分区策略不同的是,轮训是每一条消息均匀分配到不同的分区,黏性分区策略是将当前批次的数据都分配到一个分区。
自定义分区策略
生产者配置信息中,键为partitioner.class
,值为实现了org.apache.kafka.clients.producer.Partitioner接口的实现类全路径。
ACK应答策略
当producer给broker发送消息时,kafka根据根据应答策略给producer不同的应答。
- 0:生产者发送过来的数据,不需要等数据落盘应答。
- 1:生产者发送过来的数据,Leader 收到数据后应答。
- -1(all):生产者发送过来的数据,Leader和ISR队列里面的所有节点同步数据后应答。默认值是-1,-1 和all 是等价的。
AR、ISR和OSR概念
问题:Leader收到数据,所有Follower都开始同步数据,但有一个Follower,因为某种故障,迟迟不能与Leader进行同步,那这个问题怎么解决呢?
ISR
Leader 维护了一个动态的 in-sync replica set (ISR),意为和 leader 保持数据同步的 Follower+Leader集合(leader:0,isr:0,1,2)。
OSR
与ISR相反的,即没有在指定的时间内和leader保持数据同步的集合,延迟过多的数据,从ISR移除的数据,就到了ORS(Out-Sync Relipca set)。
AR
分区中的所有副本统称为AR(Assigned Repllicas),ISR和OSR是AR集合中的一个子集。即:AR = ISR + OSR。正常情况下,如果所有的follower副本都应该与leader副本保持一定程度的同步,则AR = ISR,OSR = null。
答案:如果Follower长时间未向Leader发送通信请求或同步数据,则该Follower将被踢出ISR。该时间阈值由replica.lag.time.max.ms
参数设定,默认30s。例如2超时,(leader:0, isr:0,1),这样就不用等长期联系不上或者已经故障的节点。
数据重复问题
如果ack=-1,producer给Leader发送数据后,Leader将接受到数据同步给Follower,同步所有数据后,此时Leader准备应答Producer时Leader宕机了,则从Follower中选举新的Leader。Producer没收到之前发送数据的应答,尝试重新发送数据给新的Leader。但新的Leader已经有一份旧Leader同步的Hello数据,此时又接收到Producer发来的Hello数据,所以新的Leader一共有两份同样的Hello数据。
数据可靠性保证
可靠性问题分析
如果分区副本设置为1个,或者ISR里应答的最小副本数量(min.insync.replicas
,默认为1)设置为1,和ack=1的效果是一样的(因为没有要同步数据的follower副本,和ack=1一样无需等到副本follower副本同步就可以应答),仍然有丢数的风险(leader:0,isr:0)。
数据传递语义
At Least Once
至少一次(At Least Once)= ACK级别设置为-1 + 分区副本大于等于2 + ISR里应答的最小副本数量大于等于2
At Most Once
最多一次(At Most Once)= ACK级别设置为0
Exactly Once
精准一次(Exactly Once)= At Least Once + 幂等性 + 事务
数据传递对比
At Least Once:可以保证数据不丢失,但是不能保证数据不重复。
At Most Once:可以保证数据不重复,但是不能保证数据不丢失。
Exactly Once:对于一些非常重要的信息,要求数据既不能重复也不丢失。
幂等性
优点
幂等性就是指Producer单会话不论向Broker发送多少次重复数据,Broker端都只会持久化一条,保证了不重复。幂等性解决了数据重复的问题,也保证了数据可靠性。
缺点
重复数据的判断标准:具有<PID, Partition, SeqNumber>相同主键的消息提交时,Broker只会持久化一条。所以幂等性只能保证的是在单分区单会话内不重复。
PID:是Kafka每次重启都会分配一个新的Producer ID;
Partition:表示分区号;
Sequence Number:序列号,单调自增的。
启用幂等性
开启参数 enable.idempotence
默认为 true,false 关闭。
事务
生产者事务
Producer 在使用事务功能前,必须先自定义一个唯一的 transactional.id(事务ID)。有了transactional.id,即使客户端挂掉了,它重启后也能继续处理未完成的事务。
开启事务,必须开启幂等性。
数据乱序
- kafka在1.x版本之前保证数据单分区有序,条件如下:
max.in.flight.requests.per.connection
=1(不需要考虑是否开启幂等性)。 - kafka在1.x及以后版本保证数据单分区有序,条件如下:
- 未开启幂等性
max.in.flight.requests.per.connection
需要设置未1。 - 开启幂等性
max.in.flight.requests.per.connection
需要设置为小于等于5。
因为在kafka1.x以后,启用幂等后,kafka服务端会缓存producer发来的最近5个request的元数据,故无论如何,都可以保证最近5个request的数据都是有序的。
生产者给Kafka发送5个数据请求,请求3的时候发送失败,接着请求4和请求5发送成功了,由于开启了幂等性,在数据可靠性的前提下,保证数据的精确一次,请求3在最后发送成功的时候,5个数据请求将在服务端按照序列号进行排序后再将数据落盘,保证了数据的顺序。
生产者配置列表
配置项 | 描述 |
---|---|
bootstrap.servers | 生产者连接集群所需的 broker 地 址列表,多个用逗号分隔。 |
key.serializer | key序列化所用到的序列化器。 |
value.serializer | value序列化所用到的序列化器。 |
buffer.memory | 消息累加器RecordAccumulator缓冲区总大小,默认 32m。 |
batch.size | 缓冲区一批数据最大值,默认 16k。 |
linger.ms | 数据缓存时间。如果数据迟迟未达到 batch.size,sender 等待 linger.time之后就会发送数据。单位 ms,默认值是 0ms, |
acks | 应答策略,默认:-1 0:生产者发送过来的数据,不需要等数据落盘应答。 1:生产者发送过来的数据,Leader 收到数据后应答。 -1(all):生产者发送过来的数据,Leader+和 isr 队列 里面的所有节点收齐数据后应答。 |
max.in.flight.requests.per.connection | 网络客户端在单个连接上发送的未确认请求的最大数量,默认5。 |
retries | 当消息发送出现错误的时候,系统会重发消息。retries表示重试次数。默认为Integer.MAX_VALUE。如果设置了重试,还想保证消息的有序性,需要设置 MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION=1 否则在重试此失败消息的时候,其他的消息可能发送成功。 |
retry.backoff.ms | 两次重试之间的时间间隔,默认是 100ms。 |
enable.idempotence | 是否开启幂等性,默认 true,开启幂等性。 |
compression.type | 生产者发送的所有数据的压缩方式。默认是 none,也就是不压缩。支持压缩类型:none、gzip、snappy、lz4 和 zstd。 |
消费者知识点
消费方式
- pull:默认采用 pull(拉)模式,从 broker 中读取数据。pull 模式不足之处是,如果 kafka 没有数据,消费者可能会陷入循环中,一直返回空数据。针对这一点,Kafka 的消费者在消费数据时会传入一个时长参数 timeout,如果当前没有数据可供消费,consumer 会等待一段时间之后再返回,这段时长即为 timeout。
- push:push(推)模式很难适应消费速率不同的消费者,因为消息发送速率是由 broker 决定的。它的目标是尽可能以最快速度传递消息,但是这样很容易造成 consumer 来不及处理消息,典型的表现就是拒绝服务以及网络拥塞。而 pull 模式则可以根据 consumer 的消费能力以适当的速率消费消息。
消费者组概念
Consumer Group(CG):消费者组,由多个consumer组成。同一个组内消费者的group.id相同。
- 消费者组内每个消费者负责消费不同分区的数据,一个分区只能由一个组内消费者消费。
- 消费者组之间互不影响。所有的消费者都属于某个消费者组,即消费者组是逻辑上的一个订阅者。
- 如果向消费组中添加更多的消费者,超过主题分区数量,则有一部分消费者就会闲置,不会接收任何消息。
消费者组初始化流程
- 每个consumer都向kafka发送JoinGroup请求。
- coordinator选出一个consumer作为leader。
- coordinator把要消费的topic情况发送给leader消费者。
- 消费者leader会负责制定消费方案。
- 消费者leader把消费方案发给coordinator。
- coordinator就把消费方案下发给同组的consumer。
- 每个消费者都会和coordinator保持心跳(默认3s),一旦超时(
session.timeout.ms
=45s),该消费者会被移除,并触发再平衡;或者消费者处理消息的时间过长(max.poll.interval.ms
=5分钟),也会触发再平衡。
消费者组消费流程
消费者网络客户端将消费请求发送给Kafka,Kafka根据指定分区主题的offset往下取数据并返回,并将消费到到最大位置的offset提交到__consumer_offsets,网络客户端将获取到消息经过反序列化器和拦截器返回到消费者。
消费者分区分配策略
Kafka可以同时使用多个分区分配策略,通过partition.assignment.strategy
配置消费者分区分配策略,可选策略如下:
- Range
- RoundRobin
- Sticky
- CooperativeSticky
Range
Range 是对每个 topic 而言的。首先对同一个 topic 里面的分区按照序号进行排序,并对消费者按照字母顺序进行排序。
通过 partitions数/consumer数 来决定每个消费者应该消费几个分区。如果除不尽,那么前面几个消费者将会多消费 1 个分区。
缺点:如果只是针对 1 个 topic 而言,C0消费者多消费1个分区影响不是很大。但是如果有N多个 topic,那么针对每个 topic,消费者 C0都将多消费 1 个分区,topic越多,C0消费的分区会比其他消费者明显多消费 N 个分区。容易产生数据倾斜!
个人猜想:Range分区分配策略存在数据倾斜问题,但官方默认的默认分区分配策略是Range + CooperativeSticky,根据从官网的解读理解,先按照Range进行分区分配,再平衡期间允许消费者尽可能的保持Range原先分配的分区,但是有一些为了平衡不得不迁移一些原本分配的分区到另外的消费者。
RoundRobin
RoundRobin 针对集群中所有Topic而言。RoundRobin 轮询分区策略,是把所有的partition 和所有的consumer 都列出来,然后按照 hashcode 进行排序,最后通过轮询算法来分配partition 给到各个消费者。
Sticky
粘性分区定义:可以理解为分配的结果带有粘性的。即在执行一次新的分配之前,考虑上一次分配的结果,尽量少的调整分配的变动,可以节省大量的开销。
例如,消费者C0、C1和C2按照Sticky分别消费的分区是:
C0:0,1,3
C1:2,4
C2:5,6
当C0消费者超时或者挂了之后,C1和C2将会分担C0的0,1和3分区的消费,按照Sticky策略再平衡之后的消费方案如下:
C0:GG了
C1:2,4,0,3
C2:5,6,1
对C1和C2原本的分配分案尽量少的调整分配的变动。
粘性分区是 Kafka 从 0.11.x 版本开始引入这种分配策略,首先会尽量均衡的放置分区到消费者上面,在出现同一消费者组内消费者出现问题的时候,会尽量保持原有分配的分区不变。
CooperativeSticky
官方解释:遵循与StickyAssignor相同的(粘性的)赋值逻辑,但允许在StickyAssignor遵循即时的再平衡协议时进行合作再平衡。
翻译过来就是分区分配策略和Sticky策略一样,但是不同点就是,自动平衡的时候可以和其他分配或者自定义分区分配策略进行合作。换言之就是Sticky加强版,或者说是用官方的话就是Sticky的合作版本。
offset 的默认维护位置
__consumer_offsets 主题里面采用 key 和 value 的方式存储数据。key 是group.id+topic+分区号,value 就是当前 offset 的值。每隔一段时间,kafka 内部会对这个topic 进行compact,也就是每个 group.id+topic+分区号就保留最新数据。
自动提交 offset
为了使我们能够专注于自己的业务逻辑,Kafka提供了自动提交offset的功能。自动提交offset的相关参数如下:
- enable.auto.commit:是否开启自动提交offset功能,默认是true。
- auto.commit.interval.ms:自动提交offset的时间间隔,默认是5s。
手动提交 offset
虽然自动提交offset十分简单便利,但由于其是基于时间提交的,开发人员难以把握offset提交的时机。因此Kafka还提供了手动提交offset的API。
手动提交offset的方法有两种:
- commitSync(同步提交):必须等待offset提交完毕,再去消费下一批数据。
- commitAsync(异步提交) :发送完提交offset请求后,就开始消费下一批数据了。
两者的相同点是,都会将本次提交的一批数据最高的偏移量提交;不同点是,同步提交阻塞当前线程,一直到提交成功,并且会自动失败重试(由不可控因素导致,也会出现提交失败);而异步提交则没有失败重试机制,故有可能提交失败。
漏消费
先提交offset后,消费者消费数据之前线程挂了,造成数据的漏消费。
重复消费
先消费数据,在offset提交之前线程挂了,消费者服务重新启动后会造成数据重复消费。
消费者事务
如果想完成Consumer端的精准一次性消费,那么需要Kafka消费端将消费过程和提交offset过程做原子绑定,即消费者事务。消费者消费数据和提交offset要么一起提交,要么都不提交。
消费者事务可以和SpringBoot事务等其他支持事务的框架一起使用 。
消费者配置列表
配置项 | 描述 |
---|---|
bootstrap.servers | 消费者连接集群所需的 broker 地 址列表,多个用逗号分隔。 |
key.deserializer | key序列化所用到的反序列化器。 |
value.deserializer | key序列化所用到的反序列化器。 |
group.id | 消费者所属的消费者组。 |
enable.auto.commit | 默认值为 true,消费者会自动周期性地向服务器提交偏移量。 |
auto.commit.interval.ms | 如果设置了 enable.auto.commit 的值为 true, 则该值定义了消费者偏移量向 Kafka 提交的频率,默认 5s。 |
auto.offset.reset | 当偏移量被重置(不存在)时采取的策略。 官网解释:当Kafka中没有初始偏移量或者当前偏移量在服务器上不存在(例如,因为数据已经被删除)该怎么办: earliest:自动重置偏移量到最早的偏移量。 latest:默认,自动重置偏移量为最新的偏移量。 none:如果消费组原来的(previous)偏移量 不存在,则向消费者抛异常。 anything:向消费者抛异常。 |
offsets.topic.num.partitions | __consumer_offsets 的分区数,默认是 50 个分区。 |
session.timeout.ms | Kafka 消费者和 coordinator 之间连接超时时间,默认 45s。超过该值,该消费者被移除,消费者组执行再平衡。 |
heartbeat.interval.ms | Kafka 消费者和 coordinator 之间的心跳时间,默认 3s。 该条目的值必须小于 session.timeout.ms ,也不应该高于session.timeout.ms 的 1/3。 |
max.poll.interval.ms | 消费者处理消息的最大时长,默认是 5 分钟。超过该值,该消费者被移除,消费者组执行再平衡。 |
fetch.min.bytes | 默认 1 个字节。消费者获取服务器端一批消息最小的字节数。 |
fetch.max.wait.ms | 默认 500ms。如果没有足够的数据立即满足fetch.min.bytes给出的要求,服务器在响应获取请求之前阻塞的最长时间。 |
fetch.max.bytes | 默认50M,获取请求返回的最大字节数,最小值1024。 如果服务器端一批次的数据大于该值仍然可以拉取回来这批数据,因此,这不是一个绝对最大值。一批次的大小受 message.max.bytes (broker config)or max.message.bytes (topic config)影响。 |
max.poll.records | 一次 poll 拉取数据返回消息的最大条数,默认是 500 条。 |
partition.assignment.strategy | 消 费 者 分 区 分 配 策 略 , 默 认 策 略 是 Range + CooperativeSticky。Kafka 可以同时使用多个分区分配策略。 策略可选值有:Range、RoundRobin、Sticky、CooperativeSticky。 |