一、kafka的存储机制

(1)存储机制:Kafka 中消息是以 topic 进行分类的,生产者生产消息,消费者消费消息,都是面向 topic的。

topic 是逻辑上的概念,而 partition 是物理上的概念,每个 partition 对应于一个 log 文件,该 log 文件中存储的就是 producer 生产的数据。Producer 生产的数据会被不断追加到该log 文件末端,且每条数据都有自己的 offset。消费者组中的每个消费者,都会实时记录自己消费到了哪个 offset,以便出错恢复时,从上次的位置继续消费。 数据存储示意图如下:


1、数据存储图解释解释:由于生产者生产的消息会不断追加到 log 文件末尾,为防止 log 文件过大导致数据定位效率低下,Kafka 采取了分片和索引机制,将每个 partition 分为多个 segment。每个 segment对应两个文件——“.index”文件和“.log”文件。这些文件位于一个文件夹下,该文件夹的命名规则为:topic 名称+分区序号。例如,first 这个 topic 有三个分区,则其对应的文件夹为 first-0,first-1,first-2。

2、index 和 log 文件以当前 segment 的第一条消息的 offset 命名。下图为 index 文件和 log 文件的结构示意图。其中 “.index”文件存储大量的索引信息,“.log”文件存储大量的数据,索引文件中的元 数据指向对应数据文件中 message 的物理偏移地址。


(2)分区策略

1、分区原因:

a、方便在集群中扩展,每个 Partition 可以通过调整以适应它所在的机器,而一个 topic又可以有多个 Partition 组成,因此整个集群就可以适应任意大小的数据了;

b、可以提高并发,因为可以以 Partition 为单位读写了。

2、分区器:首先需要将 producer 发送的数据封装成一个 ProducerRecord 对象。

a、若ProducerRecord对象指明 partition 的情况下,直接将指明的值直接作为 partiton 值;

b、没有指明 partition 值但有 key 的情况下,将 key 的 hash 值与 topic 的 partition 数进行取模得到 partition 值;

c、既没有 partition 值又没有 key 值的情况下,第一次调用时随机生成一个整数(后面每次调用在这个整数上自增),将这个值与 topic 可用的 partition 总数取余得到 partition 值,也就是常说的 round-robin 算法。

(3)数据的可靠性保证即kafka的ACK机制

为保证 producer 发送的数据,能可靠的发送到指定的 topic,topic 的每个 partition 收到producer 发送的数据后,都需要向 producer 发送 ack(acknowledgement 确认收到),如果producer 收到 ack,就会进行下一轮的发送,否则重新发送数据。

1、kafka何时发送ack确认给生产者已确定收到数据:确保有follower与leader同步完成,leader再发送ack,这样才能保证leader挂掉后,能在follower选举出新的leader。

2、副本同步策略即多少个follower同步完成后发送ack:全部follower完成同步,才发送ack;优点:选举新的 leader 时,容忍 n 台节点的故障,需要 n+1 个副本,缺点:延迟高

(4)ISR同步策略:

1、引入ISR的原因:采用全部follower完成同步才发送ack;当leader收到数据,所有 follower 都开始同步数据,但有一个 follower,因为某种故障,迟迟不能与 leader 进行同步,那leader就要一直等下去,直到它完成同步,才能发送ack。

2、Leader 维护了一个动态的 in-sync replica set (ISR),意为和 leader 保持同步的 follower 集合。当 ISR 中的 follower 完成数据的同步之后,leader 就会给 生产者发送 ack。如果 follower长时间未 向 leader 同 步 数 据 , 则 该 follower 将 被 踢 出 ISR , 该 时 间 阈 值 由replica.lag.time.max.ms 参数设定。Leader 发生故障之后,就会从 ISR 中选举新的 leader。

(5)kafka的ack应答机制提供了以下三种可靠机制,用户根据对可靠性和延迟的要求进行权衡,

1、ack=0时:producer 不等待broker的ack,这一操作提供了一个最低的延迟,生产者每条消息只会被发送一次,若broker故障则可能会丢失数据

2、ack=1时:producer 等待broker的ack,partition 的 leader 落盘成功后返回 ack,如果在 follower同步成功之前 leader 故障,那么将会丢失数据;

