大家好,我是半夏之沫 😁😁 一名金融科技领域的JAVA系统研发😊😊
我希望将自己工作和学习中的经验以最朴实,最严谨的方式分享给大家,共同进步👉💓👈
👉👉👉👉👉👉👉👉💓写作不易,期待大家的关注和点赞💓👈👈👈👈👈👈👈👈
👉👉👉👉👉👉👉👉💓关注微信公众号【技术探界】 💓👈👈👈👈👈👈👈👈
前言
Kafka的基础知识点还是蛮多的,本文针对Kafka的一些面试过程中常见的基础知识进行全面总结,方便大家进行查漏补缺。
正文
一. Broker概念
在Kafka中,一个Kafka服务端的实例,就叫做一个Broker。已知Kafka使用Zookeeper来维护Kafka集群信息,如下图所示。
Broker启动时,会在Zookeeper的 /brokers/ids路径上创建临时节点,将自己的id注册到Zookeeper。
Kafka组件会订阅Zookeeper的 /brokers/ids路径,当有Broker加入或者退出集群时,这些Kafka组件就能够获得通知。
二. Topic概念
可以在Broker上创建Topic,生产者向Topic发送消息,消费者订阅Topic并从Topic拉取消息。可以用下图进行示意。
三. Partition概念
一个Topic可以有多个分区,这里的分区就叫做Partition,分区的作用是提高Kafka的吞吐量。
分区可以用下图进行示意。
可以用下面的指令在创建Topic的时候指定分区,指令如下所示。
./kafka-topics.sh --bootstrap-server 127.0.0.1:9092,127.0.0.1:9093,127.0.0.1:9094 --replication-factor 3 --partitions 3 --create --topic mytopic-0
上述指令会创建一个分区数为3,副本数为3的Topic。
一个分区可以有多个副本,且一个分区的多个副本不能在同一个Broker上,例如只有3个Broker,但是创建Topic的时候,指定副本数为4,此时创建Topic会失败。
分区的多个副本可以分为leader节点和follower节点,且leader节点为客户端提供读写功能,follower节点会从leader节点同步数据但不提供读写功能,这样能避免出现读写不一致的问题。
创建Topic时,会在Broker上为这个Topic的分区创建目录,如下所示。
分区目录的内容如下所示。
每个文件含义如下。
- index文件是索引文件。记录消息的编号;
- timeindex文件是时间戳索引。记录生产者发送消息的时间和消息记录到日志文件的时间;
- log是日志文件。记录消息。
因为log文件会越来越大,此时根据index文件进行检索会影响效率,所以还需要对上述文件进行切分,切分出来的单位叫做Segment(段),可以通过log.segment.bytes来控制一段文件的大小。Segment的示意如下。
00000000000000000000.index
00000000000000000000.log
00000000000000000000.timeindex
00000000000000050000.index
00000000000000050000.log
00000000000000050000.timeindex
四. 消费者组
消费者组是Kafka提供的可扩展且具有容错性的消费者机制。一个消费者组内存在多个消费者,这些消费者共享一个Group ID。消费者组示意如下。
关于消费者组,有如下说明。
- 消费者组的不同消费者不能同时消费同一个Partition;
- 如果消费者组里消费者数量和Partition数量一样则一个消费者消费一个Partition;
- 如果消费者组里消费者数量大于Partition数量则部分消费者会无法消费到Partition;
- 如果消费者组里消费者数量小于Partition数量则部分消费者会消费多个Partition。
五. 偏移量
偏移量,即Consumer Offset,用于存储消费者对Partition消费的位移量。
偏移量存储在Kafka的内部Topic中,这个内部Topic叫做_consumer_offsets,该Topic默认有50个分区,将消费者的Group ID进行hash后再对50取模,得到的结果对应的分区就会用于存储这个消费者的偏移量。
_ consumer_offsets的每条消息格式示意图如下所示。
注意_consumer_offsets是存储在Broker上的。
六. 生产者发送消息完整流程
生产者发送消息完整流程图如下所示。
结合上述流程图,对消息发送流程说明如下。
- 生产者生成消息Record;
- Record经过拦截器链;
- key和value进行序列化;
- 使用自定义或者默认的分区器获取Record所属分区Partition;
- Record放入消息累加器RecordAccumulator。根据Topic和Partition,可以确定一个双端队列Deque,该队列每个节点为多条Record的合集即ProducerBatch,新Record会被添加到队列最后一个节点上;
- Sender将相同Broker节点的可发送ProducerBatch合并到一个Request中并发送。Sender会持续扫描RecordAccumulator中的ProducerBatch,只要满足大小为batch.size(默认16K)或者最早Record等待已经超过linger.ms,该ProducerBatch就会被Sender收集,然后Sender会合并收集的相同Broker的ProducerBatch到一个Request中并发送;
- 缓存请求Request到inFlightRequest缓冲区中。inFlightRequest中为每个Broker分配了一个队列,新Request会添加到队列头,每个队列最多容纳的Request个数由max.in.flight.requests.per.connection(默认为5)控制,队列满后不会生成新Request;
- Selector发送请求到Broker;
- Broker收到并处理Request后,对Request进行ACK;
- 客户端收到Request的ACK后,将Request从inFlightRequest中移除。
七. 分区策略
Kafka中消息的分区计算策略小结如下。
- 消息中指定了分区。此时使用指定的分区;
- 消息中未指定分区但有自定义分区器。此时使用自定义分区器计算分区;
- 消息中未指定分区也没有自定义分区器但消息键不为空。此时对键求哈希值,并用求得的哈希值对Topic的分区数取模得到分区;
- 如果前面都不满足。此时根据Topic取一个递增整数并对Topic分区数求模得到分区。
八. ISR机制
当生产者向服务端发送消息后,通常需要等待服务端的ACK,这一过程可以用下图进行示意。
即Producer会将消息发送给Topic对应分区的leader节点,然后leader与follower进行同步,如果全部正常的follower同步成功(follower完成消息落盘),那么服务端就可以向Producer发送ACK。
上面描述中的正常follower的集合,叫做ISR(In-Sync Replica Set),只有与leader节点正常通信的follower才会被放入ISR中,换言之,只有ISR中的follower才有资格让leader等待同步结果。
如果leader挂掉,那么会在ISR中选择新的leader。
九. ACK机制
Producer发送消息的时候,可以通过acks配置项来决定服务端返回ACK的策略,如下所示。
- acks设置为0,Producer不需要等待服务端返回ACK,即Producer不关心服务端是否成功将消息落盘;
- acks设置为1,leader成功将消息落盘便返回ACK,这是默认策略;
- acks设置为 -1,leader和ISR中全部follower落盘成功才返回ACK。
十. Segment生成策略
分区在磁盘上由多个Segment组成,如下所示。
有如下参数控制Segment的生成策略。
- log.segment.bytes。用于设置单个Segment大小,当某个Segment的大小超过这个值后,就需要生成新的Segment;
- log.roll.hours。用于设置每隔多少小时就生成新的Segment;
- log.index.size.max.bytes。当Segment的index文件达到这个大小时,也需要生成新的Segment。
十一. index文件
使用Kafka提供的kafka-dump-log.sh工具,可以打开index文件,打开后的index文件可以表示如下。
offset: 613 position: 5252
offset: 1284 position: 10986
offset: 1803 position: 17491
offset: 2398 position: 25792
offset: 3422 position: 35309
offset: 4446 position: 51690
offset: 5470 position: 68071
offset: 6494 position: 84452
offset: 7518 position: 100833
上述示例中,offset是偏移量,position表示这个offset对应的消息在log文件里的位置。
index文件建立的索引是稀疏索引,示意图如下。
十二. timeindex文件
每一条被发送的消息都会记录时间戳,这里的时间戳可以是发送消息时间戳,或者是消息落盘时间戳。可以配置如下。
- log.message.timestamp.type设置为createtime。表示发送消息时间戳;
- log.message.timestamp.type设置为logappendtime。表示消息落盘时间戳。
十三. 索引检索过程
- 根据offset找到在哪个Segment中;
- 从Segment的index文件根据offset找到消息的position;
- 根据position从Segment的log文件中最终找到消息。
十四. Partition存储总结
Partition存储示意图如下。
十五. Kafka中的Controller选举
Controller在Kafka集群中负责对整个集群进行协调管理,比如完成分区分配,Leader选举和副本管理等。
关于Controller的选举,有如下注意点。
- 启动时选举。集群中Broker启动时会去Zookeeper创建临时节点 /controller,最先创建成功的Broker会成为Controller;
- Controller异常时选举。如果Controller挂掉,此时其它Broker会通过Watch对象收到Controller变更的消息,然后就会尝试去Zookeeper创建临时节点 /controller,只会有一个Broker创建成功,创建失败的Broker会再次创建Watch对象来监视新Controller;
- Borker异常。如果集群中某个非Controller的Broker挂掉,此时Controller会检查挂掉的Broker上是否有某个分区的leader副本,如果有,则需要为这个分区选举新的leader副本,并更新分区的ISR集合;
- Broker加入。如果有一个Broker加入集群,则Controller会去判断新加入的Broker中是否含有当前已有分区的副本,如果有,那么需要去从leader副本中同步数据。
十六. 分区leader副本选举
一个分区有三种集合,如下所示。
- AR(Assigned Replicas)。分区中的所有副本;
- ISR(In-Sync Replicas)。与leader副本保持一定同步程度的副本,ISR包括leader副本自身;
- OSR(Out-of-Sync Replicas)。与leader副本同步程度滞后过多的副本。
上述三种集合的关系是AR = ISR + OSR。
分区leader副本选举有如下注意点。
- leader副本会维护和跟踪ISR中所有副本与leader副本的同步程度,如果某个副本的同步程度滞后过多,则leader副本会将这个副本从ISR中移到OSR中;
- 当OSR中有副本重新与leader副本保持一定同步程度,则leader副本会将其从OSR中移到ISR;
- 当leader副本发生故障时,Controller会负责为这个分区从ISR中选举新的leader副本。
十七. 主从同步
分区的leader和follower之间的主从同步示意图如下。
有两个重要概念如下所示。
- LEO(Log End Offset)。下一条待写入消息的Offset;
- HW(High Watermark)。ISR集合中的最小LEO。
那么对于上图而言,HW为6,那么消费者最多只能消费到HW之前的消息,也就是Offset为5的消息。
主从同步规则如下。
- follower会向leader发送fetch请求,然后leader向follower发送数据;
- follower接收到数据后,依次写入消息并且更新LEO;
- leader最后会更新HW。
当leader或follower发生故障时,处理策略如下。
- 如果follower挂掉,那么当follower恢复后,需要先将HW和HW之后的数据丢弃,然后再向leader发起同步;
- 如果leader挂掉,则会先从follower中选择一个成为leader,然后其它follower把HW和HW之后的数据丢弃,然后再向leader发起同步。
十八. Kafka为什么快
- 顺序读写;
- 索引;
- 批量读写和文件压缩;
- 零拷贝。
十九. 零拷贝
如果要将磁盘中的文件内容,发送到远程服务器,则整个数据流转如下所示。
步骤说明如下。
- 从磁盘文件读取文件内容,并拷贝到内核缓冲区;
- CPU控制器将内核缓冲区的数据拷贝到用户缓冲区;
- 应用程序中调用write() 方法,将用户缓冲区的数据拷贝到Socket缓冲区;
- 将Socket缓冲区的数据拷贝到网卡。
一共经历了四次拷贝(和四次CPU上下文切换),其中如下两次拷贝是多余的。
- 内核缓冲区拷贝到用户缓冲区;
- 用户缓冲区拷贝到Socket缓冲区。
而Kafka中的零拷贝,就是将上述两次多余的拷贝省掉,示意图如下。
零拷贝步骤如下所示。
- 从磁盘文件读取文件内容,并拷贝到内核缓冲区;
- 将文件描述符和数据长度加载到Socket缓冲区;
- 将数据直接从内核缓冲区拷贝到网卡。
一共只会经历两次拷贝(和两次CPU上下文切换)。
大家好,我是半夏之沫 😁😁 一名金融科技领域的JAVA系统研发😊😊
我希望将自己工作和学习中的经验以最朴实,最严谨的方式分享给大家,共同进步👉💓👈
👉👉👉👉👉👉👉👉💓写作不易,期待大家的关注和点赞💓👈👈👈👈👈👈👈👈
👉👉👉👉👉👉👉👉💓关注微信公众号【技术探界】 💓👈👈👈👈👈👈👈👈