Kafka 原理和實戰

vivo網際網路技術發表於2019-08-20
本文首發於 vivo網際網路技術 微信公眾號 https://mp.weixin.qq.com/s/bV8AhqAjQp4a_iXRfobkCQ
作者簡介:鄭志彬,畢業於華南理工大學電腦科學與技術(雙語班)。先後從事過電子商務、開放平臺、移動瀏覽器、推薦廣告和大資料、人工智慧等相關開發和架構。目前在vivo智慧平臺中心從事 AI中臺建設以及廣告推薦業務。擅長各種業務形態的業務架構、平臺化以及各種業務解決方案。
部落格地址:http://arganzheng.life

背景

最近要把原來做的那套集中式日誌監控系統進行遷移,原來的實現方案是: Log Agent => Log Server => ElasticSearch => Kibana,其中Log Agent和Log Server之間走的是Thrift RPC,自己實現了一個簡單的負載均衡(WRB)。

原來的方案其實執行的挺好的,非同步化Agent對應用效能基本沒有影響。支援我們這個每天幾千萬PV的應用一點壓力都沒有。不過有個缺點就是如果錯誤日誌暴增,Log Server這塊處理不過來,會導致訊息丟失。當然我們量級沒有達到這個程度,而且也是可以通過引入佇列緩衝一下處理。不過現在綜合考慮,其實直接使用訊息佇列會更簡單。PRC,負載均衡,負載緩衝都內建實現了。另一種方式是直接讀取日誌,類似於logstash或者flume的方式。不過考慮到靈活性還是決定使用訊息佇列的方式,反正我們已經部署了Zookeeper。調研了一下,Kafka是最適合做這個資料中轉和緩衝的。於是,打算把方案改成: Log Agent => Kafka => ElasticSearch => Kibana。

Kafka介紹

