kafka Poll輪詢機制與消費者組的重平衡分割槽策略剖析-kafka 商業環境實戰

資料雲技術社群發表於2018-11-14

本套系列部落格從真實商業環境抽取案例進行總結和分享,並給出Spark商業應用實戰指導,請持續關注本套部落格。版權宣告:本套Spark商業應用實戰歸作者(秦凱新)所有,禁止轉載,歡迎學習。

1 Consumer在單執行緒下一手遮天

注意本文采用最新版本進行Kafka的核心原理剖析,新版本每一個Consumer通過獨立的執行緒,來管理多個Socket連線,即同時與多個broker通訊實現訊息的並行讀取。這就是新版的技術革新。類似於Linux I/O模型或者Select NIO 模型。

2 Poll為什麼要設定一個超時引數

  • 條件:
  • 1:獲取足夠多的可用資料
  • 2:等待時間超過指定的超時時間。
  • 目的在於讓Consumer主執行緒定期的""甦醒"去做其他事情。比如:定期的執行常規任務,(比如寫日誌,寫庫等)。
  • 獲取訊息,然後執行業務邏輯。

3 位移精度

  • 最少一次 -> 訊息會被重複處理
  • 最多一次 -> 訊息會丟失,但不會被重複處理。
  • 精確一次 -> 一定會被處理,且也只會處理一次。

4 位移角色 <- (留一節專門剖析HW 與 LEO)

  • 上次提交位移 :last committed offset
  • 當前位置 :current position
  • 水位 : High watermark
  • 日誌終端位移: (Log End Offset)

5 位移管理

consumer的位移提交最終會向group coordinator來提交,不過這裡重點需要重新說明一下:組協調者coordinator負責管理所有的Consumer例項。而且coordinator執行在broker上(通過選舉出某個broker),不過請注意新版本coordinator只負責做組管理。

但是具體的reblance分割槽分配策略目前已經交由Consumer客戶端。這樣就解耦了組管理和分割槽分配。

權利下放的優勢:

  • 如果需要分配就貌似需要重啟整個kafka叢集。
  • 在Consumer端可以定製分割槽分配策略。
  • 每一個consumer位移提交時,都會向_consumer_offsets對應的分割槽上追加寫入一條訊息。如果某一個consumer為同一個group的同一個topic同一個分割槽提交多次位移,很顯然我們只關心最新一次提交的位移。

6 reblance的觸發條件

  • 組訂閱發生變更,比如基於正規表示式訂閱,當匹配到新的topic建立時,組的訂閱就會發生變更。
  • 組的topic分割槽數發生變更,通過命令列指令碼增加了訂閱topic的分割槽數。
  • 組成員發生變更:新加入組以及離開組。

7 reblance 分配策略

7.1 range分割槽分配策略

