文章目录

  • 1.kafka基础知识介绍
  • Kafka有四个核心API:
  • 设计思想
  • 思考
  • 基本项
  • 进阶项
  • 性能优化项
  • 动机
  • 持久化
  • Producer
  • Load balancing
  • 批量发送
  • Consumer
  • rebalance(消费者分区分配策略)
  • 什么时候会触发这个策略呢?
  • 谁来执行rebalance以及管理consumer的group呢?
  • coordinator如何确定
  • TODO 后续会细节讲解发送请求的过程
  • 如何确定使用那种分配策略呢?
  • Consumer Leader(消费者选主)
  • 如何保存消费端的offset位置
  • offset维护在哪里?
  • 确认consumer组的offset位置
  • 分区的副本机制
  • leader的容灾
  • ISR(in-sync-replica)
  • 所有Replica 不工作的情况
  • replica的同步


1.kafka基础知识介绍

kafka是一个分布式流处理平台,具有高性能、持久化、多副本备份、横向扩展能力。生产者往队列里写消息,消费者从队列里取消息进行业务逻辑。一般在架构设计中起到解耦、削峰、异步处理的作用。那这意味着什么呢?

我们知道流处理平台又以下三种特性:
1.可以让你发布和订阅的流式记录,这一方面与消息队列类似.
2.可以存储流式的记录,并且又较高的可用性.
3.可以在流式记录产生时就进行处理(实时性)
Kafka适合什么样的场景呢?

熟悉它的都知道,可以采用kafka做消息队列,可以把应用分为两大类别:
	1.构造实时流数据管道,它可以在系统或应用之间可靠的获取数据
	2.构造实时流式应用程序,对这些流式数据进行转换(流处理)
	以下为kafka整体架构图

kafka topic 批量訂閱_kafka

大概用法就是,Producers往Brokers里指定的Topic发送消息,Consumer从Topic中获取消息,然后进行业务处理.
首先了解kafka要先从以下概念说起.

- kafka作为高可用的消息队列在使用上应以一个集群出现,其中节点称之为broker
- kafka通过topic对存储对流处理进行分类,其中topic是一个逻辑上的概念,而不是真实存在的.
- 每条记录中包含key/value(键值对)/timestamp(时间戳)

Kafka有四个核心API:

- Producer API :允许一个应用程序发送一串流式的数据到一个或多个topic
- Consumer API :允许一个应用程序订阅一个或多个topic,并且对发布给他们的流式数据进行处理
- Stream API :允许一个应用程序作为一个流处理器,消费一个或多个topic产生的输入流,然后生产一个输出流到一个或多个topic中去,在输入到输出流中进行有效的转换.
- Connector API: 允许构件并允许可重用的生产者/消费者,将tpoics连接到已存在应用程序或数据系统

设计思想

如果要让我们设计一个消息队列中间件你会怎么做呢?

思考

基本项

1.通信方式采用HTTP/RPC,如果RPC通信协议如何选择(Thrift\Dubbo\protobuf\自行实现)?(必须)
2.如何实现消息队列的高可用?(必须)
3.消息强堆积能力如何做?(必须)
4.消息持久化如何选择,是考虑磁盘、分布式存储、DB、内存(必须)
5.消费关系解析,如何实现同时消费、共同消费,如何维护生产者消费者之间的关系?(必须)
6.消息队列与应用的耦合性,如何解耦?(必须)

进阶项

1.如何保证消息的可靠性?这是所有消息队列最重要也是最难实现的,目前来说在吞吐量与可靠性如何抉择?(必须)
2.是采用消息最终一致性还是强一致性?(非必须)
3.如何广播,如何进行消息推送,消费者是push消息还是pull消息?(必须)
4.如何做错峰与控流?(必须)
5.消息是否需要考虑重复消息和顺序消息(非必须)
6.消息是否考虑事务性,如果考虑,如何做一致性事务处理?(非必须)

性能优化项