一、Kafka基本概念

  • Broker:Kafka叢集包含一個或多個伺服器,這種伺服器被稱為broker。
  • Topic:每條釋出到Kafka叢集的訊息都有一個類別,這個類別被稱為Topic。
  • Message

    • 訊息是Kafka通訊的基本單位,有一個固定長度的訊息頭和一個可變長度的訊息體(payload)構成。在Java客戶端中又稱之為記錄(Record)。
    • 訊息結構各部分說明如下:

      • CRC32: CRC32校驗和,4個位元組。
      • magic: Kafka服務程式協議版本號,用於做相容。1個位元組。
      • attributes: 該欄位佔1位元組,其中低兩位用來表示壓縮方式,第三位表示時間戳型別(0表示LogCreateTime,1表示LogAppendTime),高四位為預留位置,暫無實際意義。
      • timestamp: 訊息時間戳,當 magic > 0 時訊息頭必須包含該欄位。8個位元組。
      • key-length: 訊息key長度,4個位元組。
      • key: 訊息key實際資料。
      • payload-length: 訊息實際資料長度,4個位元組。
      • payload: 訊息實際資料
    • 在實際儲存一條訊息還包括12位元組的額外開銷(LogOverhead):

      • 訊息的偏移量: 8位元組,類似於訊息的Id。
      • 訊息的總長度: 4位元組
  • Partition:

    • Partition(分割槽)是物理上的概念,每個Topic包含一個或多個Partition。
    • 每個分割槽由一系列有序的不可變的訊息組成,是一個有序佇列。
    • 每個分割槽在物理上對應為一個資料夾,分割槽的命名規則為${topicName}-{partitionId},如__consumer_offsets-0
    • 分割槽目錄下儲存的是該分割槽的日誌段,包括日誌資料檔案和兩個索引檔案。
    • 每條訊息被追加到相應的分割槽中,是順序寫磁碟,因此效率非常高,這也是Kafka高吞吐率的一個重要保證。
    • kafka只能保證一個分割槽內的訊息的有序性,並不能保證跨分割槽訊息的有序性。
  • LogSegment:

    • 日誌檔案按照大小或者時間滾動切分成一個或者多個日誌段(LogSegment),其中日誌段大小由配置項log.segment.bytes指定,預設是1GB。時間長度則是根據log.roll.ms或者log.roll.hours配置項設定;當前活躍的日誌段稱之為活躍段(activeSegment)。
    • 不同於普通的日誌檔案,Kafka的日誌段除了有一個具體的日誌檔案之外,還有兩個輔助的索引檔案:

      • 資料檔案

        • 資料檔案是以 .log 為檔案字尾名的訊息集檔案(FileMessageSet),用於儲存訊息實際資料
        • 命名規則為:由資料檔案的第一條訊息偏移量,也稱之為基準偏移量(BaseOffset),左補0構成20位數字字元組成
        • 每個資料檔案的基準偏移量就是上一個資料檔案的LEO+1(第一個資料檔案為0)
      • 偏移量索引檔案

        • 檔名與資料檔案相同,但是以.index為字尾名。它的目的是為了快速根據偏移量定位到訊息所在的位置。
        • 首先Kafka將每個日誌段以BaseOffset為key儲存到一個ConcurrentSkipListMap跳躍表中,這樣在查詢指定偏移量的訊息時,用二分查詢法就能快速定位到訊息所在的資料檔案和索引檔案
        • 然後在索引檔案中通過二分查詢,查詢值小於等於指定偏移量的最大偏移量,最後從查詢出的最大偏移量處開始順序掃描資料檔案,直到在資料檔案中查詢到偏移量與指定偏移量相等的訊息
        • 需要注意的是並不是每條訊息都對應有索引,而是採用了稀疏儲存的方式,每隔一定位元組的資料建立一條索引,我們可以通過index.interval.bytes設定索引跨度。
      • 時間戳索引檔案

        • Kafka從0.10.1.1版本開始引入了一個基於時間戳的索引檔案,檔名與資料檔案相同,但是以.timeindex作為字尾。它的作用則是為了解決根據時間戳快速定位訊息所在位置。
        • Kafka API提供了一個 offsetsForTimes(Map<TopicPartition, Long> timestampsToSearch)方法,該方法會返回時間戳大於等於待查詢時間的第一條訊息對應的偏移量和時間戳。這個功能其實挺好用的,假設我們希望從某個時間段開始消費,就可以用offsetsForTimes()方法定位到離這個時間最近的第一條訊息的偏移量,然後呼叫seek(TopicPartition, long offset)方法將消費者偏移量移動過去,然後呼叫poll()方法長輪詢拉取訊息。
  • Producer:

    • 負責釋出訊息到Kafka broker。
    • 生產者的一些重要的配置項:

      • request.required.acks: Kafka為生產者提供了三種訊息確認機制(ACK),用於配置broker接到訊息後向生產者傳送確認資訊,以便生產者根據ACK進行相應的處理,該機制通過屬性request.required.acks設定,取值可以為0, -1, 1,預設是1。

        • acks=0: 生產者不需要等待broker返回確認訊息,而連續傳送訊息。
        • acks=1: 生產者需要等待Leader副本已經成功將訊息寫入日誌檔案中。這種方式在一定程度上降低了資料丟失的可能性,但仍無法保證資料一定不會丟失。因為沒有等待follower副本同步完成。
        • acks=-1: Leader副本和所有的ISR列表中的副本都完成資料儲存時才會向生產者傳送確認訊息。為了保證資料不丟失,需要保證同步的副本至少大於1,通過引數min.insync.replicas設定,當同步副本數不足次配置項時,生產者會丟擲異常。但是這種方式同時也影響了生產者傳送訊息的速度以及吞吐率。
      • message.send.max.retries: 生產者在放棄該訊息前進行重試的次數,預設是3次。
      • retry.backoff.ms: 每次重試之前等待的時間,單位是ms,預設是100。
      • queue.buffering.max.ms: 在非同步模式下,訊息被快取的最長時間,當到達該時間後訊息被開始批量傳送;若在非同步模式下同時配置了快取資料的最大值batch.num.messages,則達到這兩個閾值的任何一個就會觸發訊息批量傳送。預設是1000ms。
      • queue.buffering.max.messages: 在非同步模式下,可以被快取到佇列中的未傳送的最大訊息條數。預設是10000。
      • queue.enqueue.timeout.ms

        • =0: 表示當佇列沒滿時直接入隊,滿了則立即丟棄
        • <0: 表示無條件阻塞且不丟棄
        • >0: 表示阻塞達到該值時長丟擲QueueFullException異常
      • batch.num.messages: Kafka支援批量訊息(Batch)向broker的特定分割槽傳送訊息,批量大小由屬性batch.num.messages設定,表示每次批量傳送訊息的最大訊息數,當生產者採用同步模式傳送時改配置項將失效。預設是200。
      • request.timeout.ms: 在需要acks時,生產者等待broker應答的超時時間。預設是1500ms。
      • send.buffer.bytes: Socket傳送緩衝區大小。預設是100kb。
      • topic.metadata.refresh.interval.ms: 生產者定時請求更新主題後設資料的時間間隔。若設定為0,則在每個訊息傳送後都會去請求更新資料。預設是5min。
      • client.id: 生產者id,主要方便業務用來追蹤呼叫定位問題。預設是console-producer
  • Consumer & Consumer Group & Group Coordinator:

    • Consumer: 訊息消費者,向Kafka broker讀取訊息的客戶端。Kafka0.9版本釋出了基於Java重新寫的新的消費者,它不再依賴scala執行時環境和zookeeper。
    • Consumer Group: 每個消費者都屬於一個特定的Consumer Group,可通過group.id配置項指定,若不指定group name則預設為test-consumer-group
    • Group Coordinator: 對於每個Consumer group,會選擇一個brokers作為消費組的協調者。
    • 每個消費者也有一個全域性唯一的id,可通過配置項client.id指定,如果不指定,Kafka會自動為該消費者生成一個格式為${groupId}-${hostName}-${timestamp}-${UUID前8個字元}的全域性唯一id。
    • Kafka提供了兩種提交consumer_offset的方式:Kafka自動提交 或者 客戶端呼叫KafkaConsumer相應API手動提交。

      • 自動提交: 並不是定時週期性提交,而是在一些特定事件發生時才檢測與上一次提交的時間間隔是否超過auto.commit.interval.ms

        • enable.auto.commit=true
        • auto.commit.interval.ms
      • 手動提交

        • enable.auto.commit=false
        • commitSync(): 同步提交
        • commitAsync(): 非同步提交
    • 消費者的一些重要的配置項:

      • group.id: A unique string that identifies the consumer group this consumer belongs to.
      • client.id: The client id is a user-specified string sent in each request to help trace calls. It should logically identify the application making the request.
      • bootstrap.servers: A list of host/port pairs to use for establishing the initial connection to the Kafka cluster.
      • key.deserializer: Deserializer class for key that implements the org.apache.kafka.common.serialization.Deserializer interface.
      • value.deserializer: Deserializer class for value that implements the org.apache.kafka.common.serialization.Deserializer interface.
      • fetch.min.bytes: The minimum amount of data the server should return for a fetch request. If insufficient data is available the request will wait for that much data to accumulate before answering the request.
      • fetch.max.bytes: The maximum amount of data the server should return for a fetch request.
      • max.partition.fetch.bytes: The maximum amount of data per-partition the server will return.
      • max.poll.records: The maximum number of records returned in a single call to poll().
      • heartbeat.interval.ms: The expected time between heartbeats to the consumer coordinator when using Kafka’s group management facilities.
      • session.timeout.ms: The timeout used to detect consumer failures when using Kafka’s group management facility.
      • enable.auto.commit: If true the consumer’s offset will be periodically committed in the background.
  • ISR: Kafka在ZK中動態維護了一個ISR(In-Sync Replica),即保持同步的副本列表,該列表中儲存的是與leader副本保持訊息同步的所有副本對應的brokerId。如果一個副本當機或者落後太多,則該follower副本將從ISR列表中移除。
  • Zookeeper:

    • Kafka利用ZK儲存相應的後設資料資訊,包括:broker資訊,Kafka叢集資訊,舊版消費者資訊以及消費偏移量資訊,主題資訊,分割槽狀態資訊,分割槽副本分片方案資訊,動態配置資訊,等等。
    • Kafka在zk中註冊節點說明:

      • /consumers: 舊版消費者啟動後會在ZK的該節點下建立一個消費者的節點
      • /brokers/seqid: 輔助生成的brokerId,當使用者沒有配置broker.id時,ZK會自動生成一個全域性唯一的id。
      • /brokers/topics: 每建立一個主題就會在該目錄下建立一個與該主題同名的節點。
      • /borkers/ids: 當Kafka每啟動一個KafkaServer時就會在該目錄下建立一個名為{broker.id}的子節點
      • /config/topics: 儲存動態修改主題級別的配置資訊
      • /config/clients: 儲存動態修改客戶端級別的配置資訊
      • /config/changes: 動態修改配置時儲存相應的資訊
      • /admin/delete_topics: 在對主題進行刪除操作時儲存待刪除主題的資訊
      • /cluster/id: 儲存叢集id資訊
      • /controller: 儲存控制器對應的brokerId資訊等
      • /isr_change_notification: 儲存Kafka副本ISR列表發生變化時通知的相應路徑
    • Kafka在啟動或者執行過程中會在ZK上建立相應的節點來儲存後設資料資訊,通過監聽機制在這些節點註冊相應的監聽器來監聽節點後設資料的變化。
