KAFKA 进阶:【十五】能否说一下 kafka 的元数据更新机制?

大家好,这是一个为了梦想而保持学习的博客。这个专题会记录我对于 KAFKA 的学习和实战经验,希望对大家有所帮助,目录形式依旧为问答的方式,相当于是模拟面试。


一、概述

首先,我们需要说明下,什么是元数据?
我所理解的元数据其实就是分布式系统中各个组件组成集群后,所需要共享的数据。换言之,既然我们每个组件都需要保存一份,干嘛不把这些公共数据抽取出来保存在一个地方呢,还方便维护?对吧。而 kafka 就选用 zk 来进行元数据的集中式管理。

在明白了元数据定义后,我们再思考一下,这些元数据有什么用呢?
在 kafka 中,元数据记录着各个 broker 的通信地址,以及各项配置信息,以及其他的集群状态、ACL 信息等等。那么简单的概括下元数据的作用如下:

  • 客户端:可以通过元数据获取服务地址,进行通信。(类似于服务发现)
  • 服务端:可以通过元数据共享集群状态,一旦出现状态变化能够快速感知到,并且让各个 broker 快速更新元数据去保持一致。

二、元数据的层级包含哪些?(broker 级别 / 配置)

我们直接看下 zk 中存放了哪些元数据:

从上图中,我圈出了我们平时比较关注的两个元数据模块:broker 级别的元数据 / 配置信息元数据,接下来让我们分别来看下这里面存放了些什么东西。

broker 级别的元数据:
总览:

1、ids 节点的信息,例如监听地址等:

2、topics 信息,例如分区信息、leader、isr 信息等:

3、seqid 信息,主要用于自动生成 brokerId,具体参见:朱小厮博客

配置信息元数据:
总览:

这些具体的项(除开 /changes),其实就是有这些维度的动态配置,也就是可以通过 kafka-configs.sh 脚本去配置对应的参数,而配置的这些动态参数,就是持久化的保存在 zk 的这些节点上的,无论 kafka 如何修改 server.properties 与重启,都不会改变存在 zk 里面的值,只有通过 kafka-configs.sh 或者调用 api 去修改才行。


三、元数据的是如何更新的?

在知道元数据是什么之后,我们需要了解下,元数据是如何更新的呢?
元数据是保存在 zk 的,那么可能很多同学一想,那必定是通过注册 Watcher,然后监听元数据变化然后更新呀。
这么说,对,也不对。为什么呢?
首先,是通过 Wachter 来更新是没有疑问的,但是我们从上面的截图可以看到,元数据的层级实际上是非常多的,也是非常零散的。如果对每个元数据层级,每个元数据相关的节点都注册监听器,那么需要注册非常多的监听器,需要写非常多的相关处理代码,光是想想都觉得麻烦。
那让我们回顾一下之前讲的 ISR 更新过程,是怎么通信的?我把图再贴一下:

可以从上图看到,重点在于往 xxx/isr_change_notification 路径下创建的一个新的节点,而 controller 只需要注册对这一个通知路径的监听,就可以实现全部 ISR 层级数据的监听与更新,相比于监听所有 Topic 下的所有 Partition 下的所有 ISR,是不是显得非常轻量与优雅?这也是从 kafka 中学习如何优雅的利用 zk 做分布式协调。

其实不止 ISR 的更新,上文中提到的 /config/changes/ 也是用于动态配置文件更新后的通信的,实现方式和 ISR 更新一样,不过更加巧妙的一点是,在 /config/changes/ 创建的新节点的时候,写入节点的数据不是具体要更新的数据本身,而是直接写的相对路径,然后根据监听器拿到这个相对路径再去读取对应出现变化的节点信息,从而进一步解耦。如果对具体细节感兴趣的同学可以阅读:推荐阅读
以 broker 更新 listeners 的源码为例,具体源码如下:

    private def processEntityConfigChangeVersion2(jsonBytes: Array[Byte], js: JsonObject) {

      val entityPath = js.get("entity_path").flatMap(_.to[Option[String]]).getOrElse {
        throw new IllegalArgumentException(s"Version 2 config change notification must specify 'entity_path'. " +
          s"Received: ${new String(jsonBytes, StandardCharsets.UTF_8)}")
      }
      // 切割
      val index = entityPath.indexOf('/')
      val rootEntityType = entityPath.substring(0, index)
      if (index < 0 || !configHandlers.contains(rootEntityType)) {
        val entityTypes = configHandlers.keys.map(entityType => s"'$entityType'/").mkString(", ")
        throw new IllegalArgumentException("Version 2 config change notification must have 'entity_path' starting with " +
          s"one of $entityTypes. Received: ${new String(jsonBytes, StandardCharsets.UTF_8)}")
      }
      val fullSanitizedEntityName = entityPath.substring(index + 1)
      // 根据相对路径获取最新的元数据信息
      val entityConfig = adminZkClient.fetchEntityConfig(rootEntityType, fullSanitizedEntityName)
      val loggableConfig = entityConfig.asScala.map {
        case (k, v) => (k, if (ScramMechanism.isScram(k)) Password.HIDDEN else v)
      }
      info(s"Processing override for entityPath: $entityPath with config: $loggableConfig")
      configHandlers(rootEntityType).processConfigChanges(fullSanitizedEntityName, entityConfig)

    }

总结一下:kafka 的元数据在出现变化时,会向约定好的 /notification 或者 /changes 目录下创建对应的节点,并写入对应的信息;而 controller 或者 broker 只需要对这些约定好的目录添加监听器即可迅速感知到集群元数据的变化,这一操作很大程度上的减小了 Wacther 的注册数量,也简化了代码实现。
最后,我们需要明确的是,集群内部的元数据更新,都是有 controller 来统一进行感知与更新的,而也只需要 controller 注册相对较多 Watcher,普通的 broker 只需要注册少量的 Wacher,这也很好的避免了羊群效应。

posted @ 2022-06-04 13:37  Keepal  阅读(1251)  评论(0)    收藏  举报