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,这也很好的避免了羊群效应。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章