TIPS

如果跟ES對應,Broker相當於Node,Topic相當於Index,Message相對於Document,而Partition相當於shard。LogSegment相對於ES的Segment。

如何檢視訊息內容(Dump Log Segments)

我們在使用kafka的過程中有時候可以需要檢視我們生產的訊息的各種資訊,這些訊息是儲存在kafka的日誌檔案中的。由於日誌檔案的特殊格式,我們是無法直接檢視日誌檔案中的資訊內容。Kafka提供了一個命令,可以將二進位制分段日誌檔案轉儲為字元型別的檔案:

$ bin/kafka-run-class.sh kafka.tools.DumpLogSegments
Parse a log file and dump its contents to the console, useful for debugging a seemingly corrupt log segment.
Option                                  Description                           
------                                  -----------                           
--deep-iteration                        使用深迭代而不是淺迭代                          
--files <file1, file2, ...>             必填。輸入的日誌段檔案,逗號分隔
--key-decoder-class                     自定義key值反序列化器。必須實現`kafka.serializer.Decoder` trait。所在jar包需要放在`kafka/libs`目錄下。(預設是`kafka.serializer.StringDecoder`)。
--max-message-size <Integer: size>      訊息最大的位元組數(預設為5242880)                           
--print-data-log                        同時列印出日誌訊息             
--value-decoder-class                   自定義value值反序列化器。必須實現`kafka.serializer.Decoder` trait。所在jar包需要放在`kafka/libs`目錄下。(預設是`kafka.serializer.StringDecoder`)。
--verify-index-only                     只是驗證索引不列印索引內容
$ bin/kafka-run-class.sh kafka.tools.DumpLogSegments --files /tmp/kafka-logs/test-0/00000000000000000000.log --print-data-log 
Dumping /tmp/kafka-logs/test-0/00000000000000000000.log
Starting offset: 0
offset: 0 position: 0 CreateTime: 1498104812192 isvalid: true payloadsize: 11 magic: 1 compresscodec: NONE crc: 3271928089 payload: hello world
offset: 1 position: 45 CreateTime: 1498104813269 isvalid: true payloadsize: 14 magic: 1 compresscodec: NONE crc: 242183772 payload: hello everyone

注意:這裡 --print-data-log  是表示檢視訊息內容的,不加此項只能看到Header,看不到payload。

也可以用來檢視index檔案:

$ bin/kafka-run-class.sh kafka.tools.DumpLogSegments --files /tmp/kafka-logs/test-0/00000000000000000000.index  --print-data-log 
Dumping /tmp/kafka-logs/test-0/00000000000000000000.index
offset: 0 position: 0

timeindex檔案也是OK的:

$ bin/kafka-run-class.sh kafka.tools.DumpLogSegments --files /tmp/kafka-logs/test-0/00000000000000000000.timeindex  --print-data-log 
Dumping /tmp/kafka-logs/test-0/00000000000000000000.timeindex
timestamp: 1498104813269 offset: 1
Found timestamp mismatch in :/tmp/kafka-logs/test-0/00000000000000000000.timeindex
  Index timestamp: 0, log timestamp: 1498104812192
Found out of order timestamp in :/tmp/kafka-logs/test-0/00000000000000000000.timeindex
  Index timestamp: 0, Previously indexed timestamp: 1498104813269

消費者平衡過程

消費者平衡(Consumer Rebalance)是指的是消費者重新加入消費組,並重新分配分割槽給消費者的過程。在以下情況下會引起消費者平衡操作:

  • 新的消費者加入消費組
  • 當前消費者從消費組退出(不管是異常退出還是正常關閉)
  • 消費者取消對某個主題的訂閱
  • 訂閱主題的分割槽增加(Kafka的分割槽數可以動態增加但是不能減少)
  • broker當機新的協調器當選
  • 當消費者在${session.timeout.ms}時間內還沒有傳送心跳請求,組協調器認為消費者已退出。