7.消息采用同步处理还是异步处理?(非必须)
8.消息是单消息消费还是批量消费?(非必须)
以上问题先不进行解答,我们带着问题进入kafka中,总结上面设计点就是高可用性、高可靠性、高吞吐量,其中细节还有很多.

动机

kafka具有高吞吐量来支持高容量的事件流,需要能够政策处理大量的数据积压,以便能够支持来自离线系统的周期性数据加载.在数据流被推送到其他数据系统进行服务的情况下,系统出现故障必须保证容灾.

持久化

kafka对消息对存储和缓存严重依赖文件系统,大家都任务磁盘速度慢,使得人们对于持久化架构提供强有力的性能产生怀疑.事实上,磁盘的速度比人们预期要慢得多,也快得多,这取决于使用方式,设计合理的磁盘结构通常可以可网络一样快
kafka在磁盘上的优化有很多,比如

1.顺序读写:目前很多大数据相关工具一般都会采用append only形式,即文件追加读写,其他操作都只能建立在append only的基础上,如删除数据只能append一条记录把删除的数据置为删除,而不是从磁盘中删除愿数据,尽可能的保证顺序读写.
原因:顺序读写和随机读写性能差距很大,基本上在几十倍到百倍.

kafka topic 批量訂閱_kafka_02

2.MMAP(内存映射文件):可以将文件内容从磁盘映射到内存,当我们将某些内容写入映射的系统,某个时间后将flush到磁盘上,实际上是间接使用内存,为什么不直接写入内存呢,而是采用MMAP将数据写入磁盘,后续会详细讲解MMAP的原理

原因:Kafka运行在JVM上,如果直接将数据写入内存,则内存开销会很多,GC会频发,因此使用MMAP可以避免此问题

3.零拷贝:使用零拷贝技术的中间件有很多比如Netty,Rocketmq,Nginx等,什么是零拷贝请自行百度,零拷贝技术可以减少数据拷贝和共享总线操作的次数,消除不必要的拷贝.总结:尽可能减少CPU Copy.一些常见的零拷贝:内存映射(mmap),sendfile,Scatter/Gather,splice

kafka topic 批量訂閱_kafka_03


Kafka两个地方都用到了零拷贝,1.Producer生产者发送数据到broker采用mmap文件映射,实现顺序的快速写入,2.Consumer从broker读取数据.采用sendfile,将磁盘文件读到系统内核中后,直接到socker buffer进行数据发送.

4.简化缓存:相比于维护尽可能多的in-memory cache ,并且在空间不足的时候flush到文件系统,kafka把这个过程倒过来了,所有数据一开始就写入到文件系统的持久化日志里,而不用cache空间不足的时候flush到磁盘(参考mysql,根据策略把cache中的数据flush到磁盘),实际上数据被转移到了内核的pagecache中.

5.采用合理的数据写方式(append):消息系统采用的持久化结构通常和BTree相关的消费者队列或用其他存储消息源数据的通用随机访问数据结构,BTree是最通用的数据结构,但是成本也会很高,BTree的复杂度是O(log n),但这条在磁盘中不成立,磁盘寻址10ms一跳,并且每个磁盘同时只能执行一次寻址,并行会收到限制,因此少量寻址也会耽误不少时间,持久化队列可以建立在简单的读取和向文件后追加两种操作上,这种架构的有点在于所有操作复杂度都是O(1),并且读操作不会阻塞写操作,读操作之间也不会相会影响

6.批量数据处理(消息块):假设以上优化已经消除了磁盘访问模式不佳的情况,该类系统性能低下的主要原因主要就剩下了两个:大量的小型I/O,过多的字节拷贝

6.1小型I/O发生在客户端和服务端之间以及服务端自身的持久化操作中,这里将消息进行合理的分组,网络请求时将消息打包成一组发送,而不是每次发送1条消息,从而减少不必要网络请求操作.批处理允许更大的网络数据包,更大的顺序读写磁盘操作,连续的内存块等,所有这些都让Kafka将随机流消息顺序写入磁盘,再由consumers消费.

