Apache Kafka Broker 端核心组件 ReplicaManager它是 Kafka 实现 副本管理、ISR(In-Sync Replicas)维护、Leader/Follower 同步、日志存储协调 的中枢。
下面我将从 整体定位、关键字段、核心机制、工作流程 四个维度,帮你系统性理解这段代码的含义和设计思想。
🧠 一、ReplicaManager 是什么?
ReplicaManager是 Kafka Broker 上负责管理所有分区(Partition)及其副本(Replica)状态的核心服务。
每个 Kafka Broker 启动时都会创建一个 ReplicaManager 实例,它:
- 持有本机上所有 托管分区(hosted partitions) 的引用;
- 负责与 Controller 通信,接收 Leader/ISR 变更指令;
- 启动 Follower Fetcher 线程,从 Leader 拉取数据;
- 维护 ISR 列表,动态增删副本;
- 处理 延迟操作(Delayed Produce/Fetch);
- 管理 日志目录故障 和 副本删除/重建。
🔑 二、关键字段解析(按功能分类)
1. 基础依赖
| 字段 | 作用 |
|---|---|
config: KafkaConfig |
Broker 配置(如 broker.id、log.dirs 等) |
zkClient: KafkaZkClient |
与 ZooKeeper 通信(旧版 Kafka,KRaft 模式下不用) |
logManager: LogManager |
管理本地日志文件(Log 对象) |
metadataCache: MetadataCache |
本地元数据缓存:保存集群所有 Topic/Partition 的 Leader、ISR、副本列表等信息(从 Controller 同步而来) |
✅ 注意注释强调:
metadataCache是从 Controller 异步同步过来的,每台 Broker 都有一份只读副本。
2. 分区状态管理
scala
private val allPartitions = new Pool[TopicPartition, HostedPartition](...)
allPartitions:本 Broker 所有托管分区的容器。HostedPartition是一个密封类(sealed trait),有三种状态:Online(Partition):正常在线Offline:所在日志目录故障,分区不可用None:未加载或已删除
💡
Partition类才是真正封装 Leader/Follower 逻辑、HW(High Watermark)、Log、Replicas 的对象。
3. 延迟操作管理(Purgatory)
Kafka 使用 "炼狱"(Purgatory)模式 处理不能立即完成的请求:
| Purgatory | 处理的请求类型 | 场景 |
|---|---|---|
delayedProducePurgatory |
PRODUCE | acks=all 且 ISR 未满足时等待 |
delayedFetchPurgatory |
FETCH | Fetch 请求要求 offset > LEO 时等待 |
delayedDeleteRecordsPurgatory |
DELETE_RECORDS | 删除记录需等待 HW 推进 |
delayedElectLeaderPurgatory |
ELECT_LEADERS | 手动触发 Leader 选举等待完成 |
✅ 这些 Purgatory 本质是 带超时和条件触发的延迟队列。
4. Fetcher 管理器
scala
val replicaFetcherManager = createReplicaFetcherManager(metrics, time, threadNamePrefix, quotaManagers.follower)
val replicaAlterLogDirsManager = createReplicaAlterLogDirsManager(quotaManagers.alterLogDirs, brokerTopicStats)
protected def createReplicaFetcherManager(metrics: Metrics, time: Time, threadNamePrefix: Option[String], quotaManager: ReplicationQuotaManager) = {
new ReplicaFetcherManager(config, this, metrics, time, threadNamePrefix, quotaManager)
}
protected def createReplicaAlterLogDirsManager(quotaManager: ReplicationQuotaManager, brokerTopicStats: BrokerTopicStats) = {
new ReplicaAlterLogDirsManager(config, this, quotaManager, brokerTopicStats)
}
replicaFetcherManager:启动 Follower 线程,持续从 Leader 拉取数据。replicaAlterLogDirsManager:处理 副本迁移(alter log dirs) 时的特殊拉取。
5. ISR 相关
scala
private val isrChangeSet: mutable.Set[TopicPartition] = new mutable.HashSet[TopicPartition]()
private val lastIsrChangeMs = new AtomicLong(System.currentTimeMillis())
private val lastIsrPropagationMs = new AtomicLong(System.currentTimeMillis())
- Kafka 不会每次 ISR 变化都立刻通知 Controller ,而是:
- 聚合变化到
isrChangeSet - 定期(每 2.5 秒)调用
maybePropagateIsrChanges()批量上报 - 避免频繁 ZK 写入(性能优化)
- 聚合变化到
6. Metrics & 监控
scala
newGauge("LeaderCount", ...)
newGauge("UnderReplicatedPartitions", ...)
val isrExpandRate / isrShrinkRate
暴露关键指标供监控系统采集,例如:
- UnderReplicatedPartitions > 0 表示有分区副本落后,需告警!
⚙️ 三、核心工作机制
1. 启动流程(startup())
scala
def startup(): Unit = {
scheduler.schedule("isr-expiration", maybeShrinkIsr _, period = config.replicaLagTimeMaxMs / 2)
scheduler.schedule("isr-change-propagation", maybePropagateIsrChanges _, period = 2500L)
logDirFailureHandler.start() // 监听日志目录故障
}
- 启动 ISR 过期检测线程:定期检查 Follower 是否落后太多(默认 30 秒),若超时则踢出 ISR。
- 启动 ISR 变更传播线程:批量上报 ISR 变化到 ZK。
- 启动 日志目录故障监听线程:若磁盘损坏,可 halt broker(取决于 IBP 版本)。
2. 处理 Controller 指令:stopReplicas
scala
/**
* 处理来自控制器的 StopReplica 请求,用于停止指定分区的副本。
*
* @param correlationId 请求的关联 ID,用于匹配请求与响应
* @param controllerId 发送该请求的控制器的 ID
* @param controllerEpoch 控制器的纪元(epoch),用于版本控制和冲突检测
* @param brokerEpoch 当前 Broker 的纪元,用于标识 Broker 的状态变更历史
* @param partitionStates 每个 TopicPartition 对应的停止状态信息映射表
* @return 返回一个元组:第一个元素是每个分区对应的操作错误码;第二个是整体操作的结果错误码
*/
def stopReplicas(correlationId: Int,
controllerId: Int,
controllerEpoch: Int,
brokerEpoch: Long,
partitionStates: Map[TopicPartition, StopReplicaPartitionState]
): (mutable.Map[TopicPartition, Errors], Errors) = {
replicaStateChangeLock synchronized {
// 记录接收到 StopReplica 请求的日志
stateChangeLogger.info(s"Handling StopReplica request correlationId $correlationId from controller " +
s"$controllerId for ${partitionStates.size} partitions")
// 如果启用了 trace 日志级别,则记录每个分区的状态详情
if (stateChangeLogger.isTraceEnabled)
partitionStates.foreach { case (topicPartition, partitionState) =>
stateChangeLogger.trace(s"Received StopReplica request $partitionState " +
s"correlation id $correlationId from controller $controllerId " +
s"epoch $controllerEpoch for partition $topicPartition")
}
val responseMap = new collection.mutable.HashMap[TopicPartition, Errors]
// 判断控制器纪元是否过期,如果小于当前已知的控制器纪元则拒绝处理并返回 STALE_CONTROLLER_EPOCH 错误
if (controllerEpoch < this.controllerEpoch) {
stateChangeLogger.warn(s"Ignoring StopReplica request from " +
s"controller $controllerId with correlation id $correlationId " +
s"since its controller epoch $controllerEpoch is old. " +
s"Latest known controller epoch is ${this.controllerEpoch}")
(responseMap, Errors.STALE_CONTROLLER_EPOCH)
} else {
// 更新本地保存的控制器纪元为最新值
this.controllerEpoch = controllerEpoch
// 存储需要被停止的分区及其状态
val stoppedPartitions = mutable.Map.empty[TopicPartition, StopReplicaPartitionState]
// 遍历所有待处理的分区状态,并根据其当前状态决定是否可以执行 StopReplica 操作
partitionStates.foreach { case (topicPartition, partitionState) =>
val deletePartition = partitionState.deletePartition
getPartition(topicPartition) match {
// 分区处于离线状态时,无法进行操作,直接标记为存储错误
case HostedPartition.Offline =>
stateChangeLogger.warn(s"Ignoring StopReplica request (delete=$deletePartition) from " +
s"controller $controllerId with correlation id $correlationId " +
s"epoch $controllerEpoch for partition $topicPartition as the local replica for the " +
"partition is in an offline log directory")
responseMap.put(topicPartition, Errors.KAFKA_STORAGE_ERROR)
// 在线分区需检查 leader epoch 是否合法
case HostedPartition.Online(partition) =>
val currentLeaderEpoch = partition.getLeaderEpoch
val requestLeaderEpoch = partitionState.leaderEpoch
// 特殊情况处理:
// - EpochDuringDelete 表示正在删除中,允许跳过 epoch 校验
// - NoEpoch 表示旧版协议未携带 epoch 字段,也跳过校验
// - 若请求中的 epoch 更大,则认为有效
if (requestLeaderEpoch == LeaderAndIsr.EpochDuringDelete ||
requestLeaderEpoch == LeaderAndIsr.NoEpoch ||
requestLeaderEpoch > currentLeaderEpoch) {
stoppedPartitions += topicPartition -> partitionState
}
// 请求中的 epoch 小于当前 epoch,说明请求已经失效,忽略该请求
else if (requestLeaderEpoch < currentLeaderEpoch) {
stateChangeLogger.warn(s"Ignoring StopReplica request (delete=$deletePartition) from " +
s"controller $controllerId with correlation id $correlationId " +
s"epoch $controllerEpoch for partition $topicPartition since its associated " +
s"leader epoch $requestLeaderEpoch is smaller than the current " +
s"leader epoch $currentLeaderEpoch")
responseMap.put(topicPartition, Errors.FENCED_LEADER_EPOCH)
}
// 请求中的 epoch 和当前一致,但不能重复应用相同操作,因此忽略
else {
stateChangeLogger.info(s"Ignoring StopReplica request (delete=$deletePartition) from " +
s"controller $controllerId with correlation id $correlationId " +
s"epoch $controllerEpoch for partition $topicPartition since its associated " +
s"leader epoch $requestLeaderEpoch matches the current leader epoch")
responseMap.put(topicPartition, Errors.FENCED_LEADER_EPOCH)
}
// 分区不存在的情况下仍尝试清理相关资源(如日志文件)
case HostedPartition.None =>
stoppedPartitions += topicPartition -> partitionState
}
}
// 先移除这些分区的数据拉取任务,再实际停止副本
val partitions = stoppedPartitions.keySet
replicaFetcherManager.removeFetcherForPartitions(partitions)
replicaAlterLogDirsManager.removeFetcherForPartitions(partitions)
// 实际执行停止副本或删除分区的操作
stoppedPartitions.foreach { case (topicPartition, partitionState) =>
val deletePartition = partitionState.deletePartition
try {
stopReplica(topicPartition, deletePartition)
responseMap.put(topicPartition, Errors.NONE)
} catch {
// 出现存储异常时记录日志并设置相应错误码
case e: KafkaStorageException =>
stateChangeLogger.error(s"Ignoring StopReplica request (delete=$deletePartition) from " +
s"controller $controllerId with correlation id $correlationId " +
s"epoch $controllerEpoch for partition $topicPartition as the local replica for the " +
"partition is in an offline log directory", e)
responseMap.put(topicPartition, Errors.KAFKA_STORAGE_ERROR)
}
}
// 返回结果:各分区的错误码 + 整体无错误标志
(responseMap, Errors.NONE)
}
}
}
/**
* 停止指定主题分区的副本操作
*
* @param topicPartition 要停止副本的主题分区
* @param deletePartition 是否删除分区数据
*/
def stopReplica(topicPartition: TopicPartition, deletePartition: Boolean): Unit = {
if (deletePartition) {
// 处理在线分区的删除逻辑
getPartition(topicPartition) match {
case hostedPartition @ HostedPartition.Online(removedPartition) =>
if (allPartitions.remove(topicPartition, hostedPartition)) {
maybeRemoveTopicMetrics(topicPartition.topic)
// 删除本地日志,如果日志位于离线目录可能会抛出异常
removedPartition.delete()
}
case _ =>
}
// 删除日志管理器中的日志和对应文件夹,防止副本管理器不再持有它们
// 这种情况可能发生在broker宕机恢复时主题正在被删除的情况下
if (logManager.getLog(topicPartition).isDefined)
logManager.asyncDelete(topicPartition)
if (logManager.getLog(topicPartition, isFuture = true).isDefined)
logManager.asyncDelete(topicPartition, isFuture = true)
}
// 如果当前是leader,可能还有等待完成的操作
// 强制完成这些操作以防止它们超时
completeDelayedFetchOrProduceRequests(topicPartition)
}
当 Controller 发送 StopReplica 请求(如删除 Topic、副本重分配):
- 校验
controllerEpoch(防止 stale controller 指令) - 停止对应分区的 Fetcher 线程
- 调用
stopReplica():- 若
deletePartition=true→ 删除本地日志 - 强制完成该分区上所有 延迟的 Produce/Fetch 请求
- 若
- 更新
allPartitions状态(移除或标记 Offline)
✅ 这是 Topic 删除、副本迁移 的关键入口。
3. 分区获取逻辑:getPartitionOrError
scala
/**
* 获取指定主题分区的分区信息,如果获取失败则返回相应的错误码
*
* @param topicPartition 主题分区对象,包含主题名称和分区ID
* @return Either[Errors, Partition] 返回Either类型,Left包含错误码,Right包含分区信息
*/
def getPartitionOrError(topicPartition: TopicPartition): Either[Errors, Partition] = {
getPartition(topicPartition) match {
case HostedPartition.Online(partition) =>
Right(partition)
case HostedPartition.Offline =>
Left(Errors.KAFKA_STORAGE_ERROR)
case HostedPartition.None if metadataCache.contains(topicPartition) =>
// 主题存在,但当前broker不再是该分区的副本,返回NOT_LEADER_OR_FOLLOWER错误
// 这会强制客户端刷新元数据以找到新的位置。这种情况可能发生在分区重新分配期间,
// 当客户端的生产请求发送到已删除本地副本的broker时。
Left(Errors.NOT_LEADER_OR_FOLLOWER)
case HostedPartition.None =>
Left(Errors.UNKNOWN_TOPIC_OR_PARTITION)
}
}
/**
* 根据主题分区获取托管分区信息
*
* @param topicPartition 主题分区对象,用于标识特定的主题和分区
* @return 返回对应的托管分区信息,如果不存在则返回HostedPartition.None
*/
def getPartition(topicPartition: TopicPartition): HostedPartition = {
// 从所有分区映射中查找指定主题分区,如果不存在则返回None实例
Option(allPartitions.get(topicPartition)).getOrElse(HostedPartition.None)
}
根据分区状态返回不同错误码:
| 状态 | 返回错误 |
|---|---|
HostedPartition.Offline |
KAFKA_STORAGE_ERROR(磁盘故障) |
HostedPartition.None + metadata 中存在 |
NOT_LEADER_OR_FOLLOWER(已不是副本) |
HostedPartition.None + metadata 中不存在 |
UNKNOWN_TOPIC_OR_PARTITION |
✅ 客户端收到这些错误会 刷新元数据,找到新 Leader。
🔄 四、与其他组件的关系
LeaderAndIsrRequest zkClient Controller ReplicaManager Partition LogManager ReplicaFetcherManager DelayedOperationPurgatory Client Produce/Fetch ZooKeeper
- Controller:下发分区状态变更(谁是 Leader、ISR 列表)
- Partition:具体实现副本同步、HW 更新
- LogManager:提供底层日志读写
- Purgatory:挂起不能立即完成的请求
- Client:通过 ReplicaManager 提供的接口读写数据
✅ 五、总结:ReplicaManager 的核心职责
| 职责 | 实现方式 |
|---|---|
| 管理本机所有分区副本 | allPartitions: Pool[TopicPartition, HostedPartition] |
| 与 Controller 同步元数据 | metadataCache + 处理 LeaderAndIsrRequest / StopReplicaRequest |
| 维护 ISR 列表 | maybeShrinkIsr() + recordIsrChange() + 批量上报 |
| 处理客户端读写请求 | 通过 Partition 对象,结合 Purgatory 实现延迟响应 |
| 副本同步(Follower) | replicaFetcherManager 拉取 Leader 数据 |
| 故障处理 | 日志目录故障 → 标记分区 Offline 或 halt broker |
| 资源清理 | 删除 Topic 时清理日志、关闭延迟请求 |
💡 补充:为什么叫 "ReplicaManager" 而不是 "PartitionManager"?
因为 Kafka 中:
- Partition 是逻辑概念(属于 Topic)
- Replica 是物理副本(分布在 Broker 上)
每个 Broker 只关心自己 托管的副本(Replica) ,而一个 Partition 在集群中有多个 Replica(1 Leader + N Follower)。
所以这个组件管理的是 Replica 的生命周期,而非 Partition 本身。
如果你正在阅读 Kafka 源码,建议重点关注:
Partition.makeFollower()/makeLeader()ReplicaManager.maybeShrinkIsr()DelayedProduce.tryComplete()
这些是理解 Kafka 副本机制的关键路径。
需要我进一步解释 ISR 收缩逻辑 或 HW/LEO 更新机制 吗?