消費者自動平衡操作提供了消費者的高可用和高可擴充套件性,這樣當我們增加或者減少消費者或者分割槽數的時候,不需要關心底層消費者和分割槽的分配關係。但是需要注意的是,在rebalancing過程中,由於需要給消費者重新分配分割槽,所以會出現在一個短暫時間內消費者不能拉取訊息的狀況。

NOTES

這裡要特別注意最後一種情況,就是所謂的慢消費者(Slow Consumers)。如果沒有在session.timeout.ms時間內收到心跳請求,協調者可以將慢消費者從組中移除。通常,如果訊息處理比session.timeout.ms慢,就會成為慢消費者。導致兩次poll()方法的呼叫間隔比session.timeout.ms時間長。由於心跳只在 poll()呼叫時才會傳送(在0.10.1.0版本中, 客戶端心跳在後臺非同步傳送了),這就會導致協調者標記慢消費者死亡。

如果沒有在session.timeout.ms時間內收到心跳請求,協調者標記消費者死亡並且斷開和它的連線。同時,通過向組內其他消費者的HeartbeatResponse中傳送IllegalGeneration錯誤程式碼 觸發rebalance操作。

在手動commit offset的模式下,要特別注意這個問題,否則會出現commit不上的情況。導致一直在重複消費。

二、Kafka的特點

  1. 訊息順序:保證每個partition內部的順序,但是不保證跨partition的全域性順序。如果需要全域性訊息有序,topic只能有一個partition。
  2. consumer group:consumer group中的consumer併發獲取訊息,但是為了保證partition訊息的順序性,每個partition只會由一個consumer消費。因此consumer group中的consumer數量需要小於等於topic的partition個數。(如需全域性訊息有序,只能有一個partition,一個consumer)
  3. 同一Topic的一條訊息只能被同一個Consumer Group內的一個Consumer消費,但多個Consumer Group可同時消費這一訊息。這是Kafka用來實現一個Topic訊息的廣播(發給所有的Consumer)和單播(發給某一個Consumer)的手段。一個Topic可以對應多個Consumer Group。如果需要實現廣播,只要每個Consumer有一個獨立的Group就可以了。要實現單播只要所有的Consumer在同一個Group裡。
  4. Producer Push訊息,Client Pull訊息模式:一些logging-centric system,比如Facebook的Scribe和Cloudera的Flume,採用push模式。事實上,push模式和pull模式各有優劣。push模式很難適應消費速率不同的消費者,因為訊息傳送速率是由broker決定的。push模式的目標是儘可能以最快速度傳遞訊息,但是這樣很容易造成Consumer來不及處理訊息,典型的表現就是拒絕服務以及網路擁塞。而pull模式則可以根據Consumer的消費能力以適當的速率消費訊息。pull模式可簡化broker的設計,Consumer可自主控制消費訊息的速率,同時Consumer可以自己控制消費方式——即可批量消費也可逐條消費,同時還能選擇不同的提交方式從而實現不同的傳輸語義。

實際上,Kafka的設計理念之一就是同時提供離線處理和實時處理。根據這一特性,可以使用Storm或Spark Streaming這種實時流處理系統對訊息進行實時線上處理,同時使用Hadoop這種批處理系統進行離線處理,還可以同時將資料實時備份到另一個資料中心,只需要保證這三個操作所使用的Consumer屬於不同的Consumer Group即可。

三、kafka的HA

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

Data Replication

Kafka從0.8開始提供partition級別的replication,replication的數量可在

$KAFKA_HOME/config/server.properties 中配置:

default.replication.factor = 1

該 Replication與leader election配合提供了自動的failover機制。replication對Kafka的吞吐率是有一定影響的,但極大的增強了可用性。預設情況下,Kafka的replication數量為1。每個partition都有一個唯一的leader,所有的讀寫操作都在leader上完成,follower批量從leader上pull資料。一般情況下partition的數量大於等於broker的數量,並且所有partition的leader均勻分佈在broker上。follower上的日誌和其leader上的完全一樣。

需要注意的是,replication factor並不會影響consumer的吞吐率測試,因為consumer只會從每個partition的leader讀資料,而與replicaiton factor無關。同樣,consumer吞吐率也與同步複製還是非同步複製無關。

Leader Election

引入Replication之後,同一個Partition可能會有多個副本(Replica),而這時需要在這些副本之間選出一個Leader,Producer和Consumer只與這個Leader副本互動,其它Replica作為Follower從Leader中複製資料。注意,只有Leader負責資料讀寫,Follower只向Leader順序Fetch資料(N條通路),並不提供任何讀寫服務,系統更加簡單且高效。

思考 為什麼follower副本不提供讀寫,只做冷備?

follwer副本不提供寫服務這個比較好理解,因為如果follower也提供寫服務的話,那麼就需要在所有的副本之間相互同步。n個副本就需要 nxn 條通路來同步資料,如果採用非同步同步的話,資料的一致性和有序性是很難保證的;而採用同步方式進行資料同步的話,那麼寫入延遲其實是放大n倍的,反而適得其反。

那麼為什麼不讓follower副本提供讀服務,減少leader副本的讀壓力呢?這個除了因為同步延遲帶來的資料不一致之外,不同於其他的儲存服務(如ES,MySQL),Kafka的讀取本質上是一個有序的訊息消費,消費進度是依賴於一個叫做offset的偏移量,這個偏移量是要儲存起來的。如果多個副本進行讀負載均衡,那麼這個偏移量就不好確定了。

TIPS