3、ack=-1时:producer 等待 broker 的 ack,partition 的 leader 和 follower 全部落盘成功后才返回 ack。但是如果在 follower 同步完成后,broker 发送 ack 之前,leader 发生故障,那么会造成数据重复。

(6)leader和follower中的HW与LEO


1、LEO(Log End Offset):每个副本最大的offset

2、HW(High WaterMark):消费者能见到的最大的offset,ISR集合中最小的LEO

3、LEO与HW所解决的问题

a、follower故障:follower 发生故障后会被临时踢出 ISR,待 该 follower 恢复后,follower 会读取本地磁盘记录的上次的 HW,并将 log 文件高于 HW 的部分截取掉,从 HW 开始向 leader 进行同步。等该 follower 的 LEO 大于等于 该Partition 的 HW(leader的HW),即 follower 追上 leader 之后,就可以重新加入 ISR 了。

b、leader故障:leader 发生故障之后,会从 ISR 中选出一个新的 leader,之后,为保证多个副本之间的数据一致性,其余的 follower 会先将各自的 log 文件高于 HW 的部分截掉,然后从新的 leader同步数据。

注意:这只能保证副本之间的数据一致性,并不能保证数据不丢失或者不重复。

4、如何解决HW进行数据恢复时可能存在的数据丢失和重复的问题:引入Lead Epoch,(epoch,offset)。epoch表示leader的版本号,从0开始,当leader变更过1次时epoch就会+1,而offset则对应于该epoch版本的leader写入第一条消息的位移。因此假设有两对值:(0, 0),(1, 120),表示第一个leader从位移0开始写入消息;共写了120条[0, 119];而第二个leader版本号是1,从位移120处开始写入消息。leader broker中会保存这样的一个缓存,并定期地写入到一个checkpoint文件中。当leader写底层log时它会尝试更新整个缓存——如果这个leader首次写消息,则会在缓存中增加一个记录;否则就不做更新。而每次副本重新成为leader时会查询这部分缓存,获取出对应leader版本的位移,这就不会发生数据不一致和丢失的情况。

(6)kafka的Exactly Once语义

1、At Least Once 语义:将服务器的 ACK 级别设置为-1,可以保证 Producer 到 Server 之间不会丢失数据,但是不能保证数据不重复

2、At Most Once 语义:将服务器 ACK 级别设置为 0,可以保证生产者每条消息只会被发送一次;可以保证数据不重复,但是不能保证数据不丢失

3、Exactly Once语义:对于一些非常重要的信息,比如说交易数据,下游数据消费者要求数据既不重复也不丢失,即 Exactly Once 语义。0.11 版本的 Kafka,引入了一项重大特性:幂等性。所谓的幂等性就是指 Producer 不论向 Server 发送多少次重复数据,Server 端都只会持久化一条。幂等性结合 At Least Once 语义,就构成了 Kafka 的 Exactly Once 语义

4、Exactly Once实现原理:首先启用幂等性,将 Producer 的参数中 enable.idompotence 设置为 true 。Kafka的幂等性实现其实就是将原来下游需要做的去重放在了数据上游。开启幂等性的 Producer 在初始化的时候会被分配一个 PID,发往同一 Partition 的消息会附带 Sequence Number。而Broker 端会对做缓存,当具有相同主键的消息提交时,Broker 只会持久化一条。 但是 PID 重启就会变化,同时不同的 Partition 也具有不同主键,所以幂等性无法保证跨分区跨会话的 Exactly Once。

二、kafka的消费者

(1)消费方式

1、kafka采用的消费模式pull:consumer采用pull(拉)的方式从broker中读取数据,若采用push(推)模式很难适应消费速率不同的消费者,因为消息发送是由broker决定的,尽管push模式的目标是尽可能以最快速度传递消息,但是这样很容易造成consumer来不及消费消息,从而导致消息堆积。而pull模式可以根据consumer的消费能力以适当的速率消费消息。

2、pull模式的不足:若kafka没有数据,消费者可能陷入循环之中,一直返回空数据,此时kafka消费者在消费数据时会传入一个时长参数 timeout,如果当前没有数据可供消费,consumer会等待一段时间之后再返回,这段时长即为 timeout。