6.2过多的字节拷贝在消息量少时,不是问题,但是在高负载的情况下,就不太乐观了,为们使用producer,broker和consumer都共享标准的消息格式(共同协议),这样数据块不用修改就能在他们之间传递.(可以将数据从pagecache直接拷贝到socket缓冲区中,可以调用零拷贝的sendfile做到这一点)

7.端到端的批量压缩:在某些情况数据传输的瓶颈不是CPU,也不是磁盘,而是网络带宽,对于需要通过广域网在数据中心之间发送消息的管道更是如此.用户可以选择不需要数据的压缩,但是这样会有非常差的压缩比和消息重复类型的冗余,比如JSON信息.高性能压缩是一次压缩一组消息.并且在日志中保持压缩,只会在consumer消费时解压.(压缩协议支持GZIP、LZ4)

优化项其实还有很多,比如producer异步发送、消费者pull消息等等,就不一一列举.

Producer

Load balancing

producer 发送数据到master partition的服务器上(partition也有leader,后续会将),不需要经过任何路由,为了让kafka实现这个功能,所有的kafka服务节点都能相应这样的元数据请求:哪些服务器是活着的,topic的哪些partition是master,分配在哪个节点上,这样生产者就能适当的直接发送它的请求到服务器上.发送分区的策略可以实现轮询、随机的负载均衡方式,或者使用特定语义的分区函数可以把业务id作为key,则用户所有数据都会被分到同一分区上.

kafka topic 批量訂閱_kafka topic 批量訂閱_04

批量发送

producer在发送数据会判断recordBatch是否达到batch.size的大小(或者batch不足以添加下一条record),则唤醒sender线程发送数据.批处理是提升性能的一个主要方式,kafka在内存中汇总数据,并用一次请求批次提交信息,可以配置消息数量,也可以指定延迟时间等,缓冲区也是可配置的.
数据发送可以总结5步:

1.获取topic的metadata信息,检测topic是否可用
2.key、value序列化
3.获取partition的值(可以重写partition()方法,默认采用hash取余的方式获取)
4.达到batch.size大小,唤起sender线程去发送RecordBatch
5.发送RecordBatch

Consumer

kafka consumer通过向broker发出fetch请求来获取想要消费的partition,consumer的每个请求都在log中指定了对应的offset,并接收从该位置开始的一块数据.因此consumer对于position的管理就很重要也比较复杂.必要时可以修改offset回退到该位置再次消费数据.

rebalance(消费者分区分配策略)

如果consumer数量大于partition数量,则有效的consumer个数为partition数量
策略:(可以在消费者里配置)
1.Ranage范围分区(前m个消费者分配n+1个分区,其余消费者分配n个分区)

假设有10个分区(p0-p9),3个消费者(c1,c2,c3)
	分区编号分别为0-9
	n=分区数量/消费者个数
	m=分区数量%消费者个数
	c1 p0-p3
	c2 p4-p6
	c3 p7-p9
弊端:只是针对一个topic而言,c1消费者多消费1个分区影响不是很大,如果有N个topic, 那么针对每个topic都要多消费一个分区,topic越多c1消费的分区会比其他分区多N个,这就是明显的弊端。(消费分区不均衡,订阅多topic可能造成消费者多消费多个分区.)

2.RoundRobin轮询
把所有的partation和所有的concumer都列出来,然后按照hash进行排序,最后通过轮询算法来分配partation给各个消费者。

其中分为两种情况
1)同一消费组内所有的消费者订阅的消息都相同
	RoundRobin策略的分区分配会是均匀的
	同一消费者组中,有 3 个消费者C1、C2和C3,都订阅了 2 个主题 t0 和 t1,并且每个主题都有 3 个分区(p0、p1、p2),那么所订阅的所以分区可以标识为t0p0、t0p1、t0p2、t1p0、t1p1、t1p2
	C1   t0p0,t1p0
	C2   t0p1,t1p1
	C3   t0p2,t1p2