Kafka的leader副本類似於ES的primary shard,follower副本相對於ES的replica。ES也是一個index有多個shard(相對於Kafka一個topic有多個partition),shard又分為primary shard和replicition shard,其中primary shard用於提供讀寫服務(sharding方式跟MySQL非常類似:shard = hash(routing) % number_of_primary_shards。但是ES引入了協調節點(coordinating node) 的角色,實現對客戶端透明。),而replication shard只提供讀服務(這裡跟Kafka一樣,ES會等待relication shard返回成功才最終返回給client)。

有傳統MySQL分庫分表經驗的同學一定會覺得這個過程是非常相似的,就是一個sharding + replication的資料架構,只是通過client(SDK)或者coordinator對你透明瞭而已。

Propagate訊息

Producer在釋出訊息到某個Partition時,先通過ZooKeeper找到該Partition的Leader,然後無論該Topic的Replication Factor為多少(也即該Partition有多少個Replica),Producer只將該訊息傳送到該Partition的Leader。Leader會將該訊息寫入其本地Log。每個Follower都從Leader pull資料。這種方式上,Follower儲存的資料順序與Leader保持一致。Follower在收到該訊息並寫入其Log後,向Leader傳送ACK。一旦Leader收到了 ISR (in-sync replicas) 中的所有Replica的ACK,該訊息就被認為已經commit了,Leader將增加 HW( High-Watermark) 並且向Producer傳送ACK。

為了提高效能,每個Follower在接收到資料後就立馬向Leader傳送ACK,而非等到資料寫入Log中。因此,對於已經commit的訊息,Kafka只能保證它被存於多個Replica的記憶體中,而不能保證它們被持久化到磁碟中,也就不能完全保證異常發生後該條訊息一定能被Consumer消費。但考慮到這種場景非常少見,可以認為這種方式在效能和資料持久化上做了一個比較好的平衡。在將來的版本中,Kafka會考慮提供更高的永續性。

Consumer讀訊息也是從Leader讀取,只有被commit過的訊息(offset低於HW的訊息)才會暴露給Consumer。

Kafka Replication的資料流如下圖所示:

關於這方面的內容比較多而且複雜,這裡就不展開了,這篇文章寫的很好,有興趣的同學可以學習

《 Kafka設計解析(二):Kafka High Availability (上)》

Kafka的幾個遊標(偏移量/offset)

下面這張圖非常簡單明瞭的顯示kafka的所有遊標

https://rongxinblog.wordpress.com/2016/07/29/kafka-high-watermark/):

下面簡單的說明一下:

0、ISR

In-Sync Replicas list,顧名思義,就是跟leader “儲存同步” 的Replicas。“保持同步”的含義有些複雜,在0.9版本,broker的引數replica.lag.time.max.ms用來指定ISR的定義,如果leader在這麼長時間沒收到follower的拉取請求,或者在這麼長時間內,follower沒有fetch到leader的log end offset,就會被leader從ISR中移除。ISR是個很重要的指標,controller選取partition的leader replica時會使用它,leader需要維護ISR列表,因此leader選取ISR後會把結果記到Zookeeper上。

在需要選舉leader的場景下,leader和ISR是由controller決定的。在選出leader以後,ISR是leader決定。如果誰是leader和ISR只存在於ZK上,那麼每個broker都需要在Zookeeper上監聽它host的每個partition的leader和ISR的變化,這樣效率比較低。如果不放在Zookeeper上,那麼當controller fail以後,需要從所有broker上重新獲得這些資訊,考慮到這個過程中可能出現的問題,也不靠譜。所以leader和ISR的資訊存在於Zookeeper上,但是在變更leader時,controller會先在Zookeeper上做出變更,然後再傳送LeaderAndIsrRequest給相關的broker。這樣可以在一個LeaderAndIsrRequest裡包括這個broker上有變動的所有partition,即batch一批變更新資訊給broker,更有效率。另外,在leader變更ISR時,會先在Zookeeper上做出變更,然後再修改本地記憶體中的ISR。

1、Last Commited Offset

Consumer最後提交的位置,這個位置會儲存在一個特殊的topic:_consumer_offsets 中。

2、Current Position

Consumer當前讀取的位置,但是還沒有提交給broker。提交之後就變成Last Commit Offset。

3、High Watermark(HW)

這個offset是所有ISR的LEO的最小位置(minimum LEO across all the ISR of this partition),consumer不能讀取超過HW的訊息,因為這意味著讀取到未完全同步(因此沒有完全備份)的訊息。換句話說就是:HW是所有ISR中的節點都已經複製完的訊息.也是消費者所能獲取到的訊息的最大offset(注意,並不是所有replica都一定有這些訊息,而只是ISR裡的那些才肯定會有)。

隨著follower的拉取進度的即時變化,HW是隨時在變化的。follower總是向leader請求自己已有messages的下一個offset開始的資料,因此當follower發出了一個fetch request,要求offset為A以上的資料,leader就知道了這個follower的log end offset至少為A。此時就可以統計下ISR裡的所有replica的LEO是否已經大於了HW,如果是的話,就提高HW。同時,leader在fetch本地訊息給follower時,也會在返回給follower的reponse裡附帶自己的HW。這樣follower也就知道了leader處的HW(但是在實現中,follower獲取的只是讀leader本地log時的HW,並不能保證是最新的HW)。但是leader和follower的HW是不同步的,follower處記的HW可能會落後於leader。

Hight Watermark Checkpoint

由於HW是隨時變化的,如果即時更新到Zookeeper,會帶來效率的問題。而HW是如此重要,因此需要持久化,ReplicaManager就啟動了單獨的執行緒定期把所有的partition的HW的值記到檔案中,即做highwatermark-checkpoint。

4、Log End Offset(LEO)

這個很好理解,就是當前的最新日誌寫入(或者同步)位置。

四、Kafka客戶端

