文章目录

  • 一、概念
  • 二、数据发送
  • 1.数据发送流程
  • 2.分区
  • 分区目的
  • 分区原则
  • 3.数据发送可靠性
  • 三、数据保存
  • 1.partition结构
  • 2.Message结构
  • offset
  • 消息大小
  • 消息体
  • 3.存储策略
  • 基于时间
  • 基于大小
  • 四、数据消费
  • 1.offset的保存方式
  • 2.分配分片给消费者
  • 选coordinator
  • 分配给消费者
  • rebalance
  • 3.消息投递语义
  • At least once
  • At most once
  • Exactly once
  • 五、角色选举
  • 1.Controller(控制器)
  • 2.leader的容灾
  • 六、数据分布与同步
  • 1.分区分配原理
  • 2.副本同步


一、概念

kafka是用于构建实时数据管道和流应用程序。具有横向扩展,容错,wicked fast(变态快)等优点,并已在成千上万家公司运行。

kafka单条消息_幂等性


producer 生产者,消息产生者,消息的入口。

consumer 消费者,消息消费,消息的出口。

consumer group 消费者组,同一组消费者消费同一个topic中的不同分区数据。

topic 标签,消息的种类。技术角度考虑就是队列,生产者把消息放入对应队列,消费者到相应的队列获取。

broker 缓存代理,kafka的实例,kafka集群中的一台或者多台服务器统称为broker。

partition topic的分区,分区的作用是负载,提高Kafka的吞吐量。

replication topic的分区副本,主分区(leader)故障后副本(replication)会替代其上位成为新的leader。

二、数据发送

1.数据发送流程

producer写入数据时会把数据写入leader,不会将数据写入follower。

大概流程如下图:

kafka单条消息_幂等性_02


原理:

kafka单条消息_幂等性_03


注:消息写入leader后,follower是主动去leader进行同步的。

producer采用push模式将数据发布到broker,每条消息追加到分区中,顺序写入磁盘,所以保证同一分区,所以可以保证同一分区内的数据是有序的:

kafka单条消息_幂等性_04

2.分区

分区目的

1.方便扩展。随着数据量的不断增长,可以通过扩展机器轻松应对。
2.提高并发。以partition为读写单位,可以多个消费者同时消费数据,提高消息的处理效率。

分区原则

1.指定partition
指定了partition,会写入指定的分区。
2.根据key hash决定partition
设置数据的key后,根据key哈希决定写入的分区。
3.轮询选定
既没有指定partition,也没设置key,轮询选出一个partition。

3.数据发送可靠性

数据不能丢失是消息中间件的基本保证。producer向kafka写入数据时,保证消息不丢失采用ACK应答机制。在生产者向队列写入数据的时候可以设置参数来确定是否确认kafka接收到数据,这个参数可设置的值为0、1、all

0
producer往集群发送数据不需要等到集群的返回,不确保消息发送成功。安全性最低但是效率最高。

1
producer往集群发送数据只要leader应答就可以发送下一条,只确保leader发送成功

-1(all)
producer往集群发送数据需要所有的follower都完成从leader的同步才会发送下一条,确保leader发送成功和所有的副本都完成备份。安全性最高,但是效率最低。

三、数据保存

producer写入数据到kafka后,集群需要对数据进行保存,采用的形式是写入磁盘,kafka会单独开辟一块磁盘空间顺序写入磁盘。

1.partition结构

Partition在服务器上的表现形式就是一个一个的文件夹,每个partition的文件夹下面会有多组segment文件,每组segment文件又包含**.index文件**、.log文件.timeindex文件三个文件, log文件就实际是存储message的地方,而index和timeindex文件为索引文件,用于检索消息。

kafka单条消息_kafka_05


说明:文件的命名是以该segment最小offset来命名的,如000.index存储offset为0~368795的消息,kafka就是利用分段+索引的方式来解决查找效率的问题。

kafka单条消息_消息队列_06

2.Message结构

上面说到log文件就实际是存储message的地方,我们在producer往kafka写入的也是一条一条的message,那存储在log中的message是什么样子的呢?消息主要包含消息体、消息大小、offset、压缩类型……
我们重点需要知道的是下面三个:

offset

offset是一个占8byte的有序id号,它可以唯一确定每条消息在parition内的位置

消息大小

消息大小占用4byte,用于描述消息的大小。

消息体

消息体存放的是实际的消息数据(被压缩过),占用的空间根据具体的消息而不一样。

有关内存映射:
即便是顺序写入硬盘,硬盘的访问速度还是不可能追上内存。所以Kafka的数据并不是实时的写入硬盘,它充分利用了现代操作系统分页存储来利用内存提高I/O效率。 Memory Mapped Files(后面简称mmap)也被翻译成内存映射文件,它的工作原理是直接利用操作系统的Page来实现文件到物理内存的直接映射。完成映射之后你对物理内存的操作会被同步到硬盘上(操作系统在适当的时候)。通过mmap,进程像读写硬盘一样读写内存,也不必关心内存的大小有虚拟内存为我们兜底。 mmap其实是Linux中的一个用来实现内存映射的函数,在Java NIO中可用MappedByteBuffer来实现内存映射