(2)kafka的消费分区策略即一个消费者如何消费分区中的数据

1、前提:一个consumer group中有多个consumer,一个topic有多个partition,所以必然会涉及到partition的分配问题,即确定那个partition由哪个consumer来消费。

2、分区策略1:Range

1)分区方式:按照消费者总数和分区总数进行整除运算来获得一个跨度,然后将分区按照跨度进行平均分配,以保证分区尽可能均匀地分配给所有的消费者。对于每一个topic,Range策略会将消费组内所有订阅这个topic的消费者按照名称的字典序排序,然后为每个消费者划分固定的分区范围,如果不够平均分配,那么字典序靠前的消费者会被多分配一个分区。即n=分区数/消费者数量,m=分区数%消费者数量,那么前m个消费者每个分配n+1个分区,后面的(消费者数量-m)个消费者每个分配n个分区。

2)分区过程如下:

a、假设目前只有一个消费者C0,订阅了一个主题,这个主题包含7个分区。如下图1。

b、此时消费组内又加入了一个新的消费者C1,按照既定的逻辑需要将原来7个分区重新分配给C0和C1,如下图2。

c、接着消费组内又加入了一个新的消费者C2,如此消费者C0、C1和C2按照下图3中的方式各自负责消费所分配到的分区。

       

        


3)缺点:针对多个主题的不同分区,可能会造成分区不均匀。

假设消费组中有2个消费者C0和C1,都订阅了主题t0和t1,并且每个主题都有3个分区,那么所订阅的所有分区可以标识为:t0p0、t0p1、t0p2、t1p0、t1p1、t1p2。最终的分配结果为:消费者C0:t0p0、t0p1、t1p0、t1p1,消费者C1:t0p2、t1p2。明显消费者消费不均匀。

3、分区策略2:RoundRobin

1)分区方式:原理是将消费组内所有消费者以及消费者所订阅的所有topic的partition按照字典序排序,然后通过轮询方式逐个将分区以此分配给每个消费者。

假设消费组中有2个消费者C0和C1,都订阅了主题t0和t1,并且每个主题都有3个分区,那么所订阅的所有分区可以标识为:t0p0、t0p1、t0p2、t1p0、t1p1、t1p2。最终的分配结果为:

消费者C0:t0p0、t0p2、t1p1;

消费者C1:t0p1、t1p0、t1p2。

2)缺点:如果同一个消费组内的消费者所订阅的信息是不相同的,那么在执行分区分配的时候就不是完全的轮询分配,有可能会导致分区分配的不均匀。如果某个消费者没有订阅消费组内的某个topic,那么在分配分区的时候此消费者将分配不到这个topic的任何分区。

假设消费组内有3个消费者C0、C1和C2,消费者C0订阅的是主题t0,消费者C1订阅的是主题t0和t1,消费者C2订阅的是主题t0、t1和t2,t0、t1、t2,这3个主题分别有1、2、3个分区,最终的分配结果为:

消费者C0:t0p0;

消费者C1:t1p0;

消费者C2:t1p1、t2p0、t2p1、t2p2;

3、分区策略3:Sticky

1)分区方式:分区的分配要尽可能的均匀;分区的分配尽可能的与上次分配的保持相同。当两者发生冲突时,第一个目标优先于第二个目标

举例,同样消费组内有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。

采用的是StickyAssignor策略,那么最终的分配结果为:

消费者C0:t0p0;

消费者C1:t1p0、t1p1;

消费者C2:t2p0、t2p1、t2p2;

假如此时消费者C0脱离了消费组,则StickyAssignor分配结果为:

消费者C1:t1p0、t1p1、t0p0;

消费者C2:t2p0、t2p1、t2p2;

可以看到StickyAssignor策略保留了消费者C1和C2中原有的5个分区的分配:t1p0、t1p1、t2p0、t2p1、t2p2。

(3)consumer消费时offset的情况:Kafka0.9版本之前,consumer默认将 offset保存在 Zookeeper中,从0.9版本开始,consumer默认将offset保存在Kafka一个内置的topic 中,该topic为__consumer_offsets。 offset的存储格式:(groupID,topic,partitionID)

