MetadataCache 是指 Broker 上的元数据缓存,这些数据是 Controller 通过 UpdateMetadataRequest 请求发送给 Broker 的。换句话说,Controller 实现了一个异步更新机制,能够将最新的集群信息广播给所有 Broker,Kafka 通过异步更新机制来保证所有 Broker 上的元数据缓存实现最终一致性。
每台 Broker 上都要保存这份相同的数据有两个原因。
- 保存了这部分数据,Broker 就能够及时响应客户端发送的元数据请求,也就是处理 Metadata 请求。Metadata 请求是为数不多的能够被集群任意 Broker 处理的请求类型之一,也就是说,客户端程序能够随意地向任何一个 Broker 发送 Metadata 请求,去获取集群的元数据信息,这完全得益于 MetadataCache 的存在。
- Kafka 的一些重要组件会用到这部分数据。比如副本管理器会使用它来获取 Broker 的节点信息,事务管理器会使用它来获取分区 Leader 副本的信息,等等。
MetadataCache的定义如下:
class MetadataCache(brokerId: Int) extends Logging {
// 保护它写入的锁对象
private val partitionMetadataLock = new ReentrantReadWriteLock()
//this is the cache state. every MetadataSnapshot instance is immutable, and updates (performed under a lock)
//replace the value with a completely new one. this means reads (which are not under any lock) need to grab
//the value of this var (into a val) ONCE and retain that read copy for the duration of their operation.
//multiple reads of this value risk getting different snapshots.
// 保存了实际的元数据信息
@volatile private var metadataSnapshot: MetadataSnapshot = MetadataSnapshot(partitionStates = mutable.AnyRefMap.empty,
controllerId = None, aliveBrokers = mutable.LongMap.empty, aliveNodes = mutable.LongMap.empty)
// 仅仅用于日志输出
this.logIdent = s"[MetadataCache brokerId=$brokerId] "
pr
上面的 metadataSnapshot 字段保存了实际的元数据信息,它的定义如下
case class MetadataSnapshot(// 这是一个 Map 类型。Key 是主题名称,Value 又是一个 Map 类型,其 Key 是分区号,Value 是一个 UpdateMetadataPartitionState 类型的字段。
// UpdateMetadataPartitionState 类型是 UpdateMetadataRequest 请求内部所需的数据结构。
partitionStates: mutable.AnyRefMap[String, mutable.LongMap[UpdateMetadataPartitionState]],
controllerId: Option[Int], // Controller 所在 Broker 的 ID。
aliveBrokers: mutable.LongMap[Broker], // 当前集群中所有存活着的 Broker 对象列表。
aliveNodes: mutable.LongMap[collection.Map[ListenerName, Node]]) // 也是一个 Map 的 Map 类型。其 Key 是 Broker ID 序号,Value 是 Map 类型,其 Key 是 ListenerName,
// 即 Broker 监听器类型,而 Value 是 Broker 节点对象。
其中UpdateMetadataPartitionState的定义如下:
static public class UpdateMetadataPartitionState implements Message {
private String topicName; // 主题名称
private int partitionIndex; // 分区号
private int controllerEpoch; // Controller Epoch值
private int leader; // Leader副本所在Broker ID
private int leaderEpoch; // Leader Epoch值
private List<Integer> isr; // ISR列表
private int zkVersion; // ZooKeeper节点Stat统计信息中的版本号
private List<Integer> replicas; // 副本列表
private List<Integer> offlineReplicas; // 离线副本列表
private List<RawTaggedField> _unknownTaggedFields; // 未知字段列表
}
下面看看MetadataCache 类的重要方法。最重要的方法就是操作 metadataSnapshot 字段的方法
我把 MetadataCache 类的方法大致分为三大类:判断类;获取类;更新类。
判断类方法
判断给定主题或主题分区是否包含在元数据缓存中的方法,比如:
// 判断给定主题是否包含在元数据缓存中
def contains(topic: String): Boolean = {
metadataSnapshot.partitionStates.contains(topic)
}
// 判断给定主题分区是否包含在元数据缓存中
def contains(tp: TopicPartition): Boolean = getPartitionInfo(tp.topic, tp.partition).isDefined
获取类方法
// 获取给定主题分区的详细数据信息。如果没有找到对应记录,返回None
def getPartitionInfo(topic: String, partitionId: Int): Option[UpdateMetadataPartitionState] = {
metadataSnapshot.partitionStates.get(topic).flatMap(_.get(partitionId))
}
// 返回当前集群元数据缓存中的所有主题。
private def getAllTopics(snapshot: MetadataSnapshot): Set[String] = {
snapshot.partitionStates.keySet
}
//参数为主题分区和 ListenerName,以获取指定监听器类型下该主题分区所有副本的 Broker 节点对象
def getPartitionReplicaEndpoints(tp: TopicPartition, listenerName: ListenerName): Map[Int, Node] = {
// 使用局部变量获取当前元数据缓存
val snapshot = metadataSnapshot
// 获取给定主题分区的数据
snapshot.partitionStates.get(tp.topic).flatMap(_.get(tp.partition)).map { partitionInfo =>
val replicaIds = partitionInfo.replicas
replicaIds.asScala
.map(replicaId => replicaId.intValue() -> {
// 获取副本所在的Broker Id
snapshot.aliveBrokers.get(replicaId.longValue()) match {
case Some(broker) =>
// 根据Broker Id去获取对应的Broker节点对象
broker.getNode(listenerName).getOrElse(Node.noNode())
case None =>
// 如果找不到节点
Node.noNode()
}}).toMap
.filter(pair => pair match {
case (_, node) => !node.isEmpty
})
}.getOrElse(Map.empty[Int, Node])
}
更新类方法
updateMetadata 方法的主要逻辑,就是读取 UpdateMetadataRequest 请求中的分区数据,然后更新本地元数据缓存。
// 基于 UpdateMetadataRequest 请求更新每个分区的状态信息,并返回需要被移除的分区集合
def updateMetadata(correlationId: Int, updateMetadataRequest: UpdateMetadataRequest): Seq[TopicPartition] = {
inWriteLock(partitionMetadataLock) {
// 第一部分,给后面的操作准备数据,即 aliveBrokers 和 aliveNodes 两个字段中保存的数据。
// 保存存活Broker对象。Key是Broker ID,Value是Broker对象
val aliveBrokers = new mutable.LongMap[Broker](metadataSnapshot.aliveBrokers.size)
// 保存存活节点对象。Key是Broker ID,Value是监听器->节点对象
val aliveNodes = new mutable.LongMap[collection.Map[ListenerName, Node]](metadataSnapshot.aliveNodes.size)
// 从UpdateMetadataRequest中获取Controller所在的Broker ID
// 如果当前没有Controller,赋值为None
val controllerId = updateMetadataRequest.controllerId match {
case id if id < 0 => None
case id => Some(id)
}
// 遍历UpdateMetadataRequest请求中的所有存活Broker对象
updateMetadataRequest.liveBrokers.asScala.foreach { broker =>
// `aliveNodes` is a hot path for metadata requests for large clusters, so we use java.util.HashMap which
// is a bit faster than scala.collection.mutable.HashMap. When we drop support for Scala 2.10, we could
// move to `AnyRefMap`, which has comparable performance.
val nodes = new java.util.HashMap[ListenerName, Node]
val endPoints = new mutable.ArrayBuffer[EndPoint]
// 遍历它的所有EndPoint类型,也就是为Broker配置的监听器
broker.endpoints.asScala.foreach { ep =>
val listenerName = new ListenerName(ep.listener)
endPoints += new EndPoint(ep.host, ep.port, listenerName, SecurityProtocol.forId(ep.securityProtocol))
// 将<监听器,Broker节点对象>对保存起来
nodes.put(listenerName, new Node(broker.id, ep.host, ep.port))
}
// 将Broker加入到存活Broker对象集合
aliveBrokers(broker.id) = Broker(broker.id, endPoints, Option(broker.rack))
// 将Broker节点加入到存活节点对象集合
aliveNodes(broker.id) = nodes.asScala
}
// 确保集群 Broker 配置了相同的监听器,同时初始化已删除分区数组对象,等待下一部分代码逻辑对它进行操作。
// 使用上一部分中的存活Broker节点对象,
// 获取当前Broker所有的<监听器,节点>对
aliveNodes.get(brokerId).foreach { listenerMap =>
val listeners = listenerMap.keySet
// 如果发现当前Broker配置的监听器与其他Broker有不同之处,记录错误日志
if (!aliveNodes.values.forall(_.keySet == listeners))
error(s"Listeners are not identical across brokers: $aliveNodes")
}
// 构造已删除分区数组,将其作为方法返回结果
val deletedPartitions = new mutable.ArrayBuffer[TopicPartition]
// UpdateMetadataRequest请求没有携带任何分区信息
if (!updateMetadataRequest.partitionStates.iterator.hasNext) {
// 构造新的MetadataSnapshot对象,使用之前的分区信息和新的Broker列表信息
metadataSnapshot = MetadataSnapshot(metadataSnapshot.partitionStates, controllerId, aliveBrokers, aliveNodes)
} else {
// 第三部分:提取 UpdateMetadataRequest 请求中的数据,然后填充元数据缓存
//since kafka may do partial metadata updates, we start by copying the previous state
// 备份现有元数据缓存中的分区数据
val partitionStates = new mutable.AnyRefMap[String, mutable.LongMap[UpdateMetadataPartitionState]](metadataSnapshot.partitionStates.size)
metadataSnapshot.partitionStates.foreach { case (topic, oldPartitionStates) =>
val copy = new mutable.LongMap[UpdateMetadataPartitionState](oldPartitionStates.size)
copy ++= oldPartitionStates
partitionStates += (topic -> copy)
}
updateMetadataRequest.partitionStates.asScala.foreach { info =>
val controllerId = updateMetadataRequest.controllerId
val controllerEpoch = updateMetadataRequest.controllerEpoch
val tp = new TopicPartition(info.topicName, info.partitionIndex)
// 如果分区处于被删除过程中
if (info.leader == LeaderAndIsr.LeaderDuringDelete) {
// 将分区从元数据缓存中移除
removePartitionInfo(partitionStates, tp.topic, tp.partition)
stateChangeLogger.trace(s"Deleted partition $tp from metadata cache in response to UpdateMetadata " +
s"request sent by controller $controllerId epoch $controllerEpoch with correlation id $correlationId")
// 将分区加入到返回结果数据
deletedPartitions += tp
} else {
// 将分区加入到元数据缓存
addOrUpdatePartitionInfo(partitionStates, tp.topic, tp.partition, info)
stateChangeLogger.trace(s"Cached leader info $info for partition $tp in response to " +
s"UpdateMetadata request sent by controller $controllerId epoch $controllerEpoch with correlation id $correlationId")
}
}
// 使用更新过的分区元数据,和第一部分计算的存活Broker列表及节点列表,构建最新的元数据缓存
metadataSnapshot = MetadataSnapshot(partitionStates, controllerId, aliveBrokers, aliveNodes)
}
// 返回已删除分区列表数组
deletedPartitions
}
}