3.存储策略

无论消息是否被消费,kafka都会存储所有消息。
对于旧数据,kafka的删除策略为:

基于时间

默认配置为168小时(7天)

基于大小

默认配置为1073741824字节。
kafka读取特定消息的时间复杂度为O(1),因此删除过期文件并不会提高kafka性能。

四、数据消费

消息队列的两种模式:点对点发布订阅模式,kafka采用的是点对点的模式,consumer主动去kafka集群拉取消息。
与producer相同的是,consumer拉取消息也是找leader拉取。

多个消费者可以组成一个消费者组(consumer group),每个消费者组都有一个组id。同一个消费组者的消费者可以消费同一topic下不同分区的数据,但是不会组内多个消费者消费同一分区的数据。

kafka单条消息_幂等性_07


如上图,该例子是组内消费者小于partition数量的情况,所以会出现某个消费者消费多个partition数据的情况。如果是消费者组的消费者多于partition的数量,多出来的消费者不消费任何partition的数据。实际应用中,消费者组的consumer的数量应该与partition的数量一致

1.offset的保存方式

消费者在消费时,需要保存offset消费到哪里。
以前的版本中,保存在zookeeper中,但是由于zookeeper的写性能不高,以前的方案都是consumer每分钟上报一次。由于zk的性能影响了消费的速度,并因此很容易产生重复消费,新版本将offset保存在一个名为__consumeroffsets topic的topic中。写进消息的keygroupid、topic、partition组成value是偏移量offset。topic配置的清理策略是compact。总是保留最新的key,其余删掉。一般情况下,每个key的offset都是缓存在内存中,查询的时候不用遍历partition,如果没有缓存,第一次就会遍历partition建立缓存,然后查询返回。

确定consumer group位移信息写入__consumers_offsets的哪个partition,具体计算公式:

__consumers_offsets partition =
           Math.abs(groupId.hashCode() % groupMetadataTopicPartitionCount)   
//groupMetadataTopicPartitionCount由offsets.topic.num.partitions指定,默认是50个分区。

2.分配分片给消费者

生产过程中broker要分配partition,消费过程这里,也要分配partition给消费者。类似broker中选了一个controller出来,消费也要从broker中选一个coordinator,用于分配partition。

选coordinator

(1)找到offset保存在哪个partition
(2)步骤(1)得到的partition所在的broker就是被选定的coordinator。
consumer group的coordinator,和保存consumer group offset的partition leader是同一台机器

分配给消费者

(1)consumer启动、或者coordinator宕机了,consumer会任意请求一个broker,发送ConsumerMetadataRequest请求,broker会按照上面说的方法,选出这个consumer对应coordinator的地址。
(2)consumer 发送heartbeat请求给coordinator,返回IllegalGeneration的话,就说明consumer的信息是旧的了,需要重新加入进来,进行reblance。返回成功,那么consumer就从上次分配的partition中继续执行。

rebalance

大致流程如下:
(1)consumer给coordinator发送JoinGroupRequest请求。
(2)这时其他consumer发heartbeat请求过来时,coordinator会告诉他们,要reblance了。
(3)其他consumer发送JoinGroupRequest请求。
(4)所有记录在册的consumer都发了JoinGroupRequest请求之后,coordinator就会在这里consumer中随便选一个leader。然后回JoinGroupRespone,这会告诉consumer你是follower还是leader,对于leader,还会把follower的信息带给它,让它根据这些信息去分配partition
(5)consumer向coordinator发送SyncGroupRequest,其中leader的SyncGroupRequest会包含分配的情况。
(6)coordinator回包,把分配的情况告诉consumer,包括leader

为了更加直观,可以分为两步:JoinSync

(1)Join 顾名思义就是加入组。这一步中,所有成员都向coordinator发送JoinGroup请求,请求加入消费组。一旦所有成员都发送了JoinGroup请求,coordinator会从中选择一个consumer担任leader的角色,并把组成员信息以及订阅信息发给leader——注意leader和coordinator不是一个概念。leader负责消费分配方案的制定。

kafka单条消息_数据_08


(2)Sync,这一步leader开始分配消费方案,即哪个consumer负责消费哪些topic的哪些partition。一旦完成分配,leader会将这个方案封装进SyncGroup请求中发给coordinator,非leader也会发SyncGroup请求,只是内容为空。coordinator接收到分配方案之后会把方案塞进SyncGroup的response中发给各个consumer。这样组内的所有成员就都知道自己应该消费哪些分区了。

kafka单条消息_kafka单条消息_09

列举一下会reblance的情况:
(1)增加partition
(2)增加消费者
(3)消费者主动关闭
(4)消费者宕机了
(5)coordinator自己也宕机了

3.消息投递语义

kafka支持3种消息投递语义
At most once:最多一次,消息可能会丢失,但不会重复
At least once:最少一次,消息不会丢失,可能会重复
Exactly once:只且一次,消息不丢失不重复,只且消费一次(0.11中实现,仅限于下游也是kafka)

At least once

先获取数据,再进行业务处理,业务处理成功后commit offset。
1、生产者生产消息异常,消息是否成功写入不确定,重做,可能写入重复的消息
2、消费者处理消息,业务处理成功后,更新offset失败,消费者重启的话,会重复消费

At most once

先获取数据,再commit offset,最后进行业务处理。
1、生产者生产消息异常,不管,生产下一个消息,消息就丢了
2、消费者处理消息,先更新offset,再做业务处理,做业务处理失败,消费者重启,消息就丢了

Exactly once

思路是这样的,首先要保证消息不丢,再去保证不重复。所以盯着At least once的原因来搞。 首先想出来的:

生产者重做导致重复写入消息----生产保证幂等性
消费者重复消费—消灭重复消费,或者业务接口保证幂等性重复消费也没问题
由于业务接口是否幂等,不是kafka能保证的,所以kafka这里提供的exactly once是有限制的,消费者的下游也必须是kafka。所以一下讨论的,没特殊说明,消费者的下游系统都是kafka(注:使用kafka conector,它对部分系统做了适配,实现了exactly once)。

生产者幂等性好做,没啥问题。

解决重复消费有两个方法:

下游系统保证幂等性,重复消费也不会导致多条记录。
把commit offset和业务处理绑定成一个事务。
本来exactly once实现第1点就ok了。

但是在一些使用场景下,我们的数据源可能是多个topic,处理后输出到多个topic,这时我们会希望输出时要么全部成功,要么全部失败。这就需要实现事务性。既然要做事务,那么干脆把重复消费的问题从根源上解决,把commit offset和输出到其他topic绑定成一个事务。

五、角色选举

kafka中分片存在两种角色,即leaderfollower
kafka会为partition选出一个leader,之后所有该partition的请求,实际操作的都是leader,然后再同步到其他的follower。
当一个broker歇菜后,所有leader在该broker上的partition都会重新选举,选出一个leader。

1.Controller(控制器)

kafka集群有一个或者多个broker,其中一个broker会被选举为controller1,它负责管理整个集群中所有的分区和副本的状态:

(1)当某个分区的leader出现故障时,由控制器负责为该分区选举新的leader
(2)当检测到某个分区的ISR2集合发生变化时,由控制器负责通知所有broker更新其元数据信息
(3)当使用kafka-topics.sh脚本为某个topic增加分区数量时,由控制器负责分区的重新分配

2.leader的容灾

1)controller会在Zookeeper的/brokers/ids节点上注册Watch,一旦有broker宕机,它就能知道。
2)当broker宕机后,controller就会给受到影响的partition选出新leader。controller从zk的/brokers/topics/[topic]/partitions/[partition]/state中,读取对应partition的ISR(in-sync replica已同步的副本)列表,选一个出来做leader。
3)选出leader后,更新zk,然后发送LeaderAndISRRequest给受影响的broker,让它们改变知道这事。(注意这里不是使用zk通知)

如果ISR列表是空,那么会根据配置,随便选一个replica做leader。

六、数据分布与同步

1.分区分配原理

1). 将所有Broker(假设共n个Broker)和待分配的Partition排序;
2). 将第i个Partition分配到第(i mod n)个Broker上 (这个就是leader);
3). 将第i个Partition的第j个Replica分配到第((i + j) mode n)个Broker上。

2.副本同步

数据同步,都是follower从leader主动拉取数据。
三种策略,参考本文中第二章第3条中的描述。


  1. controller选举:依赖于zookeeper,成功竞选为控制器的broker会在Zookeeper中创建/controller这个临时(EPHEMERAL)节点,内容大概为{“version”:1,“brokerid”:0,“timestamp”:“1529210278988”}。每个broker启动的时候会去尝试去读取/controller节点的brokerid的值,如果读取到brokerid的值不为-1,则表示已经有其它broker节点成功竞选为控制器,所以当前broker就会放弃竞选;如果Zookeeper中不存在/controller这个节点,或者这个节点中的数据异常,那么就会尝试去创建/controller这个节点,当前broker去创建节点的时候,也有可能其他broker同时去尝试创建这个节点,只有创建成功的那个broker才会成为控制器,而创建失败的broker则表示竞选失败。 ↩︎
  2. ISR即In-Sync Replicas,是一个副本的列表,里面存储的都是能跟leader 数据一致的副本。确定一个副本在isr列表中,有2个判断条件:条件1:根据副本和leader 的交互时间差,如果大于某个时间差 就认定这个副本不行了,就把此副本从isr 中剔除,此时间差根据
    配置参数rerplica.lag.time.max.ms=10000 决定 单位ms
    条件2:根据leader 和副本的信息条数差值决定是否从isr 中剔除此副本,此信息条数差值根据配置参数rerplica.lag.max.messages=4000 决定,单位条。isr 中的副本删除或者增加 都是通过一个周期调度来管理的。 ↩︎