「kafka」kafka-clients,java編寫生產者客戶端及原理剖析

從編程角度而言,生產者就是負責向Kafka發送消息的應用程序。本文使用java語言做詳細介紹。

一個正常的生產邏輯需要以下幾個步驟:

  1. 配置生產者客戶端參數及創建相應的生產者實例。
  2. 構建待發送的消息。
  3. 發送消息。
  4. 關閉生產者實例。

客戶端開發案例

本文先提供簡單的生產者客戶端程序,然后做具體的改進和分析。

<--導入依賴-->
<dependency>
   <groupId>org.apache.kafka</groupId>
   <artifactId>kafka-clients</artifactId>
   <version>2.0.0</version>
</dependency>

public class Producer {
       //鏈接地址
        public static final String brokerList = "192.168.0.18:9092";
        //主題
        public static final String topic = "topic-demo";
        //配置參數
        public static Properties initConfig(){
            Properties properties = new Properties();
            properties.put("bootstrap.servers",brokerList);
            properties.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer" );
            properties.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
            properties.put("client.id","producer.client.id.demo");
            return properties;
        }

        public static void main(String[] args) throws InterruptedException {
            //構建發送主體
            KafkaProducer<String, String> producer = new KafkaProducer<>(initConfig());
            //構建消息體
            ProducerRecord<String, String> record = new ProducerRecord<>(topic, "hello world");
            try {
                //發送消息
                producer.send(record);
            } catch (Exception e) {
                e.printStackTrace();
            }
            producer.close();
        }
}

構建的消息對象ProducerRecord并不是單純意義上的消息,它包含了多個屬性,原本需要發送的業務相關的消息體只是其中的一個value屬性,比如“hello world”,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;
//省略構造方法和getter、setter方法

其中topic和partition字段分別代表消息要發往的主題和分區號。headers字段是消息的頭部,Kafka0.11x版本才引入這個屬性,它大多用來設定一些與應用相關的信息,也可以不設置。key是用來指定消息的鍵,它不僅是消息的附加信息,還可以用來計算分區號進而可以讓消息發往特定的分區。消息以主題為單位進行歸類,而這個key可以讓消息再進行二次歸類,同一個key的消息會被劃分到同一個分區中(事實上不總是這樣,后面會解釋)。有key的消息還可以支持日志壓縮功能(以后講壓縮)。value是消息體,一般不為空,如果為空則表示特定的消息——墓碑消息。temestamp是指消息的時間戳,它有CreateTime和LogAppendTime兩種類型,前者標識消息創建的時間,后者表示消息追加到日志文件的時間。

必要的參數配置

參考initConfig方法,在創建真正的生產者實例前需要配置相應的參數,比如需要鏈接的kafka集群地址。通常有3個參數是必填的。

  • bootstrap.server:該參數用來指定生產者客戶端連接Kafka集群所需的broker地址清單,具體的內容格式是host1:port1,host2:port2,可以設置一個或者多個地址,中間以逗號隔開,此參數的默認值為“”。注意這里并非需要所有的broker地址,因為生產者會從給定的borker里查找到其他broker的信息。不過建議至少設置兩個以上的borker地址信息,當其中一個宕機時,生產者仍然可以鏈接到集群上。
  • key.serializer和value.serializer :broker端接收的消息必須以字節數組(byte[])的形式存在。在代碼中使用的KafkaProducer<String, String>ProducerRecord<String, String>中的泛型<String,String>對應的就是小溪中key和value的類型,生產者客戶端使用這種方式可以讓代碼具有更好的可讀性,不過在發往broker之前需要將消息中對應的key和value做相應的序列化操作來轉換成字節數組。key.serializer和value.serializer這兩個參數分別用來指定key和value的序列化器,這兩個參數無默認值。后面講如何自定義序列化器。

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

KafkaProducer中的參數眾多,遠非實例方法中的那樣只有4個。一般而言,開發人員無法記住所有的參數名,只能有個大概的印象。在實際使用過程中,諸如key.serializer之類的字符串經常由于認為因素而書寫錯誤。為此我們可以使用ProducerConfig類來做一定程度上的預防措施,每個參數在這個類上都有對應的名字。如下圖所示:

ProducerConfig

我們將initConfig方法做如下修改:

   public static Properties initConfig(){
            Properties properties = new Properties();
            properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,brokerList);
            properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer" );
            properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
            properties.put(ProducerConfig.CLIENT_ID_CONFIG,"producer.client.id.demo");
            return properties;
        }

