前言
本來插曲系列是應大家要求去更新的,但是好像第一篇的kafka效果還可以所以更插曲就勤快些了(畢竟誰不想看著自己被多多點贊呢hhh?),上一篇說了一個案例是為了說明如何去考量一個kafka叢集的部署,算是一個參考吧,畢竟大家在不同的公司工作肯定也會有自己的一套實施方案。
這次我們再回到原理性的問題,這次會延續第一篇的風格,帶領大家把圖一步一步畫出來。輕鬆愉快
一、Kafka的Producer原理
首先我們得先有個叢集吧,然後叢集中有若干臺伺服器,每個伺服器我們管它叫Broker,其實就是一個個Kafka程式
如果大家還記得第一篇的內容,就不難猜出來,接下來肯定會有一個controller和多個follower,還有個zookeeper叢集,一開始我們的Broker都會註冊到我們的zookeeper叢集上面。
然後controller也會監聽zookeeper叢集的變化,在叢集產生變化時更改自己的後設資料資訊。並且follower也會去它們的老大controller那裡去同步後設資料資訊,所以一個Kafka叢集中所有伺服器上的後設資料資訊都是一致的。
上述準備完成後,我們正式開始我們生產者的內容
① 名詞1 --- ProducerRecord
生產者需要往叢集傳送訊息前,要先把每一條訊息封裝成ProducerRecord物件,這是生產者內部完成的。之後會經歷一個序列化的過程。之前好幾篇專欄也是有提到過了,需要經過網路傳輸的資料都是二進位制的一些位元組資料,需要進行序列化才能傳輸。
此時就會有一個問題,我們需要把訊息傳送到一個Topic下的一個leader partition中,可是生產者是怎樣get到這個topic下哪個分割槽才是leader partition呢?
可能有些小夥伴忘了,提醒一下,controller可以視作為broker的領導,負責管理叢集的後設資料,而leader partition是做負載均衡用的,它們會分散式地儲存在不同的伺服器上面。叢集中生產資料也好,消費資料也好,都是針對leader partition而操作的。
② 名詞2 --- partitioner
怎麼知道哪個才是leader partition,只需要獲取到後設資料不就好了嘛。
說來要怎麼獲取後設資料也不難,只要隨便找到叢集下某一臺伺服器就可以了(因為叢集中的每一臺伺服器後設資料都是一樣的)
③ 名詞3 --- 緩衝區
此時生產者不著急把訊息傳送出去,而是先放到一個緩衝區
④ 名詞4 --- Sender
把訊息放進緩衝區之後,與此同時會有一個獨立執行緒Sender去把訊息分批次包裝成一個個Batch,不難想到如果Kafka真的是一條訊息一條訊息地傳輸,一條訊息就是一個網路連線,那效能就會被拉得很差。為了提升吞吐量,所以採取了分批次的做法
整好一個個batch之後,就開始傳送給對應的主機上面。此時經過第一篇所提到的Kakfa的網路設計中的模型,然後再寫到os cache,再寫到磁碟上面。
下圖是當時我們已經說明過的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
上面從生產者生產訊息到傳送這一個流程分析下來,從而引出下面的各種各樣關於整個過程的引數的設定,如果真的能清晰地理解好這些基礎知識,相信對你必定是有所幫助。之後會再帶一個生產者的案例和消費者進來。感興趣的朋友可以關注一下,謝謝。