取本地数据_深入理解Kafka服务端之Follower副本如何同步Leader副本的数据

本文详细介绍了Kafka中Follower副本如何同步Leader副本的数据,包括9个步骤,涉及LEO和HW的更新、日志截断和ISR列表维护等关键过程。通过分析Follower和Leader的不同操作,以及相关类和方法的作用,揭示了Kafka副本同步的机制。
摘要由CSDN通过智能技术生成
一、场景分析     Kafka采用的是主写主读的方式,即客户端的读写请求都由分区的Leader副本处理,那么Follower副本要想保证和Leader副本数据一致,就需要不断地从Leader副本拉取消息来进行同步。     由于同一个分区的Leader副本和Follower副本分布在不同的节点上,所以同步的过程可以简单概括为:Follower副本所在节点封装拉取数据的请求并发送给Leader副本所在节点 → Leader副本所在节点接收拉取数据的请求并进行处理,然后返回响应 → Follower副本所在节点接收到返回的响应并进行处理。这个过程中封装拉取请求和处理返回的响应是Follower副本所在节点的一个单独的线程完成的。 二、图示说明

    假设某主题只有1个分区,该分区有两个副本:Leader 副本在 Broker1 上,Follower 副本在 Broker2 上,其 Leader 副本写入数据和 Follower 副本同步数据的流程如下图:

e1ef27109d25dbad9cc949b56801ee39.png

三、源码分析     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 对象都会保存所有的副本对象,但是只有本地副本对象有对应的日志文件

整个数据写入及同步的过程分为九个步骤:

  1. leader 副本将数据写入本地磁盘
  2. leader 副本更新 LEO
  3. follower 副本发送同步数据请求,携带自身的 LEO
  4. leader 副本更新本地保存的其它副本的 LEO
  5. leader 副本尝试更新 ISR 列表
  6. leader 副本更新 HW
  7. leader 副本给 follower 副本返回数据,携带 leader 副本的 HW 值
  8. follower 副本接收响应并写入数据,更新自身 LEO
  9. follower 副本更新本地的 HW 值

   下面具体分析这几个步骤。第一、二步在分析日志对象的写数据流程时已经详细介绍过,这里不再赘述(《深入理解Kafka服务端之日志对象的读写数据流程》)。 对于后面的几个步骤,由于发生在不同的节点上,并没有按照这个顺序进行分析,而是分成了

  • Follower副本的相关操作:即 第三步、第八步、第九步
  • Leader副本的相关操作:即 第四步、第五步、第六步、第七步
    上面提到,Follower副本拉取数据是通过一个单独的线程完成的,所以在分析这几个步骤之前,先看一下这个线程相关的类:
  • 抽象类:AbstractFetcherThread
  • 实现类:ReplicaFetcherThread
先看一下 AbstractFetcherThread 类的定义:
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.截断中,表明分区副本正在执行截断操作(
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值