第一章 认识Apache Kafka
1.1 Kafka快速入门
具体看这篇文章,包含docker版kafka的下载、启动、配置、创建、生产、消费
https://blog.csdn.net/dreambyday/article/details/120254649
1.2 消息引擎系统
消息引擎系统设计两重要因素:
- 消息设计
- 传输协议设计
1.2.1 消息设计
kafka的消息是二进制方式保存,是结构化的消息
1.2.2 传输协议设计
传输协议指定消息在不同系统之间的传输方式
1.2.3 消息引擎泛型
两种常见范型:消息队列模型、发布/订阅模型
- 消息队列模型:
- 基于队列提供消息传输服务
- 多用于进程间通信以及线程通信
- 提供点对点消息传递方式,一对一
- 发布/订阅模型:
- 有主题的概念,订阅了该主题的消费者都能接到该消息
- 可以将消息推给消费者,也可以消费者主动拉取消息
1.3 Kafka概要设计
kafka设计初衷是解决大数据的实时传输,需要考虑下面四个方面的问题。
- 吞吐量/延时
- 消息持久化
- 负载均衡和故障转移
- 伸缩性
1.3.1 吞吐量/延时
- 吞吐量:每秒处理的消息数或者每秒能处理的字节数
- 延时:衡量一段时间间隔,比如发起操作与收到操作相应的时间间隔
吞吐量、延时相互影响。比如kafka发消息2ms延时,那么一条一条处理时吞吐量不超过500条/秒。
但一次发多条消息(批处理),可以让延时占比变小,吞吐量提高。如一次发500条,花了0.5s,加上延时2ms,同样的消息数量花费了更少的时间,吞吐量提升。
但并非批量越大越好,过多的消息可能让吞吐量反而下降,或对系统稳定性造成影响。
- ISR内部副本更新速度跟不上,脱离ISR,可用性降低
- 消息堆积,消费者速度跟不上
- 页空间不足
- 持久化压力增高
- 日志增多
- 系统内存、磁盘压力增大
- 等等,等回过头再想想
1.3.1.1 kafka写入端的高吞吐、低延时
- 每次写入操作系统的页缓存中,由系统决定什么时候持久化,写入磁盘
- 操作系统页缓存是在内存中分配的,速度快
- 采用追加写入的方式,不允许修改已写过的消息,避免磁盘的随机写操作,是磁盘顺序访问型
相关资料
1 什么是页缓存?
页缓存是Linux内核中的一种重要的高速磁盘缓存,是计算机随机存取器RAM(内核缓存)中的一块区域,主要是负责用户空间与磁盘文件之间的高效读写。
页缓存减少了连续读写磁盘文件的次数,操作系统自动控制文件块的缓存与回收生命周期,用访问RAM的缓存代替访问磁盘区域的机制,增强查询
2 磁盘、内存的访问速度
磁盘的顺序访问甚至可能比内存的随机访问IO速度高。
1.3.1.2 kafka消费端的高吞吐、低延时
使用linux的sendfile系统调用,零拷贝技术。
下面参考文章 https://blog.csdn.net/weixin_42096901/article/details/103017044
1 普通的数据传输方式
整个数据拷贝流程如下:
磁盘文件 ==DMAcopy=> 页缓存 ==CPUcopy=> 用户空间缓存 ==CPUcopy=> Socket缓存 ==DMAcopy=>> 网卡
- DMA direct memory access:直接存储器访问,也就是直接访问RAM,不需要依赖CPU的负载
- CPU :中央核心处理器,主要用于计算
共发生了四次拷贝,其中CPU也用于拷贝,并且发生了用户态和内核态的切换,浪费CPU的计算时间。
2 零拷贝方式
上面是零拷贝实现的两种方式,可以看出两种方式都避免了用户态和内核态的切换,并且减少了CPU拷贝的次数。
零拷贝的主要任务就是避免CPU拷贝,让其他组件干拷贝的事,减少数据在内核态和用户态的切换。
1.3.1.3 kafka达到高吞吐、低延时的总结
- 大量使用页缓存,内存操作速度快且命中率高
- kafka不直接参与IO操作,由操作系统完成
- 追加写入,磁盘顺序读写
- 使用零拷贝加强网络间数据传输效率
1.3.2 消息持久化
kafka将消息持久化到磁盘上
1.3.2.1 消息持久化的好处
- 解耦消息发送与消息消费:生产者只需要将消息生产出来交给kafka服务器保存即可,提升了整体的吞吐量。
- 我的理解是生产者消费者的生产能力和消费能力之间耦合性没那么强了,不需要生产出来就要消费
- 灵活的消息处理:可以消息重演,重复消费
1.3.2.2 持久化的方式
普通的系统持久化时尽量使用内存,内存快耗尽时一次性刷进磁盘。
kafka数据进入内存后,立刻写进磁盘持久化日志,写完后通知给客户端已成功写入,即保存数据,又避免对内存的消耗,将节省的内存留给页缓存使用。
有个疑问,kafka每次先写到内存,然后持久化,再写到页缓存,最后释放内存吗
1.3.3 负载均衡和故障转移
- 负载均衡:将系统的负载根据一定的规则均衡地分配到所有参与工作的服务器上
- 通过分区领导选举的方式
- 故障转移:当服务器意外终止时,集群快速检测其失效,并将该服务器上的服务转移到其他服务器上
- 使用会话的机制
- 注册到zookeeper上
1.3.4 伸缩性
伸缩性表示向分布式系统中增加额外的计算资源时吞吐量提升的能力。
阻碍线性扩容的一个很常见因素是状态的保存,kafka通过zookeeper维护状态,kafka只保存了很轻量的状态,内部维护一致性代价低。
1.4 Kafka基本概念与术语
kafka的标准定位是分布式流式处理平台
kafka架构图:
1.4.1 消息
消息的具体设计放在第六章详细说明,此节仅说明特点。
图为V1版本的消息格式。
两个特点:
- kafka使用二进制字节数组保存这些字段,没有多余的比特位浪费。 如果使用java对象保存,则开销可能相当大(对象头、8倍位数补齐、有页缓存,堆上的对象在页缓存中还有一份)
- 使用页缓存而非在java堆内存,在kafka broker实例崩溃时,堆上内存消失,但页缓存还在,重启后可以继续提供服务。
1.4.2 topic 、partition、offset
- topic(主题):一个逻辑概念,代表一类消息,可以用于区分一类业务
- partition(分区):一个topic有多个分区。单纯的为了提升吞吐量,可以看作多线程/多进程
- offset(位移):partition分区上每个消息的唯一序列号
kafka的消息可以用<topic,partition,offset>三元组唯一定位。
1.4.3 replica 、leader、follower
- replica(副本):依靠冗余机制保证高可靠,备份多份日志,这些日志被称为副本
- 同一个partition的多个replica不会分配在同一台broker上
- leader(领导者):只有leader向外提供服务 ,相应客户端发来的消息写入和消费请求
- follower(跟随者):不提供服务,只被动地向leader请求数据,当leader宕机时会从follower中选举leader
1.4.4 ISR、AR、OSR
- ISR:In-Sync Replicas 副本同步队列
- AR:Assigned Replicas 所有副本
- OSR:Outof-Sync Replicas 未同步副本队列
AR = ISR + OSR
这部分书中内容描述较少,后续详细描述
1.5 Kafka使用场景
- 消息传输
- 网站行为日志追踪
- 审计数据收集
- 日志收集
- Event Sourcing。使用事件序列表示状态变更
- 流式处理
第二章 Kafka发展历史
2.1 Kafka的历史
2.2 Kafka版本变迁
书中只陈列到1.0.0版本的功能变迁,最新的信息可以到Kafka最新动态OrcHome查看。
2.3 如何选择Kafka版本
2.4 Kafka与Confluent
第三章 Kafka线上环境部署
第四章 producer开发
producer的首要功能是向topic的哪个分区写入消息。
4.1 producer概览
主要流程如下:
- 构造ProducerRecord
- topic 主题
- partition 指定分区
- key 指定消息的key
- value 指定消息的值
- timestamp 时间戳,默认当前
- 使用序列化器对ProducerRecord序列化
- topic元数据缓存
- 使用分区器partitioner对消息分区
每个分区有多个副本,这些副本中只有leader副本负责响应请求和处理请求,所以要找这个分区的leader。- 消息指定了分区,则直接发到这个分区
- 没指定分区,指定了key,则根据key的哈希值选择分区
- 没指定分区和key,则分区器使用轮询的方式确认目标分区
- 进入消息缓冲池
- 请求写入broker
4.2 构造producer
一些参数信息
- bootstrap.servers 指定一组host:post对,指向broker服务器的连接。指向部分broker就可以发现集群中所有boker。可以指向多个broker,用于故障转移。
- key.serializer key的序列化器
- value.serializer value的序列化器
- acks 控制消息的持久性
- acks=0 producer不等待leader broker的处理结果,发完一条消息立即开启下一条消息的发送。吞吐量高,但容易丢失消息。
- acks=1 producer等待leader broker处理结果,但leader broker收到消息后,仅将消息写入本地日志,便发送相应结果给producer。吞吐量一般,可靠性一般。
- acks=-1 producer等待leader broker的处理结果,leader broker在将消息写入日志,并且isr中所有副本都成功写入自己的本地日志后才会发送响应结果给producer。吞吐量低,可靠性高。
- buffer.memory 指定producer端用于缓存消息的缓冲区大小
- 单位是字节,默认32MB大小
- Java版本producer启动时先创建一块内存缓冲区保存待发送的消息,另一个专属线程从缓冲区读取消息并执行发送
- 写速度超过发送消息的速度,会阻塞写,若一段时间后写还是超过发送的速度,producer会抛出异常
- compression.type 设置producer端是否压缩消息,默认不压缩
- GZIP
- Snappy
- LZ4
- Zstandard
- retries 重试次数,默认0,不重试
- 重试可能造成消息重复发送(已处理但未提交崩溃或未响应)
- 重试可能造成消息乱序 多消息发送发送部分失败导致乱序
- batch.size 每次发送消息时最大的消息数量
- 默认16KB
- 数值越大,内存压力越大。数值越少,吞吐量量越低
- linger.ms 控制消息发送延时
- 默认为0,消息立即发送,无序关系batch是否填满
- max.request.size 控制producer发送请求的大小(我的理解是单条消息的大小)
- request.timeout.ms 发送请求给broker后,broker需要在规定时间内将结果返还给producer
- 默认30s
- 超时时,producer在回调函数中抛出TimeoutException
- 负载大时30s很容易超时,适当调整
4.3 消息分区机制
默认分区机制
- 指定分区,分配到该分区
- 不指定分区,指定key,分配到key哈希到的分区
- 不指定分区和key, 分区轮询
自定义分区方法
- producer创建类,实现分区接口
- KafkaProducer中的Properties设置partitioner.class参数
4.4 消息序列化
4.4.1 默认序列化
- broker支持接收各种类型的消息
- 序列化器serializer负责在producer发送前将消息转为字节数组
- 解序列化器deserializer负责将consumer接收到的字节数组转为对应的对象
- broker也可以进行序列化、解序列化,但一般来说,broker只用于传输,不进行这两个操作
- 常用序列化器:
- ByteArraySerializer 已经是字节数组,不做操作
- ByteBufferSerializer 序列化ByteBuffer
- BytesSerializer 序列化Kafka自定义Bytes类
- DoubleSerializer 序列化Double类型
- IntegerSerializer 序列化Integer类型
- LongSerializer 序列化Long类型
- StringSerializer 序列化String类型
4.4.2 自定义序列化
- 定义数据对象格式
- 创建自定义序列化类,实现序列化接口
- 在构造KafkaProducer的Properties对象中设置key和value的序列化器
4.5 producer拦截器
- 在用户发送消息及producer回调逻辑前都可以对消息进行操作
- 可以指定多个interceptor按顺序作用同一条消息形成拦截链
- interceptor实现的接口里有
- onSend 运行在用户主线程中,确保消息被序列化以计算分区前调用该方法
- onAcknowledgement 方法在消息被应答前或消息发送失败时调用,不建议放入耗费时间的逻辑
- close 关闭interceptor,执行资源清理工作
4.6 无消息丢失配置
4.6.1 producer端配置
- block.on.buffer.full=true
- 0.9.0.0版本被弃用,用max.block.ms代替
- 内存缓冲区被填满时,producer处于阻塞并停止接受消息,而非抛出异常
- acks=all或-1
- ISR中所有follower都同步完消息后才响应给producer
- retries=Integer.MAX_VALUE
- producer只会重试可恢复的异常,设置一个非常大的值可以保证消息不丢失
- 使用带有回调机制的send
- 不带回调的send无法得到消息发送结果的通知
- Callback逻辑中显式立即关闭producer
- 显式调用KafkaProducer.close(0)
- 不使用close(0),默认情况下producer允许未完成 消息发送出去
4.6.2 broker端配置
- unclean.leader.election.enable=false
- 关闭unclean leader选举,即不允许非ISR中的副本被选举为leader,从而避免broker因日志水位截断造成消息丢失
- repliaction.factor>=3
- 参考Hadoop通用三备份原则,强调使用多个副本保存分区消息
- min.insync.replicas>1
- 控制某条消息至少被写入到ISR中多少个副本才算成功
- 必须在producer端acks被设置成-1才有意义
- 防止ISR中只有一个
- 确保replication.factor>min.insync.replicas
- 若两者相等,那么一个副本挂掉时,分区就无法正常工作
4.7 消息压缩
- 消息压缩能降低磁盘、宽带占用,但会消耗额外CPU时钟周期。
- 一般producer端压缩,broker端保持,consumer端解压缩
- 视情况调整压缩位置,producer端消耗带宽多而CPU利用少,可以进行压缩
- 一般batch越大,压缩时间越长
4.8 多线程处理
实际环境只使用一个用户主线程通常无法满足所需的吞吐量目标,因此需要构造多个线程或多个进程同时给Kafka集群发送消息,存在两种基本使用方法
- 多线程单KafkaProducer实例
- 多线程多KafkaProducer实例
分区数不多的Kafka集群,比较推荐单KafkaProducer。分区比较多的集群,推荐采用多KafkaProducer,可控性比价高。
第五章 consumer开发
5.1 consumer概览
消费者分为两类,一个是独立消费者standalone consumer,一个是消费者组consumer group
5.1.1 消费者consumer
- 一个consumer group可能有若干个consumer实例
- 对于一个group而言,topic下的每条消息只能发送到group下的一个consumer下
- topic消息可以发送到多个consumer group中
Kafka通过consumer group实现对两种消息引擎的支持
- 所有consumer的实例都属于相同group-基于队列的模型,每条消息只会被一个consumer消费
- consumer属于不同group-基于发布/订阅的模型
consumer group还可以实现高伸缩性、高容错性的机制
- 多个consumer同时读取kafka消息
- 某个consumer崩溃,group可以将崩溃的consumer负责的分区交给其他consumer负责
5.1.2 位移offset
指的是consumer端的offset,每个consumer实例为其消费的分区维护自己的位置信息。
1.将offset保存在服务器端的缺点:
- broker变成有状态,增加集群的同步成本,影响伸缩性
- 需要引入应答机制确认消费成功
- 每一个broker要保存多个consumer的offset,浪费资源
2.Kafka对offset的处理:
- 使用consumer存储offset,只需要保存一个长整型数据
- 引入检查点机制定期对offset持久化
5.1.4 位移提交
consumer需要定期向kafka集群汇报自己数据处理进度,被称为位移提交offset commit。
1. 旧版本consumer定期将位移提交到zookeeper的固定节点上
缺点:
- zookeeper只是协调服务的组件,不适合作为位移信息的存储组件
- zookeeper不擅长高并发读写操作
2. 新版consumer将位移提交到Kafka的内部topic上(__consumer_offsets)
新版本consumer不再需要连接zookeeper,位移提交到集群内部topic中
5.1.5 __consumer_offsets
5.1.6 消费者组重平衡(consumer group rebalance)
rebalance只对consumer group有效
5.2 构建consumer
5.2.1 consumer程序实例
5.2.2 consumer脚本命令
5.2.3 consumer主要参数
1. session.timeout.ms
consumer group检测组内成员发送崩溃的时间。
2. max.poll.interval.ms
consumer消息处理逻辑的最大时间。若consumer两次poll的时间间隔,也就是consumer处理某次消息的时间,超过了参数设置时间,该consumer就会被移除消费者组。
- 造成不必要的重平衡,需要重新加入group
- 被踢出group后处理的消息造成的位移变更无法提交,在重平衡后会重复消费
3. auto.offset.reset
指定了无位移信息或唯一越界时kafka的应对策略。
有三个值。
- earliest:从最早的唯一开始消费,但唯一不一定是0
- latest:从最新处位移开始消费
- none:抛出异常
4. enable.auto.commit
执行consumer是否自动提交位移。true为consumer后台自动提交唯一,false需要用户手动提交。
5. fetch.max.bytes
指定consumer端单词获取数据的最大值,若参数值过小,则consumer无法消费这些消息
6. max.poll.records
控制单词poll调用返回的最大消息数。如果发现consumer端的瓶颈在poll速度过慢,可以适当调整该参数的值。
7. heartbeat.interval.ms
8. connections.max.idle.ms
控制Socket空闲多久才开始回收。
- 当参数过小时,可能会周期性的观测到平均处理请求升高,这是因为socket关闭后需要consumer重新创建连向broker的Socket连接。
- 当参数过大或设为-1(永不关闭)时,Socket资源开销会一直存在。
5.3 订阅topic
5.4 消息轮询
5.4.1 poll内部原理
- 旧版本采用为每个要读取的分区创建专有线程去消费
- 新版本采用类似IO模型的poll或select,用一个线程同时管理多个socket连接,即同时与多个broker通信实现消息的并行读取。
consumer有两个线程
- 用户主线程,也是创建KafkaConsumer的线程
- 后台心跳线程
5.4.2 poll使用方法
- poll根据当前consumer的消费唯一返回消息集合
- 首次调用时,消费者组被创建,并根据auto.offset.reset设定唯一
- poll方法调用时,满足下面两个条件之一就可以返回数据
- 获取足够多的可用数据(比如consumer要求一次获取1MB)
- 等待时间超过了指定的超时设置
- poll的两种情形使用方式
- consumer需要定期执行其它子任务,推荐poll(较小超时时间)+运行标识布尔变量
- consumer不需要定期执行子任务,推荐poll(MAX_VALUE)+捕获异常
5.5 位移管理
5.5.1 consumer位移
consumer需要定期向kafka提交自己的位置信息。
1. 常见三种消息交付语义保证如下:
- 最多一次:消息可能丢失,但不会重复处理
- consumer获取消息,处理前就提交位移
- 最少一次:消息不丢失,但可能重复处理
- consumer获取消息,处理消息后提交位移
- 精确一次:消息一定会被处理且处理一次
- 通过事务原子性,保证精确一次
2. consumer各种位置信息
- 上次提交位移(last committed offset):consumer最近一次提交的offset值。
- 当前位置(current position):consumer已读取但尚未提交时的位置
- 水位(watermark或high watermark):属于分区日志的概念,对于水位之下的所有消息,consumer都可以读取。一般来说ISR中所有副本都保存了某条消息,水位才会上升。
- 日志终端位移(Log End Offset,LEO):日志最新位移,同样属于分区日志管辖,表示某个分区副本当前保存信息对应的最大胃一直。
5.5.2 新版本consumer位移管理
consumer会在Kafka集群的所有broker中选择一个broker作为consumer group的coordinator。
consumer是通过向所属的coordinator发送位移提交来实现
每个位移提交请求都会往内部topic(__consumer_offsets)对应分区追加写入一条消息。消息的key是group.id、topic、partition的元组,value是位移值。同一个key可能提交多次,但只有最新的key是有效的。
5.5.3 自动提交与手动提交
5.6 重平衡(rebalance)
5.6.1 概述
是一种协议,规定一个consumer group如何达成一致来分配订阅topic所有分区。由coordinator负责对组进行rebalance
5.6.2 rebalance触发条件
- 组成员变更,如新consumer加入组,现有consumer离开组或崩溃
- 订阅的topic数量发生变更,比如基于正则表达式的订阅,新增的topic会触发rebalance
- 组订阅的topic的分区数发生变更。
5.6.3 Rebalance分区分配
rebalance时,group下的所有consumer都会参数分区分配。Kafka提供了三种分配策略:
- range策略
- 基于范围的思想,将单个topic的所有分区按照顺序排列,然后把这些分区划分成固定大小的分区段,依次分配给每个consumer
- round-robin策略
- 把所欲topic的所有分区顺序摆开,然后轮询式地分配给每个consumer
- sticky策略
- 避免了无视历史分配方案的缺陷,尽可能减小两次Rebalance分配的差异
5.6.4 rebalance generation
每次rebalance都会隔离上次rebalance的数据,并且会记录此次版本。延迟提交offset携带了旧的generation信息,会被consumer group拒绝。
5.6.5 rebalance协议
- JoinGroup:consumer请求加入组
- SyncGroup:group leader把分配方案同步更新到组内所有成员
- Heartbeat:定期向coordinator汇报自己仍然存活
- LeaveGroup:主动通知coordinator自己将会离开组
- DescribeGroup:查看组的所有信息,供管理员使用
5.6.6 rebalance流程
1. 流程
- consumer group先确定coordinator所在的broker
- 创建与该broker相互通信的Socket连接
- 加入组
- 组内所有consumer向coordinator发送JoinGroup请求
- coordinator从中选择一个consumer单人group的leader,并且将成员信息及订阅信息发送给leader
- 同步更新分配方案
- leader根据分配策略决定每个consumer负责哪些分区
- leader将分配方案封装进SyncGroup请求并发送给coordinator。组内所有成员都会发送SyncGroup请求,但只有leader的请求中包含了分配方案。
- coordinator接收到方案后将每个consumer对应的方案单独抽出来作为SyncGroup请求的response返还给各个consumer
2. 好处
- 灵活性高,只需要重启consumer就可以改变分区策略
- 同一个机架上的分区数据被分配给相同机架上的consumer,减少网络传输的开销。(这个流程怎么体现是同一个机架上的数据分配给了同机架上的consumer?)
5.6.7 Rebalance监听器
Rebalance监听器可以支持用户将位移提交到外部存储中。前提是用户使用consumer group
5.7 解序列化
Kafka consumer从broker端获取的消息格式是字节数组,consumer需要把它还原成指定类型
5.7.1 默认解序列化器
- ByteArrayDeserializer:什么都不需要做,已经是字节数组
- ByteBufferDeserializer:解序列化成ByteBuffer
- BytesDeserializer:解序列化Kafka自定义Bytes类
- DoubleDeserializer:解序列化Double类型
- IntegerDeserializer
- LongDeserializer
- StringDeserializer
5.7.2 自定义解序列化器
5.8 多线程消费实例
KafkaConsumer是非线程安全的,用户无法在多线程中共享一个KafkaConsumer实例。
5.8.1 每个线程维护一个KafkaConsumer
5.8.2 单KafkaConsumer实例+多worker线程
使用全局的kafkaConsumer执行消息获取,将获取到的消息合集交给线程池中的worker线程执行工作,之后由worker处理完成后上报位移状态,由全局consumer提交位移
5.8.3 两种方法对比
5.9 独立Consumer
第六章 Kafka设计原理
6.1 broker端设计架构
6.1.1 消息设计
版本变迁
1. V0版本
- CRC校验码:4字节,用于保证消息传输过程不会被恶意篡改
- magic:1字节版本号。V0版本magic=0,V1版本magic=1
- attribute:1字节,目前只使用低3位表示消息压缩类型
- key长度:4字节,未指定key时,值为-1
- key值:由key的长度来切出key的值。
- value长度:4字节
- value值:消息的value
除去key值和value值,所有字段被称为消息头部(message header),共占14字节。
key或value长度为-1时,保存其长度还是需要4字节。
2. V1版本
V0版本有一些弊端
- 没有消息的时间信息,Kafka删除过期日志只能依靠日志段文件的“最近修改时间”,容易受到外部操作的影响。
- 许多流式处理框架需要消息保存时间信息以便对消息执行时间窗口等聚合操作
V1与V0版本差别
- V1.1版本attribute字段第4位用于指定时间戳类型
- CREATE_TIME 消息创建时由producer指定时间戳
- LOG_APPEND_TIME 发送到broker端时由broker指定的时间戳
- 新增时间戳,8字节
3. V2版本
- 增加消息总长度字段
- 保存时间戳增量
- 保存位移增量
- 增加消息头部
- 去除消息级CRC校验。不再为每条消息计算CRC32值,而是对整个消息batch进行CRC校验
- 废弃attribute字段,压缩类型、时间戳等信息都保存在外层的batch格式字段中
6.1.2 集群管理
6.1.3 副本与ISR设计
副本: 利用多份相同备份共同提供冗余机制保持系统高可用性,这些备份被称为副本。
ISR: Kafka集群动态维护的一组同步副本集合
follower副本同步: follower副本只做一件事,向leader副本请求数据
- 起始位移:该副本当前所含第一条消息的offset
- 高水印值HW:保存该副本最新一条已提交消息的位移。超过HW的位移都是未提交的。只有leader的HW决定consumer可获取消息范围。
- 日志末端位移(log end offset,LEO):副本日志中下条待写入消息的offset。ISR中所有副本都更新对应的LEO后,leader副本才会移动HW值表名消息写入成功。
ISR设计: ISR是与leader同步的副本集合
- 0.9.0.0版本前,使用replica.lag.max.messages
- 这个参数控制follower副本落后leader副本消息数,一旦超过这个数,该follower视为不同步,移除ISR
- leader与follower不同步的几种情况
- follower请求速度跟不上,网络IO开销过大
- follower进程卡主,如Full GC或Bug
- 新创建的follower副本。
- 缺陷
- 一次发送多条消息可能造成follower落后过多
- 参数不易设置,需要用户根据业务场景猜测该值
- 0.9.0.0版本之后,使用replica.lag.time.max.ms,默认10s
- 参数控制follower副本落后leader副本的时间间隔
6.1.4 水印watermark和leader epoch
1. LEO更新机制
LEO指向下一条待写入消息的位置,所以其指向的位置上是没有消息的。
follower的LEO
- follower副本只向leader副本请求数据
- 存储在两个地方,一个存储在follower副本所在broker缓存上,一个存储在leader副本所在broker的缓存上
- follower副本所在broker上的LEO用于更新follower自身HW值。leader副本所在broker上的follower的LEO用于确定leader副本何时更新HW
- 两个地方的LEO更新时间
- follower副本端的follower副本的LEO在leader将数据返回给follower并且向底层log写数据后更新
- leader副本端的follower副本的LEO。leader处理follower的fetch请求时,从自己的log中读取数据,但在给follower返回数据前先更新follower的LEO
2. HW更新机制
follower的HW值不会超过leader的HW值。
follower的HW更新机制
- follower更新HW发生在其更新LEO后
- 比较当前LEO和FETCH相应中leader的HW值,取小者为HW
leader的HW更新机制
- leader尝试更新分区HW值的四种情况
- 副本成为leader副本时。
- broker出现崩溃导致副本被踢出ISR时,需要检查是否会波及此分区
- producer向leader副本写入消息时,会更新leader的LEO,需要查看HW是否需要更新
- leader处理follower的FETCH请求时,从底层的log读取数据,尝试更新分区HW值。
leader更新HW的方式
- 确定可以比较LEO的副本
- 处在ISR中
- 副本LEO落后于leader的LEO时长不大于replica.lag.time.max.ms参数值
因为存在已经追上leader的非ISR的副本,这些副本处于具有进入ISR的资格但是还没进入的状态。如果不把这些算进来,就能出现分区HW值大于ISR的HW值的情况。
- leader根据所有满足条件的副本的LEO(leader所在broker存储了follower的LEO),取出最小的LEO作为HW。
3. Kafka备份原理
初始状态
- leader以及follower的HW和LEO都是0(源代码会初始化LEO为-1)(两个问题,一是HW应该指向当前ISR已同步的数据,也就是HW指的位置应该是有值啊。另一个是LEO初始化为0可以理解,-1是什么含义)
- follower不断给leader发送fetch请求,但是初始状态没数据,请求会被寄存在leader的purgatory中,500ms(replica.fetch.wait.max.ms)超时后会强制完成。如果寄存期间producer端发来数据,Kafka会唤醒FETCH请求,让leader处理
purgatory(n. 炼狱; 受难的处所(或状态); 惩戒所; 折磨; 磨难;)奇怪的翻译 是Kafka暂存请求对象的地方。
leader副本写入消息后follower副本发送fetch请求
1)leader初始状态LEO=0,HW=0,remote LEO=0
2)leader接受生产者消息后,将消息写入底层日志,更新LEO=1
3)尝试更新leader副本的HW值,假设此时follower尚未发送FETCH请求,那么leader端保存的remoteLEO仍是0,取自己的LEO与remoteLEO的最小值,发现为0,与之前相同,不更新HW值
4)写入消息成功后,leader端HW=0,LEO=1,remoteLEO=0
5)follower发送FETCH请求,leader端开始处理请求
6)leader读取log数据
7)根据FETCH请求中的FETCH OFFSET确定remoteLEO=0
8)更新leader的分区HW=min(leader的LEO,remoteLEO)=0
9)把数据和当前分区HW=0值发送给follower副本
10)follower接收到response开始进行如下操作
11)写入本地log
12)更新follower HW=min(follower LEO,leader HW)=0
可以看到,第一轮FETCH结束,虽然leader和follower都已经保存了这条消息,但是分区的HW还是0,它是在第二轮的FETCH中被更新的。
FETCH请求保存在purgatory中时生产者发来消息
1)producer发送消息,leader写入本地log,更新LEO,唤醒purgatory中的请求
2)更新分区HW
3)后续同上
两种情况都存在缺陷,有可能引起如下问题
- 备份数据丢失
- 备份数据不一致
4. 基于水印备份机制的缺陷
备份数据丢失如图
在第二轮fetch的时候,leader在相应fetch后,会向producer发送同步完成确认。follower在接收相应后,未更新HW便宕机,然后重启,此时LEO会调整为之前的HW值,进行日志截断。调整完成后leader宕机,此时原来的follower会变成leader,即使原来的leader重启也会变成follower,在fetch后会将自己的HW设为0,原来的msg0便会永久丢失。
数据不一致
5. 0.11.0.0版本解决之道
采用leader epoch值解决。在leader中开辟缓存,并记录版本号
6.1.5 日志存储设计
6.1.6 通信协议
6.1.7 controller设计
1. controller概览
controller控制器是用来管理和协调Kafka集群的,每个集群只能有一个controller
2. controller管理状态
controller维护的状态分为两类:
- 每台broker上的分区副本
- 每个分区的leader副本
从维度上看状态分为两类:
- 副本状态
- 分区状态
副本状态机之七种状态,特指副本的状态:
- NewReplica:controller创建副本的初始状态,这个状态的副本只能为follower副本
- OnlineReplica:启动副本后变成这个状态,这个状态下,副本既可以成为follower副本也可以成为leader副本
- OfflineReplica:副本所在broker崩溃后,副本会变为该状态
- ReplicaDeletionStarted:开启topic删除操作,topic下所有分区的所有副本会被删除,此时副本进入该状态
- ReplicaDeletionIneligible:副本删除失败后进入该状态
- NonExistentReplica:副本被成功删除后进入该状态
分区状态机之四种状态,管理的对象是分区:
- NonExistent:分区不存在或已删除的分区
- NewPartition:一旦被创建,分区就处于该状态。Kafka已经为分区确定了副本列表,但尚未选举出leader和ISR
- OnlinePartition:一旦该分区的leader被选举,就进入该状态
- OfflinePartition:选举leader后,若leader所在broker宕机,则分区将进入该状态,表名无法正常工作。
3. controller职责
- 更新集群元数据信息
- 创建、删除、分区扩展Topic
- 分区重分配
- preferred leader副本选举
- broker加入集群、崩溃、受控关闭
- controller leader选举
6.2 producer端设计
6.3 consumer端设计
6.4 实现精确一次处理语义
kafka默认提供至少一次语义。
通过事务可以实现精确一次exactly once
6.4.1 幂等性producer
一个操作执行一次与多次的结果是相同的,这种操作是幂等操作。
发送到broker端的每批消息都会被赋予序列号用于消息去重,并且序列号会被保存在底层日志中
Kafka还会为每个producer实例分配producer id,简称PID。消息发送到的每个分区都有对应的序列号值,从0开始单调增加。类似Map,key为(PID,分区号),value为序列号。如果发现发送的消息的序列号小于等于保存的序列号,那么broker就会拒绝写入这条消息。
每个producer分配不同的PID,只能保证单producer实例的精确一次
6.4.2 事务
- 一组消息可以放到一个原子性单元中统一处理。处于事务中的这组消息可以从多个分区消费,也可以发送到多个分区中去。
- Kafka为实现事务要求应用程序必须提供位移的id表征事务,这个id被称为事务id。事务id由用户显示提供,PID由producer自行分配。
- 通过事务id,Kafka可以保证
- 跨应用会话间的幂等发送语义
- 支持跨会话间的事务恢复
- consumer角度,事务的支持弱一些
- 对于compacted的topic而言,事务中的消息可能已经被删除了
- 事务可能跨多个日志段,因此若老的日志段被删除,用户将丢失事务中的部分消息
- consumer程序可能seek定位事务中的任意位置,造成丢失部分消息
- consumer可能选择不消费事务中的所有消息,即无法保证读取事务中的全部消息
使用了一种控制消息,和普通消息基本一样,但是在消息属性字段中专门用1位表明它是控制消息,有两类:COMMIT和ABORT,其目的是让consumer能够识别事务边界,从而读取整个事务下所有的消息