对于消费者,kafka中有两个设置的地方:对于老的消费者,由--zookeeper参数设置;对于新的消费者,由--bootstrap-server参数设置

如果使用了--zookeeper参数,那么consumer的消费信息如offset将会存放在zk之中

如果使用了--bootstrap-server参数,那么consumer的消费信息如offset将会存放在kafka之中

(4)kafka如何高效读写数据

1、顺序写磁盘:Kafka的producer生产数据,要写入到log文件中,写的过程是一直追加到文件末端,为顺序写。随机写会导致磁头不停地换道,造成效率的极大降低;顺序写磁头几乎不用换道,或者换道的时间很短,省去了大量磁头寻址的时间。

2、零复制技术:零拷贝避免让CPU做大量的数据拷贝任务,减少不必要的拷贝,让CPU解脱出来专注于别的任务。

1)传统读取文件并发送到网络的步骤

a、操作系统将数据从磁盘文件中读取到内核空间的页面缓存;

b、应用程序将数据从内核空间读入用户空间缓冲区;

c、应用程序将读到数据写回内核空间并放入socket缓冲区;

e、数据从socket缓冲区通过网络发送。

“零拷贝技术”只用将磁盘文件的数据复制到页面缓存中一次,然后将数据拷贝到socket缓冲区中,最后直接发送到网络中(发送给不同的订阅者时,都可以使用同一个页面缓存),避免了重复复制操作。

(5)zookeeper在kafka中的作用?

kafka使用zookeeper来保存broker,topic和partition的元数据信息,在kafka0.9版本之前消费者使用kafka保存消费分区的偏移量。

1、broker注册:Broker是分布式部署并且相互之间相互独立,但是需要有一个注册系统能够将整个集群中的Broker管理起来,此时就使用到了Zookeeper。在Zookeeper上会有一个专门用来进行Broker服务器列表记录的节点:/brokers/ids;每个Broker在启动时,都会到Zookeeper上进行注册,即到/brokers/ids下创建属于自己的节点,如/brokers/ids/[0...N]。Kafka使用了全局唯一的数字来指代每个Broker服务器,不同的Broker必须使用不同的Broker ID进行注册,创建完节点后,每个Broker就会将自己的IP地址和端口信息记录到该节点中去。其中,Broker创建的节点类型是临时节点,一旦Broker宕机,则对应的临时节点也会被自动删除。

2、topic和partition注册:在Kafka中,同一个Topic的消息会被分成多个分区并将其分布在多个Broker上,这些分区信息及与Broker的对应关系也都是由Zookeeper在维护,由专门的节点来记录,如:/borkers/topics;如/brokers/topics/login/3->2,这个节点表示Broker ID为3的一个Broker服务器,对于"login"这个Topic的消息,提供了2个分区进行消息存储,同样,这个分区节点也是临时节点。

3、保存消费者分区的offset。

(6)kafka的事务特性

1、Produce事务:期望一个 Producer 在 Fail 恢复后能主动 abort 上次未完成的事务(接上之前未完成的事务),然后重新开始一个事务,这种情况应该怎么办?之前幂等性引入的 PID 是无法解决这个问题的,因为每次 Producer 在重启时,PID 都会更新为一个新值。为了实现跨分区跨会话的事务,Kafka 在 Producer 端引入了一个 TransactionalId 来解决这个问题。TransactionalId 的引入还有一个好处,就是跟 consumer group 类似,它可以用来标识一个事务操作,便于这个事务的所有操作都能在一个地方进行处理;

为了管理 Transaction,Kafka 引入了一个新的组件 Transaction Coordinator。Producer 就是通过和 Transaction Coordinator 交互获得 Transaction ID 对应的任务状态。Transaction Coordinator 还负责将事务所有写入 Kafka 的一个内部 Topic,这样即使整个服务重启,由于事务状态得到保存,进行中的事务状态可以得到恢复,从而继续进行。

2、consumer的事务特性:对于 Consumer 而言,事务的保证就会相对较弱,尤其时无法保证 Commit 的信息被精确消费。这是由于 Consumer 可以通过 offset 访问任意信息,而且不同的 Segment File 生命周期不同,同一事务的消息可能会出现重启后被删除的情况。