2)同一消费者组内的消费者订阅的消息不同(一般也不会这样使用)
	在执行分区分配的时候,就不是完全的轮询分配,有可能会导致分区分配的不均匀,如果某个消费者没有订阅消费组内的topic,那么在分配的时候,此消费者将不会分配到这个topic的任何分区
	同一消费者组中,有3个消费者C0、C1和C2,他们共订阅了 3 个主题:t0、t1 和 t2,这 3 个主题分别有 1、2、3 个分区(即:t0有1个分区(p0),t1有2个分区(p0、p1),t2有3个分区(p0、p1、p2)),即整个消费者所订阅的所有分区可以标识为 t0p0、t1p0、t1p1、t2p0、t2p1、t2p2。具体而言,消费者C0订阅的是主题t0,消费者C1订阅的是主题t0和t1,消费者C2订阅的是主题t0、t1和t2(订阅结果如下图),最终分区分配结果如下:
	C0   t0p0
	C1   t1p0
	C2   t1p1,t2p0,t2p1,t2p2

kafka topic 批量訂閱_数据_05


3.Stricky(尽可能均匀分配)

有三个消费者C0,C1,C2 有4个topic t0,t1,t2,t3,每个topic有2个分区 p0\p1,共有 t0p0,t0p1,t1p0,t1p1,t2p0,t2p1,t3p0,t3p1,初始分配与轮询分配一致。
	C0   t0p0,t1p1,t3p0
	C1   t0p1,t2p0,t3p1
	C2   t1p0,t2p1
如果C1挂了
	C0   t0p0,t1p1,t3p0,t2p0
	C2   t1p0,t2p1,t0p1,t3p1
如果采用轮询算法结果会如下(希望可以形成对比)
	C0   t0p0,t1p0,t2p0,t3p0
	C2   t0p1,t1p1,t2p1,t3p1
什么时候会触发这个策略呢?

当出现以下几种情况时,kafka会进行一次分区分配操作,也就是kafka consumer的rebalance

  • 同一个consumer group内新增了消费者
  • 消费者离开当前所属的consumer group ,比如停止消费、宕机.
  • topic中进行了分区扩容或者缩容等.
    kafka consumer的rebalance机制规定了一个group 下的所有consumer 如何达成一致来分配订阅topic的每个partition,分配策略是可插拔的实现方式,我们可以自己实现分配机制.(出去以上3中策略)
谁来执行rebalance以及管理consumer的group呢?

kafka提供了GroupCoordinator来执行对于group的管理,当group的第一个consumer启动时,它会去和kafka server确定谁是它的coordinator.之后该group内的所有成员都会和该coordinator进行协调通信.

coordinator如何确定

消费者向kafka集群中的任意一个broker发送一个GroupCoordinatorRequest请求,该消费组的offset 的partition leader 则为该消费组的coordinator.
Math.abs(groupName.hashCode()) % numPartitions为该offset的partition leader.
GroupCoordinator(仅针对与当前消费组)角色.

TODO 后续会细节讲解发送请求的过程

第一个阶段:JoinGroup ->选举Consumer Leader

第二阶段:SyncGroup ->传递rebalance策略

1.consumer向broker发送joinGroup请求

2.broker收到后判断consumer是否为leader,若为leader则返回当前所有成员的信息,若为follower则返回为空,group进入PreparingRebalance状态。

3.consumer收到请求后,若发现自己为leader,则根据返回的信息,执行rebalance计算,计算后将rebalance结果通过SyncGroup发送给broker(group coordinator)

4.group coordinator收到请求,broker自己是否是Controller如果是则将rebalance结果送给各个consumer,如果不是将请求转给controller

5.consumer收到syncGroup结果,则调用相应的回调方法(onPartitionRebalance)按照最新的rebalance结果进行消费

kafka topic 批量訂閱_持久化_06

如何确定使用那种分配策略呢?

