1.定义
Kafka是一个分布式的基于发布/订阅模式的消息队列,主要应用于大数据实时处理领域。
这个定义已经暗示得很明确了。适用于大数据的实时处理领域,并且一般是搭建集群来使用,是消息队列的一种。
大数据技术栈生态:
kafka位于数据传输层和数据存储层,是为数不多的覆盖超过一层的技术。
2.回顾消息队列
①解耦:允许独立的扩展或修改两边的处理过程,只要确保它们遵守同样的接口约束。
②可恢复性:系统的部分组件失效不会影响整个系统,由于消息队列降低了进程间的耦合度,所以即使一个处理消息的进程挂掉,加入队列中的消息仍然可以在系统恢复后被处理。
③缓冲:解决生成者生产速率和消费者消费速率不一致的问题,有助于控制和优化数据流经过系统的速度。
④流量削峰:和缓冲意思差不多,当有大量的超负荷请求时,通过消息队列来缓冲分散成一段时间来处理。
⑤异步通信:不需要立即处理的消息放入消息队列,不需要同步等待,在需要处理的时候异步去处理它们。
3.组成
3.1Producer
消息生产者,就是向 kafka broker 发消息的客户端。
3.2Consumer
消息消费者,向 kafka broker 取消息的客户端。
3.3Broker
一台kafka服务器就是一个broker。一个broker可以容纳多个topic。一个集群由多个broker组成。
3.4Topic
一般在消息队列里面指的是主题,但是kafka里不仅如此,还可以理解为一个队列,因为生产者和消费者都面向一个topic。
3.5Consumer Group
由多个消费者组成,也叫消费者组。组中每个消费者负责消费不同分区的数据(也就是说同一组内的消费者不能消费同一分区),消费者组之间互不影响。消费者是逻辑上的一个订阅者。
3.6Partition
即分区,可以说是kafka中的一个核心概念。一个topic可以分布到多个broker上,一个topic可以分为多个partition,每个partition都可以理解为一个有序队列。
3.7Replica
由于kafka一般都是集群部署的,为了保证集群中某个节点故障时,节点上的partition数据不丢失且kafka能正常工作,需要副本机制,每个topic的每个分区都有若干副本,然后这些副本会被分为一个leader和若干个follower
3.8leader
每个topic的每个分区的若干个副本之一,且是这些副本的“老大”,是生产者发送数据和消费者消费数据的对象,也负责提供其他副本数据的同步。
3.9follower
每个topic的每个分区的若干个副本中除leader之外的副本,实时从leader中同步数据,当leader挂了,就会有follower成为新的leader。
4.架构
4.1文件存储机制
首先我们要知道kafka中的消息是以topic进行分类的,topic是逻辑上的概念,而partition是物理上的概念,每个partition对应一个log文件夹。假如我们创建了一个topic“aaa”,有两个分区,我们会在kafka下的data中发现:
以topic_分区号(从0开始)命名的log文件夹,这就是存储生产者生产的数据。
一个log文件夹中含有多个segment(这个大小可以在server.properties中通过log.segment.bytes设置大小),如下图:
每个segment对应两个文件,一个是index为后缀,另一个是log为后缀,如下:
其中index和log是成双成对的出现,且命名是以该log的当前segment的第一条消息的offset命名。数据存在log文件夹里面,那么为什么还需要index呢?
因为如果log文件过大就会导致数据定位速度变慢,每次从头开始找也不太好,所以需要分片和索引,分片就是我们上面提到的每个partition分成多个segment,那么索引就是通过index文件实现。
也就是说.log文件存储大量的数据,.index文件存储大量的索引信息。索引文件中的元数据指向对应数据文件中 message 的物理偏移地址。如下图所示:
目前有两个segment:
第一个segment
00000000000000000000.index文件
0 | 0 |
1 | 233 |
2 | 542 |
3 | 765 |
4 | 933 |
00000000000000000000.log文件
Message-0 |
Message-1 |
Message-2 |
Message-3 |
Message-4 |
第二个segment
00000000000000000005.index文件
0 | 0 |
1 | 211 |
2 | 488 |
3 | 695 |
4 | 856 |
00000000000000000005.log文件
Message-5 |
Message-6 |
Message-7 |
Message-8 |
Message-9 |
假如我们要找offset为3的Message。先通过二分查找找到对应的segment,因为第一个segment的log起始offset为0,第二个segment的log起始offset为5,所以在第一个segment中;然后查找.index文件,得到message在对应.log中的物理偏移量为765,即可在数据文件中快速定位。
整体的文件架构如下:
4.2生产者分区策略
4.2.1分区原因
①方便在集群中扩展。每个partition可以通过调整以适应它所在的机器,而一个topic又可以有多个partition组成,因此整个集群就可以适应任意大小的数据了;
②提高并发。可以以partition为单位读写。
4.2.2具体原则
我们以java代码为例,由于生产者发消息的时候需要封装一个ProducerRecord对象。
观察其构造函数。
如下是均指明了partition的:
这是第一种情况:
①指明了partition,那么直接将指明的值作为partition值。
第二种情况:
②没有指明partition但指明了key, 取key的hash值与topic的分区数
取余得到partition值。
第三种情况:
③没有指明partition也没有指明key。这时候就是轮询算法,不过并不一定是从partition为0开始,是随机生成一个整数并与topic中可用的partition总数取余得到开始分区。
4.3消费者分区分配策略
4.3.1消费方式
传统的消息队列消费者存在两种消费方式:拉模式与推模式。
kafka采用的拉模式,从broker中主动读取数据。
因为消息发送速率是由broker决定的,推模式很难适应消费速率不同的消费者,如果传递消息速率过快,那么消费者可能来不及处理消息。而拉模式可以根据消费者的消费能力以适当的速率消费消息。
当然拉模式不是完美的,如果kafka中没有消息了,那么消费者会陷入循环中且返回空数据。
当然,kafka也不会放任这个问题不管,可以通过设置一个时长参数timeout,如果当前没有消息了,消费者会等待timeout时间后再返回。
4.3.2具体原则
由于一个topic中有多个分区,一个消费者组中有多个消费者,这样也使得分区问题应运而生。
说白了,就是决定哪个分区由哪个消费者消费
kafka有两种分配策略:Range和RoundRobin。
1)RoundRobin
假如一个topic有7个分区,一个消费者组有三个消费者,那么分配如下:
假入有两个主题topic t1和t2,各有3个分区,假如有一个消费者组有两个消费者,分配如下:
这样的话可能会导致一个问题,如果消费者组中的某个消费者没有订阅其他同组消费者的主题,那么把多主题当成一个整体就可能会导致消费者去消费未订阅的主题
2)Range
假如一个topic有7个分区,一个消费者组有三个消费者,那么分配如下:
假入有两个主题topic t1和t2,各有3个分区,假如有一个消费者组有两个消费者,分配如下:
这种方式会导致分区分配不均匀,且可能不均匀的数值差距会随着主题增多而增多。
5.特性
5.1高效读写
①顺序写磁盘。生产者生产数据,要写入到log文件中,内容是以追加到文件末端形式写入。顺序写省去了大量的磁头寻址时间,比随机写要快得多。
②零拷贝。
传统的拷贝文件数据如上图所示:
①操作系统将数据从磁盘文件中读取到内核空间的页面缓存。
②应用程序将数据从内核空间读入用户空间的缓冲区。
③应用程序将读到数据写回内核空间并放入socket缓冲区。
④操作系统将数据从socket缓冲区复制到网卡接口。
经过以上流程后数据才能通过网络发送。
而kafka的零拷贝技术如下图:
只用将磁盘文件的数据复制到页面缓存一次,然后将数据从页面缓存直接发送到网络中,减少了很多操作步骤,少了内核空间到用户空间的来回切换。
并且发送给不同订阅者都可以使用同一个页面缓存。所以说,如果有10个消费者:
①在传统方式下。数据复制次数为4*10=40次
②而零拷贝技术只需要从磁盘复制到页面缓存为1次,10个消费者各自读取一次页面缓存为10次,总共1+10=11次。
5.2数据可靠性
生产者发送数据到指定的topic中这个过程需要保证数据可靠性。
所以topic的每个分区收到数据后,都需要回送ack(学过计算机网络的话,应该对ack不陌生),生产者收到ack才进行下一轮的发送,否则重发数据。
5.2.1何时发送ack
由于集群中一个分区一般有多个副本,数据发送到分区后会有一个副本数据同步的过程,因此引申出了一个问题,即ack应该是什么时候回送?是否需要等待所有follower同步完数据?
方式 | 优点 | 缺点 |
半数以上的follower完成同步,就发送ack | 延迟低 | 选举新的leader时,容忍n台节点的故障需要2n+1个副本。 |
全部follower完成同步,才发送ack | 选举新的leader时,容忍n台节点的故障需要n+1个副本。 | 延迟高 |
kafka选择的是第二种方式:
①由于kafka每个分区有大量数据,第一种方案在容忍相同台节点故障需要的副本更多,容易造成大量的数据冗余。
②网络延迟对kafka的影响较小,即使第二种方案相对于第一种方案延迟更高。
对于第二种方式仍然有弊端:假如有一个follower故障导致数据无法同步,那么leader就德一直等它才能发ack,这样明显不科学。
kafka的解决方案是维护一个动态的in-sync replica set,简称为ISR,可以理解为与leader保持同步的follower集合。
也就是说leader等待全部follower完成同步里面的全部并不是指“所有”的follower,而是指在ISR中的follower。对于ISR的维护,如果follower长时间没有向leader同步数据,那么follower就会被踢出ISR,这个时间可由replica.lag.time.max.ms设定。当然如果leader挂掉的话,就会从ISR中的follower里选取新的leader。
5.2.2ack应答机制
当然有时候对于数据的可靠性要求不高,反而延迟要求尽量低,这时候还是要等ISR中的follower全部接收成功就没必要了。因此kafka提供了三种可靠性级别,可以让用户自己在可靠性和延迟之间做权衡。
acks参数配置为0
生产者不等待broker的ack,broker一接收到还没有写入磁盘就已经返回。这种方式的延迟最低,但是如果broker故障就会丢失数据。
acks参数配置为1
生产者等待broker的ack,但是分区的leader接收到并写入磁盘就返回ack,这样的话,如果follower在将数据同步完全之前leader挂了就会丢失数据。
acks参数配置为-1
生产者等待broker的ack,分区的leader接收到数据写入磁盘和ISR中的follower全部同步成功后返回ack。这样的话除非ISR中此时没有follower就会退化到acks为1的情况会导致数据丢失,其他情况基本不会丢失数据。但是会造成数据重复,即broker在发送ack之前且follower同步完数据之后,leader挂了。
5.2.3副本数据一致性
需要先了解两个名词:
①LEO:即Log End Offset,每个副本最大的offset。
②HW:即High Watermark,可以理解为ISR队列中最小的LEO,即消费者能见到的最大offset
具体如何通过这两个参数来保证副本一致性呢?我们假设两种情况:
情况一follower故障:
follower故障后会被临时踢出ISR,follower恢复后,就会读取本地磁盘记录的上次的HW,将log文件中高于HW的部分截取掉,之后就开始从HW向leader同步数据,当其LEO不小于该分区现在的HW后,就可以重新加入ISR队列。
情况二leader故障:
leader故障后,会从ISR中选出一个新的leader,这时候就其他follower就需要将各自log文件中高于HW的部分截取掉,并从HW开始向新的leader同步数据。
5.3Exactly Once
在学习Exactly Once之前,应该先了解At Least Once和At Most Once。
At Least Once:至少一次,即当我们把acks设置为-1,时,能保证数据不丢失(不考虑特别极端情况,比如上面提到的),但不能保证数据不重复。
At Most Once:至多一次,即当我们把acks设置为0时,能保证数据不重复,但不保证数据不丢失
那么Exactly Once要做到的是刚好一次,即能保证数据不丢失也能保证数据不重复。
那么Exactly Once = At Least Once + 幂等性。
幂等性就是说不论生产者向broker发送多少次重复数据,都只会持久化一次。
在kafka中启用幂等性也比较简单,将Producer 的参数中 enable.idompotence 设置为 true 即可。
开始幂等性后,生产者在初始化的时候会被分配一个PID,发往同一个分区的消息会附带序列号,broker端会对<PID,Partition,SeqNumber>做缓存,把这个三个参数理解为一个联合主键,只要这三个参数一样,就视为重复,只持久化一条。
由于PID重启会改变,所以幂等性无法保证跨会话的Exactly Once,由于联合主键有Partition,所以也无法保证跨分区的Exactly Once。
5.4offset
由于消费者在消费过程中可能会挂掉,那么消费者恢复后,需要从之前的位置继续消费,那么需要通过实时记录offset来完成这个过程。Kafka 0.9 版本之前,consumer 默认将 offset 保存在 Zookeeper 中,从 0.9 版本开始, consumer 默认将 offset 保存在 Kafka 一个内置的 topic 中,该 topic 为__consumer_offsets。
5.5事务
kafka支持事务,使得生产和消费可以跨分区跨会话,做到要么都成功要么都失败。
5.5.1Producer事务
要实现跨分区跨会话的事物,需要引入一个全局的TransactionID,将生产者的PID与其绑定,这样的话生产者重启也可以通过正在进行的TransactionID获取原来的PID。
当然引入了TransactionID,那么必然需要去管理事物,所以kafka引入了一个新的组件Transaction Coordinator,生产者与该组件交互获得TransactionID对应的任务状态。Transaction Coordinator还负责将事务写入kafka的一个内部topic中,这样可以做到整个服务重启也可以恢复原来进行中的事务状态。
5.5.2Consumer事务
对于消费者,事务的保证相对较弱,不能保证提交的信息被精确消费。
因为消费者通过offset访问任意信息,而不同的segment文件的生命周期不同,同一事务的消息可能会出现重启后被删除的情况。
6.消息发送流程
kafka的producer采用异步发送的方式发送消息。
在发送过程中,涉及到两个线程,main线程和Sender线程以及一个线程共享变量RecordAccumulator。
主要交互就是:①main线程将消息发送给RecordAccumulator。②Sender线程不断从RecordAccumulator中拉取消息发送到Kafka broker。
kafka在发送消息时需要先经过如下过程: