一、简介
在 Kafka 启动的时候,会初始化 ReplicaManager 这样的一个类,主要就是负责数据写入磁盘的,在构建 ReplicaManager 的时候,在其内部构建了一个 ReplicaFetcherManager 用来处理 leader , follow 之间的数据同步。 这个 ReplicaFetcherManager 会创建一个后台线程,构建线程自己的 Channel , Selector , NetWorkClient 进行网络通信,调用 dowork() 开始工作
override def doWork() {
val fetchRequest = inLock(partitionMapLock) {
// 针对 leader 都在某个 broker 上的一批 follow 分区,构建了一个 fetchRequest
// 然后发送给 Broker
val fetchRequest = buildFetchRequest(partitionMap)
if (fetchRequest.isEmpty) {
trace("There are no active partitions. Back off for %d ms before sending a fetch request".format(fetchBackOffMs))
partitionMapCond.await(fetchBackOffMs, TimeUnit.MILLISECONDS)
}
fetchRequest
}
if (!fetchRequest.isEmpty)// 处理请求
processFetchRequest(fetchRequest)
}
二、同步流程
2.1、构建 Fetch 请求
首先会通过 buildFetchRequest() 方法,构建一个 fetch 请求,会将要拉取的 partition 和 partition 的信息封装一下
- 需要从哪开始拉取
- 最多拉取多少数据,默认为 1024 * 1024
- 最少拉取多少数据,默认为 1 ,如果连 1 都没有的话,会默认等待 500ms ,如果等待 500ms 也是没有数据的话,就会直接返回
- Topic Name
- 最大等待时间
- 需要拉取的分区
protected def buildFetchRequest(partitionMap: Map[TopicAndPartition, PartitionFetchState]): FetchRequest = {
val requestMap = mutable.Map.empty[TopicPartition, JFetchRequest.PartitionData]
partitionMap.foreach { case ((TopicAndPartition(topic, partition), partitionFetchState)) =>
if (partitionFetchState.isActive)
requestMap(new TopicPartition(topic, partition)) = new JFetchRequest.PartitionData(partitionFetchState.offset, fetchSize)
// 从哪开始拉,拉取的数据量有多大,默认是 1M
}
// 一次 fetch 至少拉取1字节数据,如果一个字节数据都没有,会等待一段时间
// 默认最多等待 500 ms ,如果 500ms 还没有的话,就会返回
new FetchRequest(new JFetchRequest(replicaId, maxWait, minBytes, requestMap.asJava))
}
2.2、发送 Fetch 请求
这里主要就是采用了一个同步的请求方式,必须要等到获取到响应才可以,其底层发送逻辑和之前服务端分析的是一样的,Kafka 基于 NIO 自己封装的那套组件。
val header = apiVersion.fold(networkClient.nextRequestHeader(apiKey))(networkClient.nextRequestHeader(apiKey, _))
try {
if (!networkClient.blockingReady(sourceNode, socketTimeout)(time))
throw new SocketTimeoutException(s"Failed to connect within $socketTimeout ms"
#### 2.3、处理 Fetch 请求
##### 2.3.1、拉取数据
(1)首先根据 topic 和 partition 确定文件。
(2)通过 startOffset 确定 log 文件的 Segment 日志段。
(3)读取 segment 数据,通过稀疏索引确定文件的 position 。
(4)位置确认后,底层通过 NIO 将数据读取为 ByteBuffer ,封装为 FileMessageSet,然后封装为 FetchDataInfo。
##### 2.3.2、更新元数据
- follower 更新 fetch 时间
follow 每次发送 fetch 请求过来之后,都会更新一下 fetch 请求的时间。
- follower 更新 LEO
在 KafkaApis 处理 Fetch 请求,执行完拉取数据操作之后,会通过拉取到的数据更新 Follow 的 LEO。
- ISR 列表更新
收缩:在 **ReplicaManager** 启动的时候,内部会初始化一个调度任务,默认是 **每隔 10秒 调度一次** , 它主要就是负责检查所有 follow 的 fetch 请求时间,follow 在发送 fetch 请求的时候,不仅会更新 LEO ,也会更新自己发送 fetch 请求的时候, **当前时间 - 上一次更新时间 > 最大等待时间 (maxLagMs , 默认是 10s) ** 就认为应该要踢出 ISR 列表,这时就会对 ISR 列表进行更新,但这里对 ISR 列表进行更新之后,需要对 Leader 的 HW 进行更新,就比如说此时 Leader 的 LEO = 90 ,follow1 的 LEO = 80 ,follow2 的 LEO = 90,那么此时 Leader 的 HW = 80,但如果此时剔除掉 follow1 ,那么此时就应该更新 Leader 的 HW = 90。
增加:在 处理 Fetch 请求拉取完数据之后,会判断当前 follow 的 LEO 是否 **大于等于 Leader 的 HW** ,如果满足条件就会重新加入到 ISR 集合当中,并且因为 ISR 集合的变动,会触发更新 Leader 的 HW 操作。
更新 HW :这里更新 HW 的逻辑比较简单,就是获取全部 follow 的 LEO ,找到 LEO 的最小值,作为 Leader 新的 HW ,如果新的 HW 小于旧的 HW 则不会进行更新。
```scala
private def maybeIncrementLeaderHW(leaderReplica: Replica): Boolean = {
val allLogEndOffsets = inSyncReplicas.map(_.logEndOffset) // 获取所有副本的 LEO
// 找到所有 LEO 的最小值
val newHighWatermark = allLogEndOffsets.min(new LogOffsetMetadata.OffsetOrdering)
val oldHighWatermark = leaderReplica.highWatermark
// 如果新的比旧的大,就做一个变更
if (oldHighWatermark.messageOffset < newHighWatermark.messageOffset || oldHighWatermark.onOlderSegment(newHighWatermark)) {
leaderReplica.highWatermark = newHighWatermark
debug("High watermark for partition [%s,%d] updated to %s".format(topic, partitionId, newHighWatermark))
true
} else {
debug("Skipping update high watermark since Old hw %s is larger than new hw %s for partition [%s,%d]. All leo's are %s"
.format(oldHighWatermark, newHighWatermark, topic, partitionId, allLogEndOffsets.mkString(",")))
false
}
}
- fetch 的时候如果刚好没有新的数据,如何延迟执行
当 fetch 请求没有拉取到数据的时候,会将 fetch 请求封装为一个 delayedFetch ,然后 放入延迟调度队列中 ,默认是 500ms ,如果 500ms 都没有新数据写入的话,500ms 一到,就会从延迟队列中出来,返回一个空的数据包回去。 如果 500ms 内有新数据加入,也就是说 leader 有数据写入,当 log.append() 执行完毕之后,会唤醒延迟调度队列中,这个 topic ,partition 对应的那个延迟任务,因为封装 delayFetch 是有一个回调函数的,这时就会通过这个回调函数返回一个空的数据包回去,因为返回的数据是空的,所以会再次发送一个 Fetch 请求,这时因为 leader 已经写完数据了,在进行 fetch 肯定是可以获取到的。
- ack = all 的时候,如何延迟返回响应,等待副本拉取
这个是在追加消息的时候,对 ack 进行判断,如果是 -1 的话,会封装为一个 delayedProduce,设置好超时时间,回调函数,然后放入到 延迟队列里去,等待所有 ISR 集合中的 follow 拉取完毕,当 follow 拉取完数据之后,会对这个 topic,partition 对应的延迟任务从队列中移除,然后 delayedProducer 就会通过回调函数,将响应信息返回。
2.4、返回 Fetch 结果
当请求处理完毕之后,就会通过回调函数,将响应结果进行返回。
2.5、写入数据
当 Follow 收到响应之后,就会对数据进行处理,将数据写入,基本流程与数据追加流程一致。