如果每个消费者的分区策略都是不一样的,partition应该如何分配给各个消费者呢?
其实在consumer选主之后,consumer leader 将所有的分区策略进行统计,最终选取合适的策略进行策略统一.而不是每个consumer 采用自己的消费策略,consumer将策略信息同步给GroupCoordinator,GroupCoordinator会把结果返回给所有消费者.

Consumer Leader(消费者选主)

在kafka的消费端,会有一个消费者协调器和消费者组,组协调器GroupCoordinator需要为消费组内的消费者选举出一个消费组leader,如果组内没有leader,那么第一个加入消费组的消费者即成为leader,如果某一时刻leader由于某些原因退出了消费组,那么就会重新选消费组leader.

private  val members =  new mutable.HashMap[String, MemberMetadata] leaderId = members.keys.headOption

leaderId选举为Hashmap中的第一个键值对.

如何保存消费端的offset位置

前面提到过offset,每个topic可以划分多喝分区(至少有1个分区),同一个topic下的不同分区包含的消息是不同的,每个消息在被添加到分区时,都会分配一个offset,它是消息在此分区中的唯一编号,kafka通过offset保证消息在分区内的顺序,offset的顺序不跨分区,只能保证同一个分区内有序.每次消费一个消息并且提交后,会保存offset,那么offset保存在哪里呢?

kafka topic 批量訂閱_持久化_07

offset维护在哪里?

kafka 0.9以前的版本:

维护在zk中,利用zk的数据强一致性,就可以保证数据不会丢失或传输出错等问题,但是由于offset更新频率跟高,zk每次都要二阶段提交保证数据一致性,这样效率很低也会影响zk的稳定性以及kafka的吞吐率.(zk不适合大规模的频繁数据写入)

zk保存的路径: /consumers/{consumer}/offsets/{topic}/1

表示:{topic} 中的1分区消费者{consumer}消费的offset位置.

kafka 0.9以后的版本:

使用broker来维护offset,broker默认topic有**__consumer_offsets** 用于存储所有的offset信息,默认分区为50个,格式: group.id,topic+partition,offset,如下:

kafka topic 批量訂閱_kafka_08

确认consumer组的offset位置

上面说了,__consumer_offsets默认分区为50个,它会根据求模运算分散到不同的partition中: Math.abs(groupName.hashCode()) % numPartitions

分区的副本机制

我们已经知道了kafka中的每个topic都可以分为多个partition,并且多个partition会均有的分配在集群的各个节点下.虽然这种方式能有效的进行数据分片,但是对于每个partition来说都是单点的.当其中一个partition不可用时,那么这部分消息就没法进行消费.所以为了保证partition的可靠性而提供了Replica的概念(大数据里很多组件都采用了副本机制).
思考:但是如果只是依靠Replica来保证高可用是否可行呢?
如果follower副本从leader上拉取写入的消息,这个过程一定会有延迟,导致follower副本中保存的消息少于leader副本,但只要是没有超出阀值都可以被选举成leader.这样要维护一个比较稳定的partition列表.

leader的容灾

每个分区都多个副本,并且在副本集合中会存在一个leader partition,所有读写的请求都是有leader副本来进行处理.(是不是有点像zk,但是这里实现是最终一致性,而不是强一致性,可以思考强一致性会怎么样?),同一个分区的多个副本会被均匀分配到集群中的不用broker上,当leader副本所在的broker出现故障后,可以重新选举新的leader副本(一般在isr中,特殊情况是无可用的isr会根据策略进行选择等待还是选数据较为接近的partition)继续对外提供服务,这样副本机制提高了kafka的可用性.

kafka topic 批量訂閱_持久化_09

其实在zk里可以看到很多选举以及leader的元信息
{"controller_epoch":11,"leader":1753,"version":1,"leader_epoch":0,"isr":[1753,1754]} 比如 这段信息表示 controller 的版本,该partition 的leader在1753的broker上,该partition经历过0次partition的选举,isr(in-sync-replica):可用的同步副本(与partition leader数据比较接近的,后续会详细讲TODO)

ISR(in-sync-replica)

