插曲:Kafka的生產者原理及重要引數說明

說出你的願望吧發表於2019-11-24

前言

本來插曲系列是應大家要求去更新的,但是好像第一篇的kafka效果還可以所以更插曲就勤快些了(畢竟誰不想看著自己被多多點贊呢hhh?),上一篇說了一個案例是為了說明如何去考量一個kafka叢集的部署,算是一個參考吧,畢竟大家在不同的公司工作肯定也會有自己的一套實施方案。

這次我們再回到原理性的問題,這次會延續第一篇的風格,帶領大家把圖一步一步畫出來。輕鬆愉快

一、Kafka的Producer原理

首先我們得先有個叢集吧,然後叢集中有若干臺伺服器,每個伺服器我們管它叫Broker,其實就是一個個Kafka程式

插曲:Kafka的生產者原理及重要引數說明

如果大家還記得第一篇的內容,就不難猜出來,接下來肯定會有一個controller和多個follower,還有個zookeeper叢集,一開始我們的Broker都會註冊到我們的zookeeper叢集上面。

插曲:Kafka的生產者原理及重要引數說明

然後controller也會監聽zookeeper叢集的變化,在叢集產生變化時更改自己的後設資料資訊。並且follower也會去它們的老大controller那裡去同步後設資料資訊,所以一個Kafka叢集中所有伺服器上的後設資料資訊都是一致的。

插曲:Kafka的生產者原理及重要引數說明

上述準備完成後,我們正式開始我們生產者的內容

① 名詞1 --- ProducerRecord

生產者需要往叢集傳送訊息前,要先把每一條訊息封裝成ProducerRecord物件,這是生產者內部完成的。之後會經歷一個序列化的過程。之前好幾篇專欄也是有提到過了,需要經過網路傳輸的資料都是二進位制的一些位元組資料,需要進行序列化才能傳輸。

此時就會有一個問題,我們需要把訊息傳送到一個Topic下的一個leader partition中,可是生產者是怎樣get到這個topic下哪個分割槽才是leader partition呢?

可能有些小夥伴忘了,提醒一下,controller可以視作為broker的領導,負責管理叢集的後設資料,而leader partition是做負載均衡用的,它們會分散式地儲存在不同的伺服器上面。叢集中生產資料也好,消費資料也好,都是針對leader partition而操作的。

② 名詞2 --- partitioner

怎麼知道哪個才是leader partition,只需要獲取到後設資料不就好了嘛。

說來要怎麼獲取後設資料也不難,只要隨便找到叢集下某一臺伺服器就可以了(因為叢集中的每一臺伺服器後設資料都是一樣的)

插曲:Kafka的生產者原理及重要引數說明

③ 名詞3 --- 緩衝區

此時生產者不著急把訊息傳送出去,而是先放到一個緩衝區

④ 名詞4 --- Sender

把訊息放進緩衝區之後,與此同時會有一個獨立執行緒Sender去把訊息分批次包裝成一個個Batch,不難想到如果Kafka真的是一條訊息一條訊息地傳輸,一條訊息就是一個網路連線,那效能就會被拉得很差。為了提升吞吐量,所以採取了分批次的做法

整好一個個batch之後,就開始傳送給對應的主機上面。此時經過第一篇所提到的Kakfa的網路設計中的模型,然後再寫到os cache,再寫到磁碟上面。

插曲:Kafka的生產者原理及重要引數說明

下圖是當時我們已經說明過的Kafka網路設計模型

插曲:Kafka的生產者原理及重要引數說明

⑤ 生產者程式碼

1.設定引數部分

// 建立配置檔案物件
Properties props = new Properties();

// 這個引數目的是為了獲取kafka叢集的後設資料
// 寫一臺主機也行,多個更加保險
// 這裡使用的是主機名,要根據server.properties來決定
// 使用主機名的情況需要配置電腦的hosts檔案(重點)
props.put("bootstrap.servers", "hadoop1:9092,hadoop2:9092,hadoop3:9092");  

// 這個就是負責把傳送的key從字串序列化為位元組陣列
// 我們可以給每個訊息設定key,作用之後再闡述
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
// 這個就是負責把你傳送的實際的message從字串序列化為位元組陣列
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");

// 以下屬於調優,之後再解釋
props.put("acks", "-1");
props.put("retries", 3);
props.put("batch.size", 323840);
props.put("linger.ms", 10);
props.put("buffer.memory", 33554432);
props.put("max.block.ms", 3000);
複製程式碼

2.建立生產者例項

// 建立一個Producer例項:執行緒資源,跟各個broker建立socket連線資源
KafkaProducer<String, String> producer = new KafkaProducer<String, String>(props);
複製程式碼

3.建立訊息

ProducerRecord<String, String> record = new ProducerRecord<>(
				"test-topic", "test-value");
複製程式碼

當然你也可以指定一個key,作用之後會說明

ProducerRecord<String, String> record = new ProducerRecord<>(
				"test-topic", "test-key", "test-value");
複製程式碼

4.傳送訊息

帶有一個回撥函式,如果沒有異常就返回訊息傳送成功

// 這是非同步傳送的模式
producer.send(record, new Callback() {
	@Override
	public void onCompletion(RecordMetadata metadata, Exception exception) {
		if(exception == null) {
			// 訊息傳送成功
			System.out.println("訊息傳送成功");  
		} else {
			// 訊息傳送失敗,需要重新傳送
		}
	}
});
Thread.sleep(10 * 1000); 
		
// 這是同步傳送的模式(是一般不會使用的,效能很差,測試可以使用)
// 你要一直等待人家後續一系列的步驟都做完,傳送訊息之後
// 有了訊息的回應返回給你,你這個方法才會退出來
producer.send(record).get(); 
複製程式碼

5.關閉連線

producer.close();
複製程式碼

二、乾貨時間:調優部分的程式碼

區分是不是一個勤于思考的打字員的部分其實就是在1那裡還沒有講到的那部分調優,一個個拿出來單獨解釋,就是下面這一大串

props.put("acks", "-1");
props.put("retries", 3);
props.put("batch.size", 32384);
props.put("linger.ms", 100);
props.put("buffer.memory", 33554432);
props.put("max.block.ms", 3000);
複製程式碼

① acks 訊息驗證

props.put("acks", "-1");
複製程式碼
acks 訊息傳送成功判斷
-1 leader & all follower接收
1 leader接收
0 訊息傳送即可

這個acks引數有3個值,分別是-1,0,1,設定這3個不同的值會成為kafka判斷訊息傳送是否成功的依據。Kafka裡面的分割槽是有副本的,如果acks為-1.則說明訊息在寫入一個分割槽的leader partition後,這些訊息還需要被另外所有這個分割槽的副本同步完成後,才算傳送成功(對應程式碼就是輸出System.out.println("訊息傳送成功")),此時傳送資料的效能降低

如果設定acks為1,需要傳送的訊息只要寫入了leader partition,即算髮送成功,但是這個方式存在丟失資料的風險,比如在訊息剛好傳送成功給leader partition之後,這個leader partition立刻當機了,此時剩餘的follower無論選舉誰成為leader,都不存在剛剛傳送的那一條訊息。

如果設定acks為0,訊息只要是傳送出去了,就預設傳送成功了。啥都不管了。

② retries 重試次數(重要)

這個引數還是非常重要的,在生產環境中是必須設定的引數,為設定訊息重發的次數

props.put("retries", 3);
複製程式碼

在kafka中可能會遇到各種各樣的異常(可以直接跳到下方的補充異常型別),但是無論是遇到哪種異常,訊息傳送此時都出現了問題,特別是網路突然出現問題,但是叢集不可能每次出現異常都丟擲,可能在下一秒網路就恢復了呢,所以我們要設定重試機制。

這裡補充一句:設定了retries之後,叢集中95%的異常都會自己乘風飛去,我真沒開玩笑?

程式碼中我配置了3次,其實設定5~10次都是合理的,補充說明一個,如果我們需要設定隔多久重試一次,也有引數,沒記錯的話是retry.backoff.ms,下面我設定了100毫秒重試一次,也就是0.1秒

props.put("retry.backoff.ms",100);
複製程式碼

③ batch.size 批次大小

批次的大小預設是16K,這裡設定了32K,設定大一點可以稍微提高一下吞吐量,設定這個批次的大小還和訊息的大小有關,假設一條訊息的大小為16K,一個批次也是16K,這樣的話批次就失去意義了。所以我們要事先估算一下叢集中訊息的大小,正常來說都會設定幾倍的大小。

props.put("batch.size", 32384);
複製程式碼

④ linger.ms 傳送時間限制

比如我現在設定了批次大小為32K,而一條訊息是2K,此時已經有了3條訊息傳送過來,總大小為6K,而生產者這邊就沒有訊息過來了,那在沒夠32K的情況下就不傳送過去叢集了嗎?顯然不是,linger.ms就是設定了固定多長時間,就算沒塞滿Batch,也會傳送,下面我設定了100毫秒,所以就算我的Batch遲遲沒有滿32K,100毫秒過後都會向叢集傳送Batch。

props.put("linger.ms", 100);
複製程式碼

⑤ buffer.memory 緩衝區大小

當我們的Sender執行緒處理非常緩慢,而生產資料的速度很快時,我們中間的緩衝區如果容量不夠,生產者就無法再繼續生產資料了,所以我們有必要把緩衝區的記憶體調大一點,緩衝區預設大小為32M,其實基本也是合理的。

props.put("buffer.memory", 33554432);
複製程式碼

那應該如何去驗證我們這時候應該調整緩衝區的大小了呢,我們可以用一般Java計算結束時間減去開始時間的方式測試,當結束時間減去開始時間大於100ms,我們認為此時Sender執行緒處理速度慢,需要調大緩衝區大小。

當然一般情況下我們是不需要去設定這個引數的,32M在普遍情況下已經足以應付了。

Long startTime=System.currentTime();
producer.send(record, new Callback() {
	@Override
	public void onCompletion(RecordMetadata metadata, Exception exception) {
		if(exception == null) {
			// 訊息傳送成功
			System.out.println("訊息傳送成功");  
		} else {
			// 訊息傳送失敗,需要重新傳送
		}
	}
});
Long endTime=System.currentTime();
If(endTime - startTime > 100){//說明記憶體被壓滿了
 說明有問題
複製程式碼

}

⑦ compression.type 壓縮方式

compression.type,預設是none,不壓縮,但是也可以使用lz4壓縮,效率還是不錯的,壓縮之後可以減小資料量,提升吞吐量,但是會加大producer端的cpu開銷

props.put("compression.type", lz4);
複製程式碼

⑧ max.block.ms

留到原始碼時候說明,是設定某幾個方法的阻塞時間

props.put("max.block.ms", 3000);
複製程式碼

⑨ max.request.size 最大訊息大小

max.request.size:這個引數用來控制傳送出去的訊息的大小,預設是1048576位元組,也就1M,這個一般太小了,很多訊息可能都會超過1mb的大小,所以需要自己優化調整,把它設定更大一些(企業一般設定成10M),不然程式跑的好好的突然來了一條2M的訊息,系統就報錯了,那就得不償失

props.put("max.request.size", 1048576);    
複製程式碼

⑩ request.timeout.ms 請求超時

request.timeout.ms:這個就是說傳送一個請求出去之後,他有一個超時的時間限制,預設是30秒,如果30秒都收不到響應(也就是上面的回撥函式沒有返回),那麼就會認為異常,會丟擲一個TimeoutException來讓我們進行處理。如果公司網路不好,要適當調整此引數

props.put("request.timeout.ms", 30000); 
複製程式碼

補充:kafka中的異常

不管是非同步還是同步,都可能讓你處理異常,常見的異常如下:

1)LeaderNotAvailableException:這個就是如果某臺機器掛了,此時leader副本不可用,會導致你寫入失敗,要等待其他follower副本切換為leader副本之後,才能繼續寫入,此時可以重試傳送即可。如果說你平時重啟kafka的broker程式,肯定會導致leader切換,一定會導致你寫入報錯,是LeaderNotAvailableException

2)NotControllerException:這個也是同理,如果說Controller所在Broker掛了,那麼此時會有問題,需要等待Controller重新選舉,此時也是一樣就是重試即可

3)NetworkException:網路異常,重試即可 我們之前配置了一個引數,retries,他會自動重試的,但是如果重試幾次之後還是不行,就會提供Exception給我們來處理了。 引數:retries 預設值是3 引數:retry.backoff.ms 兩次重試之間的時間間隔

finally

上面從生產者生產訊息到傳送這一個流程分析下來,從而引出下面的各種各樣關於整個過程的引數的設定,如果真的能清晰地理解好這些基礎知識,相信對你必定是有所幫助。之後會再帶一個生產者的案例和消費者進來。感興趣的朋友可以關注一下,謝謝。

相關文章