基础架构
1)Producer :消息生产者,就是向 kafka broker 发消息的客户端;
2)Consumer :消息消费者,向 kafka broker 取消息的客户端;
3)Consumer Group (CG):消费者组,由多个 consumer 组成。消费者组内每个消费者负责消费不同分区的数据,一个分区只能由一个组内消费者消费;消费者组之间互不影响。所有的消费者都属于某个消费者组,即消费者组是逻辑上的一个订阅者。
4)Broker :一台 kafka 服务器就是一个 broker。一个集群由多个 broker 组成。一个 broker 可以容纳多个 topic。
5)Topic :消息主题,生产者和消费者面向的都是一个 topic;
6)Partition:为了实现扩展性,一个非常大的 topic 可以分布到多个 broker(即服务器)上, 一个 topic 可以分为多个 partition,每个 partition 是一个有序的队列;
7)Replica:副本,为保证集群中的某个节点发生故障时,该节点上的 partition 数据不丢失,且 kafka 仍然能够继续工作,kafka 提供了副本机制,一个 topic 的每个分区都有若干个副本,副本数一共包括了一个 leader 和若干个 follower。
8)leader:每个分区多个副本的“主”,生产者发送数据的对象,以及消费者消费数据的对象都是 leader。
9)follower:每个分区多个副本中的“从”,实时从 leader 中同步数据,保持和 leader 数据 的同步。leader 发生故障时,某个 follower 会成为新的 follower。
Brocker
消息存储机制
Kafka 中消息是以 topic 进行分类的,生产者生产消息,消费者消费消息,都是面向 topic 的。 topic 是逻辑上的概念,而 partition 是物理上的概念,每个 partition 对应于一个 log 文 件,该 log 文件中存储的就是 producer 生产的数据。Producer 生产的数据会被不断追加到该 log 文件末端,且每条数据都有自己的 offset。消费者组中的每个消费者,都会实时记录自己消费到了哪个 offset,以便出错恢复时,从上次的位置继续消费。
由于生产者生产的消息会不断追加到 log 文件末尾,为防止 log 文件过大导致数据定位效率低下,Kafka 采取了分片和索引机制,将每个 partition 分为多个 segment。每个 segment 对应两个文件——“.index”文件和“.log”文件。这些文件位于一个文件夹下,该文件夹的命名 规则为:topic 名称+分区序号。
log文件存储具体的消息内容, index文件存取offset和消息内容的地址
生产者(Producer)
分区策略
topic可以分为多个partition
, 生产者发送消息时, 该条消息具体分配到哪一个partition
, 就是分区策略决定的.
生产者在发送消息时可以自己指定分区号, 若不指定, 则会通过默认的分区器进行分区, 不同的kafka API客户端对分区策略的支持可能不一样, 比如在pykafka
这个包中, producer
调用produce
方法时, 不能手动指定具体的分区号, 只能通过分区器进行分区. 而在kafka-python
中, 调用send
方法时, 可以指定具体的分区号, 若未指定则会通过分区器进行分区
自定义分区器
分区器可以进行自定义, 以pykafka
为例, 在partitioner
文件中预定义了一些分区器, 如RandomPartitioner
, HashingPartitioner
, GroupHashingPartitioner
等, 自定义的分区器实现__call__
方法即可, 其接收两个参数, partitions(分区列表)
和key(分区依据)
, pykafka
中默认的分区器为RandomPartitioner
, 它并没有使用到key
这个参数, 只是初始化一个0, 然后一直对partitions
进行取模, 而HashingPartitioner
就是通过对key
进行哈希, 再对partitions
进行取模得到最后的分区号
ACK应答机制
为了保证kafka的broker确实收到并保存了生产者发送的消息, kafka有一个ack应答机制. 该机制有三种应答方式可以选择
- (acks=0): 生产者发送消息后, 就认为是发送成功了, 不等待broker的返回. 这样发送的速度最快, 但是无法保证broker准确接收并保存了消息, 可能出现数据丢失.
- (acks=1): 生产者发送消息后, 只等待leader的返回结果, 不管follower是否同步完成, 只要leader返回成功, 那么就认为消息发送成功了. 这样如果leader接收到消息后, 还没有同步至follower就挂掉了, 那么这条消息也就丢失了.
- (acks=-1): 生产者发送消息后, 等leader和follower都同步完成了, 才认为是消息发送成功了. 这种情况速度最慢, 但是消息丢失的概率最小. 但也有可能发生数据重复, 当follower同步完成后, leader发送ack前, leader挂掉了, 那么生产者长时间为接收到ack, 可能进行重发, 此时发送到了新选举出来的leader上, 该leader上次本身就已经同步了一次数据了, 现在又接受一次, 那么就造成了数据重复.
ISR(in-sync replica)
对于上述的第三中情况, 如果副本非常多, 那么等每个副本都同步完成, 必然需要一定的时间, 因此kafka在此基础上, 提出了ISR的概念, 意为和 leader 保持同步的 follower 集合(并不一定是所有的follower)。当 ISR 中的 follower 完成数据的同步之后,leader 就会给 follower 发送 ack。如果 follower 长时间未向 leader 同步数据 ,则该 follower 将被踢出 ISR. 该时间阈值由replica.lag.time.max.ms
参数设定。Leader 发生故障之后,就会从 ISR 中选举新的 leader。
注:在0.9版本前, 除了通过时间来选择follower是否进入ISR外, 还可以通过数据条数来选择follower, 比如设定一个数据量阈值(10), 如果follower中的数据条数与leader的条数差小于10, 那么就把该follower加入ISR中, 否则踢出ISR. 但如果根据条数来选择的话, 因为生产者生产消息可能是按批次持续发送的, 那么follower的同步也可能是一直在进行的, 也就意味着follower与leader的条数差可能一直在变动, 比如上一时刻follower的条数差小于阈值, 被加出了ISR, 下一时刻条数差又大于阈值, 又被踢出了ISR, 后面同步过来了后又加入了ISR, 这样容易造成频繁更新ISR, 并且ISR的作用只是一个leader的预备队列, 是以防万一的策略, 正常情况leader一直是正常工作的, 所以就没有必要花费太多的资源去频繁操作ISR.
因此kafka中对于第三种情况, 并不是会等所有的follower都同步完成才返回ack, 而只需要等ISR中的follower同步完成即可发送ack
在pykafka
中, 创建Producer
对象时, 可以传入required_acks
参数, 该参数默认为1, 即只等待leader接收到数据就返回ack, 也可以传入0或者-1, 此外还可以传入>=2的数, 即自己 设置等待同步的副本数
副本同步故障细节
例如有1个leader, 2个follower共三个副本, 生产者第一批数据发送了10条消息, offset为0到9, 三个副本都同步完成了, offset都同步到了9
此时生产者发送了第二批共10条消息, 首先是leader接受消息, offset从9变成了19. 然后与两个follower进行同步. 两个follower的同步速率总会有一个快一个慢. 假如某一时刻, 第一个follower的offset同步到了12, 第二个follower的offset同步到了15.
那么对于这个时刻来说, 19/12/15这三个offset就称为每个副本的LEO(Log End Offset), offset12就称为当前的高水位(High Watermark), 即:
LEO:指的是每个副本最大的 offset;
HW:指的是ISR 队列中最小的 LEO。也是消费者能见到的最大的 offset;
此时:
-
若leader挂掉了, 那么需要从两个follower中选举一个来当新的leader, 此时为了保证两个follower的数据统一, 不管选择哪一个follower, 都会将高于HW的offset全部舍弃. 即使老的leader(LEO为19的leader)又启动了, 那么老的leader此时只能变成follower, 舍弃高于HW的offset, 从新leader中重新同步数据
-
若follower挂掉了, 那么就会将该follower踢出ISR, 如果不久后该follower又活了, 那么也会舍弃大于上一次保存的HW后的offset, 重新从HW开始同步leader的数据. 若符合加入ISR的条件, 那么可以继续加入ISR.
例: 若第二个follower(LEO为15的follower)挂掉了, 则踢出ISR, 恢复后. 就会舍弃offset13到15的数据, 重新从HW12开始与leader进行同步
精准一次性消费(Exactly Once)
kafka在0.11版本引入了幂等性, 即Producer不管向server发送了多少次重复数据, Server端只会存储一条.
要启用幂等性,只需要将 Producer 的参数中enable.idompotence
设置为true
即可。开启幂等性的 Producer 在 初始化的时候会被分配一个 PID,发往同一 Partition 的消息会附带 Sequence Number。而 Broker 端会对做缓存,当具有相同主键的消息提交时,Broker 只 会持久化一条。但是Producer重启后 PID 就会变化,并且不同的 Partition 也具有不同主键id,所以幂等性无法保证跨分区跨会话的 Exactly Once。
消费者(consumer)
消费方式
consumer 采用pull(拉)模式从 broker 中读取数据。因为push(推)模式很难适应消费速率不同的消费者, 容易造成 consumer 来不及处理消息, 导致服务拒绝或者网络阻塞.
pull 模式不足之处是,如果 kafka 没有数据,消费者可能会陷入循环中,一直返回空数据。针对这一点,Kafka 的消费者在消费数据时会传入一个时长参数 timeout,如果当前没有数据可供消费,consumer 会等待一段时间之后再返回,这段时长即为 timeout
分区分配策略
消费者消费时是以组的形式进行消费的, 即消费者组. 即使只启动了一个消费者, 也会默认为其创建消费者组.
一个分区Partition, 只能被消费者组中的某一个消费者消费. 即一个消费者组中, 不可能有两个及以上消费者消费同一个Partition, 因为一个消费者组从逻辑上来说就是一个大的消费者, 那么组里的两个消费者消费同一个分区, 就是重复消费了.
如果组内有多个消费者, 他们都消费了同一个主题, 并且这个主题存在多个分区, 那么就会存在分区的消费分配问题, 即哪一个分区分配给哪一个消费者消费. 当然如果组内只有一个消费者, 那么就不存在分区分配问题
kafka有三种分区分配策略, RangeAssignor/RoundRobin(轮询)/StickyAssignor
RangeAssignor
单主题
所有消费者共同订阅多主题
这样会导致分配不均匀, 即Consumer1消费了四个分区, Consumer2只消费了2个分区, 如果主题数更多, 那么偏差会更大
RoundRobin
RoundRobin: 即轮询方式分配, 把每个主题的分区和消费者排序后进行轮询, 第一个partition分配给第一个消费者, 第二个partition分配给第二个消费者, 依次循环类推
单主题
所有消费者共同订阅多主题
这种方式解决的RangeAssignor的问题, 但是下面的情况下又会出现不均匀
每个消费者分别订阅不同主题
注:红线是订阅,其他颜色的线是分配分区
StickyAssignor
从0.11.x版本开始引入这种分配策略,它主要有两个目的:
① 分区的分配要尽可能的均匀;
② 分区的分配尽可能的与上次分配的保持相同。
当两者发生冲突时,第一个目标优先于第二个目标。鉴于这两个目标,StickyAssignor策略的具体实现要比RangeAssignor和RoundRobinAssignor这两种分配策略要复杂很多。
所有消费者共同订阅多主题
这样初看上去似乎与采用RoundRobinAssignor策略所分配的结果相同,但事实是否真的如此呢?再假设此时消费者C1脱离了消费组,那么消费组就会执行再平衡操作,进而消费分区会重新分配。如果采用RoundRobinAssignor策略,那么此时的分配结果如下:
如分配结果所示,RoundRobinAssignor策略会按照消费者C0和C2进行重新轮询分配。而如果此时使用的是StickyAssignor策略,那么分配结果为:
可以看到分配结果中保留了上一次分配中对于消费者C0和C2的所有分配结果,并将原来消费者C1的“负担”分配给了剩余的两个消费者C0和C2,最终C0和C2的分配还保持了均衡。
如果发生分区重分配,那么对于同一个分区而言有可能之前的消费者和新指派的消费者不是同一个,对于之前消费者进行到一半的处理还要在新指派的消费者中再次复现一遍,这显然很浪费系统资源。StickyAssignor策略如同其名称中的“sticky”一样,让分配策略具备一定的“粘性”,尽可能地让前后两次分配相同,进而减少系统资源的损耗以及其它异常情况的发生。
每个消费者分别订阅不同主题
从结果上看StickyAssignor策略比另外两者分配策略而言显得更加的优异,这个策略的代码实现也是异常复杂,如果大家在一个 group 里面,不同的 Consumer 订阅不同的 topic, 那么设置Sticky 分配策略还是很有必要的.