Kafka支援JVM語言(java、scala),同是也提供了高效能的C/C++客戶端,和基於librdkafka封裝的各種語言客戶端。如,Python客戶端: confluent-kafka-python 。Python客戶端還有純python實現的:kafka-python

下面是Python例子(以confluent-kafka-python為例):

Producer:

from confluent_kafka import Producer
 
p = Producer({'bootstrap.servers': 'mybroker,mybroker2'})
for data in some_data_source:
    p.produce('mytopic', data.encode('utf-8'))
p.flush()

Consumer:

from confluent_kafka import Consumer, KafkaError
 
c = Consumer({'bootstrap.servers': 'mybroker', 'group.id': 'mygroup',
              'default.topic.config': {'auto.offset.reset': 'smallest'}})
c.subscribe(['mytopic'])
running = True
while running:
    msg = c.poll()
    if not msg.error():
        print('Received message: %s' % msg.value().decode('utf-8'))
    elif msg.error().code() != KafkaError._PARTITION_EOF:
        print(msg.error())
        running = False
c.close()

跟普通的訊息佇列使用基本是一樣的。

五、Kafka的offset管理

kafka讀取訊息其實是基於offset來進行的,如果offset出錯,就可能出現重複讀取訊息或者跳過未讀訊息。在0.8.2之前,kafka是將offset儲存在ZooKeeper中,但是我們知道zk的寫操作是很昂貴的,而且不能線性擴充,頻繁的寫入zk會導致效能瓶頸。所以在0.8.2引入了Offset Management,將這個offset儲存在一個 compacted kafka topic(_consumer_offsets),Consumer通過傳送OffsetCommitRequest請求到指定broker(偏移量管理者)提交偏移量。這個請求中包含一系列分割槽以及在這些分割槽中的消費位置(偏移量)。偏移量管理者會追加鍵值(key-value)形式的訊息到一個指定的topic(__consumer_offsets)。key是由consumerGroup-topic-partition組成的,而value是偏移量。同時為了提供效能,記憶體中也會維護一份最近的記錄,這樣在指定key的情況下能快速的給出OffsetFetchRequests而不用掃描全部偏移量topic日誌。如果偏移量管理者因某種原因失敗,新的broker將會成為偏移量管理者並且通過掃描偏移量topic來重新生成偏移量快取。

如何檢視消費偏移量

0.9版本之前的Kafka提供了kafka-consumer-offset-checker.sh指令碼,可以用來檢視某個消費組對一個或者多個topic的消費者消費偏移量情況,該指令碼呼叫的是

kafka.tools.Consumer.OffsetChecker。0.9版本之後已不再建議使用該指令碼了,而是建議使用kafka-consumer-groups.sh指令碼,該指令碼呼叫的是kafka.admin.ConsumerGroupCommand。這個指令碼其實是對消費組進行管理,不只是檢視消費組的偏移量。這裡只介紹最新的kafka-consumer-groups.sh指令碼使用。

用ConsumerGroupCommand工具,我們可以使用list,describe,或delete消費者組。

例如,要列出所有主題中的所有消費組資訊,使用list引數:

$ bin/kafka-consumer-groups.sh --bootstrap-server broker1:9092 --list
 
test-consumer-group

要檢視某個消費組當前的消費偏移量則使用describe引數:

$ bin/kafka-consumer-groups.sh --bootstrap-server broker1:9092 --describe --group test-consumer-group
 
GROUP                          TOPIC                          PARTITION  CURRENT-OFFSET  LOG-END-OFFSET  LAG             OWNER
test-consumer-group            test-foo                       0          1               3               2               consumer-1_/127.0.0.1

NOTES

該指令碼只支援刪除不包括任何消費組的消費組,而且只能刪除消費組為老版本消費者對應的消費組(即分組後設資料儲存在zookeeper的才有效),因為這個指令碼刪除操作的本質就是刪除ZK中對應消費組的節點及其子節點而已。

如何管理消費偏移量

上面介紹了通過指令碼工具方式查詢Kafka消費偏移量。事實上,我們也可以通過API的方式查詢消費偏移量。

Kafka消費者API提供了兩個方法用於查詢消費者消費偏移量的操作:

  1. committed(TopicPartition partition): 該方法返回一個OffsetAndMetadata物件,通過它可以獲取指定分割槽已提交的偏移量。
  2. position(TopicPartition partition): 該方法返回下一次拉取位置的position。

除了檢視消費偏移量,有些時候我們需要人為的指定offset,比如跳過某些訊息,或者redo某些訊息。在0.8.2之前,offset是存放在ZK中,只要用ZKCli操作ZK就可以了。但是在0.8.2之後,offset預設是存放在kafka的__consumer_offsets佇列中,只能通過API修改了:

Class KafkaConsumer<K,V> Kafka allows specifying the position using  seek(TopicPartition, long) to specify the new position. Special methods for seeking to the earliest and latest offset the server maintains are also available (seekToBeginning(TopicPartition…)  and  seekToEnd(TopicPartition…) respectively).

參考文件: Kafka Consumer Offset Management

Kafka消費者API提供了重置消費偏移量的方法:

  1. seek(TopicPartition partition, long offset): 該方法用於將消費起始位置重置到指定的偏移量位置。
  2. seekToBeginning(): 從訊息起始位置開始消費,對應偏移量重置策略

    auto.offset.reset=earliest。

  3. seekToEnd(): 從最新訊息對應的位置開始消費,也就是說等待新的訊息寫入後才開始拉取,對應偏移量重置策略是

    auto.offset.reset=latest。

當然前提你得知道要重置的offset的位置。一種方式就是根據時間戳獲取對應的offset。再seek過去。

部署和配置

Kafka是用Scala寫的,所以只要安裝了JRE環境,執行非常簡單。直接下載官方編譯好的包,解壓配置一下就可以直接執行了。

一、kafka配置

