kafka生產者剖析

Kafka生產者程序概述

開發一個生產者的步驟:

  1. 構造生產者對象所需的參數對象;
  2. 利用第一步的參數對象,創建 KafkaProducer 對象實例;
  3. 使用 KafkaProducer 的 send 方法發送消息;
  4. 調用 KafkaProducer 的close方法關閉生產者並釋放各種系統資源;
Properties props = new Properties ();
props.put(“參數 1, “參數 1 的值”);
props.put(“參數 2, “參數 2 的值”);
……
// try-with-resource,自動釋放資源
try (Producer<String, String> producer = new KafkaProducer<>(props)) {
            producer.send(new ProducerRecord<String, String>(……), callback);
	……
}

生產者消息分區機制

Kafka 主題(Topic)是承載真實數據的邏輯容器,而在主題之下還分爲若干個分區,也就是說 Kafka 的消息組織方式實際上是三級結構:主題 - 分區 - 消息。主題下的每條消息只會保存在某一個分區中,而不會在多個分區中被保存多份。而如何將大量的消息數據均勻的分到到Kafka的各個Broker上,就是一個比較重要的問題;

分區的作用

分區主要作用就是提供負載均衡的能力,或者說對數據進行分區的主要原因,就是爲了實現系統的高伸縮性(Scalability)。不同的分區能夠被放置到不同節點的機器上,而數據的讀寫操作也都是針對分區這個粒度而進行的,這樣每個節點的機器都能獨立地執行各自分區的讀寫請求處理。並且,我們還可以通過添加新的節點機器來增加整體系統的吞吐量。
另外,通過分區,也可以實現一些業務級別的需求;比如實現業務級別的消息順序問題;

分區策略

分區策略:決定生產者將消息發送到哪個分區的算法。Kafka支持自定義分區策略;

自定義分區策略:
	1. 編寫生產者程序時,編寫一個實現 `org.apache.kafka.clients.producer.Partitioner` 接口的類;
		通常實現 partition()方法即可;
	int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster);
	2. 顯式的配置生產者端參數:`partitioner.class`
  • 輪詢策略:Round-robin策略,順序分配;分區位置 = 消息offset % 分區數;輪詢爲Kafka Java生產者API默認提供的分區策略;
    輪詢策略有非常優秀的負載均衡表現,它總是能保證消息最大限度地被平均分配到所有分區上,故默認情況下它是最合理的分區策略,也是我們最常用的分區策略之一。
  • 隨機策略:Randomnesscelue;隨機的將消息放置到任意分區;隨機策略是老版本生產者使用的分區策略,新版本已改爲輪詢;
List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
return ThreadLocalRandom.current().nextInt(partitions.size());
  • 按消息鍵保序策略:Key-ordering策略;Kafka允許爲每條消息定義消息鍵;可以保證具有相同消息鍵的一組消息被分配到同一個分區,由於每個分區下的消息處理都是有序的,故稱之爲按消息鍵保序策略;
List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
return Math.abs(key.hashCode()) % partitions.size();

Kafka 默認分區策略實際上同時實現了兩種策略:如果指定了 Key,那麼默認實現按消息鍵保序策略;如果沒有指定 Key,則使用輪詢策略。

  • 其他分區策略:地理位置分區策略(根據Broker所在IP地址分區)

生產者壓縮算法

壓縮(compression):秉承用時間換空間的經典trade-off思想,具體來說就是用 CPU 時間去換磁盤空間或網絡 I/O 傳輸量,希望以較小的 CPU 開銷帶來更少的磁盤佔用或更少的網絡 I/O 傳輸。

Kafka壓縮方式-消息格式

Kafka消息格式有兩大類:社區分別稱之爲 V1 版本和 V2 版本。V2 版本是 Kafka 0.11.0.0 中正式引入的。V2 版本主要是針對 V1 版本的一些弊端做了修正,和消息壓縮相關的修改就有
把消息的公共部分抽取出來放到外層消息集合裏面,這樣就不用每條消息都保存這些信息了。
V2 版本還有一個和壓縮息息相關的改進,就是保存壓縮消息的方法發生了變化。之前 V1 版本中保存壓縮消息的方法是把多條消息進行壓縮然後保存到外層消息的消息體字段中;而 V2 版本的做法是對整個消息集合進行壓縮。顯然後者應該比前者有更好的壓縮效果。

