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,這也很好的避免了羊羣效應。


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