Kafka 生產者解析

下半夜的風發表於2022-05-18

一、訊息傳送

1.1 資料生產流程

資料生產流程圖解:

  1. Producer建立時,會建立⼀個Sender執行緒並設定為守護執行緒
  2. ⽣產訊息時,內部其實是非同步流程;⽣產的訊息先經過攔截器->序列化器->分割槽器,然後將訊息快取在緩衝區(該緩衝區也是在Producer建立時建立)
  3. 批次傳送的條件為:緩衝區資料⼤⼩達到 batch.size 或者 linger.ms 達到上限,哪個先達到就算哪個
  4. 批次傳送後,發往指定分割槽,然後落盤到 broker;如果⽣產者配置了retrires引數⼤於0並且失敗原因允許重試,那麼客戶端內部會對該訊息進⾏重試
  5. 落盤到broker成功,返回⽣產後設資料給⽣產者
  6. 後設資料返回有兩種⽅式:⼀種是通過阻塞直接返回,另⼀種是通過回撥返回

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(沒有壓縮)。允許的值:nonegzipsnappylz4。壓縮是對整個訊息批次來講的。訊息批的效率也影響壓縮的⽐例。訊息批越⼤,壓縮效率越好。字串型別的值。預設是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 自定義攔截器

自定義攔截器步驟:

  1. 實現ProducerInterceptor接⼝
  2. 在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)分割槽計算:

  1. 如果record提供了分割槽號,則使⽤record提供的分割槽號
  2. 如果record沒有提供分割槽號,則使⽤key的序列化後的值的hash值對分割槽數量取模
  3. 如果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 自定義分割槽器

如果要⾃定義分割槽器,則需要

  1. ⾸先開發Partitioner接⼝的實現類
  2. 在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,...]

相關文章