不論是哪個版本,Kafka 的消息層次都分爲兩層:消息集合(message set)以及消息(message)。
一個消息集合中包含若干條日誌項(record item),而日誌項纔是真正封裝消息的地方。
Kafka 底層的消息日誌由一系列消息集合日誌項組成。
Kafka 通常不會直接操作具體的一條條消息,它總是在消息集合這個層面上進行寫入操作。

壓縮時機

Kafka中,壓縮可能發生在兩個地方: 生產者端 和 Broker端。
compression.type: 生產者程序配置參數;表示啓用指定類型的壓縮算法。

 Properties props = new Properties();
 props.put("bootstrap.servers", "localhost:9092");
 props.put("acks", "all");
 props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
 props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
 // 開啓 GZIP 壓縮
 props.put("compression.type", "gzip");
 Producer<String, String> producer = new KafkaProducer<>(props);

Broker端可能壓縮的場景:

  • Broker 端指定了和 Producer 端不同的壓縮算法。
    在Broker端也有參數 compression.type,當Broker端和 Producer端配置不一致時就會出現預料之外的壓縮/解壓縮操作,通常表現爲Broker端CPU使用率飆升;
    Broker端參數 compression.type 默認值爲 producer,表示以Producer端壓縮算法爲準;
  • Broker 端發生了消息格式轉換。
    在一個生產環境中,Kafka 集羣中同時保存多種版本的消息格式非常常見。爲了兼容老版本的格式,Broker 端會對新版本消息執行向老版本格式的轉換。這個過程中會涉及消息的解壓縮和重新壓縮。一般情況下這種消息格式轉換對性能是有很大影響的,除了這裏的壓縮之外,它還讓 Kafka 喪失了引以爲豪的 Zero Copy 特性。

解壓時機

通常解壓縮發生在消費者程序中;Kafka 會將啓用了哪種壓縮算法封裝進消息集合中,這樣當 Consumer 讀取到消息集合時,它自然就知道了這些消息使用的是哪種壓縮算法。
Producer 端壓縮、Broker 端保持、Consumer 端解壓縮。

在Broker端還會有另一種解壓縮: 每個壓縮過的消息集合在 Broker 端寫入時都要發生解壓縮操作,目的就是爲了對消息執行各種驗證。我們必須承認這種解壓縮對 Broker 端性能是有一定影響的,特別是對 CPU 的使用率而言。

壓縮算法

kafka 2.1.0之前
	1. GZIP
	2. Snappy
	3. LZ4
kafka 2.1.0
	4. Zstandard(zstd):facebook開源壓縮算法,提供超高壓縮比;

壓縮算法優劣指標:壓縮比 和 壓縮/解壓縮吞吐量
下面是下面這張表是 Facebook Zstandard 官網提供的一份壓縮算法 benchmark 比較結果:
在這裏插入圖片描述
在實際使用中,GZIP、Snappy、LZ4 甚至是 zstd 的表現各有千秋。但對於 Kafka 而言,它們的性能測試結果卻出奇得一致,即:
在吞吐量方面:LZ4 > Snappy > zstd 和 GZIP;
在壓縮比方面:zstd > LZ4 > GZIP > Snappy。

具體到物理資源,使用 Snappy 算法佔用的網絡帶寬最多,zstd 最少,這是合理的,畢竟 zstd 就是要提供超高的壓縮比;在 CPU 使用率方面,各個算法表現得差不多,只是在壓縮時 Snappy 算法使用的 CPU 較多一些,而在解壓縮時 GZIP 算法則可能使用更多的 CPU。
建議:對於CPU資源充足 或者 寬帶資源有限的環境,建議啓用壓縮。

TCP連接管理

Apache Kafka的所有通信都是基於TCP的,而不是基於HTTP或者其他協議。
開發客戶端時,用到了TCP的高級功能:多路複用請求,同時輪詢多個連接;

  • 多路複用請求:multiplexing request,指兩個或多個數據流合併到底層單一物理連接中的過程。TCP的多路複用請求會在一條物理連接上創建若干個虛擬連接,每個虛擬連接負責流轉各自對應的數據流;TCP嚴格意義上並不是多路複用,只是提供可靠的消息交付語義保證;eg: 自動重傳丟失的報文。

