目录
前言
kafka3.x版本
生产者
消息发送原理
生产者如何提高吞吐量
- linger.ms:默认为0ms即无延迟发送(batch.size参数无效)。更大的linger.ms使producer等待更长的时间才发送消息,这样就能够缓存更多的消息填满batch进而提升tps,但是会增加消息的延时。
- batch.size:默认16K。更大的batch.size可以使更多的消息封装进同一个请求中,减少总请求数。
- compression.type:默认为none即不进行压缩。对消息进行压缩可以减小数据量,提升吞吐量,但是会加大producer端的cpu开销。
- buffer.memory:默认为32MB。当缓冲区被填满后producer立即进入阻塞状态直到有空闲内存被释放出来。阻塞时间一旦超过max.block.ms(默认1分钟)就会抛出TimeoutException。如果经常发生超时则要调大buffer.memory的值降低阻塞。
分区策略
kafka的默认分区器为DefaultPartitioner,官方对其解释如下:
The default partitioning strategy:
If a partition is specified in the record, use it
If no partition is specified but a key is present choose a partition based on a hash of the key
If no partition or key is present choose the sticky partition that changes when the batch is full. See KIP-480 for details about sticky partitioning.默认分区策略:
1.如果指定了分区,那么就使用它。
2.如果没指定分区但设置了key则根据key进行hash后对分区数进行取模
3.如果没指定分区和key则按照粘性分区策略,当这批次数据满时进行下一分区的发送
数据可靠性
ack为0时(可靠性差,效率高):
ack为1时(可靠性中等,效率中):
ack为-1时(可靠性高,效率低):
在ack设置为-1时,当leader收到数据后follower开始同步数据时假设有一个follower宕机了无法同步,那leader会进行ack应答吗?
针对这问题,在leader中维护了一个动态的ISR(in-sync replica set),意为和leader保持同步的leader+follower集合。
如果follower长时间未向leader发送通信请求或同步数据,则该follower将被踢出ISR。该时间的阈值由replica.lag.time.max.ms参数控制,默认30s。
值得一提的是如果分区副本数为1或者ISR应答的最小副本数量(由min.insync.replicas参数控制)为1,则和ack=1的效果是一样的。
那么如果需要数据完全可靠,那么需要如下条件:
ACK级别设置为-1 + 分区副本数大于等于2 + ISR里应答的最小副本数量大于等于2。
数据重复性
在生产者方,想要保证exactly once,就得介绍一下幂等性和事务。
幂等
通过幂等想要做到exactly once,那么必须有如下条件:
幂等性 + 至少一次(ack = -1 + 分区副本数>=2 + ISR最小副本数>=2)
在Kafka中,Producer 默认不是幂等性的,但我们可以配置参数(enable.idempotence:true)创建幂等性 Producer。
判断重复数据的标准如下:
具有<PID,Partition,SeqNumber>相同主键的消息进行提交时,Broker只会持久化一条。
PID:Kafka每次重启都会分配一个新的
Partition:分区号
SeqNumber:单调递增
由以上3个参数可以看出,幂等性只能保证单会话、单分区的exactly once。
事务
开启事务的前提必须开启幂等。
数据顺序
- kafka在1.x版本之前需设置max.in.flight.requests.per.connection=1能保证单分区有序。该参数指定了生产者在收到服务器响应之前可以发送多少个消息。它的值越高,就会占用越多的内存,不过也会提升吞吐量。把它设为 1 可以保证消息是按照发送的顺序写入服务器的,即使发生了重试。
- 在1.x版本之后,未开启幂等时,max.in.flight.requests.per.connection必须设置为1才能保证单分区有序;开启幂等后,max.in.flight.requests.per.connection需要设置<=5(在kafka1.x以后,在启用幂等的前提下kafka服务端会缓存producer发来的最近5个request的元数据,并会在服务端重新排序,故能保证单分区有序)。
Broker
工作流程
broker的工作流程如下:
- broker节点启动后向zk进行注册(注册broker id信息)。
- 向zk中注册controller(每个broker节点都有属于自己的controller,谁先注册谁就是controller leader)。
- 由选举出来的controller leader监听brokers节点变化。
- controller leader进行分区leader副本的选举(选举规则为:在ISR中存活为前提,按照AR中排在前面的优先。例如AR[1,0,2],ISR[1,0,2],那么leader就会按照1,0,2的顺序轮询)。
- Controller leader将信息上报给zk。
- 其他controller从zk同步相关信息。(防止controller leader宕机导致数据丢失)
假设leader副本所在的broker节点挂了之后,controller leader在监听到节点变化后会重新进行leader选举,然后上报到zk更新leader以及ISR信息。
副本
Leader选举流程
故障处理
Leader故障处理
Follower故障处理
文件存储
文件存储机制
log和index文件都是以当前文件中最小的偏移量值进行命名的。索引文件中的元数据对应数据文件中消息的物理偏移地址。在查找消息的时候会根据offset值定位到相应segment的索引文件,然后在索引文件中找到小于等于offset的最大索引记录,通过该索引记录的position值在log文件中进行向后顺序遍历查找到对应的message。
注意:index文件中并没有为数据文件中的每条消息都建立索引,而是采用了稀疏索引的方式大约每往log文件写入4kb数据时会建立一条索引(log.index.interval.bytes参数可控制,默认4kb)。这样避免了索引文件占用过多的空间,从而可以将索引文件保留在内存中。但缺点是没有建立索引的message不能一次定位到其所在数据文件的位置,从而需要做一次顺序扫描,但是顺序扫描的范围会变得很小。
文件清理策略
kafka中默认的日志保存时间为7天,如需修改日志保存时间,可使用如下参数:
-
log.retention.hours: 小时 。
-
log.retention.minutes: 分钟。
-
log.retention.ms: 毫秒。
delete日志删除(默认)
配置参数为:log.cleanup.policy = delete(所有数据启用删除策略)
-
基于时间:默认打开 。 以 segment 中所有记录中的最大时间戳作为该文件时间戳。
-
基于大小:默认关闭 。超过设置的所有日志总大小,删除最早的 segment 。log.retention.bytes ,默认等于 -1 ,表示无穷大。
compact日志压缩
配置参数为:log.cleanup.policy = compact(所有数据启用压缩策略)
对于相同key值的数据,只会保留最后一个版本 。压缩后的offset可能是不连续的,当从某个不存在的offset消费消息时,将会拿到比这个offset大的offset对应的消息并从这个位置开始消费。
高效读写数据
- 分布式集群 + 分区,并行度高。
- 文件采取分片和索引机制,利用稀疏索引避免占用过多空间且又能快速定位具体的消息。
- 顺序写磁盘,在producer发送消息时是一直以追加的形式添加到文件末端。
- 页缓存 + 零拷贝。
PageCache
PageCache是系统级别的缓存,它把尽可能多的空闲内存当作磁盘缓存使用以此来提高IO效率。
当上层有写操作时,操作系统只是将数据写到PageCache中,同时标记Page的熟悉为dirty(脏页),待合适的时机再将脏页进行落盘;当有读操作时,会先从PageCache中查找数据,发生缺页才会进行磁盘调度。
在kafka的producer发送消息到broker后,消息并不是直接落盘的,而是直接进到PageCache中,PageCache中的数据会被内核的处理线程在合适的时机进行落盘操作。
consumer在消费消息时,会先从PageCache中获取消息,获取不到才会到磁盘中读取,并且会预读出相邻的块放入PageCache中。
零拷贝
wiki中对零拷贝有如下定义:
"Zero-copy" describes computer operations in which the CPU does not perform the task of copying data from one memory area to another.
从wiki的定义中,我们看到"零拷贝"是指计算机操作的过程中,CPU不需要为数据在内存之间的拷贝消耗资源。而它通常是指计算机在网络上发送文件时,不需要将文件内容拷贝到用户空间(User Space),而是直接在内核空间(Kernel Space)中传输到网络。
传统的网络IO步骤如下:
- 调用 read() 时,操作系统从磁盘中读取数据复制到内核空间的reader buffer
- cpu控制将内核空间的数据复制到用户空间的缓冲区
- 调用 write() 时,将用户空间的数据复制到内核空间的socket buffer
- 将内核空间的socket buffer的数据复制到网卡设备中进行发送
可以发现,同一份数据需要进行4次copy。
通过SendFile系统调用进行优化之后,直接把数据从内核空间的read buffer拷贝到socket buffer,然后发送到网卡,避免了在内核空间与用户空间来回拷贝的弊端:
经过上述优化,数据只经过了2次copy就从磁盘发送出去了,并没有经过内核空间与用户空间的多余的拷贝过程。
消费者
消费者组
消费者组由多个consumer组成。 消费者组内每个消费者负责消费不同分区的数据,一个分区只能由一个组内消费者消费;消费者组之间互不影响。
消费者组初始化流程
注:_consumer_offsets为存储消费者消费的offset主题,分区数默认为50。
消费者组消费流程
分区分配策略
可以通过配置参数partition.assignment.strategy修改分区的分配策略(默认策略是Range + CooperativeSticky)。其中CooperativeSticky是3.0开始才有的。
Range
RoundRobin
轮询分区策略看似完美但是有个很大的弊端
例如:同一消费者组中,有3个消费者C0、C1和C2,他们共订阅了 3 个主题:t0、t1 和 t2,这 3 个主题分别有 1、2、3 个分区(即:t0有1个分区(p0),t1有2个分区(p0、p1),t2有3个分区(p0、p1、p2)),即整个消费者所订阅的所有分区可以标识为 t0p0、t1p0、t1p1、t2p0、t2p1、t2p2。具体而言,消费者C0订阅的是主题t0,消费者C1订阅的是主题t0和t1,消费者C2订阅的是主题t0、t1和t2,最终分区分配结果如下:
消费者C0 | 消费 t0p0 |
消费者C1 | 消费 t1p0 分区 |
消费者C2 | 消费 t1p1、t2p0、t2p1、t2p2 分区 |
Sticky
Guarantees an assignment that is maximally balanced while preserving as many existing partition assignments as possible.
CooperativeSticky
CooperativeSticky和Sticky类似,只是支持了cooperative协议。
Rebalance重平衡
kafka触发rebalance的条件如下:
- 消费者订阅的主题发生变化
- 消费者数量发生变化
- 主题的分区数量发生变化
- 消费者被coordinator认为是dead状态,这可能是由于消费者发送心跳超时(session.timeout.ms=45s)或者处理消息时间过长(max.poll.interval.ms=5分钟)
在kafka2.3之前,rebalance各种分配策略基本都是基于eager协议的(包括RangeAssignor,RoundRobinAssignor)。尽管StickyAssignor这个策略能够在consumer在rebalance后能够维持原有的分配方案,可惜的是这个分配策略依旧是在eager协议的框架之下,rebalance时仍然需要每个consumer都先放弃当前持有的资源(分区)。因此在kafka2.3版本时应用了cooperative协议。
具体eager协议和cooperative协议的区别可参考: