Kafka的生產者(二)

 

Kafka的歷史變遷

在 Kafka 的歷史變遷中,一共有兩個大版本的生產者客戶端:第一個是於 Kafka 開源之初使用 Scala 語言編寫的客戶端,;第二個是從 Kafka 0.9.x 版本開始推出的使用 Java 語言編寫的客戶端,它彌補了舊版客戶端中存在的諸多設計缺陷。雖然 Kafka 是用 Java/Scala 語言編寫的,但這並不妨礙它對於多語言的支持,在 Kafka 官網中,“CLIENTS”的入口提供了一份多語言的支持列表,其中包括常用的 C/C++、Python、Go 等語言

 

Kafka 的系統架構

包括生產者發送消息到kafka,消費者從kafka中拉取消息,kakfa的broker的信息註冊到zookeeper

 

kafka生產者客戶端開發

在 Kafka 中,我們把產生消息的那一方稱爲生產者,生產者產生的消息是發送到 Kafka 應用程序發送過程如圖:

2-1

整個生產者客戶端由兩個線程協調運行,這兩個線程分別爲主線程和 Sender 線程(發送線程)。在主線程中由 KafkaProducer 創建消息,然後通過可能的攔截器、序列化器和分區器的作用之後緩存到消息累加器(RecordAccumulator,也稱爲消息收集器)中。Sender 線程負責從 RecordAccumulator 中獲取消息並將其發送到 Kafka 中。

主要流程包括:

1.創建一個ProducerRecord 消息對象開始,ProducerRecord 是 Kafka 中的一個核心類,它代表了一組 Kafka 需要發送的 key/value 鍵值對,它由記錄要發送到的主題名稱(Topic Name),可選的分區號(Partition Number)以及可選的鍵值對構成。

2.在消息發送前做一些準備工作,比如按照某個規則過濾不符合要求的消息、修改消息的內容等,也可以用來在發送回調邏輯前做一些定製化的需求,比如統計類工作。這個時候就需要經過生產者攔截器來進行操作

3.接下來發送 ProducerRecord 時,我們需要將鍵值對對象由序列化器轉換爲字節數組(序列化器完成),這樣它們才能夠在網絡上傳輸。然後消息到達了分區器。

3.發送過程中指定了有效的分區號,那麼在發送記錄時將使用該分區。如果發送過程中未指定分區,則將使用key 的 hash 函數映射指定一個分區。如果發送的過程中既沒有分區號也沒有key,則將以循環的方式分配一個分區。確定好分區後,生產者就知道向哪個主題和分區發送數據了。

此外,ProducerRecord 還有關聯的時間戳,如果用戶沒有提供時間戳,那麼生產者將會在記錄中使用當前的時間作爲時間戳。Kafka 最終使用的時間戳取決於 topic 主題配置的時間戳類型。

  • 如果將主題配置爲使用 CreateTime,則生產者記錄中的時間戳將由 broker 使用。

  • 如果將主題配置爲使用LogAppendTime,則生產者記錄中的時間戳在將消息添加到其日誌中時,將由 broker 重寫。

4.接下來這條消息被存放在一個記錄批次裏(消息累加器),這個批次裏的所有消息會被髮送到相同的主題和分區上(一個分區對應一個消息累加器)。然後由一個獨立的發送線程負責把它們發到 Kafka Broker 上。

5.Kafka Broker 在收到消息時會返回一個響應,如果寫入成功,會返回一個 RecordMetaData 對象,它包含了主題和分區信息,以及記錄在分區裏的偏移量,上面兩種的時間戳類型也會返回給用戶。如果寫入失敗,會返回一個錯誤。生產者在收到錯誤之後會嘗試重新發送消息,幾次之後如果還是失敗的話,就返回錯誤消息。

 

kafka生產者的使用

pom.xml中kafka的依賴

<dependency>
    <groupId>org.apache.kafka</groupId>
    <artifactId>kafka-clients</artifactId>
    <version>2.0.0</version>
</dependency>

生產者hello world代碼

public class KafkaProducer {
    public static final String brokerList = "localhost:9092";
    public static final String topic = "topic-demo";

    public static Properties initConfig(){
        Properties props = new Properties();
        props.put("bootstrap.servers", brokerList);
        props.put("key.serializer",
                "org.apache.kafka.common.serialization.StringSerializer");
        props.put("value.serializer",
                "org.apache.kafka.common.serialization.StringSerializer");
        props.put("client.id", "producer.client.id.demo");
        return props;
    }

