• <menu id="gyiem"><menu id="gyiem"></menu></menu>
  • <menu id="gyiem"><code id="gyiem"></code></menu>

    Kafka設計解析(二)- Kafka High Availability (上)

    原創文章,轉載請務必將下面這段話置于文章開頭處。(已授權InfoQ中文站發布
    本文轉發自技術世界原文鏈接 http://www.luozeyang.com/2015/04/24/KafkaColumn2

    摘要

      Kafka在0.8以前的版本中,并不提供High Availablity機制,一旦一個或多個Broker宕機,則宕機期間其上所有Partition都無法繼續提供服務。若該Broker永遠不能再恢復,亦或磁盤故障,則其上數據將丟失。而Kafka的設計目標之一即是提供數據持久化,同時對于分布式系統來說,尤其當集群規模上升到一定程度后,一臺或者多臺機器宕機的可能性大大提高,對于Failover機制的需求非常高。因此,Kafka從0.8開始提供High Availability機制。本文從Data Replication和Leader Election兩方面介紹了Kafka的HA機制。

    Kafka為何需要High Available

    為何需要Replication

      在Kafka在0.8以前的版本中,是沒有Replication的,一旦某一個Broker宕機,則其上所有的Partition數據都不可被消費,這與Kafka數據持久性及Delivery Guarantee的設計目標相悖。同時Producer都不能再將數據存于這些Partition中。

    • 如果Producer使用同步模式則Producer會在嘗試重新發送message.send.max.retries(默認值為3)次后拋出Exception,用戶可以選擇停止發送后續數據也可選擇繼續選擇發送。而前者會造成數據的阻塞,后者會造成本應發往該Broker的數據的丟失。
    • 如果Producer使用異步模式,則Producer會嘗試重新發送message.send.max.retries(默認值為3)次后記錄該異常并繼續發送后續數據,這會造成數據丟失并且用戶只能通過日志發現該問題。

      由此可見,在沒有Replication的情況下,一旦某機器宕機或者某個Broker停止工作則會造成整個系統的可用性降低。隨著集群規模的增加,整個集群中出現該類異常的幾率大大增加,因此對于生產系統而言Replication機制的引入非常重要。   

    為何需要Leader Election

      (本文所述Leader Election主要指Replica之間的Leader Election)
      引入Replication之后,同一個Partition可能會有多個Replica,而這時需要在這些Replica中選出一個Leader,Producer和Consumer只與這個Leader交互,其它Replica作為Follower從Leader中復制數據。
      因為需要保證同一個Partition的多個Replica之間的數據一致性(其中一個宕機后其它Replica必須要能繼續服務并且即不能造成數據重復也不能造成數據丟失)。如果沒有一個Leader,所有Replica都可同時讀/寫數據,那就需要保證多個Replica之間互相(N×N條通路)同步數據,數據的一致性和有序性非常難保證,大大增加了Replication實現的復雜性,同時也增加了出現異常的幾率。而引入Leader后,只有Leader負責數據讀寫,Follower只向Leader順序Fetch數據(N條通路),系統更加簡單且高效。      

    Kafka HA設計解析

    如何將所有Replica均勻分布到整個集群

      為了更好的做負載均衡,Kafka盡量將所有的Partition均勻分配到整個集群上。一個典型的部署方式是一個Topic的Partition數量大于Broker的數量。同時為了提高Kafka的容錯能力,也需要將同一個Partition的Replica盡量分散到不同的機器。實際上,如果所有的Replica都在同一個Broker上,那一旦該Broker宕機,該Partition的所有Replica都無法工作,也就達不到HA的效果。同時,如果某個Broker宕機了,需要保證它上面的負載可以被均勻的分配到其它幸存的所有Broker上。
      Kafka分配Replica的算法如下:

    1. 將所有Broker(假設共n個Broker)和待分配的Partition排序
    2. 將第i個Partition分配到第(i mod n)個Broker上
    3. 將第i個Partition的第j個Replica分配到第((i + j) mod n)個Broker上

    Data Replication

      Kafka的Data Replication需要解決如下問題:

    • 怎樣Propagate消息
    • 在向Producer發送ACK前需要保證有多少個Replica已經收到該消息
    • 怎樣處理某個Replica不工作的情況
    • 怎樣處理Failed Replica恢復回來的情況

    Propagate消息

      Producer在發布消息到某個Partition時,先通過 Metadata (通過 Broker 獲取并且緩存在 Producer 內) 找到該 Partition 的Leader,然后無論該Topic的Replication Factor為多少(也即該Partition有多少個Replica),Producer只將該消息發送到該Partition的Leader。Leader會將該消息寫入其本地Log。每個Follower都從Leader pull數據。這種方式上,Follower存儲的數據順序與Leader保持一致。Follower在收到該消息并寫入其Log后,向Leader發送ACK。一旦Leader收到了ISR中的所有Replica的ACK,該消息就被認為已經commit了,Leader將增加HW并且向Producer發送ACK。
    為了提高性能,每個Follower在接收到數據后就立馬向Leader發送ACK,而非等到數據寫入Log中。因此,對于已經commit的消息,Kafka只能保證它被存于多個Replica的內存中,而不能保證它們被持久化到磁盤中,也就不能完全保證異常發生后該條消息一定能被Consumer消費。但考慮到這種場景非常少見,可以認為這種方式在性能和數據持久化上做了一個比較好的平衡。在將來的版本中,Kafka會考慮提供更高的持久性。
    Consumer讀消息也是從Leader讀取,只有被commit過的消息(offset低于HW的消息)才會暴露給Consumer。
    Kafka Replication的數據流如下圖所示
    Kafka Replication Data Flow

    ACK前需要保證有多少個備份

      和大部分分布式系統一樣,Kafka處理失敗需要明確定義一個Broker是否“活著”。對于Kafka而言,Kafka存活包含兩個條件,一是它必須維護與Zookeeper的session(這個通過Zookeeper的Heartbeat機制來實現)。二是Follower必須能夠及時將Leader的消息復制過來,不能“落后太多”。
      Leader會跟蹤與其保持同步的Replica列表,該列表稱為ISR(即in-sync Replica)。如果一個Follower宕機,或者落后太多,Leader將把它從ISR中移除。這里所描述的“落后太多”指Follower復制的消息落后于Leader后的條數超過預定值(該值可在$KAFKA_HOME/config/server.properties中通過replica.lag.max.messages配置,其默認值是4000)或者Follower超過一定時間(該值可在$KAFKA_HOME/config/server.properties中通過replica.lag.time.max.ms來配置,其默認值是10000)未向Leader發送fetch請求。。
      Kafka的復制機制既不是完全的同步復制,也不是單純的異步復制。事實上,同步復制要求所有能工作的Follower都復制完,這條消息才會被認為commit,這種復制方式極大的影響了吞吐率(高吞吐率是Kafka非常重要的一個特性)。而異步復制方式下,Follower異步的從Leader復制數據,數據只要被Leader寫入log就被認為已經commit,這種情況下如果Follower都復制完都落后于Leader,而如果Leader突然宕機,則會丟失數據。而Kafka的這種使用ISR的方式則很好的均衡了確保數據不丟失以及吞吐率。Follower可以批量的從Leader復制數據,這樣極大的提高復制性能(批量寫磁盤),極大減少了Follower與Leader的差距。
      需要說明的是,Kafka只解決fail/recover,不處理“Byzantine”(“拜占庭”)問題。一條消息只有被ISR里的所有Follower都從Leader復制過去才會被認為已提交。這樣就避免了部分數據被寫進了Leader,還沒來得及被任何Follower復制就宕機了,而造成數據丟失(Consumer無法消費這些數據)。而對于Producer而言,它可以選擇是否等待消息commit,這可以通過request.required.acks來設置。這種機制確保了只要ISR有一個或以上的Follower,一條被commit的消息就不會丟失。   

    Leader Election算法

      上文說明了Kafka是如何做Replication的,另外一個很重要的問題是當Leader宕機了,怎樣在Follower中選舉出新的Leader。因為Follower可能落后許多或者crash了,所以必須確保選擇“最新”的Follower作為新的Leader。一個基本的原則就是,如果Leader不在了,新的Leader必須擁有原來的Leader commit過的所有消息。這就需要作一個折衷,如果Leader在標明一條消息被commit前等待更多的Follower確認,那在它宕機之后就有更多的Follower可以作為新的Leader,但這也會造成吞吐率的下降。
      一種非常常用的Leader Election的方式是“Majority Vote”(“少數服從多數”),但Kafka并未采用這種方式。這種模式下,如果我們有2f+1個Replica(包含Leader和Follower),那在commit之前必須保證有f+1個Replica復制完消息,為了保證正確選出新的Leader,fail的Replica不能超過f個。因為在剩下的任意f+1個Replica里,至少有一個Replica包含有最新的所有消息。這種方式有個很大的優勢,系統的latency只取決于最快的幾個Broker,而非最慢那個。Majority Vote也有一些劣勢,為了保證Leader Election的正常進行,它所能容忍的fail的follower個數比較少。如果要容忍1個follower掛掉,必須要有3個以上的Replica,如果要容忍2個Follower掛掉,必須要有5個以上的Replica。也就是說,在生產環境下為了保證較高的容錯程度,必須要有大量的Replica,而大量的Replica又會在大數據量下導致性能的急劇下降。這就是這種算法更多用在Zookeeper這種共享集群配置的系統中而很少在需要存儲大量數據的系統中使用的原因。例如HDFS的HA Feature是基于majority-vote-based journal,但是它的數據存儲并沒有使用這種方式。
      實際上,Leader Election算法非常多,比如Zookeeper的Zab, RaftViewstamped Replication。而Kafka所使用的Leader Election算法更像微軟的PacificA算法。
      Kafka在Zookeeper中動態維護了一個ISR(in-sync replicas),這個ISR里的所有Replica都跟上了leader,只有ISR里的成員才有被選為Leader的可能。在這種模式下,對于f+1個Replica,一個Partition能在保證不丟失已經commit的消息的前提下容忍f個Replica的失敗。在大多數使用場景中,這種模式是非常有利的。事實上,為了容忍f個Replica的失敗,Majority Vote和ISR在commit前需要等待的Replica數量是一樣的,但是ISR需要的總的Replica的個數幾乎是Majority Vote的一半。   

    如何處理所有Replica都不工作

      上文提到,在ISR中至少有一個follower時,Kafka可以確保已經commit的數據不丟失,但如果某個Partition的所有Replica都宕機了,就無法保證數據不丟失了。這種情況下有兩種可行的方案:

    • 等待ISR中的任一個Replica“活”過來,并且選它作為Leader
    • 選擇第一個“活”過來的Replica(不一定是ISR中的)作為Leader

      這就需要在可用性和一致性當中作出一個簡單的折衷。如果一定要等待ISR中的Replica“活”過來,那不可用的時間就可能會相對較長。而且如果ISR中的所有Replica都無法“活”過來了,或者數據都丟失了,這個Partition將永遠不可用。選擇第一個“活”過來的Replica作為Leader,而這個Replica不是ISR中的Replica,那即使它并不保證已經包含了所有已commit的消息,它也會成為Leader而作為consumer的數據源(前文有說明,所有讀寫都由Leader完成)。Kafka0.8.*使用了第二種方式。根據Kafka的文檔,在以后的版本中,Kafka支持用戶通過配置選擇這兩種方式中的一種,從而根據不同的使用場景選擇高可用性還是強一致性。   

    如何選舉Leader

      最簡單最直觀的方案是,所有Follower都在Zookeeper上設置一個Watch,一旦Leader宕機,其對應的ephemeral znode會自動刪除,此時所有Follower都嘗試創建該節點,而創建成功者(Zookeeper保證只有一個能創建成功)即是新的Leader,其它Replica即為Follower。
      但是該方法會有3個問題:   

    • split-brain 這是由Zookeeper的特性引起的,雖然Zookeeper能保證所有Watch按順序觸發,但并不能保證同一時刻所有Replica“看”到的狀態是一樣的,這就可能造成不同Replica的響應不一致
    • herd effect 如果宕機的那個Broker上的Partition比較多,會造成多個Watch被觸發,造成集群內大量的調整
    • Zookeeper負載過重 每個Replica都要為此在Zookeeper上注冊一個Watch,當集群規模增加到幾千個Partition時Zookeeper負載會過重。

      Kafka 0.8.*的Leader Election方案解決了上述問題,它在所有broker中選出一個controller,所有Partition的Leader選舉都由controller決定。controller會將Leader的改變直接通過RPC的方式(比Zookeeper Queue的方式更高效)通知需為此作出響應的Broker。同時controller也負責增刪Topic以及Replica的重新分配。

    HA相關Zookeeper結構

      (本節所示Zookeeper結構中,實線框代表路徑名是固定的,而虛線框代表路徑名與業務相關)
      admin (該目錄下znode只有在有相關操作時才會存在,操作結束時會將其刪除)
    Kafka Zookeeper Admin Structure

      /admin/preferred_replica_election數據結構

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
       Schema:
    {
    "fields":[
    {
    "name":"version",
    "type":"int",
    "doc":"version id"
    },
    {
    "name":"partitions",
    "type":{
    "type":"array",
    "items":{
    "fields":[
    {
    "name":"topic",
    "type":"string",
    "doc":"topic of the partition for which preferred replica election should be triggered"
    },
    {
    "name":"partition",
    "type":"int",
    "doc":"the partition for which preferred replica election should be triggered"
    }
    ],
    }
    "doc":"an array of partitions for which preferred replica election should be triggered"
    }
    }
    ]
    }

    Example:
    {
    "version": 1,
    "partitions":
    [
    {
    "topic": "topic1",
    "partition": 8
    },
    {
    "topic": "topic2",
    "partition": 16
    }
    ]
    }

      /admin/reassign_partitions用于將一些Partition分配到不同的broker集合上。對于每個待重新分配的Partition,Kafka會在該znode上存儲其所有的Replica和相應的Broker id。該znode由管理進程創建并且一旦重新分配成功它將會被自動移除。其數據結構如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
       Schema:
    {
    "fields":[
    {
    "name":"version",
    "type":"int",
    "doc":"version id"
    },
    {
    "name":"partitions",
    "type":{
    "type":"array",
    "items":{
    "fields":[
    {
    "name":"topic",
    "type":"string",
    "doc":"topic of the partition to be reassigned"
    },
    {
    "name":"partition",
    "type":"int",
    "doc":"the partition to be reassigned"
    },
    {
    "name":"replicas",
    "type":"array",
    "items":"int",
    "doc":"a list of replica ids"
    }
    ],
    }
    "doc":"an array of partitions to be reassigned to new replicas"
    }
    }
    ]
    }

    Example:
    {
    "version": 1,
    "partitions":
    [
    {
    "topic": "topic3",
    "partition": 1,
    "replicas": [1, 2, 3]
    }
    ]
    }

      /admin/delete_topics數據結構

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    Schema:
    { "fields":
    [ {"name": "version", "type": "int", "doc": "version id"},
    {"name": "topics",
    "type": { "type": "array", "items": "string", "doc": "an array of topics to be deleted"}
    } ]
    }

    Example:
    {
    "version": 1,
    "topics": ["topic4", "topic5"]
    }

      brokers
    Kafka Zookeeper brokers structure

      broker(即/brokers/ids/[brokerId])存儲“活著”的Broker信息。數據結構如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    Schema:
    { "fields":
    [ {"name": "version", "type": "int", "doc": "version id"},
    {"name": "host", "type": "string", "doc": "ip address or host name of the broker"},
    {"name": "port", "type": "int", "doc": "port of the broker"},
    {"name": "jmx_port", "type": "int", "doc": "port for jmx"}
    ]
    }

    Example:
    {
    "jmx_port":-1,
    "host":"node1",
    "version":1,
    "port":9092
    }

      topic注冊信息(/brokers/topics/[topic]),存儲該Topic的所有Partition的所有Replica所在的Broker id,第一個Replica即為Preferred Replica,對一個給定的Partition,它在同一個Broker上最多只有一個Replica,因此Broker id可作為Replica id。數據結構如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    Schema:
    { "fields" :
    [ {"name": "version", "type": "int", "doc": "version id"},
    {"name": "partitions",
    "type": {"type": "map",
    "values": {"type": "array", "items": "int", "doc": "a list of replica ids"},
    "doc": "a map from partition id to replica list"},
    }
    ]
    }
    Example:
    {
    "version":1,
    "partitions":
    {"12":[6],
    "8":[2],
    "4":[6],
    "11":[5],
    "9":[3],
    "5":[7],
    "10":[4],
    "6":[8],
    "1":[3],
    "0":[2],
    "2":[4],
    "7":[1],
    "3":[5]}
    }

      partition state(/brokers/topics/[topic]/partitions/[partitionId]/state) 結構如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    Schema:
    { "fields":
    [ {"name": "version", "type": "int", "doc": "version id"},
    {"name": "isr",
    "type": {"type": "array",
    "items": "int",
    "doc": "an array of the id of replicas in isr"}
    },
    {"name": "leader", "type": "int", "doc": "id of the leader replica"},
    {"name": "controller_epoch", "type": "int", "doc": "epoch of the controller that last updated the leader and isr info"},
    {"name": "leader_epoch", "type": "int", "doc": "epoch of the leader"}
    ]
    }

    Example:
    {
    "controller_epoch":29,
    "leader":2,
    "version":1,
    "leader_epoch":48,
    "isr":[2]
    }

      controller
      /controller -> int (broker id of the controller)存儲當前controller的信息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    Schema:
    { "fields":
    [ {"name": "version", "type": "int", "doc": "version id"},
    {"name": "brokerid", "type": "int", "doc": "broker id of the controller"}
    ]
    }
    Example:
    {
    "version":1,
      "brokerid":8
    }

      /controller_epoch -> int (epoch)直接以整數形式存儲controller epoch,而非像其它znode一樣以JSON字符串形式存儲。      

    broker failover過程簡介

    1. Controller在Zookeeper注冊Watch,一旦有Broker宕機(這是用宕機代表任何讓系統認為其die的情景,包括但不限于機器斷電,網絡不可用,GC導致的Stop The World,進程crash等),其在Zookeeper對應的znode會自動被刪除,Zookeeper會fire Controller注冊的watch,Controller讀取最新的幸存的Broker
    2. Controller決定set_p,該集合包含了宕機的所有Broker上的所有Partition
    3. 對set_p中的每一個Partition
        3.1 從/brokers/topics/[topic]/partitions/[partition]/state讀取該Partition當前的ISR
        3.2 決定該Partition的新Leader。如果當前ISR中有至少一個Replica還幸存,則選擇其中一個作為新Leader,新的ISR則包含當前ISR中所有幸存的Replica。否則選擇該Partition中任意一個幸存的Replica作為新的Leader以及ISR(該場景下可能會有潛在的數據丟失)。如果該Partition的所有Replica都宕機了,則將新的Leader設置為-1。
         3.3 將新的Leader,ISR和新的leader_epochcontroller_epoch寫入/brokers/topics/[topic]/partitions/[partition]/state。注意,該操作只有其version在3.1至3.3的過程中無變化時才會執行,否則跳轉到3.1
    4. 直接通過RPC向set_p相關的Broker發送LeaderAndISRRequest命令。Controller可以在一個RPC操作中發送多個命令從而提高效率。
        Broker failover順序圖如下所示。
      broker failover sequence diagram

    下篇預告

      下篇文章將詳細介紹Kafka HA相關的異常情況處理,例如,怎樣處理Broker failover,Follower如何從Leader fetch消息,如何重新分配Replica,如何處理Controller failure等。

    Kafka系列文章

    郭俊 Jason wechat
    歡迎關注作者微信公眾號【大數據架構】
    您的贊賞將支持作者繼續原創分享
    速赢彩app 鞍山 | 陇南 | 新乡 | 雅安 | 台南 | 衢州 | 七台河 | 张掖 | 万宁 | 南安 | 新余 | 鞍山 | 漯河 | 河北石家庄 | 安庆 | 塔城 | 泗阳 | 乐平 | 德清 | 安岳 | 吐鲁番 | 张北 | 池州 | 昌吉 | 包头 | 台南 | 菏泽 | 河南郑州 | 鄂尔多斯 | 东莞 | 山西太原 | 宝应县 | 哈密 | 秦皇岛 | 十堰 | 义乌 | 陇南 | 恩施 | 东海 | 台南 | 锡林郭勒 | 鄂州 | 贵港 | 乌海 | 吉林长春 | 池州 | 克孜勒苏 | 珠海 | 深圳 | 秦皇岛 | 湘潭 | 阿克苏 | 宁国 | 南平 | 陕西西安 | 海宁 | 海门 | 新余 | 河源 | 姜堰 | 梧州 | 诸暨 | 铜川 | 澄迈 | 南京 | 保山 | 吴忠 | 南安 | 德宏 | 包头 | 林芝 | 大兴安岭 | 楚雄 | 改则 | 怒江 | 余姚 | 泗洪 | 厦门 | 永新 | 贵港 | 临夏 | 仁寿 | 海南 | 包头 | 宜都 | 乳山 | 安岳 | 泰兴 | 廊坊 | 桐乡 | 扬州 | 固原 | 长葛 | 朔州 | 安顺 | 辽源 | 潮州 | 北海 | 晋中 | 邳州 | 保定 | 娄底 | 安顺 | 林芝 | 乐平 | 黔东南 | 昌都 | 忻州 | 深圳 | 丹东 | 牡丹江 | 馆陶 | 佛山 |