ISR表示目前“可用且消息量与leader较为接近的副本集合”,这是整个partition集合的一个子集,那么什么叫做较为接近呢?都有哪些策略配置较为接近?

ISR集合中的副本必须满足两个条件
	1.副本所在节点必须维持与zk的连接(基本可用)
	2.副本最后一条消息的offset之间的差值不能超过指定的阀值(0.9版本以前是replica.lag.max.messages参数)之后移除了,**思考:为什么指定数量的参数移除了?**该参数新增了(replica.lag.time.max.ms),默认为10s,在一定时间内未进行数据同步,则把该副本剔除ISR,被剔除的副本继续同步,等达到当前时间与副本的最后拉取数据的时间(lastCaughUpTimeMS) 小于 replica.lag.time.max.ms,则继续放入ISR中
所有Replica 不工作的情况

上面说.在ISR中至少要有一个follower时,Kafka可以确保已经commit的数据不丢失.但如果某个Partition的所有Replica都宕机了,就无法保证数据不丢失了.

这里有两个策略(默认采用第一种策略)
	1.等待ISR中的任意一个”活“过来,那不可用的时间就可能较长,而且ISR中所有Replica都无法活过来了,那么这个Partition就永远处于不可用状态
	2.选择第一个“活”过来的Replica(不一定在ISR)作为Leader,而这个Replica不在ISR中即使它并不保证已经包含了所有已commit的消息,它也会成为Leader而作为consumer的数据源.
replica的同步

上面我们了解了replica如何选主、如何保证patition的高可用,但是有两个比较重要的问题.

1.它是如何进行数据同步的呢(follower -> master )?

2.leader向producer返回ack(确认信息)之前需要保证多少个replica已经接收到这个消息呢?

如下图:红色表示partition leader ,白色表示partition follower

kafka topic 批量訂閱_数据_10

下面解释几个名词解释:
1.LEO(Log End Offset):日志末端位移,记录了该副本log日志中下一条消息的位移值,这里单位是消息个数,如果LEO=10,表示该副本保存了10条消息,位移值是[0,9],另外leader LEO和follower LEO的更新是有区别的.最后一个offset即为该副本的LEO
2.HW(High Watermark):表示水位值,对于同一个副本对象而言,HW值不会大于LEO的值,小于等于HW的值的所有消息都被认为是已备份成功的.同理,leader副本和follower副本的HW更新也是有区别的.大于HW的消息是不被consumer 可见的
3.remote LEO:保存在leader副本上的follower副本的LEO,可以看出leader副本上保存所有副本的LEO(也包括自己的)

leader持有的HW即为整个分区的HW,同时leader所在的broker还保存了所有follower副本的LEO
关系:leader的LEO >=follower的LEO >=leader保存的follower的LEO >=leader的HW>=follower的hw

LEO的更新时间:

1.follower的LEO更新时间:每当follower副本写入一条消息时,LEO会被更新

2.leader端的follower副本的LEO更新时间:当follower 从leader处fetch消息时,leader获取follower的fetch 请求中的offset参数,更新保存在leader端的followerLEO中

3.leader本身的LEO的更新时间:leader向log写消息时.

kafka topic 批量訂閱_kafka topic 批量訂閱_11

第一个问题:
数据处理过程(四步:找leader->leader写日志->Folloer拉取日志->Leader收到全部ISR的ACK返回确认ACK):

  • 先通过zk找到该partition 的leader/brokers/topics/<topic>/partitions/2/state,即使Partition有多少个Replica,Producer只将该消息发送到该Partition的Leader
  • Leader会将消息写入其本地的Log,每个Follower都从Leader fetch (阻塞式长轮询)数据,这种方式Follower存储的数据顺序与Leader保持一致
  • Follower在收到消息并写入其Log后,向Leader发送ACK
  • Leader收到了ISR中所有的Replica的ACK,该消息就被认为已经commit了,Leader将增加HW(HighWatermark)—高水位,并且向Producer发送ACK.