應用程序使用 KafkaConsumer向 Kafka 訂閱主題,并從訂閱的主題上接收消息 。 從 Kafka 讀取數據不同于從其他悄息系統讀取數據,它涉及一些獨特的概念和想法。如果不先理解 這些概念,就難以理解如何使用消費者 API。所以我們接下來先解釋這些重要的概念,然 后再舉幾個例子,橫示如何使用消費者 API 實現不同的應用程序。
消費者和消費者群組
假設我們有一個應用程序需要從-個 Kafka主題讀取消息井驗證這些消息,然后再把它們 保存起來。應用程序需要創建一個消費者對象,訂閱主題并開始接收消息,然后驗證消息 井保存結果。過了 一陣子,生產者往主題寫入消息的速度超過了應用程序驗證數據的速 度,這個時候該怎么辦?如果只使用單個消費者處理消息,應用程序會遠跟不上消息生成 的速度。顯然,此時很有必要對消費者進行橫向伸縮。就像多個生產者可以向相同的 主題 寫入消息一樣,我們也可以使用多個消費者從同一個主題讀取消息,對消息進行分流。
Kafka 消費者從屬于消費者群組。一個群組里的消費者訂閱的是同一個主題,每個消費者 接收主題一部分分區的消息。
假設主題 T1 有 4 個分區,我們創建了消費者 C1 ,它是群組 G1 里唯 一 的消費者,我們用 它訂閱主題 T1。消費者 Cl1將收到主題 T1全部 4個分區的消息,如圖 4-1 所示。
如果在群組 G1 里新增一個消費者 C2,那么每個消費者將分別從兩個分區接收消息。我 假設消費者 C1接收分區 0 和分區 2 的消息,消費者 C2 接收分區 1 和分區 3 的消息,如圖 4-2 所示。
如果群組 G1 有 4 個消費者,那么每個消費者可以分配到 一個分區,如圖 4-3 所示。
如果我們往群組里添加更多的消費者,超過主題的分區數量,那么多出的消費者就會被閑置,不會接收到任何消息。
往群組里增加消費者是橫向伸縮消費能力的主要方式。 Kafka 消費者經常會做一些高延遲的操作,比如把數據寫到數據庫或 HDFS,或者使用數據進行比較耗時的計算。在這些情況下,單個消費者無法跟上數據生成的速度,所以可以增加更多的消費者,讓它們分擔負載,每個消費者只處理部分分區的消息,這就是橫向伸縮的主要手段。我們有必要為主題創建大量的分區,在負載增長時可以加入更多的消費者。不過要性意,不要讓消費者的數量超過主題分區的數量,多余的消費者只會被閑置。
除了通過增加消費者來橫向伸縮單個應用程序外,還經常出現多個應用程序從同一個主題讀取數據的情況。實際上, Kafka 設計的主要目標之一 ,就是要讓 Kafka 主題里的數據能夠滿足企業各種應用場景的需求。在這些場景里,每個應用程序可以獲取到所有的消息, 而不只是其中的 一部分。只要保證每個應用程序有自己的消費者群組,就可以讓它們獲取到主題所有的消息。不同于傳統的消息系統,橫向伸縮 Kafka消費者和消費者群組并不會對性能造成負面影響。
在上面的例子里,如果新增一個只包含一個消費者的群組 G2,那么這個消費者將從主題 T1 上接收所有的消息,與群組 G1 之間互不影響。群組 G2 可以增加更多的消費者,每個消費者可以消費若干個分區,就像群組 G1 那樣,如圖 4-5 所示。總的來說,群組 G2 還是會接收到所有消息,不管有沒有其他群組存在。
簡而言之,為每一個需要獲取一個或多個主題全部消息的應用程序創建一個消費者群組, 然后往群組里添加消費者來伸縮讀取能力和處理能力,群組里的每個消費者只處理一部分消息。
消費者群組和分區再均衡
我們已經從上一個小節了解到,群組里的消費者共同讀取主題的分區。一個新的消費者加 入群組時,它讀取的是原本由其他消費者讀取的消息。當一個消費者被關閉或發生崩潰時,它就離開群組,原本由它讀取的分區將由群組里的其他消費者來讀取。在主題發生變化時 , 比如管理員添加了新的分區,會發生分區重分配。
分區的所有權從一個消費者轉移到另一個消費者,這樣的行為被稱為再均衡。再均衡非常重要, 它為消費者群組帶來了高可用性和伸縮性(我們可以放心地添加或移除消費者), 不過在正常情況下,我們并不希望發生這樣的行為。在再均衡期間,消費者無法讀取消息,造成整個群組一小段時間的不可用。另外,當分區被重新分配給另 一個消費者時,消費者當前的讀取狀態會丟失,它有可能還需要去刷新緩存 ,在它重新恢復狀態之前會拖慢應用程序。我們將在本章討論如何進行安全的再均衡,以及如何避免不必要的再均衡。
消費者通過向被指派為 群組協調器的 broker (不同的群組可以有不同的協調器)發送 心跳 來維持它們和群組的從屬關系以及它們對分區的所有權關系。只要消費者以正常的時間間隔發送心跳,就被認為是活躍的,說明它還在讀取分區里的消息。消費者會在輪詢消息 (為了獲取消息)或提交偏移量時發送心跳。如果消費者停止發送心跳的時間足夠長,會話就會過期,群組協調器認為它已經死亡,就會觸發一次再均衡。
如果一個消費者發生崩潰,井停止讀取消息,群組協調器(broker)會等待幾秒鐘,確認它死亡了才會觸發再均衡。在這幾秒鐘時間里,死掉的消費者不會讀取分區里的消息。在清理消費者時,消費者會通知協調器它將要離開群組,協調器會立即觸發一次再均衡,盡量降低處理停頓。在本章的后續部分,我們將討論一些用于控制發送心跳頻率和會話過期時間的配置參數,以及如何根據實際需要來配置這些參數 。
分配分區是怎樣的一個過程
當消費者要加入群組時,它會向群組協調器發送 一 個 JoinGroup 請求。第 一 個加入群組的消費者將成為“群主”。群主從協調器那里獲得群組的成員列 表(列表中包含了所有最近發送過心跳的消費者,它們被認為是活躍的), 并負責給每一個消費者分配分區。它使用 一個實現了 PartitionAssignor接口的類來決定哪些分 區應該被分配給哪個消費者 。
Kafka 內置了兩種分配策略,在后面的配置參數小節我們將深入討論。分配完畢之后,群主把分配情況列表發送給群組協調器,協調器再把這些信息發送給所有消費者。每個消費者只能看到自己的分配信息,只有群 主知道群組 里所有消費者的分配信息。這個過程會在每次再均衡時重復發生。
創建 Kafka消費者
在讀取消息之前,需要先創建 一個 KafkaConsumer對象 。 創建 KafkaConsumer 對象與創建 KafkaProducer對象非常相似——把想要傳給消費者的屬性放在 Properties 對象里。本章 后續部分會深入討論所有的屬性。在這里,我們只需要使用 3個必要的屬性: bootstrap.servers、 key.deserializer、 value.deserializer。
下面代碼演示了如何創建一個KafkaConsumer對象:
Properties props = new Properties();
props.put("bootstrap.servers", "broker1:9092, broker2:9092");
props.put("group.id", "CountryCounter");
props.put("key.deserializer", "org.apache.kafka.common.serializaiton.StrignDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serializaiton.StrignDeserializer");
KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(props);
deserializer使用指定的類(反序列化器)把字節數組轉成 Java對象。
group.id指定了KafkaConsumer 屬于哪一個消費者群組。
group.id不是必需的,不過我們現在姑且認為它是必需的。它指定了 KafkaConsumer 屬于哪一個消費者群組。創建不屬于任何一個群組的消費者也是可以的,只是這樣做不太常見。
訂閱主題
創建好消費者之后,下一步可以開始訂閱主題了。subscribe()方法接受一個主題列表作為參數
consumer.subscribe(Collections.singletonList("customerCountries"));
在這里我們創建了一個包含單個元素的列表,主題的名字叫作“customerCountries”,我們也可以在調用subscribe()方法時傳入一個正則表達式,正則表達式可以匹配多個主題如果有人創建了新的主題,并且主題名與正則表達式匹配,那么會立即觸發一次再均衡,消費者就可以讀取新添加的主題。如果應用程序需要讀取多個主題,并且可以處理不同類型的數據,那么這種訂閱方式就很管用。在Kafka和其他系統之間復制數據時,使用正則表達式的方式訂閱多個主題時很常見的做法。
要訂閱所有test相關的主題,可以這樣做:consumer.subscribe("test.*");
輪詢
消息輪詢是消費者 API 的核心,通過一個簡單的輪詢向服務器請求數據。一旦消費者訂閱了主題 ,輪詢就會處理所有的細節,包括群組協調、分區再均衡、發送心跳和獲取數據, 開發者只需要使用一組簡單的 API 來處理從分區返回的數據。消費者代碼的主要部分如下所示 :
輪詢不只是獲取數據那么簡單。在第一次調用新消費者的 poll() 方法時,它會負責查找 GroupCoordinator, 然后加入群組,接受分配的分區。 如果發生了再均衡,整個過程也是在輪詢期間進行的。當然 ,心跳也是從輪詢里發迭出去的。所以,我們要確保在輪詢期間所做的任何處理工作都應該盡快完成。
線程安全
在同一個群組中,我們無法讓一個線程運行多個消費者,也無法讓多個線程安全地共享一個消費者。按照規則,一個消費者使用一個線程。如果要在同一個消費者群組里運行多個消費者,需要讓每個消費者運行在自己的線程里。最好是把消費者的邏輯封裝在自己的對象里,然后使用Java的ExecutorService啟動多個線程,使每個消費者運行在自己的線程上。Confluent的博客(https://www.confluent.io/blog/)上有一個教程介紹如何處理這種情況。
消費者的配置
到目前為止,我們學習了如何使用消費者 API,不過只介紹了幾個配置屬’性一一如bootstrap.servers、 key.deserializer、 value.deserializer、group.id。 Kafka的文檔列出了所有與消費者相關的配置說明。大部分參數都有合理的默認值,一般不需要修改它們,不過有一些參數與消費 者的性能和可用性有很大關系。接下來介紹這些重要的屬性。
1. fetch.min.bytes
該屬性指定了消費者從服務器獲取記錄的最小字節數。 broker 在收到消費者的數據請求時, 如果可用的數據量小于 fetch.min.bytes指定的大小,那么它會等到有足夠的可用數據時才把它返回給消費者。這樣可以降低消費者和 broker 的工作負載,因為它們在主題不是很活躍的時候(或者一天里的低谷時段)就不需要來來回回地處理消息。如果沒有很多可用數據,但消費者的 CPU 使用率卻很高,那么就需要把該屬性的值設得比默認值大。如果消費者的數量比較多,把該屬性的值設置得大一點可以降低 broker 的工作負載。
2. fetch.max.wait.ms
我們通過 fetch.min.bytes 告訴 Kafka,等到有足夠的數據時才把它返回給消費者。而 fetch.max.wait.ms則用于指定 broker的等待時間,默認是 500ms。如果沒有足夠的數據流入 Kafka,消費者獲取最小數據量的要求就得不到滿足,最終導致500ms的延遲。 如果要降低潛在的延遲(為了滿足 SLA),可以把該參數值設置得小一些。如果 fetch.max.wait.ms被設 為 100ms,并且 fetch.min.bytes 被設為 1MB,那么 Kafka在收到消費者的請求后,要么返 回 1MB 數據,要么在 100ms 后返回所有可用的數據 , 就看哪個條件先得到滿足。
3. max.parition.fetch.bytes
該屬性指定了服務器從每個分區里返回給消費者的最大字節數。它的默認值是 1MB,也 就是說, KafkaConsumer.poll() 方法從每個分區里返回的記錄最多不超過 max.parition.fetch.bytes 指定的字節。如果一個主題有 20個分區和 5 個消費者,那么每個消費者需要至少 4MB 的可用內存來接收記錄。在為消費者分配內存時,可以給它們多分配一些,因 為如果群組里有消費者發生崩潰,剩下的消費者需要處理更多的分區。 max.parition.fetch.bytes 的值必須比 broker能夠接收的最大消息的字節數(通過 max.message.size屬 性配置 )大, 否則消費者可能無法讀取這些消息,導致消費者一直掛起重試。在設置該屬性時,另一個需要考慮的因素是消費者處理數據的時間。 消費者需要頻繁調用 poll() 方法來避免會話過期和發生分區再均衡,如果單次調用 poll() 返回的數據太多,消費者需要更多的時間來處理,可能無法及時進行下一個輪詢來避免會話過期。如果出現這種情況, 可以把 max.parition.fetch.bytes 值改小 ,或者延長會話過期時間。
4. session.timeout.ms
該屬性指定了消費者在被認為死亡之前可以與服務器斷開連接的時間,默認是 3s。如果消費者沒有在 session.timeout.ms 指定的時間內發送心跳給群組協調器,就被認為已經死亡,協調器就會觸發再均衡,把它的分區分配給群組里的其他消費者 。該屬性與 heartbeat.interval.ms緊密相關。heartbeat.interval.ms 指定了poll()方法向協調器 發送心跳的頻 率, session.timeout.ms 則指定了消費者可以多久不發送心跳。所以, 一般需要同時修改這兩個屬性, heartbeat.interval.ms 必須比 session.timeout.ms 小, 一般是 session.timeout.ms 的三分之一。如果 session.timeout.ms 是 3s,那么 heartbeat.interval.ms 應該是 ls。 把 session.timeout.ms 值設 得比默認值小,可以更快地檢測和恢 復崩潰的節點,不過長時間的輪詢或垃圾收集可能導致非預期的再均衡。把該屬性的值設置得大一些,可以減少意外的再均衡 ,不過檢測節點崩潰需要更長的時間。
5. auto.offset.reset
該屬性指定了消費者在讀取一個沒有偏移量的分區或者偏移量無效的情況下(因消費者長時間失效,包含偏移量的記錄已經過時井被刪除)該作何處理。它的默認值是latest, 意 思是說,在偏移量無效的情況下,消費者將從最新的記錄開始讀取數據(在消費者 啟動之 后生成的記錄)。另一個值是 earliest,意思是說,在偏移量無效的情況下,消費者將從 起始位置讀取分區的記錄。
6. enable.auto.commit
我們稍后將介紹 幾種 不同的提交偏移量的方式。該屬性指定了消費者是否自動提交偏移量,默認值是 true。為了盡量避免出現重復數據和數據丟失,可以把它設為 false,由自己控制何時提交偏移量。如果把它設為 true,還可以通過配置 auto.commit.interval.mls 屬性來控制提交的頻率。
7. partition.assignment.strategy
我們知道,分區會被分配給群組里的消費者。 PartitionAssignor 根據給定的消費者和主題,決定哪些分區應該被分配給哪個消費者。 Kafka 有兩個默認的分配策略 。
- Range
該策略會把主題的若干個連續的分區分配給消費者。假設悄費者 C1 和消費者 C2 同時 訂閱了主題 T1 和主題 T2,井且每個主題有 3 個分區。那么消費者 C1 有可能分配到這 兩個主題的分區 0 和 分區 1,而消費者 C2 分配到這兩個主題 的分區 2。因為每個主題 擁有奇數個分區,而分配是在主題內獨立完成的,第一個消費者最后分配到比第二個消費者更多的分區。只要使用了 Range策略,而且分區數量無法被消費者數量整除,就會出現這種情況。
- RoundRobin
該策略把主題的所有分區逐個分配給消費者。如果使用 RoundRobin 策略來給消費者 C1 和消費者 C2分配分區,那么消費者 C1 將分到主題 T1 的分區 0和分區 2以及主題 T2 的分區 1,消費者 C2 將分配到主題 T1 的分區 l 以及主題T2 的分區 0和分區 2。一般 來說,如果所有消費者都訂閱相同的主題(這種情況很常見), RoundRobin策略會給所 有消費者分配相同數量 的分區(或最多就差一個分區)。
可以通過設置 partition.assignment.strategy 來選擇分區策略。默認使用的是 org. apache.kafka.clients.consumer.RangeAssignor, 這個類實現了 Range策略,不過也可以 把它改成 org.apache.kafka.clients.consumer.RoundRobinAssignor。我們還可以使用自定 義策略,在這種情況下 , partition.assignment.strategy 屬性的值就是自定義類的名字。
8. client.id
該屬性可以是任意字符串 , broker用它來標識從客戶端發送過來的消息,通常被用在日志、度量指標和配額里。
9. max.poll.records
該屬性用于控制單次調用 call() 方法能夠返回的記錄數量,可以幫你控制在輪詢里需要處理的數據量。
10. receive.buffer.bytes 和 send.buffer.bytes
socket 在讀寫數據時用到的 TCP 緩沖區也可以設置大小。如果它們被設為-1,就使用操作系統的默認值。如果生產者或消費者與 broker處于不同的數據中心內,可以適當增大這些值,因為跨數據中心的網絡一般都有 比較高的延遲和比較低的帶寬 。