注意到上面的代碼中key和value對應的序列化器名字也容易寫錯,這里通過java的技巧來做進一步修改:

 properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
 properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());

KafkaProducer是線程安全的,可以再多個線程中共享單個實例,也可以將KafkaProducer實例進行池化。
KafkaProducer中有多個構造函數,假如在創建實例過程中沒有指定key.serializer和value.serializer這兩個參數的話,實例就得這么構建:

KafkaProducer<String, String> producer 
                    = new KafkaProducer<>(initConfig(),new StringSerializer(),new StringSerializer());

小編不會這么做,一般都是在initConfig函數里指定所有的參數。

消息的發送(同步、異步、回調)

ProducerRecord是消息的載體。topic屬性和value屬性是必填的,其余屬性是選填的,其構造方法也有很多:

    public ProducerRecord(String topic, Integer partition, Long timestamp, K key, V value) {
        this(topic, partition, timestamp, key, value, (Iterable)null);
    }

    public ProducerRecord(String topic, Integer partition, K key, V value, Iterable<Header> headers) {
        this(topic, partition, (Long)null, key, value, headers);
    }

    public ProducerRecord(String topic, Integer partition, K key, V value) {
        this(topic, partition, (Long)null, key, value, (Iterable)null);
    }

    public ProducerRecord(String topic, K key, V value) {
        this(topic, (Integer)null, (Long)null, key, value, (Iterable)null);
    }

    public ProducerRecord(String topic, V value) {
        this(topic, (Integer)null, (Long)null, (Object)null, value, (Iterable)null);
    }

實際應用開發過程中,創建ProducerRecord對象是一個非常頻繁的動作。創建完消息體后,就可以開始發送消息了。消息的發送主要有三種模式

  1. 發后即忘(fire-and-forget)
  2. 同步(sync)
  3. 異步(async)
    案例中的發送方式就是發后即忘,它只管往kafka中發送消息而不關心消息是否正確送達。在大多數情況下,這種發送方式沒什么問題,不過在某些時候(比如發生不可重試異常)會造成消息丟失。這種發送方式性能最高,可靠性也最差。

send方法返回的并非是void類型,而是Future<RecordMetadata>類型,send()方法有兩個重載方法:

 Future<RecordMetadata> send(ProducerRecord<K, V> var1);

    Future<RecordMetadata> send(ProducerRecord<K, V> var1, Callback var2);

要實現同步可以利用返回的Future對象實現,如下所示:

       try {
            producer.send(record).get();//實現同步發送
       } catch (InterruptedException |ExecutionException   e) {
             e.printStackTrace();
       } 

實際上,send方法本身就是異步的,send()方法返回的Future對象可以使調用方稍后獲得發送的結果。案例中,send方法之后直接鏈式調用了get()方法來阻塞等待Kafka的響應,知道消息發送成功或發生異常。如果發生異常,那么就需要捕獲異常并交由邏輯處理層。

也可在調用send方法之后不直接調用get方法,比如下面的一種實現同步的方式:

   Future<RecordMetadata> retu =  producer.send(record);//send方法本身是異步的
    RecordMetadata recordMetadata = null;
     try {
            recordMetadata = retu.get();//實現同步發送
            System.out.println("同步發送成功到:"+recordMetadata.topic());
          } catch (InterruptedException |ExecutionException  e) {
            e.printStackTrace();
          } 

這樣可以獲取一個RecordMetadata對象,它包含了消息的一些元數據信息,比如當前消息的主題、分區號、分區中的偏移量、時間戳等。如果你需要這些信息,則可以使用這個方式。如果不需要直接使用producer.send(record).get();方式更省事。
Future表示一個任務的聲明周期,并提供了響應的方法來判斷任務是否已經完成或取消,以及獲取任務的結果和取消任務等。也可以使用retu.get(3, TimeUnit.SECONDS);方式來實現超時阻塞。
KafkaProducer一般會發生兩種類型的異常:可重試異常和不可重試異常。常見的可重試異常包括:NetworkException、LerderNotAvailableException、UnknownTopicOrPartitionExceptionNotEnoughReplicasExceptionNotCoordinatorException等。比如,NetworkException表示網絡異常,這個有可能是由于網絡瞬時故障而導致的異常,可以通過重試解決;又比如LerderNotAvailableException表示分區的leader副本不可用,這個異常通常發生在leader副本下線而新的leader副本選舉完成之前,重試之后可以重新恢復。不可重試的異常,如RecordTooLargeException,暗示了所發送的消息太大,對此不會進行任何重試,直接拋出異常。
對于可重試的異常,如果配置了retries參數,那么只要在規定的重試次數內自行恢復了,就不會拋出異常。retries的默認值為0,配置也很簡單:

properties.put(ProducerConfig.RETRIES_CONFIG,10);

如果重試10次后還沒有恢復,那么仍然會拋出異常,進而發送的外層邏輯處理就要處理這些異常了。
同步方式可靠性高,要么消息發送成功,要么發生異常,如果發生異常則可以捕獲并進行相應的處理,不會造成消息丟失。不過性能確實差很多,需要阻塞等待一條消息發送完成后再繼續發送下一條消息。

來了解一下異步發送方式,一般是在send方法里指定一個Callback回調函數,Kafka在返回響應時調用該函數來實現異步發送確認。Kafka有響應時就會回調,要么發送成功,要么拋出異常。如:

       //異步發送
        public  static void sendAsyn(ProducerRecord<String, String> record,KafkaProducer<String, String> producer){
            //異步發送
            producer.send(record, new Callback() {
                @Override
                public void onCompletion(RecordMetadata recordMetadata, Exception e) {
                    if (e!=null){
                        e.printStackTrace();
                    }else{
                        System.out.println("異步發送成功"+recordMetadata.topic()+":"+recordMetadata.partition());
                    }
                }
            });
        }

onComplete方法的兩個參數是互斥的,消息發送成功時,meta不為null而exception為null;而消息發送異常時,metadata為null而exception不為null。
對于同一個分區而言,如果消息record1于record2之前發送,系統可以保證對應的callback1在callback2之前調用,也就是說回調函數的調用也是可以保證分區有序。
最后producer.close();方法會阻塞等待之前所有的發送請求完成后再關閉KafkaProducer,同時,還提供了一個帶超時的close方法,這個很少用。

序列化

生產者需要用序列化器把對象轉換成字節數組才能發給kafka。消費者必須用反序列器把從kafka收到的字節數組轉換成相應的對象。上文講的序列化器StringSerializer實現了org.apache.kafka.common.serialization.Serializer接口,此外還有
ByteArray、ByteBuffer、BytesDouble、Integer、Long等序列化器,都實現了Serializer接口,該接口有3個方法:

    void configure(Map<String, ?> var1, boolean var2);
    byte[] serialize(String var1, T var2);
    void close();

configure用來配置當前類,serialize方法用來執行序列化操作。而close方法用來關閉當前的序列化器,一般情況下close是個空方法,如果實現了此方法,必須保證此方法的冪等性,因為KafkaProducer可能會調用多次該方法。
我們先來看一下StringSerializer的源碼,從而引出自定義序列化器的編寫


public class StringSerializer implements Serializer<String> {
    private String encoding = "UTF8";

    public StringSerializer() {
    }

    public void configure(Map<String, ?> configs, boolean isKey) {
        String propertyName = isKey ? "key.serializer.encoding" : "value.serializer.encoding";
        Object encodingValue = configs.get(propertyName);
        if (encodingValue == null) {
            encodingValue = configs.get("serializer.encoding");
        }

        if (encodingValue instanceof String) {
            this.encoding = (String)encodingValue;
        }

    }

    public byte[] serialize(String topic, String data) {
        try {
            return data == null ? null : data.getBytes(this.encoding);
        } catch (UnsupportedEncodingException var4) {
            throw new SerializationException("Error when serializing string to byte[] due to unsupported encoding " + this.encoding);
        }
    }

    public void close() {
    }
}

首先是configure()方法,是在創建KafkaProducer實例的時候調用的,主要用來確定以編碼類型,不過一般客戶端對于key.serializer.encoding和value.serializer.encodeing這幾個參數是不會設置的,默認為UTF-8。serialize()方法非常直觀,就是將String類型轉換為byte[]類型。
如果Kafka客戶端提供的幾種序列化器都無法滿足你,則可以使用Avro/JSON/Thrift/ProtoBuf和Protostuff等通用的序列化工具來實現,或者使用自定義類型的序列化器來實現。下面看如何自定義:
首先創建一個業務類:

public class User {
    private String name;
    private int age = -1;
    public String getName() {
        return name;
    }
    public User setName(String name) {
        this.name = name;
        return this;
    }
    public int getAge() {
        return age;
    }
    public User setAge(int age) {
        this.age = age;return this;
    }
}