    public static void main(String[] args) {
        Properties props = initConfig();//初始化配置參數
        KafkaProducer<String, String> producer = new KafkaProducer<>(props);//創建生產者
        ProducerRecord<String, String> record =
                new ProducerRecord<>(topic, "Hello, Kafka!");//創建消息
        try {
            producer.send(record);//發送
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

 

創建生產者對象

首先需要創建一個KafkaProducer 生產者對象,它是線程安全的,可以在多個線程中共享單個 KafkaProducer 實例,也可以將 KafkaProducer 實例進行池化來供其他線程調用。KafkaProducer 中有多個構造方法,示例如下:

  public KafkaProducer(Map<String, Object> configs) {
        this(new ProducerConfig(configs), (Serializer)null, (Serializer)null, (Metadata)null, (KafkaClient)null);
    }

    public KafkaProducer(Map<String, Object> configs, Serializer<K> keySerializer, Serializer<V> valueSerializer) {
        this(new ProducerConfig(ProducerConfig.addSerializerToConfig(configs, keySerializer, valueSerializer)), keySerializer, valueSerializer, (Metadata)null, (KafkaClient)null);
    }

    public KafkaProducer(Properties properties) {
        this(new ProducerConfig(properties), (Serializer)null, (Serializer)null, (Metadata)null, (KafkaClient)null);
    }

    public KafkaProducer(Properties properties, Serializer<K> keySerializer, Serializer<V> valueSerializer) {
        this(new ProducerConfig(ProducerConfig.addSerializerToConfig(properties, keySerializer, valueSerializer)), keySerializer, valueSerializer, (Metadata)null, (KafkaClient)null);
    }

構造方法主要就是爲了初始化kafka生產者並配置相應的參數,比如需要連接的 Kafka 集羣地址等,在 Kafka 生產者客戶端 KafkaProducer 中有3個參數是必填的。

  • bootstrap.servers:用來指定生產者客戶端連接 Kafka 集羣所需的 broker 地址清單,具體的內容格式爲 host1:port1,host2:port2,可以設置一個或多個地址,中間以逗號隔開,此參數的默認值爲“”。注意這裏並非需要所有的 broker 地址,因爲生產者會從給定的 broker 裏查找到其他 broker 的信息。不過建議至少要設置兩個以上的 broker 地址信息,當其中任意一個宕機時,生產者仍然可以連接到 Kafka 集羣上。
  • key.serializer 和 value.serializer:broker 端接收的消息必須是字節數組(byte[])。key.serializer 和 value.serializer 這兩個參數分別用來指定 key 和 value 序列化操作的序列化器。(注意必須填寫序列化器的全限定名,兩個參數無默認值),上面的生產者使用的 KafkaProducer<String, String>和 ProducerRecord<String, String> 中的泛型 <String, String> 對應的是消息中 key 和 value 的類型,不過在發往 broker 之前會將消息中對應的 key 和 value 做相應的序列化操作來轉換成字節數組byte[]。

key.serializer 必須被設置爲一個實現了org.apache.kafka.common.serialization.Serializer 接口的類,生產者會使用這個類把鍵對象序列化爲字節數組,value.serializer 指定的類會將值序列化。

Serializer 類

Serializer 是一個接口,它表示類將會採用何種方式序列化,它的作用是把對象轉換爲字節,實現了 Serializer 接口的類主要有 ByteArraySerializer、StringSerializer、IntegerSerializer ,其中 ByteArraySerialize 是 Kafka 默認使用的序列化器,其他的序列化器還有很多,你可以通過 這裏 查看其他序列化器。要注意的一點:key.serializer 是必須要設置的,即使你打算只發送值的內容。

可選參數的設置

在上面的 initConfig() 方法裏還設置了一個參數 client.id,這個參數用來設定 KafkaProducer 對應的客戶端id,默認值爲“”。如果客戶端不設置,則 KafkaProducer 會自動生成一個非空字符串,內容形式如“producer-1”、“producer-2”,即字符串“producer-”與數字的拼接。

此外,我們可以通過直接使用客戶端中的 org.apache.kafka.clients.producer.ProducerConfig 類來看到其他的參數,每個參數在 ProducerConfig 類中都有對應的名稱,直接通過類名.去調用,更不容易出錯。

public class ProducerConfig extends AbstractConfig {
    private static final ConfigDef CONFIG;
    public static final String BOOTSTRAP_SERVERS_CONFIG = "bootstrap.servers";
    public static final String METADATA_MAX_AGE_CONFIG = "metadata.max.age.ms";
    private static final String METADATA_MAX_AGE_DOC = "The period of time in milliseconds after which we force a refresh of metadata even if we haven't seen any partition leadership changes to proactively discover any new brokers or partitions.";
    public static final String BATCH_SIZE_CONFIG = "batch.size";
    private static final String BATCH_SIZE_DOC = "The producer will attempt to batch records together into fewer requests whenever multiple records are being sent to the same partition. This helps performance on both the client and the server. This configuration controls the default batch size in bytes. <p>No attempt will be made to batch records larger than this size. <p>Requests sent to brokers will contain multiple batches, one for each partition with data available to be sent. <p>A small batch size will make batching less common and may reduce throughput (a batch size of zero will disable batching entirely). A very large batch size may use memory a bit more wastefully as we will always allocate a buffer of the specified batch size in anticipation of additional records.";
    public static final String ACKS_CONFIG = "acks";
    private static final String ACKS_DOC = "The number of acknowledgments the producer requires the leader to have received before considering a request complete. This controls the  durability of records that are sent. The following settings are allowed:  <ul> <li><code>acks=0</code> If set to zero then the producer will not wait for any acknowledgment from the server at all. The record will be immediately added to the socket buffer and considered sent. No guarantee can be made that the server has received the record in this case, and the <code>retries</code> configuration will not take effect (as the client won't generally know of any failures). The offset given back for each record will always be set to -1. <li><code>acks=1</code> This will mean the leader will write the record to its local log but will respond without awaiting full acknowledgement from all followers. In this case should the leader fail immediately after acknowledging the record but before the followers have replicated it then the record will be lost. <li><code>acks=all</code> This means the leader will wait for the full set of in-sync replicas to acknowledge the record. This guarantees that the record will not be lost as long as at least one in-sync replica remains alive. This is the strongest available guarantee. This is equivalent to the acks=-1 setting.";
    public static final String LINGER_MS_CONFIG = "linger.ms";
    private static final String LINGER_MS_DOC = "The producer groups together any records that arrive in between request transmissions into a single batched request. Normally this occurs only under load when records arrive faster than they can be sent out. However in some circumstances the client may want to reduce the number of requests even under moderate load. This setting accomplishes this by adding a small amount of artificial delay&mdash;that is, rather than immediately sending out a record the producer will wait for up to the given delay to allow other records to be sent so that the sends can be batched together. This can be thought of as analogous to Nagle's algorithm in TCP. This setting gives the upper bound on the delay for batching: once we get <code>batch.size</code> worth of records for a partition it will be sent immediately regardless of this setting, however if we have fewer than this many bytes accumulated for this partition we will 'linger' for the specified time waiting for more records to show up. This setting defaults to 0 (i.e. no delay). Setting <code>linger.ms=5</code>, for example, would have the effect of reducing the number of requests sent but would add up to 5ms of latency to records sent in the absence of load.";
    public static final String CLIENT_ID_CONFIG = "client.id";
    public static final String SEND_BUFFER_CONFIG = "send.buffer.bytes";
    public static final String RECEIVE_BUFFER_CONFIG = "receive.buffer.bytes";
    public static final String MAX_REQUEST_SIZE_CONFIG = "max.request.size";
    private static final String MAX_REQUEST_SIZE_DOC = "The maximum size of a request in bytes. This setting will limit the number of record batches the producer will send in a single request to avoid sending huge requests. This is also effectively a cap on the maximum record batch size. Note that the server has its own cap on record batch size which may be different from this.";
    public static final String RECONNECT_BACKOFF_MS_CONFIG = "reconnect.backoff.ms";
    public static final String RECONNECT_BACKOFF_MAX_MS_CONFIG = "reconnect.backoff.max.ms";
    public static final String MAX_BLOCK_MS_CONFIG = "max.block.ms";
    private static final String MAX_BLOCK_MS_DOC = "The configuration controls how long <code>KafkaProducer.send()</code> and <code>KafkaProducer.partitionsFor()</code> will block.These methods can be blocked either because the buffer is full or metadata unavailable.Blocking in the user-supplied serializers or partitioner will not be counted against this timeout.";
    public static final String BUFFER_MEMORY_CONFIG = "buffer.memory";
    private static final String BUFFER_MEMORY_DOC = "The total bytes of memory the producer can use to buffer records waiting to be sent to the server. If records are sent faster than they can be delivered to the server the producer will block for <code>max.block.ms</code> after which it will throw an exception.<p>This setting should correspond roughly to the total memory the producer will use, but is not a hard bound since not all memory the producer uses is used for buffering. Some additional memory will be used for compression (if compression is enabled) as well as for maintaining in-flight requests.";
    public static final String RETRY_BACKOFF_MS_CONFIG = "retry.backoff.ms";
    public static final String COMPRESSION_TYPE_CONFIG = "compression.type";
    private static final String COMPRESSION_TYPE_DOC = "The compression type for all data generated by the producer. The default is none (i.e. no compression). Valid  values are <code>none</code>, <code>gzip</code>, <code>snappy</code>, or <code>lz4</code>. Compression is of full batches of data, so the efficacy of batching will also impact the compression ratio (more batching means better compression).";
    public static final String METRICS_SAMPLE_WINDOW_MS_CONFIG = "metrics.sample.window.ms";
    public static final String METRICS_NUM_SAMPLES_CONFIG = "metrics.num.samples";
    public static final String METRICS_RECORDING_LEVEL_CONFIG = "metrics.recording.level";
    public static final String METRIC_REPORTER_CLASSES_CONFIG = "metric.reporters";
    public static final String MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION = "max.in.flight.requests.per.connection";
    private static final String MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION_DOC = "The maximum number of unacknowledged requests the client will send on a single connection before blocking. Note that if this setting is set to be greater than 1 and there are failed sends, there is a risk of message re-ordering due to retries (i.e., if retries are enabled).";
    public static final String RETRIES_CONFIG = "retries";
    private static final String RETRIES_DOC = "Setting a value greater than zero will cause the client to resend any record whose send fails with a potentially transient error. Note that this retry is no different than if the client resent the record upon receiving the error. Allowing retries without setting <code>max.in.flight.requests.per.connection</code> to 1 will potentially change the ordering of records because if two batches are sent to a single partition, and the first fails and is retried but the second succeeds, then the records in the second batch may appear first.";
    public static final String KEY_SERIALIZER_CLASS_CONFIG = "key.serializer";
    public static final String KEY_SERIALIZER_CLASS_DOC = "Serializer class for key that implements the <code>org.apache.kafka.common.serialization.Serializer</code> interface.";
    public static final String VALUE_SERIALIZER_CLASS_CONFIG = "value.serializer";
    public static final String VALUE_SERIALIZER_CLASS_DOC = "Serializer class for value that implements the <code>org.apache.kafka.common.serialization.Serializer</code> interface.";
    public static final String CONNECTIONS_MAX_IDLE_MS_CONFIG = "connections.max.idle.ms";
    public static final String PARTITIONER_CLASS_CONFIG = "partitioner.class";
    private static final String PARTITIONER_CLASS_DOC = "Partitioner class that implements the <code>org.apache.kafka.clients.producer.Partitioner</code> interface.";
    public static final String REQUEST_TIMEOUT_MS_CONFIG = "request.timeout.ms";
    private static final String REQUEST_TIMEOUT_MS_DOC = "The configuration controls the maximum amount of time the client will wait for the response of a request. If the response is not received before the timeout elapses the client will resend the request if necessary or fail the request if retries are exhausted. This should be larger than replica.lag.time.max.ms (a broker configuration) to reduce the possibility of message duplication due to unnecessary producer retries.";
    public static final String INTERCEPTOR_CLASSES_CONFIG = "interceptor.classes";
    public static final String INTERCEPTOR_CLASSES_DOC = "A list of classes to use as interceptors. Implementing the <code>org.apache.kafka.clients.producer.ProducerInterceptor</code> interface allows you to intercept (and possibly mutate) the records received by the producer before they are published to the Kafka cluster. By default, there are no interceptors.";
    public static final String ENABLE_IDEMPOTENCE_CONFIG = "enable.idempotence";
    public static final String ENABLE_IDEMPOTENCE_DOC = "When set to 'true', the producer will ensure that exactly one copy of each message is written in the stream. If 'false', producer retries due to broker failures, etc., may write duplicates of the retried message in the stream. Note that enabling idempotence requires <code>max.in.flight.requests.per.connection</code> to be less than or equal to 5, <code>retries</code> to be greater than 0 and acks must be 'all'. If these values are not explicitly set by the user, suitable values will be chosen. If incompatible values are set, a ConfigException will be thrown.";
    public static final String TRANSACTION_TIMEOUT_CONFIG = "transaction.timeout.ms";
    public static final String TRANSACTION_TIMEOUT_DOC = "The maximum amount of time in ms that the transaction coordinator will wait for a transaction status update from the producer before proactively aborting the ongoing transaction.If this value is larger than the transaction.max.timeout.ms setting in the broker, the request will fail with a `InvalidTransactionTimeout` error.";
    public static final String TRANSACTIONAL_ID_CONFIG = "transactional.id";
    public static final String TRANSACTIONAL_ID_DOC = "The TransactionalId to use for transactional delivery. This enables reliability semantics which span multiple producer sessions since it allows the client to guarantee that transactions using the same TransactionalId have been completed prior to starting any new transactions. If no TransactionalId is provided, then the producer is limited to idempotent delivery. Note that enable.idempotence must be enabled if a TransactionalId is configured. The default is <code>null</code>, which means transactions cannot be used. Note that transactions requires a cluster of at least three brokers by default what is the recommended setting for production; for development you can change this, by adjusting broker setting `transaction.state.log.replication.factor`.";

 

創建消息

消息對象: ProducerRecord,它包含了多個屬性,與業務相關的消息體只是其中的一個 value 屬性。ProducerRecord 類的定義如下(只截取成員變量):

public class ProducerRecord<K, V> {
    private final String topic; //主題
    private final Integer partition; //分區號
    private final Headers headers; //消息頭部
    private final K key; //鍵
    private final V value; //值
    private final Long timestamp; //消息的時間戳
    //省略其他成員方法和構造方法
}

其中 :

  • topic 字段代表消息要發往的主題(必填)
  • partition 字段代表消息要發往的分區號
  • headers 字段是消息的頭部,Kafka 0.11.x 版本引入,用來設定一些與應用相關的信息,如無需要也可以不用設置。
  • key 是用來指定消息的鍵,不僅是消息的附加信息,還可以用來計算分區號進而可以讓消息發往特定的分區。前面提及消息以主題爲單位進行歸類,而這個 key 可以讓消息再進行二次歸類,同一個 key 的消息會被劃分到同一個分區中,此外有 key 的消息還可以支持日誌壓縮的功能。
  • value 是指消息體,一般不爲空,如果爲空則表示特定的消息—墓碑消息(必填)
  • timestamp 是指消息的時間戳,它有 CreateTime 和 LogAppendTime 兩種類型,前者表示消息創建的時間,後者表示消息追加到日誌文件的時間。

構造方法

 在ProducerRecord 的屬性結構中 topic 屬性和 value 屬性是必填項,其餘屬性是選填項,對應的 ProducerRecord 的構造方法如下:

public ProducerRecord(String topic, Integer partition, Long timestamp, 
                      K key, V value, Iterable<Header> headers)
public ProducerRecord(String topic, Integer partition, Long timestamp,
                      K key, V value)
public ProducerRecord(String topic, Integer partition, K key, V value, 
                      Iterable<Header> headers)
public ProducerRecord(String topic, Integer partition, K key, V value)
public ProducerRecord(String topic, K key, V value)
public ProducerRecord(String topic, V value)

上面的代碼中發送消息使用的是最後一種構造方法,也是最簡單的一種,這種方式相當於將 ProducerRecord 中除 topic 和 value 外的屬性全部值設置爲 null。在實際的應用中,還會用到其他構造方法,針對不同的消息,需要構建不同的 ProducerRecord 對象。

 

發送消息

消息發送主要有三種模式:發後即忘(fire-and-forget)、同步(sync)及異步(async)

發送消息send()方法的API

public Future<RecordMetadata> send(ProducerRecord<K, V> record)
public Future<RecordMetadata> send(ProducerRecord<K, V> record, 
                                   Callback callback)

發後即忘:只管往 Kafka 中發送消息而並不關心消息是否正確到達。在大多數情況下,這種發送方式沒有什麼問題,不過在某些時候(比如發生不可重試異常時)會造成消息的丟失。這種發送方式的性能最高,可靠性也最差。上面的hello world就是採用這種模式

同步模式:KafkaProducer 的 send()方法並非是 void 類型,而是 Future類型,send()方法有2個重載方法,具體定義如下:

要實現同步的發送方式,可以利用返回的 Future 對象的阻塞等待 Kafka 的響應即可實現,直到消息發送成功,或者發生異常。如果發生異常,那麼就需要捕獲異常並交由外層邏輯處理。如下:

try {
    producer.send(record).get();
} catch (ExecutionException | InterruptedException e) {
    e.printStackTrace();
}

send() 方法返回的 Future 對象可以獲得發送的結果,返回一個 RecordMetadata 對象,在 RecordMetadata 對象裏包含了消息的一些元數據信息,比如當前消息的主題、分區號、分區中的偏移量(offset)、時間戳等。

try {
    Future<RecordMetadata> future = producer.send(record);
    RecordMetadata metadata = future.get();
    System.out.println(metadata.topic() + "-" +
            metadata.partition() + ":" + metadata.offset());
} catch (ExecutionException | InterruptedException e) {
    e.printStackTrace();
}

同步發送消息都有個問題,那就是同一時間只能有一個消息在發送,這會造成許多消息無法直接發送,造成消息滯後,無法發揮效益最大化。

比如消息在應用程序和 Kafka 集羣之間一個來回需要 10ms。如果發送完每個消息後都等待響應的話,那麼發送100個消息需要 1 秒,但是如果是異步方式的話,發送 100 條消息所需要的時間就會少很多很多。大多數時候,雖然Kafka 會返回 RecordMetadata 消息,但是我們並不需要等待響應。

異步模式:爲了在異步發送消息的同時能夠對異常情況進行處理,生產者提供了回調支持。一般是在 send() 方法裏指定一個 Callback 的回調函數,Kafka 在返回響應時調用該函數來實現異步的發送確認。

Future 本身就可以用作異步的邏輯處理。只是 Future 裏的 get() 方法在何時調用,以及怎麼調用都是需要面對的問題,使用 Callback 的方式非常簡潔明瞭,Kafka 有響應時就會回調,要麼發送成功,要麼拋出異常。異步發送方式的示例如下:

producer.send(record, new Callback() {
    @Override
    public void onCompletion(RecordMetadata metadata, Exception exception) {
        if (exception != null) {
            exception.printStackTrace();
        } else {
            System.out.println(metadata.topic() + "-" +
                    metadata.partition() + ":" + metadata.offset());
        }
    }
});

首先實現回調需要定義一個實現了org.apache.kafka.clients.producer.Callback的類,這個接口只有一個 onCompletion方法。如果 kafka 返回一個錯誤,onCompletion 方法會拋出一個非空(non null)異常,在實際應用中應該使用更加穩妥的方式來處理,比如可以將異常記錄以便日後分析,也可以做一定的處理來進行消息重發。onCompletion() 方法的兩個參數是互斥的,消息發送成功時,metadata 不爲 null 而 exception 爲 null;消息發送異常時,metadata 爲 null 而 exception 不爲 null。

對於同一個分區而言,如果消息 record1 於 record2 之前先發送,那麼 KafkaProducer 就可以保證對應的 callback1 在 callback2 之前調用,也就是說,回調函數的調用也可以保證分區有序

producer.send(record1, callback1);
producer.send(record2, callback2);

關閉資源:通常,一個 KafkaProducer 是發送多條消息,在發送完這些消息之後,需要調用 KafkaProducer 的 close() 方法來回收資源。下面的示例中發送了100條消息,之後就調用了 close() 方法來回收所佔用的資源

close() 方法會阻塞等待之前所有的發送請求完成後再關閉 KafkaProducer。與此同時,KafkaProducer 還提供了一個帶超時時間的 close() 方法,具體定義如下:

public void close(long timeout, TimeUnit timeUnit)

如果調用了帶超時時間 timeout 的 close() 方法,那麼只會在等待 timeout 時間內來完成所有尚未完成的請求處理,然後強行退出。在實際應用中,一般使用的都是無參的 close() 方法

 

消息發送異常處理

KafkaProducer 中一般會發生兩種類型的異常:可重試的異常和不可重試的異常

常見的可重試異常有:NetworkException、LeaderNotAvailableException、UnknownTopicOrPartitionException、NotEnoughReplicasException、NotCoordinatorException 等。比如 NetworkException 表示網絡異常,這個有可能是由於網絡瞬時故障而導致的異常,可以通過重試解決;又比如 LeaderNotAvailableException 表示分區的 leader 副本不可用,這個異常通常發生在 leader 副本下線而新的 leader 副本選舉完成之前,重試之後可以重新恢復。

不可重試的異常,如前面提及的 RecordTooLargeException 異常,暗示了所發送的消息太大,KafkaProducer 對此不會進行任何重試,直接拋出異常。

對於可重試的異常,如果配置了 retries 參數,那麼只要在規定的重試次數內自行恢復了,就不會拋出異常。如果重試了10次之後還沒有恢復,那麼仍會拋出異常,進而發送的外層邏輯就要處理這些異常了。retries 參數的默認值爲0,配置方式參考如下:

props.put(ProducerConfig.RETRIES_CONFIG, 10);

 

消息發送後的流轉

消息通過 send() 方法發往 broker 的過程中,有可能需要經過攔截器(Interceptor)、序列化器(Serializer)和分區器(Partitioner)的一系列作用之後之後才能被真正地發往 broker。攔截器一般不是必需的,而序列化器是必需的。消息經過序列化之後就需要確定它發往的分區,如果消息 ProducerRecord 中指定了 partition 字段,那麼就不需要分區器的作用,因爲 partition 代表的就是所要發往的分區號。

 

生產者攔截器

攔截器(Interceptor)是早在 Kafka 0.10.0.0 中就已經引入的一個功能,Kafka 一共有兩種攔截器:生產者攔截器和消費者攔截器。下面主要講述生產者攔截器的相關內容

生產者攔截器既可以用來在消息發送前做一些準備工作,比如按照某個規則過濾不符合要求的消息、修改消息的內容等,也可以用來在發送回調邏輯前做一些定製化的需求,比如統計類工作。

生產者攔截器的使用也很方便,主要是自定義實現 org.apache.kafka.clients.producer. ProducerInterceptor 接口。ProducerInterceptor 接口中包含3個方法:

public ProducerRecord<K, V> onSend(ProducerRecord<K, V> record);
public void onAcknowledgement(RecordMetadata metadata, Exception exception);
public void close();

消息在通過 send() 方法發往 broker 的過程中,首先會調用生產者攔截器的 onSend() 方法來對消息進行相應的定製化操作。一般來說最好不要修改消息 ProducerRecord 的 topic、key 和 partition 等信息,(比如修改 key 不僅會影響分區的計算,同樣會影響 broker 端日誌壓縮(Log Compaction)的功能)。

KafkaProducer 會在消息被應答(Acknowledgement)之前或消息發送失敗時調用生產者攔截器的 onAcknowledgement() 方法,優先於用戶設定的 Callback 之前執行。這個方法運行在 Producer 的I/O線程中,所以這個方法中實現的代碼邏輯越簡單越好,否則會影響消息的發送速度。

close() 方法主要用於在關閉攔截器時執行一些資源的清理工作。在這3個方法中拋出的異常都會被捕獲並記錄到日誌中,但並不會再向上傳遞。

ProducerInterceptor 接口還有一個同樣的父接口 Configurable(後面分析)

自定義生產者攔截器

自定義一個ProducerInterceptorPrefix ,通過 onSend() 方法來爲每條消息添加一個前綴“prefix1-”,並且通過 onAcknowledgement() 方法來計算髮送消息的成功率。如下:

//代碼清單4-5生產者攔截器示例
public class ProducerInterceptorPrefix implements 
        ProducerInterceptor<String,String>{
    private volatile long sendSuccess = 0;
    private volatile long sendFailure = 0;

    @Override
    public ProducerRecord<String, String> onSend(
            ProducerRecord<String, String> record) {
        String modifiedValue = "prefix1-" + record.value();
        return new ProducerRecord<>(record.topic(), 
                record.partition(), record.timestamp(),
                record.key(), modifiedValue, record.headers());
    }

    @Override
    public void onAcknowledgement(
            RecordMetadata recordMetadata, 
            Exception e) {
        if (e == null) {
            sendSuccess++;
        } else {
            sendFailure ++;
        }
    }

    @Override
    public void close() {
        double successRatio = (double)sendSuccess / (sendFailure + sendSuccess);
        System.out.println("[INFO] 發送成功率="
                + String.format("%f", successRatio * 100) + "%");
    }

    @Override
    public void configure(Map<String, ?> map) {}
}

實現自定義的 ProducerInterceptorPrefix 之後,需要在 KafkaProducer 的配置參數 interceptor.classes 中指定這個攔截器,此參數的默認值爲“”。示例如下:

properties.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG,
        ProducerInterceptorPrefix.class.getName());

然後使用指定了 ProducerInterceptorPrefix 的生產者連續發送10條內容爲“kafka”的消息,在發送完之後客戶端打印出如下信息:

[INFO] 發送成功率=100.000000%

如果消費這10條消息,會發現消費了的消息都變成了“prefix1-kafka”,而不是原來的“kafka”。

KafkaProducer 中不僅可以指定一個攔截器,還可以指定多個攔截器以形成攔截鏈。攔截鏈會按照 interceptor.classes 參數配置的攔截器的順序來一一執行(配置的時候,各個攔截器之間使用逗號隔開)。如果攔截鏈中的某個攔截器的執行需要依賴於前一個攔截器的輸出,那麼就有可能產生“副作用”。設想一下,如果前一個攔截器由於異常而執行失敗,那麼這個攔截器也就跟着無法繼續執行。在攔截鏈中,如果某個攔截器執行失敗,那麼下一個攔截器會接着從上一個執行成功的攔截器繼續執行。

 

消息的序列化

生產者需要用序列化器(Serializer)把對象轉換成字節數組才能通過網絡發送給 Kafka。收到消息後,消費者需要用反序列化器(Deserializer)把從 Kafka 中收到的字節數組轉換成相應的對象。生產者使用的序列化器和消費者使用的反序列化器是需要相互對應的,用什麼序列化就應該用什麼反序列化

序列化接口

如消息的 key 和 value 都使用了字符串,對應的序列化器也使用了自帶org.apache.kafka.common.serialization.StringSerializer除了用於 String 類型的序列化器,還有 ByteArray、ByteBuffer、Bytes、Double、Integer、Long 這幾種類型,它們都實現了現org.apache.kafka.common.serialization.Serializer 接口,此接口有3個方法:

public void configure(Map<String, ?> configs, boolean isKey)
public byte[] serialize(String topic, T data)
public void close()
  • configure() 方法用來配置當前類
  • serialize() 方法用來執行序列化操作
  • close() 方法用來關閉當前的序列化器,一般情況下 close() 是一個空方法,如果實現了此方法,則必須確保此方法的冪等性,因爲這個方法很可能會被 KafkaProducer 調用多次

自定義序列化器

如果 Kafka 客戶端提供的幾種序列化器都無法滿足應用需求,則可以選擇使用如 Avro、JSON、Thrift、ProtoBuf 和 Protostuff 等通用的序列化工具來實現,或者使用自定義類型的序列化器來實現。下面就以一個簡單的例子來介紹自定義類型的使用方法。

假設我們要發送的消息都是 Company 對象,這個 Company 的定義很簡單,只有名稱 name 和地址 address, Company 對應的序列化器 CompanySerializer,示例代碼如下

//代碼清單4-2 自定義的序列化器CompanySerializer
public class CompanySerializer implements Serializer<Company> {
    @Override
    public void configure(Map configs, boolean isKey) {}

    @Override
    public byte[] serialize(String topic, Company data) {
        if (data == null) {
            return null;
        }
        byte[] name, address;
        try {
            if (data.getName() != null) {
                name = data.getName().getBytes("UTF-8");
            } else {
                name = new byte[0];
            }
            if (data.getAddress() != null) {
                address = data.getAddress().getBytes("UTF-8");
            } else {
                address = new byte[0];
            }
            ByteBuffer buffer = ByteBuffer.
                    allocate(4+4+name.length + address.length);
            buffer.putInt(name.length);
            buffer.put(name);
            buffer.putInt(address.length);
            buffer.put(address);
            return buffer.array();
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return new byte[0];
    }

    @Override
    public void close() {}
}

如何使用自定義的序列化器

使用自定義的序列化器 CompanySerializer 只需將 KafkaProducer 的 value.serializer 參數設置爲 CompanySerializer 類的全限定名即可。假如我們要發送一個 Company 對象到 Kafka

//代碼清單4-3 自定義序列化器使用示例
Properties properties = new Properties();
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,
        StringSerializer.class.getName());
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
        CompanySerializer.class.getName());
properties.put("bootstrap.servers", brokerList);

KafkaProducer<String, Company> producer =
        new KafkaProducer<>(properties);
Company company = Company.builder().name("hiddenkafka")
        .address("China").build();
ProducerRecord<String, Company> record =
        new ProducerRecord<>(topic, company);
producer.send(record).get();

注意,示例中消息的 key 對應的序列化器還是 StringSerializer

 

分區器

消息經過序列化之後就需要確定它發往的分區,如果消息 ProducerRecord 中指定了 partition 字段,那麼就不需要分區器的作用,因爲 partition 代表的就是所要發往的分區號。如果消息 ProducerRecord 中沒有指定 partition 字段,那麼就需要依賴分區器分區器是根據 key 這個字段來計算 partition 的值。分區器的作用就是爲消息分配分區

Kafka 中提供的默認分區器是 org.apache.kafka.clients.producer.internals.DefaultPartitioner,它實現了 org.apache.kafka.clients.producer.Partitioner 接口,這個接口中定義了2個方法,具體如下所示。

public int partition(String topic, Object key, byte[] keyBytes, 
                     Object value, byte[] valueBytes, Cluster cluster);
public void close();
  • partition() 方法用來計算分區號,返回值爲 int 類型。partition() 方法中的參數分別表示主題、鍵、序列化後的鍵、值、序列化後的值,以及集羣的元數據信息,通過這些信息可以實現功能豐富的分區器。
  • close() 方法在關閉分區器的時候用來回收一些資源。

Partitioner 接口和生產者攔截器ProducerInterceptor 一樣,也有一個父接口 org.apache.kafka.common.Configurable,這個接口中只有一個方法:Configurable 接口中的 configure() 方法主要用來獲取配置信息及初始化數據。

void configure(Map<String, ?> configs);

分區策略

如果 key 爲 null,那麼消息將默認會以順序輪詢的方式發往主題內的各個可用分區。當然也可以實現隨機輪詢,實現隨機分配的代碼只需要兩行,如下

List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
return ThreadLocalRandom.current().nextInt(partitions.size());

先計算出該主題總的分區數,然後隨機地返回一個小於它的正整數。

本質上看隨機策略也是力求將數據均勻地打散到各個分區,但從實際表現來看,它要遜於輪詢策略,所以如果追求數據的均勻分佈,還是使用輪詢策略比較好。事實上,隨機策略是老版本生產者使用的分區策略,在新版本中已經改爲輪詢了。

按照 key 進行消息保存(同一個key發送到同一個分區)

在默認分區器 DefaultPartitioner 的實現中,close() 是空方法,而在 partition() 方法中定義了主要的分區分配邏輯。如果 key 不爲 null,那麼默認的分區器會對 key 進行哈希(採用 MurmurHash2 算法,具備高運算性能及低碰撞率),最終根據得到的哈希值來計算分區號,所以擁有相同 key 的消息會被寫入同一個分區。這個策略也叫做 key-ordering 策略,Kafka 中每條消息都會有自己的key,一旦消息被定義了 Key,那麼你就可以保證同一個 Key 的所有消息都進入到相同的分區裏面,由於每個分區下的消息處理都是有順序的,故這個策略被稱爲按消息鍵保序策略,如下圖所示

 

注意:如果 key 不爲 null,那麼計算得到的分區號會是所有分區中的任意一個;如果 key 爲 null 並且有可用分區時,那麼計算得到的分區號僅爲可用分區中的任意一個,注意兩者之間的差別。

在不改變主題分區數量的情況下,key 與分區之間的映射可以保持不變。不過,一旦主題中增加了分區,那麼就難以保證 key 與分區之間的映射關係了。

自定義分區器

同 DefaultPartitioner 一樣實現 Partitioner 接口即可。默認的分區器在 key 爲 null 時不會選擇非可用的分區,我們可以通過自定義的分區器 DemoPartitioner 來打破這一限制,如下:

//代碼清單4-4 自定義分區器實現
public class DemoPartitioner implements Partitioner {
    private final AtomicInteger counter = new AtomicInteger(0);

    @Override
    public int partition(String topic, Object key, byte[] keyBytes,
                         Object value, byte[] valueBytes, Cluster cluster) {
        List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
        int numPartitions = partitions.size();
        if (null == keyBytes) {
            return counter.getAndIncrement() % numPartitions;
        }else
            return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
    }

    @Override public void close() {}

    @Override public void configure(Map<String, ?> configs) {}
}

實現自定義的 DemoPartitioner 類之後,同樣需要通過配置參數 partitioner.class 來顯式指定這個分區器。示例如下:

props.put(ProducerConfig.PARTITIONER_CLASS_CONFIG,
        DemoPartitioner.class.getName());

這個自定義分區器的實現比較簡單,也可以根據自身業務的需求來靈活實現分配分區的計算方式,比如一般大型電商都有多個倉庫,可以將倉庫的名稱或 ID 作爲 key 來靈活地記錄商品信息。

生產者壓縮機制

壓縮一詞簡單來講就是一種互換思想,它是一種經典的用 CPU 時間去換磁盤空間或者 I/O 傳輸量的思想,希望以較小的 CPU 開銷帶來更少的磁盤佔用或更少的網絡 I/O 傳輸。

Kafka 壓縮是什麼

Kafka 的消息分爲兩層:消息集合 和 消息。一個消息集合中包含若干條日誌項,而日誌項纔是真正封裝消息的地方。Kafka 底層的消息日誌由一系列消息集合日誌項組成。Kafka 通常不會直接操作具體的一條條消息,它總是在消息集合這個層面上進行寫入操作。

在 Kafka 中,壓縮會發生在兩個地方:Kafka Producer 和 Kafka Consumer,爲什麼啓用壓縮?說白了就是消息太大,需要變小一點 來使消息發的更快一些。

Kafka Producer 中使用 compression.type 來開啓壓縮

private Properties properties = new Properties();
properties.put("bootstrap.servers","192.168.1.9:9092");
properties.put("key.serializer","org.apache.kafka.common.serialization.StringSerializer");
properties.put("value.serializer","org.apache.kafka.common.serialization.StringSerializer");
properties.put("compression.type", "gzip");

Producer<String,String> producer = new KafkaProducer<String, String>(properties);

ProducerRecord<String,String> record =
  new ProducerRecord<String, String>("CustomerCountry","Precision Products","France");

上面代碼表明該 Producer 的壓縮算法使用的是 GZIP

有壓縮必有解壓縮,Producer 使用壓縮算法壓縮消息後併發送給服務器後,由 Consumer 消費者進行解壓縮,因爲採用的何種壓縮算法是隨着 key、value 一起發送過去的,所以消費者知道採用何種壓縮算法。

 

整體架構

主線程和Sender線程

整個生產者客戶端由兩個線程協調運行,這兩個線程分別爲主線程和 Sender 線程(發送線程)。主線程中由 KafkaProducer 創建消息,然後通過可能的攔截器、序列化器和分區器的作用之後緩存到消息累加器(RecordAccumulator,也稱爲消息收集器)中。Sender 線程負責從 RecordAccumulator 中獲取消息並將其發送到 Kafka 中。

消息收集器RecordAccumulator的構造及相關參數

RecordAccumulator 主要用來緩存消息以便 Sender 線程可以批量發送,進而減少網絡傳輸的資源消耗以提升性能。RecordAccumulator 緩存的大小可以通過生產者客戶端參數 buffer.memory 配置,默認值爲 33554432B,即32MB。如果生產者發送消息的速度超過發送到服務器的速度,則會導致生產者空間不足,這個時候 KafkaProducer 的 send() 方法調用要麼被阻塞,要麼拋出異常,這個取決於參數 max.block.ms 的配置,此參數的默認值爲60000,即60秒。(當buffer滿了或者metadata獲取不到(比如leader掛了),或者序列化沒完成分區函數沒計算完等等情況下的最大阻塞時間,默認60000ms (60秒))

主線程中發送過來的消息都會被追加到 RecordAccumulator 的某個雙端隊列(Deque)中,在 RecordAccumulator 的內部爲每個分區都維護了一個雙端隊列,隊列中的內容就是 ProducerBatch,即 Deque。消息寫入緩存時,追加到雙端隊列的尾部;Sender 讀取消息時,從雙端隊列的頭部讀取。使用ProducerBatch 可以使字節的使用更加緊湊。與此同時,將較小的 ProducerRecord 拼湊成一個較大的 ProducerBatch,也可以減少網絡請求的次數以提升整體的吞吐量。如果生產者客戶端需要向很多分區發送消息,則可以將 buffer.memory 參數適當調大以增加整體的吞吐量。(當消息發送速度大於kafka服務器接收的速度,producer會阻塞max_block_ms,超時會報異常,buffer_memory用來保存等待發送的消息,默認33554432(32MB))

ByteBuffer 

消息在網絡上都是以字節Byte形式傳輸的,在發送之前需要創建一塊內存區域來保存對應的消息。在 Kafka 生產者客戶端中,通過 java.io.ByteBuffer 實現消息內存的創建和釋放。不過頻繁的創建和釋放內存是比較耗費資源的,所以在RecordAccumulator 的內部定義了一個 BufferPool,它主要用來實現 ByteBuffer 的複用,以實現緩存的高效利用。不過 BufferPool 只針對特定大小的 ByteBuffer 進行管理,而其他大小的 ByteBuffer 不會緩存進 BufferPool 中,這個特定的大小由 batch.size 參數來指定,默認值爲16384B,即16KB。我們可以適當地調大 batch.size 參數以便多緩存一些消息。

ProducerBatch和batch.size 的關係

ProducerBatch 的大小和 batch.size 參數也有着密切的關係。當一條消息(ProducerRecord)流入 RecordAccumulator 時,會先尋找與消息分區所對應的雙端隊列Deque(如果沒有則新建),再從這個雙端隊列的尾部獲取一個 ProducerBatch(如果沒有則新建),查看 ProducerBatch 中是否還可以寫入這個 ProducerRecord,如果可以則寫入,如果不可以則需要創建一個新的 ProducerBatch。在新建 ProducerBatch 時評估這條消息的大小是否超過 batch.size 參數的大小,如果不超過,那麼就以 batch.size 參數的大小來創建 ProducerBatch,這樣在使用完這段內存區域之後,可以通過 BufferPool 的管理來進行復用;如果超過,那麼就以評估的大小來創建 ProducerBatch,這段內存區域不會被複用。

消息的轉換

Sender 從 RecordAccumulator 中獲取緩存的消息之後,會進一步將原本<分區, Deque< ProducerBatch>> 的保存形式轉變成 <Node, List< ProducerBatch> 的形式,其中 Node 表示 Kafka 集羣的 broker 節點。對於網絡連接來說,生產者客戶端是與具體的 broker 節點建立的連接,也就是向具體的 broker 節點發送消息,而並不關心消息屬於哪一個分區;而對於 KafkaProducer 的應用邏輯而言,我們只關注向哪個分區中發送哪些消息,所以在這裏需要做一個應用邏輯層面到網絡I/O層面的轉換。

在轉換成 <Node, List> 的形式之後,Sender 還會進一步封裝成 <Node, Request> 的形式,這樣就可以將 Request 請求發往各個 Node 了,這裏的 Request 是指 Kafka 的各種協議請求,對於消息發送而言就是指具體的 ProduceRequest。

請求在從 Sender 線程發往 Kafka 之前還會保存到 InFlightRequests 中,InFlightRequests 保存對象的具體形式爲 Map<NodeId, Deque>,它的主要作用是緩存了已經發出去但還沒有收到響應的請求(NodeId 是一個 String 類型,表示節點的 id 編號)。與此同時,InFlightRequests 還提供了許多管理類的方法,並且通過配置參數還可以限制每個連接(也就是客戶端與 Node 之間的連接)最多緩存的請求數。這個配置參數爲 max.in.flight.requests. per. connection,默認值爲5,即每個連接最多隻能緩存5個未響應的請求,超過該數值之後就不能再向這個連接發送更多的請求了,除非有緩存的請求收到了響應(Response)。通過比較 Deque 的 size 與這個參數的大小來判斷對應的 Node 中是否已經堆積了很多未響應的消息,如果真是如此,那麼說明這個 Node 節點負載較大或網絡連接有問題,再繼續向其發送請求會增大請求超時的可能。

 

元數據的更新

上面提及的 InFlightRequests 還可以獲得 leastLoadedNode,即所有 Node 中負載最小的那一個。這裏的負載最小是通過每個 Node 在 InFlightRequests 中還未確認的請求決定的,未確認的請求越多則認爲負載越大。對於下圖中的 InFlightRequests 來說,圖中展示了三個節點 Node0、Node1和Node2,很明顯 Node1 的負載最小。也就是說,Node1 爲當前的 leastLoadedNode。選擇 leastLoadedNode 發送請求可以使它能夠儘快發出,避免因網絡擁塞等異常而影響整體的進度。leastLoadedNode 的概念可以用於多個應用場合,比如元數據請求、消費者組播協議的交互。

元數據

hello world的發送消息的方法中,我們只知道主題的名稱,對於其他一些必要的信息卻一無所知。KafkaProducer 要將此消息追加到指定主題的某個分區所對應的 leader 副本之前,首先需要知道主題的分區數量,然後經過計算得出(或者直接指定)目標分區,之後 KafkaProducer 需要知道目標分區的 leader 副本所在的 broker 節點的地址、端口等信息才能建立連接,最終才能將消息發送到 Kafka,在這一過程中所需要的信息都屬於元數據信息。

元數據是指 Kafka 集羣的元數據,這些元數據具體記錄了集羣中有哪些主題,這些主題有哪些分區,每個分區的 leader 副本分配在哪個節點上,follower 副本分配在哪些節點上,哪些副本在 AR、ISR 等集合中,集羣中有哪些節點,控制器節點又是哪一個等信息。

當客戶端中沒有需要使用的元數據信息時,比如沒有指定的主題信息,或者超過 metadata.max.age.ms 時間沒有更新元數據都會引起元數據的更新操作。客戶端參數 metadata.max.age.ms 的默認值爲300000,即5分鐘。元數據的更新操作是在客戶端內部進行的,對客戶端的外部使用者不可見。當需要更新元數據時,會先挑選出 leastLoadedNode,然後向這個 Node 發送 MetadataRequest 請求來獲取具體的元數據信息。這個更新操作是由 Sender 線程發起的,在創建完 MetadataRequest 之後同樣會存入 InFlightRequests,之後的步驟就和發送消息時的類似。元數據雖然由 Sender 線程負責更新,但是主線程也需要讀取這些信息,這裏的數據同步通過 synchronized 和 final 關鍵字來保障。

 

生產者相關參數詳解

1. acks

這個參數用來指定分區中必須要有多少個副本收到這條消息,之後生產者纔會認爲這條消息是成功寫入的。acks 是生產者客戶端中一個非常重要的參數,它涉及消息的可靠性和吞吐量之間的權衡。acks 參數有3種類型的值(都是字符串類型)。

  • acks = 1。默認值即爲1。生產者發送消息之後,只要分區的 leader 副本成功寫入消息,那麼它就會收到來自服務端的成功響應。如果消息無法寫入 leader 副本,比如在 leader 副本崩潰、重新選舉新的 leader 副本的過程中,那麼生產者就會收到一個錯誤的響應,爲了避免消息丟失,生產者可以選擇重發消息。如果消息寫入 leader 副本並返回成功響應給生產者,且在被其他 follower 副本拉取之前 leader 副本崩潰,那麼此時消息還是會丟失,因爲新選舉的 leader 副本中並沒有這條對應的消息。acks 設置爲1,是消息可靠性和吞吐量之間的折中方案。
  • acks = 0。生產者發送消息之後不需要等待任何服務端的響應。如果在消息從發送到寫入 Kafka 的過程中出現某些異常,導致 Kafka 並沒有收到這條消息,那麼生產者也無從得知,消息也就丟失了。在其他配置環境相同的情況下,acks 設置爲0可以達到最大的吞吐量。
  • acks = -1 或 acks = all。生產者在消息發送之後,需要等待 ISR 中的所有副本都成功寫入消息之後才能夠收到來自服務端的成功響應。在其他配置環境相同的情況下,acks 設置爲 -1(all) 可以達到最強的可靠性。但這並不意味着消息就一定可靠,因爲ISR中可能只有 leader 副本,這樣就退化成了 acks=1 的情況。要獲得更高的消息可靠性需要配合 min.insync.replicas 等參數的聯動,消息可靠性分析的具體內容可以參考《圖解Kafka之核心原理》。

注意 acks 參數配置的值是一個字符串類型,而不是整數類型。舉個例子,將 acks 參數設置爲0,需要採用下面這兩種形式:

properties.put("acks", "0");
# 或者
properties.put(ProducerConfig.ACKS_CONFIG, "0");

2. max.request.size

這個參數用來限制生產者客戶端能發送的消息的最大值,默認值爲1048576B,即1MB。一般情況下,這個默認值就可以滿足大多數的應用場景了。

不建議讀者盲目地增大這個參數的配置值。因爲這個參數還涉及一些其他參數的聯動,比如 broker 端的 message.max.bytes 參數,如果配置錯誤可能會引起一些不必要的異常。比如將 broker 端的 message.max.bytes 參數配置爲10,而 max.request.size 參數配置爲20,那麼當我們發送一條大小爲15B的消息時,生產者客戶端就會報出如下的異常:

org.apache.kafka.common.errors.RecordTooLargeException: The request included a message larger than the max message size the server will accept.

3. retries和retry.backoff.ms

retries 參數用來配置生產者重試的次數,默認值爲0,即在發生異常的時候不進行任何重試動作。消息在從生產者發出到成功寫入服務器之前可能發生一些臨時性的異常,比如網絡抖動、leader 副本的選舉等,這種異常往往是可以自行恢復的,生產者可以通過配置 retries 大於0的值,以此通過內部重試來恢復而不是一味地將異常拋給生產者的應用程序。如果重試達到設定的次數,那麼生產者就會放棄重試並返回異常。不過並不是所有的異常都是可以通過重試來解決的,比如消息太大,超過 max.request.size 參數配置的值時,這種方式就不可行了。

重試還和另一個參數 retry.backoff.ms 有關,這個參數的默認值爲100,它用來設定兩次重試之間的時間間隔,避免無效的頻繁重試。在配置 retries 和 retry.backoff.ms 之前,最好先估算一下可能的異常恢復時間,這樣可以設定總的重試時間大於這個異常恢復時間,以此來避免生產者過早地放棄重試。

Kafka 可以保證同一個分區中的消息是有序的。如果生產者按照一定的順序發送消息,那麼這些消息也會順序地寫入分區,進而消費者也可以按照同樣的順序消費它們。

對於某些應用來說,順序性非常重要,比如 MySQL 的 binlog 傳輸,如果出現錯誤就會造成非常嚴重的後果。如果將acks參數配置爲非零值,並且 max.in.flight.requests.per.connection 參數配置爲大於1的值,那麼就會出現錯序的現象:如果第一批次消息寫入失敗,而第二批次消息寫入成功,那麼生產者會重試發送第一批次的消息,此時如果第一批次的消息寫入成功,那麼這兩個批次的消息就出現了錯序。一般而言,在需要保證消息順序的場合建議把參數 max.in.flight.requests.per.connection 配置爲1,而不是把 acks 配置爲0,不過這樣也會影響整體的吞吐。

4. compression.type

這個參數用來指定消息的壓縮方式,默認值爲“none”,即默認情況下,消息不會被壓縮。該參數還可以配置爲“gzip”“snappy”和“lz4”。對消息進行壓縮可以極大地減少網絡傳輸量、降低網絡I/O,從而提高整體的性能。消息壓縮是一種使用時間換空間的優化方式,如果對時延有一定的要求,則不推薦對消息進行壓縮。下面是各壓縮算法的對比

5. connections.max.idle.ms

這個參數用來指定在多久之後關閉閒置的連接,默認值是540000(ms),即9分鐘。

6. linger.ms

這個參數用來指定生產者發送 ProducerBatch 之前等待更多消息(ProducerRecord)加入 ProducerBatch 的時間,默認值爲0。生產者客戶端會在 ProducerBatch 被填滿或等待時間超過 linger.ms 值時發送出去。增大這個參數的值會增加消息的延遲,但是同時能提升一定的吞吐量。這個 linger.ms 參數與 TCP 協議中的 Nagle 算法有異曲同工之妙。

7. receive.buffer.bytes

這個參數用來設置 Socket 接收消息緩衝區(SO_RECBUF)的大小,默認值爲32768(B),即32KB。如果設置爲-1,則使用操作系統的默認值。如果 Producer 與 Kafka 處於不同的機房,則可以適地調大這個參數值。

8. send.buffer.bytes

這個參數用來設置 Socket 發送消息緩衝區(SO_SNDBUF)的大小,默認值爲131072(B),即128KB。與 receive.buffer.bytes 參數一樣,如果設置爲-1,則使用操作系統的默認值。

9. request.timeout.ms

這個參數用來配置 Producer 等待請求響應的最長時間,默認值爲30000(ms)。請求超時之後可以選擇進行重試。注意這個參數需要比 broker 端參數 replica.lag.time.max.ms 的值要大,這樣可以減少因客戶端重試而引起的消息重複的概率。

其他已經介紹的參數總結:

參 數 名 稱 默 認 值 參 數 釋 義
bootstrap.servers “” 指定連接 Kafka 集羣所需的 broker 地址清單
key.serializer “” 消息中 key 對應的序列化類,需要實現 org.apache.kafka.common.serialization.Serializer 接口
value.serializer “” 消息中 value 對應的序列化類,需要實現 org.apache.kafka.common.serialization.Serializer 接口
buffer.memory 33554432(32MB) 生產者客戶端中用於緩存消息的緩衝區大小
batch.size 16384(16KB) 用於指定 ProducerBatch 可以複用內存區域的大小
client.id “” 用來設定 KafkaProducer 對應的客戶端id
max.block.ms 60000 用來控制 KafkaProducer 中 send() 方法和 partitionsFor() 方法的阻塞時間。當生產者的發送緩衝區已滿,或者沒有可用的元數據時,這些方法就會阻塞
partitioner.class org.apache.kafka.clients.producer.internals.DefaultPartitioner 用來指定分區器,需要實現 org.apache.kafka. clients.producer.Partitioner 接口
enable.idempotence false 是否開啓冪等性功能
interceptor.classes “” 用來設定生產者攔截器,需要實現 org.apache. kafka.clients.producer. ProducerInterceptor 接口。
max.in.flight.requests.per.connection 5 限制每個連接(也就是客戶端與 Node 之間的連接)最多緩存的請求數
metadata.max.age.ms 300000(5分鐘) 如果在這個時間內元數據沒有更新的話會被強制更新
transactional.id null 設置事務id,必須唯一

 

總結:

到目前爲止主要講述了生產者客戶端的具體用法及其整體架構,主要內容包括配置參數的詳解、消息的發送方式、序列化器、分區器、攔截器等。在實際應用中,一套封裝良好的且靈活易用的客戶端可以避免開發人員重複勞動,也提高了開發效率,還可以提高程序的健壯性和可靠性,而 Kafka 的客戶端正好包含了這些特質。對於 KafkaProducer 而言,它是線程安全的,我們可以在多線程的環境中複用它,後面的消費者客戶端 KafkaConsumer 而言,它是非線程安全的,因爲它具備了狀態。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章