舉例如下: 一個擁有十個分割槽(0,1,2.....,9)的topic,相同group擁有三個consumerid為a,b,c的消費者:

  • consumer a分配對應的分割槽號為[0,4),即0,1,2,3前面四個分割槽

  • consumer b 分配對應分割槽4,5,6中間三個分割槽

  • consumer c 分配對應分割槽7,8,9最後三個分割槽。

    class RangeAssignor() extends PartitionAssignor with Logging {

    def assign(ctx: AssignmentContext) = {
      val valueFactory = (topic: String) => new mutable.HashMap[TopicAndPartition, ConsumerThreadId]
      val partitionAssignment =
        new Pool[String, mutable.Map[TopicAndPartition, ConsumerThreadId]](Some(valueFactory))
      for (topic <- ctx.myTopicThreadIds.keySet) {
        val curConsumers = ctx.consumersForTopic(topic)
        val curPartitions: Seq[Int] = ctx.partitionsForTopic(topic)
    
        val nPartsPerConsumer = curPartitions.size / curConsumers.size
        val nConsumersWithExtraPart = curPartitions.size % curConsumers.size
    
        info("Consumer " + ctx.consumerId + " rebalancing the following partitions: " + curPartitions +
          " for topic " + topic + " with consumers: " + curConsumers)
    
        for (consumerThreadId <- curConsumers) {
          val myConsumerPosition = curConsumers.indexOf(consumerThreadId)
          assert(myConsumerPosition >= 0)
          val startPart = nPartsPerConsumer * myConsumerPosition + myConsumerPosition.min(nConsumersWithExtraPart)
          val nParts = nPartsPerConsumer + (if (myConsumerPosition + 1 > nConsumersWithExtraPart) 0 else 1)
    
          /**
           *   Range-partition the sorted partitions to consumers for better locality.
           *  The first few consumers pick up an extra partition, if any.
           */
          if (nParts <= 0)
            warn("No broker partitions consumed by consumer thread " + consumerThreadId + " for topic " + topic)
          else {
            for (i <- startPart until startPart + nParts) {
              val partition = curPartitions(i)
              info(consumerThreadId + " attempting to claim partition " + partition)
              // record the partition ownership decision
              val assignmentForConsumer = partitionAssignment.getAndMaybePut(consumerThreadId.consumer)
              assignmentForConsumer += (TopicAndPartition(topic, partition) -> consumerThreadId)
            }
          }
        }
      }
    複製程式碼

    原始碼剖析如下:

         curConsumers=(a,b,c)
         curPartitions=(0,1,2,3,4,5,6,7,8,9)
         nPartsPerConsumer=10/3  =3
         nConsumersWithExtraPart=10%3  =1
         
         a:
         myConsumerPosition= curConsumers.indexof(a) =0
         startPart= 3*0+0.min(1) = 0
         nParts = 3+(if (0 + 1 > 1) 0 else 1)=3+1=4
         b:
         myConsumerPosition=1
         c:
         myConsumerPosition
    複製程式碼

7.2 round-robin分割槽分配策略

如果同一個消費組內所有的消費者的訂閱資訊都是相同的,那麼RoundRobinAssignor策略的分割槽分配會是均勻的。 舉例如下: 假設消費組中有2個消費者C0和C1,都訂閱了主題topic0 和 topic1,並且每個主題都有3個分割槽,進行hashCode 排序 後,順序為:topic0_0、topic0_1、topic0_2、topic1_0、topic1_1、topic1_2。最終的分配結果為:

消費者consumer0:topic0_0、topic0_2 、 topic1_1

消費者consumer1:topic0_1、topic1_0、 topic1_2

使用RoundRobin策略有兩個前提條件必須滿足:

  • 同一個Consumer Group裡面的所有消費者的num.streams必須相等;
  • 每個消費者訂閱的主題必須相同。

所以這裡假設前面提到的2個消費者的num.streams = 2。RoundRobin策略的工作原理:將所有主題的分割槽組成 TopicAndPartition 列表,然後對 TopicAndPartition 列表按照 hashCode 進行排序,最後按照round-robin風格將分割槽分別分配給不同的消費者執行緒。

val allTopicPartitions = ctx.partitionsForTopic.flatMap { case(topic, partitions) =>
	  info("Consumer %s rebalancing the following partitions for topic %s: %s"
	       .format(ctx.consumerId, topic, partitions))
	  partitions.map(partition => {
	    TopicAndPartition(topic, partition)
	  })
	}.toSeq.sortWith((topicPartition1, topicPartition2) => {
	  /*
	   * Randomize the order by taking the hashcode to reduce the likelihood of all partitions of a given topic ending
	   * up on one consumer (if it has a high enough stream count).
	   */
	  topicPartition1.toString.hashCode < topicPartition2.toString.hashCode
	})
複製程式碼

7.3 StickyAssignor分割槽分配策略(摘錄)

  • 分割槽的分配要儘可能的均勻;
  • 分割槽的分配儘可能的與上次分配的保持相同。 當兩者發生衝突時,第一個目標優先於第二個目標。鑑於這兩個目標,StickyAssignor策略的具體實現要比RangeAssignor和RoundRobinAssignor這兩種分配策略要複雜很多。

假設消費組內有3個消費者:C0、C1和C2,它們都訂閱了4個主題:t0、t1、t2、t3,並且每個主題有2個分割槽,也就是說整個消費組訂閱了t0p0、t0p1、t1p0、t1p1、t2p0、t2p1、t3p0、t3p1這8個分割槽。最終的分配結果如下:

消費者C0:t0p0、t1p1、t3p0
消費者C1:t0p1、t2p0、t3p1
消費者C2:t1p0、t2p1
複製程式碼

假設此時消費者C1脫離了消費組,那麼消費組就會執行再平衡操作,進而消費分割槽會重新分配。如果採用RoundRobinAssignor策略,那麼此時的分配結果如下:

消費者C0:t0p0、t1p0、t2p0、t3p0
消費者C2:t0p1、t1p1、t2p1、t3p1
複製程式碼

RoundRobinAssignor策略會按照消費者C0和C2進行重新輪詢分配。而如果此時使用的是StickyAssignor策略,那麼分配結果為:

消費者C0:t0p0、t1p1、t3p0、t2p0
消費者C2:t1p0、t2p1、t0p1、t3p1
複製程式碼

可以看到分配結果中保留了上一次分配中對於消費者C0和C2的所有分配結果,並將原來消費者C1的“負擔”分配給了剩餘的兩個消費者C0和C2,最終C0和C2的分配仍然保持了均衡。

如果發生分割槽重分配,那麼對於同一個分割槽而言有可能之前的消費者和新指派的消費者不是同一個,對於之前消費者進行到一半的處理還要在新指派的消費者中再次復現一遍,這顯然很浪費系統資源。StickyAssignor策略如同其名稱中的“sticky”一樣,讓分配策略具備一定的“粘性”,儘可能地讓前後兩次分配相同,進而減少系統資源的損耗以及其它異常情況的發生。

7 reblance generation (代代不同)

主要作用在於防止無效的offset提交,原因在於若上一屆的consumer成員因為某些原因延遲提交了offset,同時被踢出group組,那麼新一屆的group組成員分割槽分配結束後,老一屆的consumer再次提交老的offset就會出問題。因此採用reblance generation ,老的請求就會被拒絕。

8 reblance 掃尾工作

每一次reblance操作之前,都會檢查使用者是否設定了自動提交位移,如果是,則幫助使用者提交。如沒有設定,會在監聽器中回撥使用者的提交程式。

9 總結

本文翻閱大量資料,極少程度,耗費大量時間,實屬不易,作者一直堅持原創,當然也借鑑了經典案例。

秦凱新 於深圳 2018 1:30

相關文章