定義序列化器

package serializer;

import bean.User;
import org.apache.kafka.common.serialization.Serializer;

import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.Map;

//自定義序列化器
public class UserSerializer implements Serializer<User> {

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

    }

    @Override
    public byte[] serialize(String s, User user) {
        if (user == null) {
            return null;
        }
        byte[] name;
        int age = user.getAge();

        try {
            if (user.getName() != null) {
                name = user.getName().getBytes("UTF-8");
            } else {
                name = new byte[0];
            }
            //數組總共的長度
            ByteBuffer byteBuffer = ByteBuffer.allocate(4+4+name.length);
            //name字節數
            byteBuffer.putInt(name.length);
            //放name字節數組
            byteBuffer.put(name);
            //放age,age本身就是int類型的
            byteBuffer.putInt(age);
            return byteBuffer.array();
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return new byte[0];
    }

    @Override
    public void close() {

    }

}

關于ByteBuffer怎么用,可以參考筆者的《「高并發通信框架Netty4 源碼解讀(四)」NIO緩沖區之字節緩沖區ByteBuffer詳解》

定義消費端的反序列化器

public class UserDeserializer implements Deserializer<User> {
    @Override
    public void configure(Map<String, ?> map, boolean b) {

    }
    @Override
    public User deserialize(String s, byte[] bytes) {
        ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
        int nameLength = byteBuffer.getInt();
        byte name[] = new byte[nameLength];
        byteBuffer.get(name,0,nameLength);
        int age = byteBuffer.getInt();
        return new User().setAge(age).setName(new String(name));
    }
    @Override
    public void close() {
    }
}

更改序列化器:

properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, UserSerializer.class.getName());

分區器

消息在發送過程中,有可能需要經過攔截器、序列化器和分區器的一系列作用之后才能被真正發往broker。攔截器不是必須的,后面講。序列化器是必須的,消息經過序列化后就需要確定它發往的分區,如果ProducerRecord消息中指定了partition字段那么就不需要分區器的作用了,因為partition代表的就是所要發往的分區號。

如果消息中沒有指定partition字段,那么就需要依賴分區器,根據key這個字段來計算partition的值。分區器的作用就是為消息分配分區。

Kafka中提供的默認分區器是DefaultPartitioner,它實現了Partitioner這個接口,定義的方法如下:

public interface Partitioner extends Configurable, Closeable {
    int partition(String var1, Object var2, byte[] var3, Object var4, byte[] var5, Cluster var6);
    void close();
}

其中,parition方法用來計算分區號,返回值為int類型。partition方法中的參數分別表示主題、鍵、序列化后的鍵、值、序列化后的值,以及集群的元數據信息,通過這些信息可以實現豐富的分區器。

在默認的分區器DefaultPartitioner實現中,如果key不為null,那么默認的分區器會對key進行哈希(采用MurmurHash2算法,具備高運算性能及低碰撞率),最終根據得到的哈希值來計算分區號,擁有相同的key的消息會被寫入同一個分區,如果key為null,那么消息將會以輪詢的方式發往主題內的各個可用分區內,在不改變主題分區的情況下,key與分區之間的映射可以保持不變。不過,一旦主題中增加了新的分區,映射就破壞了。
指定分區器的方式:

properties.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, DefaultPartitioner.class.getName());//指定分區器,配合key使用

攔截器

生產者可以用來在消息發送前做一些準備工作,比如按照某個規則過濾不符合要求的消息、修改消息的內容,也可以用來在發送回調邏輯前做一些定制化要求,比如統計類工作。
生產者攔截器的使用很簡單,主要自定義實現ProducerInterceptor接口

public interface ProducerInterceptor<K, V> extends Configurable {
    ProducerRecord<K, V> onSend(ProducerRecord<K, V> var1);
    void onAcknowledgement(RecordMetadata var1, Exception var2);
    void close();
}

KafkaProducer在將消息序列化和計算分區之前會調用攔截器的onSend方法來對消息進行相應的定制化操作。在消息被應答之前或消息發送失敗時調用生產者攔截器的onAcknowledgement方法,優先于用戶設定的Callback之前執行。這個方法運行在producer的IO線程中,所以這個方法的實現越簡單越好,否則影響消息的發送速度。
自定義攔截器實現:

public class ProducerInterceptor implements org.apache.kafka.clients.producer.ProducerInterceptor<String ,String> {
   private volatile long sendSuccess = 0;
   private volatile long sendFailure = 0;

   @Override
   public ProducerRecord onSend(ProducerRecord<String ,String> producerRecord) {
       //消息發送前,進行修改操作
       String modifieldValue = "prefix-"+producerRecord.value();
       return new ProducerRecord<String ,String>(producerRecord.topic(),
               producerRecord.partition(),
               producerRecord.timestamp(),
               producerRecord.key(),
               modifieldValue,
               producerRecord.headers());
   }

   @Override
   public void onAcknowledgement(RecordMetadata recordMetadata, Exception e) {
       if(e == null){
           sendSuccess++;
           System.out.println("發送成功的消息:"+sendSuccess);
       }else{
           sendFailure++;
           System.out.println("發送失敗的消息:"+sendFailure);
       }
   }

   @Override
   public void close() {
       System.out.println("發送成功率:"+(double)sendSuccess/(sendSuccess+sendFailure));
   }
   @Override
   public void configure(Map<String, ?> map) {

   }
}

我們在onSend方法上修改了內容,發送內容前加上了prefix-前綴,onAcknowledgement用來統計發送成功與失敗的消息數。
攔截器的配置:

properties.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG,
             ProducerInterceptor.class.getName()+","+ ProducerInterceptor2.class.getName());

多個攔截器用逗號分隔,攔截器的調用順序會按配置順序調用。
小總結:生產者調用順序 :攔截器->序列化器->分區器

原理剖析

客戶端整體架構:


客戶端整體架構

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

RecordAccumulator主要用來緩存消息以便Sender線程可以批量發送,進而減少網絡傳輸的資源消耗以提升性能。RecordAccumulator緩存的大小可以通過客戶端參數buffer.memory配置,默認值為33554432B,即32MB。

//設置緩沖區大小
properties.put(ProducerConfig.BUFFER_MEMORY_CONFIG,"33554432");

如果生產者發送消息的速度超過發送服務器的速度,則會導致生產者空間不足,這個時候KafkaProducer的send()方法調用要么被阻塞,要么拋異常,這個取決于參數max.block.ms的配置,此參數的默認值為60000,即60秒。

 //設置阻塞異常的
properties.put(ProducerConfig.MAX_BLOCK_MS_CONFIG,"5000");//3s

主線程中發送過來的消息都會被追加到RecordAccumulator的某個雙端隊列中,在RecordAccumulator內部為每個分區都維護了一個雙端隊列,隊列中的內容就是ProducerBatch,即Deque<ProducerBatch>。消息寫入緩存時,追加到雙端隊列尾部;Sender讀取消息時,從雙端隊列的頭部讀取。注意ProducerBatch不是ProducerRecord,ProducerBatch中可以包含一個至多個ProducerRecord。通俗的說,ProducerRecord是生產者中創建的消息,而ProducerBatch是指一個消息批次,ProducerRecord會包含在ProducerBatch中,這樣可以使字節的使用更加緊湊。與此同時,將較小的ProducerRecord拼湊成一個較大的ProducerBatch,也可以減少網絡請求次數以提升整體的吞吐量。如果生產者客戶端需要向很多分區發送消息,則可以將buffer.memory參數適當調大以增加整體的吞吐量。

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

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

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

轉變成<Node,List<ProducerBatc>>的形式后,sender進一步封裝成<Node,Request>的形式,這樣就可以發往各個Node了,這里的Request是Kafka的各種協議請求。

請求在發往Kafka前,還會保存在InFlightRequests中,它保存對象的具體形式為Map<NodedId,Deque<Request>>,它的主要作用是緩存了已經發出去但還沒有收到響應的請求,NodedId是一個String類型,表示節點的ID編號。

與此同時InFlightRequests還提供了許多管理類的方法,并且通過參數配置還可以限制每個鏈接(也就是客戶端與Node之間的鏈接)最多緩存的請求數,這個配置參數為max.in.flight.requests.per.connection,默認值為5,即每個鏈接最多只能緩存5個未響應的請求,超過該數值后就不能向這個連接發送更多的請求了,除非緩存中有請求收到了響應。通過比較Deque<Request>的size與這個參數的大小來判斷對應的Node中是否已經堆積了很多未響應的消息,如果真是如此,那么說明這個Node節點負載較大或者網絡連接有問題,再繼續向其發送請求會增大請求超時的可能。

