假设某主题只有1个分区,该分区有两个副本:Leader 副本在 Broker1 上,Follower 副本在 Broker2 上,其 Leader 副本写入数据和 Follower 副本同步数据的流程如下图:
三、源码分析 Kafka分区的Leader副本接收客户端生产的数据,写入本地存储;然后Follower副本拉取数据写入本地存储,并更新一系列关键的偏移量。整个流程比较复杂,这里先通过一个简单的方法调用流程来看一下这个过程:1.leader 副本将数据写入本地磁盘 KafkaApis.handleProduceRequest(){
replicaManager.appendRecords(){
appendToLocalLog(){
Partition.appendRecordsToLeader(){
Log.appendAsLeader(){
Log.append(){
//通过LogSegment.append()方法写入磁盘 LogSegment.append() } } } } } }2.leader 副本更新LEO KafkaApis.handleProduceRequest(){
replicaManager.appendRecords(){
appendToLocalLog(){
Partition.appendRecordsToLeader(){
Log.appendAsLeader(){
Log.append(){
//更新Leader副本的LEO值 updateLogEndOffset(appendInfo.lastOffset + 1) } } } } } }3.follower 副本同步数据,携带自身的LEO AbstractFetchThread.doWork(){
maybeFetch(){
buildFetch(fetchStates){
//这里的fetchState.fetchOffset 就是Follower副本的LEO值 builder.add(topicPartition, new FetchRequest.PartitionData( fetchState.fetchOffset, logStartOffset, fetchSize, Optional.of(fetchState.currentLeaderEpoch))) } } }4.leader 副本更新本地保存的Follower副本的LEO ReplicaManager.fetchMessages(){
//获取读取结果 val logReadResults = readFromLog(){
if (isFromFollower) updateFollowerLogReadResults(replicaId, result){
//TODO 更新leader保存的各个follower副本的LEO partition.updateReplicaLogReadResult(replica, readResult){
//TODO 最终更新所有的replica的LEO的值 replica.updateLogReadResult(logReadResult){
//更新LEO对象 logEndOffsetMetadata = logReadResult.info.fetchOffsetMetadata } } } } }5.leader 副本尝试更新ISR列表 ReplicaManager.fetchMessages(){
//获取读取结果 val logReadResults = readFromLog(){
if (isFromFollower) updateFollowerLogReadResults(replicaId, result){
//TODO 尝试更新ISR列表 val leaderHWIncremented = maybeExpandIsr(replicaId, logReadResult){
//更新ISR列表 updateIsr(newInSyncReplicas) } } } }6.leader 副本更新HW ReplicaManager.fetchMessages(){
//获取读取结果 val logReadResults = readFromLog(){
if (isFromFollower) updateFollowerLogReadResults(replicaId, result){
//TODO 尝试更新ISR列表及Leader副本的HW val leaderHWIncremented = maybeExpandIsr(replicaId, logReadResult){
//TODO 尝试更新leader的HW maybeIncrementLeaderHW(leaderReplica, logReadResult.fetchTimeMs){
//取ISR列表中副本的最小的LEO作为新的HW val newHighWatermark = allLogEndOffsets.min(new LogOffsetMetadata.OffsetOrdering) //获取旧的HW val oldHighWatermark = leaderReplica.highWatermark //如果新的HW值大于旧的HW值,就更新 if (oldHighWatermark.messageOffset < newHighWatermark.messageOffset || (oldHighWatermark.messageOffset == newHighWatermark.messageOffset && oldHighWatermark.onOlderSegment(newHighWatermark))) {
//更新 Leader 副本的 HW leaderReplica.highWatermark = newHighWatermark } } } } } }7.leader 副本给 follower副本 返回数据,携带leader 副本的 HW 值 ReplicaManager.fetchMessages(){
//获取读取结果 val logReadResults = readFromLog(){
readFromLocalLog(){
read(){
val readInfo = partition.readRecords(){
//获取Leader Replica的高水位 val initialHighWatermark = localReplica.highWatermark.messageOffset } } } } }8.follower 副本写入数据,更新自身LEO、 ReplicaFetcherThread.processPartitionData(){
partition.appendRecordsToFollowerOrFutureReplica(records, isFuture = false){
doAppendRecordsToFollowerOrFutureReplica(){
Log.appendAsFollower(){
Log.append(){
//更新Follower副本的LEO值 updateLogEndOffset(appendInfo.lastOffset + 1) } } } } }9.follower 副本更新本地的 HW 值 ReplicaFetcherThread.processPartitionData(){
//根据leader返回的HW,更新Follower本地的HW:取Follower本地LEO 和 Leader HW 的较小值 val followerHighWatermark = replica.logEndOffset.min(partitionData.highWatermark) //TODO 更新Follower副本的 HW 对象 replica.highWatermark = new LogOffsetMetadata(followerHighWatermark) }
注意:
对于HW,Leader 副本和 Follower 副本只保存自身的
对于LEO,Follower 副本只保存自身的,但是 Leader 副本除了保存自身的外,还会保存所有 Follower 副本的 LEO 值
无论是Leader副本所在节点,还是Follower副本所在节点,分区对应的Partition 对象都会保存所有的副本对象,但是只有本地副本对象有对应的日志文件
整个数据写入及同步的过程分为九个步骤:
- leader 副本将数据写入本地磁盘
- leader 副本更新 LEO
- follower 副本发送同步数据请求,携带自身的 LEO
- leader 副本更新本地保存的其它副本的 LEO
- leader 副本尝试更新 ISR 列表
- leader 副本更新 HW
- leader 副本给 follower 副本返回数据,携带 leader 副本的 HW 值
- follower 副本接收响应并写入数据,更新自身 LEO
- follower 副本更新本地的 HW 值
下面具体分析这几个步骤。第一、二步在分析日志对象的写数据流程时已经详细介绍过,这里不再赘述(《深入理解Kafka服务端之日志对象的读写数据流程》)。 对于后面的几个步骤,由于发生在不同的节点上,并没有按照这个顺序进行分析,而是分成了
- Follower副本的相关操作:即 第三步、第八步、第九步
- Leader副本的相关操作:即 第四步、第五步、第六步、第七步
- 抽象类:AbstractFetcherThread
- 实现类:ReplicaFetcherThread
abstract class AbstractFetcherThread(name: String,//线程名称 clientId: String,//Cliend ID,用于日志输出 val sourceBroker: BrokerEndPoint,//数据源Broker地址 failedPartitions: FailedPartitions,//线程处理过程报错的分区集合 fetchBackOffMs: Int = 0,//拉取的重试间隔,默认是 Broker 端参数 replica.fetch.backoff.ms 值。 isInterruptible: Boolean = true)//是否允许线程中断 extends ShutdownableThread(name, isInterruptible) {
type FetchData = FetchResponse.PartitionData[Records] type EpochData = OffsetsForLeaderEpochRequest.PartitionData //泛型 PartitionFetchState:表征分区读取状态,包含已读取偏移量和对应的副本读取状态 //副本状态由 ReplicaState 接口定义,包含 读取中 和 截断中 两个 private val partitionStates = new PartitionStates[PartitionFetchState] ...}
其中,type 的用法是:给指定的类起一个别名,如:
type FetchData = FetchResponse.PartitionData[Records]
后面就可以用 FetchData 来表示 FetchResponse.PartitionData[Records] 类;EpochData 同理。
FetchResponse.PartitionData:FetchResponse是封装的FETCH请求的响应类,PartitionData是一个嵌套类,表示响应中单个分区的拉取信息,包括对应Leader副本的高水位,分区日志的起始偏移量,拉取到的消息集合等。
public static final class PartitionData<T extends BaseRecords> {
public final Errors error;//错误码 public final long highWatermark;//从Leader返回的分区的高水位值 public final long lastStableOffset;// 最新LSO值 public final long logStartOffset;//日志起始偏移量 public final Optional preferredReadReplica;// 期望的Read Replica;KAFKA 2.4之后支持部分Follower副本可以对外提供读服务 public final List abortedTransactions;// 该分区对应的已终止事务列表 public final T records;//消息集合}
OffsetsForLeaderEpochRequest.PartitionData:里面包含了Follower副本在本地保存的leader epoch 和从Leader副本获取到的leader epoch
public static class PartitionData {
public final Optional currentLeaderEpoch; public final int leaderEpoch;}
分区读取的状态:
PartitionFetchState:样例类,用来表征分区的读取状态。包含已拉取的偏移量,当前leader的epoch,副本读取状态等
case class PartitionFetchState(fetchOffset: Long,//已拉取的偏移量 currentLeaderEpoch: Int,//当前epoch delay: DelayedItem, state: ReplicaState//副本读取状态 ) {
//表征分区的读取状态 //1.可拉取,表明副本获取线程当前能够读取数据。判断条件是:副本处于Fetching且未被推迟执行 def isReadyForFetch: Boolean = state == Fetching && !isDelayed //2.截断中,表明分区副本正在执行截断操作(