配置檔案在config目錄下的server.properties,關鍵配置如下(有些屬性配置檔案中預設沒有,需自己新增):

broker.id:Kafka叢集中每臺機器(稱為broker)需要獨立不重的id
port:監聽埠
delete.topic.enable:設為true則允許刪除topic,否則不允許
message.max.bytes:允許的最大訊息大小,預設是1000012(1M),建議調到到10000012(10M)。
replica.fetch.max.bytes: 同上,預設是1048576,建議調到到10048576。
log.dirs:Kafka資料檔案的存放目錄,注意不是日誌檔案。可以配置為:/home/work/kafka/data/kafka-logs
log.cleanup.policy:過期資料清除策略,預設為delete,還可設為compact
log.retention.hours:資料過期時間(小時數),預設是1073741824,即一週。過期資料用log.cleanup.policy的規則清除。可以用log.retention.minutes配置到分鐘級別。
log.segment.bytes:資料檔案切分大小,預設是1073741824(1G)。
retention.check.interval.ms:清理執行緒檢查資料是否過期的間隔,單位為ms,預設是300000,即5分鐘。
zookeeper.connect:負責管理Kafka的zookeeper叢集的機器名:埠號,多個用逗號分隔
TIPS 傳送和接收大訊息

需要修改如下引數:

  • broker:message.max.bytes

    & replica.fetch.max.bytes

  • consumer:fetch.message.max.bytes

更多引數的詳細說明見官方文件:

http://kafka.apache.org/documentation.html#brokerconfigs

二、ZK配置和啟動

然後先確保ZK已經正確配置和啟動了。Kafka自帶ZK服務,配置檔案在config/zookeeper.properties檔案,關鍵配置如下:

dataDir=/home/work/kafka/data/zookeeper
clientPort=2181
maxClientCnxns=0
tickTime=2000
initLimit=10
syncLimit=5
server.1=nj03-bdg-kg-offline-01.nj03:2888:3888
server.2=nj03-bdg-kg-offline-02.nj03:2888:3888
server.3=nj03-bdg-kg-offline-03.nj03:2888:3888

NOTES Zookeeper叢集部署

ZK的叢集部署要做兩件事情:

  1. 分配serverId: 在dataDir目錄下建立一個myid檔案,檔案中只包含一個1到255的數字,這就是ZK的serverId。
  2. 配置叢集:格式為server.{id}={host}:{port}:{port},其中{id}就是上面提到的ZK的serverId。

然後啟動:

bin/zookeeper-server-start.sh -daemon config/zookeeper.properties。

三、啟動kafka

然後可以啟動Kafka:JMX_PORT=8999 bin/kafka-server-start.sh -daemon config/server.properties,非常簡單。

TIPS

我們在啟動命令中增加了JMX_PORT=8999環境變數,這樣可以暴露JMX監控項,方便監控。

Kafka監控和管理

不過不像RabbitMQ,或者ActiveMQ,Kafka預設並沒有web管理介面,只有命令列語句,不是很方便,不過可以安裝一個,比如,Yahoo的 Kafka Manager: A tool for managing Apache Kafka。它支援很多功能:

  • Manage multiple clusters
  • Easy inspection of cluster state (topics, consumers, offsets, brokers, replica distribution, partition distribution)
  • Run preferred replica election
  • Generate partition assignments with option to select brokers to use
  • Run reassignment of partition (based on generated assignments)
  • Create a topic with optional topic configs (0.8.1.1 has different configs than 0.8.2+)
  • Delete topic (only supported on 0.8.2+ and remember set delete.topic.enable=true in broker config)
  • Topic list now indicates topics marked for deletion (only supported on 0.8.2+)
  • Batch generate partition assignments for multiple topics with option to select brokers to use
  • Batch run reassignment of partition for multiple topics
  • Add partitions to existing topic
  • Update config for existing topic
  • Optionally enable JMX polling for broker level and topic level metrics.
  • Optionally filter out consumers that do not have ids/ owners/ & offsets/ directories in zookeeper.

安裝過程蠻簡單的,就是要下載很多東東,會很久。具體參見: kafka manager安裝。不過這些管理平臺都沒有許可權管理功能。

需要注意的是,Kafka Manager的conf/application.conf配置檔案裡面配置的kafka-manager.zkhosts是為了它自身的高可用,而不是指向要管理的Kafka叢集指向的zkhosts。所以不要忘記了手動配置要管理的Kafka叢集資訊(主要是配置名稱,和zk地址)。Install and Evaluation of Yahoo’s Kafka Manager

Kafka Manager主要是提供管理介面,監控的話還要依賴於其他的應用,比如:

  1. Burrow: Kafka Consumer Lag Checking. Linkedin開源的cusumer log監控,go語言編寫,貌似沒有介面,只有HTTP API,可以配置郵件報警。
  2. Kafka Offset Monitor: A little app to monitor the progress of kafka consumers and their lag wrt the queue.

這兩個應用的目的都是監控Kafka的offset。

刪除主題

刪除Kafka主題,一般有如下兩種方式:

1、手動刪除各個節點${log.dir}目錄下該主題分割槽資料夾,同時登陸ZK客戶端刪除待刪除主題對應的節點,主題後設資料儲存在/brokers/topics和/config/topics節點下。

2、執行kafka-topics.sh指令碼執行刪除,若希望通過該指令碼徹底刪除主題,則需要保證在啟動Kafka時載入的server.properties檔案中配置 delete.topic.enable=true,該配置項預設為false。否則執行該指令碼並未真正刪除topic,而是在ZK的/admin/delete_topics目錄下建立一個與該待刪除主題同名的topic,將該主題標記為刪除狀態而已。