元數據的更新

KafkaProducer要將消息追加到指定主題的某個分區所對應的leader副本之前,首先需要知道主題的分區數量,然后經過計算得出(或直接指定)目標分區,之后需要知道目標分區的leader副本所在的broker節點的地址、端口號等信息才能建立連接,最終才能將消息發送到Kafka,在這過程中需要的信息都屬于元數據信息。

案例中,我們了解到bootstrap.servers參數只需要配置部分broker節點的地址即可, 不需要配置所有broker節點的地址,因為客戶端自己可以發現其他broker的節點地址,這一過程也屬于元數據相關的更新操作。與此同時,分區數量及leader副本的分布都會動態地變化,客戶端也需要動態的捕獲這些變化。

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

當客戶端沒有需要使用的元數據時,比如沒有指定的主題信息,或者超過metadata.max.age.ms時間沒有更新元數據都會引起元數據的更新操作。該參數的默認值為300000,即5分鐘 。元數據的更新操作是在客戶端的內部進行的,對客戶端的外部使用者不可見。

//客戶端更新kafka集群元數據的時間間隔,默認5分鐘
properties.put(ProducerConfig.METADATA_MAX_AGE_CONFIG,300000);

重要的生產者參數

在KafkaProducer中,大部分參數都有合理的默認值,一般不需要修改它們,不過了解這些參數可以讓我們更合理的使用生產者客戶端,其中還有一些參數涉及程序的可用性和性能。

acks

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

  • acks = 1
    默認值就為1。生產者發送消息之后,只要leader副本成功寫入消息,那么它就會收到來自服務端的成功響應。如果消息無法寫入leader副本,比如在leader副本崩潰、重新選舉新的leader副本的過程中,那么生產者就會收到一個錯誤的響應,為了避免消息丟失,生產者可以重發消息。如果消息寫入leader副本并返回成功響應給生產者,且在被其他follower副本拉取之前leader副本崩潰,那么此時消息還是會丟失,因為重新選舉的leader副本中并沒有這條對應的消息。acks=1是消息可靠性和吞吐量之間的折中方案。

  • acks=0
    生產者發送消息后不需要等待任何服務端的響應。在其他配置環境下,acks=0可達到最大吞吐量。

  • acks=-1或acks=all
    生產者在發送消息后,需要等到所有ISR中的所有副本都成功寫入消息之后才能夠收到來自服務端的成功響應。在其他配置環境下相同的情況下,可以達到最強的可靠性。但這不意味著消息一定可靠,因為ISR中可能只有leader副本,這樣就退化成了acks=1的情況,要獲得最強的消息可靠性要配合min.insync.replicas等參數的聯動配合。

   //leader副本成功收到消息后返回響應,不管follower副本
   properties.put(ProducerConfig.ACKS_CONFIG,"1");

max.request.size

這個參數用來限制生產者客戶端能發送消息的最大值,默認為1048576B,即1MB。一般情況下這個默認值就可以滿足大多數的應用場景了。筆者不建議盲目的增大這個參數值,尤其是對Kafka整體脈絡沒有足夠把控的時候。因為這個參數還涉及其他一些參數的聯動,比如broker端的message.max.bytes參數,如果配置錯誤會引起一些不必要的異常。

retries 和 retry.backoff.ms

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

重試還和另外一個參數retry.backoff.ms有關,這個參數的默認值為100,它用來設定兩次重試之間的時間間隔,避免無效的頻繁重試。

Kafka可以保證同一個分區中的消息是有序的。如果生產者按照一定順序發送消息,那么這些消息也會順訊的寫入分區,進而消費者也可以按照順序消費。如果將acks參數配置為非零值,并且max.in.flight.request.per.connection參數配置大于1的值,那么就會出現錯序的現象:如果第一批消息寫入失敗,而第二批消息寫入成功,那么生產者會重試發送第一批次的消息,此時第一批次的消息寫入成功,那么這兩批消息的順序就會發生錯序。一般而言,在需要保證消息順序的場合建議把參數max.in.flight.requests.per.connection配置為1,而不是把acks配置為0,不過這樣會影響整體的吞吐。

compression.type

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

connections.max.idle.ms

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

linger.ms

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

receive.buffer.bytes

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

send.buffer.bytes

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

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

還有一些參數沒有提及,這些參數同樣非常重要,他們需要單獨的文章或場景來描述。以后的博文會慢慢降到,歡迎大家關注。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。