TCP連接建立

在創建 KafkaProducer 實例時,生產者應用會在後臺創建並啓動一個名爲 Sender 的線程, 該 Sender 線程開始運行時首先會創建與 Broker 的連接;
注:Producer 並不知道和哪個 Broker 建立連接, 而是會和 bootstrap.servers 參數指定的所有Broker建立連接。
建議:不建議將集羣中所有的Broker 信息都配置到 bootstrap.servers中,通常配置3~4臺即可。因爲Producer一旦連接到集羣中任意一臺Broker(通過向一臺Broker發送METADATA請求,及嘗試獲取集羣的元數據信息),就能獲取到整個集羣的Broker信息。

KafkaProducer實例

KafkaProducer類是線程安全的;KafkaProducer實例創建的線程和Sender線程共享的可變數據結構只有 RecordAccumulator 類,故維護 RecordAccumulator類線程安全,即可實現KafkaProducer類的線程安全。
RecordAccumulator類數據結構:ConcurrentMap<TopicPartition, Deque>,TopicPartion 是Kafka用來表示主題分區的java對象,本身是不可變對象。而Deque使用處都有鎖保護,所以基本可以認定RecordAccumulator類是線程安全的。

  • 縱然KafkaProducer 是線程安全的,也不贊同創建 KafkaProducer 實例時啓動 Sender 線程的做法。寫了《Java 併發編程實踐》的那位布賴恩·格茨(Brian Goetz)大神,明確指出了這樣做的風險:在對象構造器中啓動線程會造成 this 指針的逃逸。理論上,Sender 線程完全能夠觀測到一個尚未構造完成的 KafkaProducer 實例。當然,在構造對象時創建線程沒有任何問題,但最好是不要同時啓動它。

TCP連接建立的時機

  • TCP連接在KafkaProducer實例創建的時候建立;
  • 更新元數據後,會建立TCP連接;當Producer更新集羣的元數據信息之後,如果發現與某些Broker當前沒有連接,那麼他會創建一個TCP連接;
  • 消息發送時,會建立TCP連接;當要發送消息時,Producer發現尚不存在與目標Broker之間的連接會創建;
    更新集羣元數據信息的場景:
  1. 場景一:當 Producer 嘗試給一個不存在的主題發送消息時,Broker 會告訴 Producer 說這個主題不存在。此時 Producer 會發送 METADATA 請求給 Kafka 集羣,去嘗試獲取最新的元數據信息。
  2. 場景二:Producer 通過 metadata.max.age.ms 參數定期地去更新元數據信息。該參數的默認值是 300000,即 5 分鐘,也就是說不管集羣那邊是否有變化,Producer 每 5 分鐘都會強制刷新一次元數據以保證它是最及時的數據。

TCP連接關閉的時機

  1. 用戶主動關閉:主動關閉實際上是廣義的主動關閉,甚至包括用戶調用 kill -9 主動“殺掉”Producer 應用。當然最推薦的方式還是調用 producer.close() 方法來關閉。
  2. Kafka自動關閉
    這與 Producer 端參數 connections.max.idle.ms 的值有關。默認情況下該參數值是 9 分鐘,即如果在 9 分鐘內沒有任何請求“流過”某個 TCP 連接,那麼 Kafka 會主動幫你把該 TCP 連接關閉。用戶可以在 Producer 端設置 connections.max.idle.ms=-1 禁掉這種機制。一旦被設置成 -1,TCP 連接將成爲永久長連接。當然這只是軟件層面的“長連接”機制,由於 Kafka 創建的這些 Socket 連接都開啓了 keepalive,因此 keepalive 探活機制還是會遵守的。
    TCP 連接是在 Broker 端被關閉的,但其實這個 TCP 連接的發起方是客戶端,因此在 TCP 看來,這屬於被動關閉的場景,即 passive close。被動關閉的後果就是會產生大量的 CLOSE_WAIT 連接,因此 Producer 端或 Client 端沒有機會顯式地觀測到此連接已被中斷。—殭屍連接
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章