一、訊息傳送
1.1 資料生產流程
資料生產流程圖解:
- Producer建立時,會建立⼀個Sender執行緒並設定為守護執行緒
- ⽣產訊息時,內部其實是非同步流程;⽣產的訊息先經過攔截器->序列化器->分割槽器,然後將訊息快取在緩衝區(該緩衝區也是在Producer建立時建立)
- 批次傳送的條件為:緩衝區資料⼤⼩達到
batch.size
或者linger.ms
達到上限,哪個先達到就算哪個 - 批次傳送後,發往指定分割槽,然後落盤到 broker;如果⽣產者配置了
retrires
引數⼤於0並且失敗原因允許重試,那麼客戶端內部會對該訊息進⾏重試 - 落盤到broker成功,返回⽣產後設資料給⽣產者
- 後設資料返回有兩種⽅式:⼀種是通過阻塞直接返回,另⼀種是通過回撥返回
1.2 必要的引數配置
先來看看我們一般在程式中是怎麼配置的:
最常用的配置項:
屬性 | 說明 | 重要性 |
---|---|---|
bootstrap.servers | ⽣產者客戶端與broker叢集建⽴初始連線需要的broker地址列表,由該初始連線發現Kafka叢集中其他的所有broker。該地址列表不需要寫全部的Kafka叢集中broker的地址,但也不要寫⼀個,以防該節點當機的時候不可⽤。形式為:host1:port1,host2:port2,.... |
high |
key.serializer | 實現了接⼝org.apache.kafka.common.serialization.Serializer的key序列化類。 | high |
value.serializer | 實現了接⼝org.apache.kafka.common.serialization.Serializer的value序列化類。 | high |
acks | 該選項控制著已傳送訊息的永續性。acks=0 :⽣產者不等待broker的任何訊息確認。只要將訊息放到了socket的緩衝區,就認為訊息已傳送。不能保證伺服器是否收到該訊息,retries設定也不起作⽤,因為客戶端不關⼼訊息是否傳送失敗。客戶端收到的訊息偏移量永遠是-1。acks=1 :leader將記錄寫到它本地⽇志,就響應客戶端確認訊息,⽽不等待follower副本的確認。如果leader確認了訊息就當機,則可能會丟失訊息,因為follower副本可能還沒來得及同步該訊息。acks=all :leader等待所有同步的副本確認該訊息。保證了只要有⼀個同步副本存在,訊息就不會丟失。這是最強的可⽤性保證。等價於acks=-1。預設值為1,字串。可選值:[all, -1, 0, 1] |
high |
compression.type | ⽣產者⽣成資料的壓縮格式。預設是none (沒有壓縮)。允許的值:none ,gzip ,snappy 和lz4 。壓縮是對整個訊息批次來講的。訊息批的效率也影響壓縮的⽐例。訊息批越⼤,壓縮效率越好。字串型別的值。預設是none 。 |
high |
retries | 設定該屬性為⼀個⼤於1的值,將在訊息傳送失敗的時候重新傳送訊息。該重試與客戶端收到異常重新傳送並⽆⼆⾄。允許重試但是不設定max.in.flight.requests.per.connection 為 1,存在訊息亂序的可能,因為如果兩個批次傳送到同⼀個分割槽,第⼀個失敗了重試,第⼆個成功了,則第⼀個訊息批在第⼆個訊息批後。int型別的值,預設:0,可選值:[0,...,2147483647] |
high |
1.3 攔截器
1.3.1 攔截器介紹
Producer 的攔截器(Interceptor)和 Consumer 的 Interceptor 主要⽤於實現Client端的定製化控制邏輯。
對於Producer⽽⾔,Interceptor使得⽤戶在訊息傳送前以及Producer回撥邏輯前有機會對訊息做⼀些定製化需求,⽐如修改訊息等。同時,Producer允許⽤戶指定多個Interceptor按序作⽤於同⼀條訊息從⽽形成⼀個攔截鏈(Interceptor Chain)。Intercetpor 的實現接⼝是org.apache.kafka.clients.producer.ProducerInterceptor
,其定義的⽅法包括:
onSend(ProducerRecord)
:該⽅法封裝進KafkaProducer.send
⽅法中,即運⾏在⽤戶主執行緒中。Producer確保在訊息被序列化以計算分割槽前調⽤該⽅法。⽤戶可以在該⽅法中對訊息做任何操作,但最好保證不要修改訊息所屬的topic和分割槽,否則會影響⽬標分割槽的計算。onAcknowledgement(RecordMetadata, Exception)
:該⽅法會在訊息被應答之前或訊息傳送失敗時調⽤,並且通常都是在Producer回撥邏輯觸發之前。onAcknowledgement
運⾏在Producer的IO執行緒中,因此不要在該⽅法中放⼊很重的邏輯,否則會拖慢Producer的訊息傳送效率。close
:關閉Interceptor,主要⽤於執⾏⼀些資源清理⼯作。
如前所述,Interceptor可能被運⾏在多個執行緒中,因此在具體實現時⽤戶需要⾃⾏確保執行緒安全。另外倘若指定了多個Interceptor,則Producer將按照指定順序調⽤它們,並僅僅是捕獲每個Interceptor可能丟擲的異常記錄到錯誤⽇志中⽽⾮在向上傳遞。這在使⽤過程中要特別留意。
1.3.2 自定義攔截器
自定義攔截器步驟:
- 實現ProducerInterceptor接⼝
- 在KafkaProducer的設定中設定⾃定義的攔截器
自定義攔截器 1:
public class InterceptorOne<Key, Value> implements ProducerInterceptor<Key, Value> {
private static final Logger LOGGER = LoggerFactory.getLogger(InterceptorOne.class);
@Override
public ProducerRecord<Key, Value> onSend(ProducerRecord<Key, Value> record) {
System.out.println("攔截器1---go");
// 此處根據業務需要對相關的資料作修改
String topic = record.topic();
Integer partition = record.partition();
Long timestamp = record.timestamp();
Key key = record.key();
Value value = record.value();
Headers headers = record.headers();
// 新增訊息頭
headers.add("interceptor", "interceptorOne".getBytes());
ProducerRecord<Key, Value> newRecord = new ProducerRecord<Key, Value>(topic,
partition, timestamp, key, value, headers);
return newRecord;
}
@Override
public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
System.out.println("攔截器1---back");
if (exception != null) {
// 如果發⽣異常,記錄⽇志中
LOGGER.error(exception.getMessage());
}
}
@Override
public void close() {
}
@Override
public void configure(Map<String, ?> configs) {
}
}
照著 攔截器 1 再加兩個攔截器。
生產者
public class MyProducer1 {
public static void main(String[] args) throws InterruptedException, ExecutionException, TimeoutException {
Map<String, Object> configs = new HashMap<>();
// 設定連線Kafka的初始連線⽤到的伺服器地址
// 如果是叢集,則可以通過此初始連線發現叢集中的其他broker
configs.put("bootstrap.servers", "192.168.0.102:9092");
// 設定key的序列化器
configs.put("key.serializer", IntegerSerializer.class);
// 設定⾃定義的序列化類
configs.put("value.serializer", UserSerializer.class);
// 設定自定義分割槽器
configs.put("partitioner.class", "com.mfc.config.MyPartitioner");
// 設定攔截器
configs.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG,
"com.mfc.interceptor.InterceptorOne,"
+ "com.mfc.interceptor.InterceptorTwo,"
+ "com.mfc.interceptor.InterceptorThree");
KafkaProducer<Integer, User> producer = new KafkaProducer<>(configs);
User user = new User();
user.setUserId(1001);
user.setUsername("阿彪");
// ⽤於封裝Producer的訊息
ProducerRecord<Integer, User> record = new ProducerRecord<>(
"topic_1", // 主題名稱
0, // 分割槽編號
user.getUserId(), // 數字作為key
user // user 物件作為value
);
producer.send(record, new Callback() {
@Override
public void onCompletion(RecordMetadata metadata, Exception e) {
if (e == null) {
System.out.println("訊息傳送成功:" + metadata.topic() + "\t"
+ metadata.partition() + "\t"
+ metadata.offset());
} else {
System.out.println("訊息傳送異常");
}
}
});
// 關閉⽣產者
producer.close();
}
}
1.4 序列化器
1.4.1 Kafka 自帶序列化器
Kafka使⽤org.apache.kafka.common.serialization.Serializer
接⼝⽤於定義序列化器,將泛型指定型別的資料轉換為位元組陣列。
package org.apache.kafka.common.serialization;
import java.io.Closeable;
import java.util.Map;
/**
將物件轉換為byte陣列的接⼝
該接⼝的實現類需要提供⽆參構造器
@param <T> 從哪個型別轉換
*/
public interface Serializer<T> extends Closeable {
/*
類的配置資訊
@param configs key/value pairs
@param isKey key的序列化還是value的序列化
*/
void configure(Map<String, ?> var1, boolean var2);
/*
將物件轉換為位元組陣列
@param topic 主題名稱
@param data 需要轉換的物件
@return 序列化的位元組陣列
*/
byte[] serialize(String var1, T var2);
/*
關閉序列化器
該⽅法需要提供冪等性,因為可能調⽤多次。
*/
void close();
}
系統提供了該接⼝的⼦接⼝以及實現類:
org.apache.kafka.common.serialization.ByteArraySerializer
org.apache.kafka.common.serialization.ByteBufferSerializer
org.apache.kafka.common.serialization.BytesSerializer
org.apache.kafka.common.serialization.DoubleSerializer
org.apache.kafka.common.serialization.FloatSerializer
org.apache.kafka.common.serialization.IntegerSerializer
org.apache.kafka.common.serialization.StringSerializer
org.apache.kafka.common.serialization.LongSerializer
org.apache.kafka.common.serialization.ShortSerializer
1.4.2 自定義序列化器
資料的序列化⼀般⽣產中使⽤ avro
。
⾃定義序列化器需要實現 org.apache.kafka.common.serialization.Serializer<T>
接⼝,並實現其中的serialize
⽅法。
實體類
public class User {
private Integer userId;
private String username;
// set、get方法省略
}
自定義序列化器
public class UserSerializer implements Serializer<User> {
@Override
public void configure(Map<String, ?> map, boolean b) {
// do Nothing
}
@Override
public byte[] serialize(String topic, User user) {
try {
// 如果資料是null,則返回null
if (user == null) return null;
Integer userId = user.getUserId();
String username = user.getUsername();
int length = 0;
byte[] bytes = null;
if (null != username) {
bytes = username.getBytes("utf-8");
length = bytes.length;
}
ByteBuffer buffer = ByteBuffer.allocate(4 + 4 + length);
buffer.putInt(userId);
buffer.putInt(length);
buffer.put(bytes);
return buffer.array();
} catch (UnsupportedEncodingException e) {
throw new SerializationException("序列化資料異常");
}
}
@Override
public void close() {
// do Nothing
}
}
生產者:
public class MyProducer1 {
public static void main(String[] args) throws InterruptedException, ExecutionException, TimeoutException {
Map<String, Object> configs = new HashMap<>();
// 設定連線Kafka的初始連線⽤到的伺服器地址
// 如果是叢集,則可以通過此初始連線發現叢集中的其他broker
configs.put("bootstrap.servers", "192.168.0.102:9092");
// 設定key的序列化器
configs.put("key.serializer", IntegerSerializer.class);
// 設定⾃定義的序列化類
configs.put("value.serializer", UserSerializer.class);
KafkaProducer<Integer, User> producer = new KafkaProducer<>(configs);
User user = new User();
user.setUserId(1001);
user.setUsername("阿彪");
// ⽤於封裝Producer的訊息
ProducerRecord<Integer, User> record = new ProducerRecord<>(
"topic_1", // 主題名稱
0, // 分割槽編號
user.getUserId(), // 數字作為key
user // user 物件作為value
);
producer.send(record, new Callback() {
@Override
public void onCompletion(RecordMetadata metadata, Exception e) {
if (e == null) {
System.out.println("訊息傳送成功:" + metadata.topic() + "\t"
+ metadata.partition() + "\t"
+ metadata.offset());
} else {
System.out.println("訊息傳送異常");
}
}
});
// 關閉⽣產者
producer.close();
}
}
1.5 分割槽器
1.5.1 Kafka 自帶分割槽器
預設(DefaultPartitioner
)分割槽計算:
- 如果record提供了分割槽號,則使⽤record提供的分割槽號
- 如果record沒有提供分割槽號,則使⽤key的序列化後的值的hash值對分割槽數量取模
- 如果record沒有提供分割槽號,也沒有提供key,則使⽤輪詢的⽅式分配分割槽號。
- 會⾸先在可⽤的分割槽中分配分割槽號
- 如果沒有可⽤的分割槽,則在該主題所有分割槽中分配分割槽號。
看一下kafka的生產者(KafkaProducer
)原始碼:
再看Kafka自帶的預設分割槽器(DefaultPartitioner
):
預設的分割槽器實現了 Partitioner
介面,先看一下介面:
public interface Partitioner extends Configurable, Closeable {
/**
* 為指定的訊息記錄計算分割槽值
*
* @param topic 主題名稱
* @param key 根據該key的值進⾏分割槽計算,如果沒有則為null
* @param keyBytes key的序列化位元組陣列,根據該陣列進⾏分割槽計算。如果沒有key,則為null
* @param value 根據value值進⾏分割槽計算,如果沒有,則為null
* @param valueBytes value的序列化位元組陣列,根據此值進⾏分割槽計算。如果沒有,則為null
* @param cluster 當前叢集的後設資料
*/
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster);
/**
* 關閉分割槽器的時候調⽤該⽅法
*/
public void close();
}
1.5.2 自定義分割槽器
如果要⾃定義分割槽器,則需要
- ⾸先開發Partitioner接⼝的實現類
- 在KafkaProducer中進⾏設定:
configs.put("partitioner.class", "xxx.xx.Xxx.class")
實現Partitioner接⼝⾃定義分割槽器:
public class MyPartitioner implements Partitioner {
@Override
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
return 0;
}
@Override
public void close() {
}
@Override
public void configure(Map<String, ?> configs) {
}
}
然後在⽣產者中配置:
二、訊息傳送原理
原理圖解:
由上圖可以看出:KafkaProducer
有兩個基本執行緒:
- 主執行緒:負責訊息建立,攔截器,序列化器,分割槽器等操作,並將訊息追加到訊息收集器
RecoderAccumulator
中;- 訊息收集器
RecoderAccumulator
為每個分割槽都維護了⼀個Deque<ProducerBatch>
型別的雙端佇列。 ProducerBatch
可以理解為是ProducerRecord
的集合,批量傳送有利於提升吞吐量,降低⽹絡影響;- 由於⽣產者客戶端使⽤
java.io.ByteBuffer
在傳送訊息之前進⾏訊息儲存,並維護了⼀個BufferPool
實現ByteBuffer
的復⽤;該快取池只針對特定⼤⼩(batch.size
指定)的ByteBuffer
進⾏管理,對於訊息過⼤的快取,不能做到重複利⽤。 - 每次追加⼀條
ProducerRecord
訊息,會尋找/新建對應的雙端佇列,從其尾部獲取⼀個ProducerBatch
,判斷當前訊息的⼤⼩是否可以寫⼊該批次中。若可以寫⼊則寫⼊;若不可以寫⼊,則新建⼀個ProducerBatch
,判斷該訊息⼤⼩是否超過客戶端引數配置batch.size
的值,不超過,則以batch.size
建⽴新的ProducerBatch
,這樣⽅便進⾏快取重複利⽤;若超過,則以計算的訊息⼤⼩建⽴對應的ProducerBatch
,缺點就是該記憶體不能被複⽤了。
- 訊息收集器
Sender
執行緒:- 該執行緒從訊息收集器獲取快取的訊息,將其處理為
<Node, List<ProducerBatch>
的形式, Node 表示叢集的broker節點。 - 進⼀步將
<Node, List<ProducerBatch>
轉化為<Node, Request>
形式,此時才可以向服務端傳送資料。 - 在傳送之前,
Sender
執行緒將訊息以Map<NodeId, Deque<Request>>
的形式儲存到InFlightRequests
中進⾏快取,可以通過其獲取leastLoadedNode
,即當前Node中負載壓⼒最⼩的⼀個,以實現訊息的儘快發出。
- 該執行緒從訊息收集器獲取快取的訊息,將其處理為
三、更多生產者引數配置
引數名稱 | 描述 |
---|---|
retry.backoff.ms | 在向⼀個指定的主題分割槽重發訊息的時候,重試之間的等待時間。 ⽐如3次重試,每次重試之後等待該時間⻓度,再接著重試。在⼀些失敗的場景,避免了密集迴圈的重新傳送請求。 long型值,預設100。可選值:[0,...] |
retries | retries重試次數 當訊息傳送出現錯誤的時候,系統會重發訊息。 跟客戶端收到錯誤時重發⼀樣。 如果設定了重試,還想保證訊息的有序性,需要設定 MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION=1 否則在重試此失敗訊息的時候,其他的訊息可能傳送成功了 |
request.timeout.ms | 客戶端等待請求響應的最⼤時⻓。如果服務端響應超時,則會重發請求,除⾮達到重試次數。該設定應該⽐replica.lag.time.max.ms (a broker configuration) 要⼤,以免在伺服器延遲時間內重發訊息。int型別值,預設:30000,可選值:[0,...] |
interceptor.classes | 在⽣產者接收到該訊息,向Kafka叢集傳輸之前,由序列化器處理之前,可以通過攔截器對訊息進⾏處理。 要求攔截器類必須實現 org.apache.kafka.clients.producer.ProducerInterceptor 接⼝。預設沒有攔截器。Map<String, Object> configs 中通過List集合配置多個攔截器類名。 |
acks | 預設值:all。 acks=0: ⽣產者不等待broker對訊息的確認,只要將訊息放到緩衝區,就認為訊息已經傳送完成。 該情形不能保證broker是否真的收到了訊息,retries配置也不會⽣效。傳送的訊息的返回的訊息偏移量永遠是-1。 acks=1 表示訊息只需要寫到主分割槽即可,然後就響應客戶端,⽽不等待副本分割槽的確認。 在該情形下,如果主分割槽收到訊息確認之後就當機了,⽽副本分割槽還沒來得及同步該訊息,則該訊息丟失。 acks=all ⾸領分割槽會等待所有的ISR副本分割槽確認記錄。 該處理保證了只要有⼀個ISR副本分割槽存活,訊息就不會丟失。 這是Kafka最強的可靠性保證,等效於acks=-1 |
batch.size | 當多個訊息傳送到同⼀個分割槽的時候,⽣產者嘗試將多個記錄作為⼀個批來處理。批處理提⾼了客戶端和伺服器的處理效率。 該配置項以位元組為單位控制預設批的⼤⼩。 所有的批⼩於等於該值。 傳送給broker的請求將包含多個批次,每個分割槽⼀個,幷包含可傳送的資料。 如果該值設定的⽐較⼩,會限制吞吐量(設定為0會完全禁⽤批處理)。如果設定的很⼤,⼜有⼀點浪費記憶體,因為Kafka會永遠分配這麼⼤的記憶體來參與到訊息的批整合中。 |
client.id | ⽣產者傳送請求的時候傳遞給broker的id字串。 ⽤於在broker的請求⽇志中追蹤什麼應⽤傳送了什麼訊息。 ⼀般該id是跟業務有關的字串。 |
compression.type | ⽣產者傳送的所有資料的壓縮⽅式。預設是none,也就是不壓縮。 ⽀持的值:none、gzip、snappy和lz4。 壓縮是對於整個批來講的,所以批處理的效率也會影響到壓縮的⽐例。 |
send.buffer.bytes | TCP傳送資料的時候使⽤的緩衝區(SO_SNDBUF)⼤⼩。如果設定為0,則使⽤作業系統預設的。 |
buffer.memory | ⽣產者可以⽤來快取等待傳送到伺服器的記錄的總記憶體位元組。如果記錄的傳送速度超過了將記錄傳送到伺服器的速度,則⽣產者將阻塞max.block.ms 的時間,此後它將引發異常。此設定應⼤致對應於⽣產者將使⽤的總記憶體,但並⾮⽣產者使⽤的所有記憶體都⽤於緩衝。⼀些額外的記憶體將⽤於壓縮(如果啟⽤了壓縮)以及維護運⾏中的請求。long型資料。預設值:33554432,可選值:[0,...] |
connections.max.idle.ms | 當連線空閒時間達到這個值,就關閉連線。long型資料,預設:540000 |
linger.ms | ⽣產者在傳送請求傳輸間隔會對需要傳送的訊息進⾏累積,然後作為⼀個批次傳送。⼀般情況是訊息的傳送的速度⽐訊息累積的速度慢。有時客戶端需要減少請求的次數,即使是在傳送負載不⼤的情況下。該配置設定了⼀個延遲,⽣產者不會⽴即將訊息傳送到broker,⽽是等待這麼⼀段時間以累積訊息,然後將這段時間之內的訊息作為⼀個批次傳送。該設定是批處理的另⼀個上限:⼀旦批訊息達到了batch.size 指定的值,訊息批會⽴即傳送,如果積累的訊息位元組數達不到batch.size 的值,可以設定該毫秒值,等待這麼⻓時間之後,也會傳送訊息批。該屬性預設值是0(沒有延遲)。如果設定linger.ms=5 ,則在⼀個請求傳送之前先等待5ms。long型值,預設:0,可選值:[0,...] |
max.block.ms | 控制KafkaProducer.send() 和KafkaProducer.partitionsFor() 阻塞的時⻓。當快取滿了或後設資料不可⽤的時候,這些⽅法阻塞。在⽤戶提供的序列化器和分割槽器的阻塞時間不計⼊。long型值,預設:60000,可選值:[0,...] |
max.request.size | 單個請求的最⼤位元組數。該設定會限制單個請求中訊息批的訊息個數,以免單個請求傳送太多的資料。伺服器有⾃⼰的限制批⼤⼩的設定,與該配置可能不⼀樣。int型別值,預設1048576,可選值:[0,...] |
partitioner.class | 實現了接⼝org.apache.kafka.clients.producer.Partitioner 的分割槽器實現類。預設值為:org.apache.kafka.clients.producer.internals.DefaultPartitioner |
receive.buffer.bytes | TCP接收快取(SO_RCVBUF),如果設定為-1,則使⽤作業系統預設的值。int型別值,預設32768,可選值:[-1,...] |
security.protocol | 跟broker通訊的協議:PLAINTEXT, SSL, SASL_PLAINTEXT, SASL_SSL. string型別值,預設:PLAINTEXT |
max.in.flight.requests.per.connection | 單個連線上未確認請求的最⼤數量。達到這個數量,客戶端阻塞。如果該值⼤於1,且存在失敗的請求,在重試的時候訊息順序不能保證。 int型別值,預設5。可選值:[1,...] |
reconnect.backoff.max.ms | 對於每個連續的連線失敗,每臺主機的退避將成倍增加,直⾄達到此最⼤值。在計算退避增量之後,新增20%的隨機抖動以避免連線⻛暴。 long型值,預設1000,可選值:[0,...] |
reconnect.backoff.ms | 嘗試重連指定主機的基礎等待時間。避免了到該主機的密集重連。該退避時間應⽤於該客戶端到broker的所有連線。 long型值,預設50。可選值:[0,...] |