kafka-topic –delete –zookeeper server-1:2181,server-2:2181 –topic test`

執行結果:

Topic test is marked for deletion.
Note: This will have no impact if delete.topic.enable is not set to true.

此時若希望能夠徹底刪除topic,則需要通過手動刪除相應檔案及節點。當該配置項為true時,則會將該主題對應的所有檔案目錄以及後設資料資訊刪除。

過期資料自動清除

對於傳統的message queue而言,一般會刪除已經被消費的訊息,而Kafka叢集會保留所有的訊息,無論其被消費與否。當然,因為磁碟限制,不可能永久保留所有資料(實際上也沒必要),因此Kafka提供兩種策略去刪除舊資料。一是基於時間,二是基於partition檔案大小。可以通過配置$KAFKA_HOME/config/server.properties ,讓Kafka刪除一週前的資料,也可通過配置讓Kafka在partition檔案超過1GB時刪除舊資料:

############################# Log Retention Policy #############################
 
# The following configurations control the disposal of log segments. The policy can
# be set to delete segments after a period of time, or after a given size has accumulated.
# A segment will be deleted whenever *either* of these criteria are met. Deletion always happens
# from the end of the log.
 
# The minimum age of a log file to be eligible for deletion
log.retention.hours=168
 
# A size-based retention policy for logs. Segments are pruned from the log as long as the remaining
# segments don't drop below log.retention.bytes.
#log.retention.bytes=1073741824
 
# The maximum size of a log segment file. When this size is reached a new log segment will be created.
log.segment.bytes=1073741824
 
# The interval at which log segments are checked to see if they can be deleted according
# to the retention policies
log.retention.check.interval.ms=300000
 
# By default the log cleaner is disabled and the log retention policy will default to
# just delete segments after their retention expires.
# If log.cleaner.enable=true is set the cleaner will be enabled and individual logs
# can then be marked for log compaction.
log.cleaner.enable=false

這裡要注意,因為Kafka讀取特定訊息的時間複雜度為O(1),即與檔案大小無關,所以這裡刪除檔案與Kafka效能無關,選擇怎樣的刪除策略只與磁碟以及具體的需求有關。

Kafka的一些問題

1、只保證單個主題單個分割槽內的訊息有序,但是不能保證單個主題所有分割槽訊息有序。如果應用嚴格要求訊息有序,那麼kafka可能不大合適。

2、消費偏移量由消費者跟蹤和提交,但是消費者並不會經常把這個偏移量寫會kafka,因為broker維護這些更新的代價很大,這會導致異常情況下訊息可能會被多次消費或者沒有消費。

具體分析如下:訊息可能已經被消費了,但是消費者還沒有像broker提交偏移量(commit offset)確認該訊息已經被消費就掛掉了,接著另一個消費者又開始處理同一個分割槽,那麼它會從上一個已提交偏移量開始,導致有些訊息被重複消費。但是反過來,如果消費者在批處理訊息之前就先提交偏移量,但是在處理訊息的時候掛掉了,那麼這部分訊息就相當於『丟失』了。通常來說,處理訊息和提交偏移量很難構成一個原子性操作,因此無法總是保證所有訊息都剛好只被處理一次。

3、主題和分割槽的數目有限

Kafka叢集能夠處理的主題數目是有限的,達到1000個主題左右時,效能就開始下降。這些問題基本上都跟Kafka的基本實現決策有關。特別是,隨著主題數目增加,broker上的隨機IO量急劇增加,因為每個主題分割槽的寫操作實際上都是一個單獨的檔案追加(append)操作。隨著分割槽數目增加,問題越來越嚴重。如果Kafka不接管IO排程,問題就很難解決。

當然,一般的應用都不會有這麼大的主題數和分割槽數要求。但是如果將單個Kafka叢集作為多租戶資源,這個時候這個問題就會暴露出來。

4、手動均衡分割槽負載

Kafka的模型非常簡單,一個主題分割槽全部儲存在一個broker上,可能還有若干個broker作為該分割槽的副本(replica)。同一分割槽不在多臺機器之間分割儲存。隨著分割槽不斷增加,叢集中有的機器運氣不好,會正好被分配幾個大分割槽。Kafka沒有自動遷移這些分割槽的機制,因此你不得不自己來。監控磁碟空間,診斷引起問題的是哪個分割槽,然後確定一個合適的地方遷移分割槽,這些都是手動管理型任務,在Kafka叢集環境中不容忽視。

如果叢集規模比較小,資料所需的空間較小,這種管理方式還勉強奏效。但是,如果流量迅速增加或者沒有一流的系統管理員,那麼情況就完全無法控制。

注意:如果向叢集新增新的節點,也必須手動將資料遷移到這些新的節點上,Kafka不會自動遷移分割槽以平衡負載量或儲存空間的。

5、follow副本(replica)只充當冷備(解決HA問題),無法提供讀服務

不像ES,replica shard是同時提供讀服務,以緩解master的讀壓力。kafka因為讀服務是有狀態的(要維護commited offset),所以follow副本並沒有參與到讀寫服務中。只是作為一個冷備,解決單點問題。

6、只能順序消費訊息,不能隨機定位訊息,出問題的時候不方便快速定位問題

這其實是所有以訊息系統作為非同步RPC的通用問題。假設傳送方發了一條訊息,但是消費者說我沒有收到,那麼怎麼排查呢?訊息佇列缺少隨機訪問訊息的機制,如根據訊息的key獲取訊息。這就導致排查這種問題不大容易。

推薦閱讀

  1. Centralized Logging Solutions Overview
  2. Logging and Aggregation at Quora
  3. ELK在廣告系統監控中的應用 及 Elasticsearch簡介
  4. Centralized Logging
  5. Centralized Logging Architecture 

更多內容敬請關注 vivo 網際網路技術 微信公眾號

注:轉載文章請先與微訊號:labs2020 聯絡。

相關文章