Kafka版本
- kafka版本1.1.1,可能絕大部分也適用於kafka 0.10.x及以上版本。
Java API
-
發送的一般步驟
- 構造配置信息,即java.util.Properties對象
- 使用Properties對象構造KafkaProducer實例
- 構造待發送的ProducerRecord消息對象,指定key、value、topic
- 調用KafkaProducer的send方法發送消息
- 關閉KafkaProducer
-
producer發送消息底層完全是異步發送,通過Future同時提供了同步發送和異步回調發送
- 同步:通過future.get()無限等待結果返回,實現同步發送的效果
- 異步:通過
org.apache.kafka.clients.producer.Callback
接口處理消息發送後的邏輯。此接口比較粗糙,只有一個onCompletion方法,其實如果提供一個onSuccess和一個OnFailed方法就好了。onCompletion方法的兩個參數RecordMetadata和Exception不會同時非空,即至少只有一個是null。消息發送成功時Exception爲null,消息發送失敗時,RecordMetadata時null。
-
kafka的錯誤類型主要包含兩類,可重試異常和不可重試異常
-
可重試異常,對於可重試異常,如果在producer中配置了重試次數,只要在規定的重試次數內自定恢復了,便不會出現在onCompletion方法的exception中。如果超過了重試次數仍沒有成功,則仍然會進exception中,此時需要程序自行處理
-
LeaderNotAvailableException: 通常出現在 leader換屆選舉期間,表示分區的leader副本不可用。一般是瞬時異常,重試之後可以自行恢復
-
NotControllerException:表示Controller在經歷新一輪的選舉,controller 當前不可用。一般可以通過重試機制自行恢復
-
NetworkException:網絡瞬時故障導致的異常,可重試。
-
所有可重試異常都繼承
org.apache.kafka.common.errors.RetriableException
.所以未即成此異常類的異常都屬於不可重試異常,即無法處理的問題,比如發送消息大小過大,序列化異常等
-
同步發送
-
發送實例代碼
@Test public void testSync() { Properties props = new Properties(); props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka-master:9092,kafka-slave1:9093,kafka-slave2:9094"); props.put(ProducerConfig.ACKS_CONFIG, "all"); props.put(ProducerConfig.COMPRESSION_TYPE_CONFIG, "lz4"); props.put(ProducerConfig.RETRIES_CONFIG, "10"); props.put(ProducerConfig.RETRY_BACKOFF_MS_CONFIG, "1000"); props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer"); props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer"); KafkaProducer<Integer, String> producer = new KafkaProducer<>(props); //同一個key的消息放到同一個分區,不指定key則均衡分佈,消息分區的選擇是在客戶端進行的 String key = "test"; String topic = "testTopic"; for (int i = 0; i < 100; i++) { try { String messageStr = "hello world " + i; ProducerRecord producerRecord = new ProducerRecord(topic, key, messageStr); Future<RecordMetadata> future = producer.send(producerRecord); List<PartitionInfo> partitionInfos = producer.partitionsFor(topic); for (PartitionInfo partitionInfo : partitionInfos) { logger.info(partitionInfo.toString()); } //同步調用 RecordMetadata recordMetadata = future.get(); logger.info(ToStringBuilder.reflectionToString(recordMetadata)); } catch (InterruptedException e) { Thread.currentThread().interrupt(); logger.error(e.getMessage(), e); } catch (ExecutionException e) { logger.error(e.getMessage(), e); } } producer.close(); }
-
ProducerRecord
public class ProducerRecord<K, V> { //消息主題 private final String topic; //消息分區 private final Integer partition; //headers 字段是消息的頭部,Kafka 0.11.x 版本才引入的,它大多用來設定一些與應用相關的信息 private final Headers headers; //消息Key private final K key; //消息體 private final V value; //消息的時間戳, private final Long timestamp; ... 省略 ... }
異步發送
-
異步發送實例。對於同一個分區來說,如果消息1在消息2之前發送,那麼KafkaProducer可以保證對應的callback1在callback2之前調用,即回調函數的調用可以保證分區有序
@Test public void testASync() { Properties props = new Properties(); props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka-master:9092,kafka-slave1:9093,kafka-slave2:9094"); props.put(ProducerConfig.ACKS_CONFIG, "all"); props.put(ProducerConfig.COMPRESSION_TYPE_CONFIG, "lz4"); props.put(ProducerConfig.RETRIES_CONFIG, "10"); props.put(ProducerConfig.RETRY_BACKOFF_MS_CONFIG, "1000"); props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer"); props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer"); KafkaProducer<Integer, String> producer = new KafkaProducer<>(props); String key = "testAsync"; String topic = "testTopic"; CountDownLatch countDownLatch = new CountDownLatch(100); for (int i = 100; i < 200; i++) { String messageStr = "hello world " + i; ProducerRecord producerRecord = new ProducerRecord(topic, key, messageStr); producer.send(producerRecord, new Callback() { @Override public void onCompletion(RecordMetadata recordMetadata, Exception e) { //exception與recordMetadata不會同時非空,即至少有一個爲null if (e != null) { if (e instanceof RetriableException) { //處理可重試瞬時異常 } else { //處理不可重試瞬時異常 logger.error(e.getMessage(), e); } } //消息發送成功 if (recordMetadata != null) { logger.info(ToStringBuilder.reflectionToString(recordMetadata)); } countDownLatch.countDown(); } }); } try { countDownLatch.await(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } producer.close(); }
分區策略
-
ProducerRecord
中的key
有兩個用途- 作爲消息的附加信息
- 決定消息該寫到
Topic(主題)
的哪個Partition(分區)
,默認分區策略將擁有相同key
的消息寫到同一個Partition(分區)
-
如果
key
爲null,並且使用了默認分區器,則消息將被隨機發送到Topic(主題)
內各個**可用的Partition(分區)**上,默認分區器使用輪訓算法(Round Robin)將消息均衡地分佈到各個Partition(分區)
上。 -
如果
key
不爲null,並且使用了默認分區器,kafka客戶端會對key
進行散列(使用kafka自己的算法,java版本的升級不影響散列值),根據散列值把消息映射到特定的Partition(分區)
上。在進行映射時,會使用Topic(主題)
所有的Partition(分區)
(不僅僅是可用Partition(分區)
),如果寫入數據的Partition(分區)
是不可用的就會發生錯誤。 -
只有在不改變
Topic(主題)Partition(分區)
數量的情況下,key
與Partition(分區)
之間的映射才能保持不變。如果要使用key
來映射分區,最好在創建Topic(主題)
的時候就把Partition(分區)
規劃好,並且永遠不要增加新的Partition(分區)
-
默認的分區策略實現類
org.apache.kafka.clients.producer.internals.DefaultPartitioner
public class DefaultPartitioner implements Partitioner { private final ConcurrentMap<String, AtomicInteger> topicCounterMap = new ConcurrentHashMap<>(); public void configure(Map<String, ?> configs) {} 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 (keyBytes == null) { int nextValue = nextValue(topic); List<PartitionInfo> availablePartitions = cluster.availablePartitionsForTopic(topic); if (availablePartitions.size() > 0) { int part = Utils.toPositive(nextValue) % availablePartitions.size(); return availablePartitions.get(part).partition(); } else { // no partitions are available, give a non-available partition return Utils.toPositive(nextValue) % numPartitions; } } else { // hash the keyBytes to choose a partition return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions; } } private int nextValue(String topic) { AtomicInteger counter = topicCounterMap.get(topic); if (null == counter) { counter = new AtomicInteger(ThreadLocalRandom.current().nextInt()); AtomicInteger currentCounter = topicCounterMap.putIfAbsent(topic, counter); if (currentCounter != null) { counter = currentCounter; } } return counter.getAndIncrement(); } public void close() {} }
-
自定義分區策略
- 實現
org.apache.kafka.clients.producer.Partitioner
接口 - Properties對象中設置 partitioner.class參數
- 實現
-
自定義分區策略
public class SmsPartition implements Partitioner { /** * @param topic topic名稱 * @param key 消息key或者null * @param keyBytes 消息鍵值序列化字節數組或 null * @param value 消息體或 null * @param valueBytes 消息體序列化字節數組或 null * @param cluster 集羣元數據 */ @Override public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) { List<PartitionInfo> partitionInfos = cluster.partitionsForTopic(topic); //這裏定義key的類型是字符串,序列化也是字符串 if (keyBytes == null || !(key instanceof String)) { throw new IllegalArgumentException("key不能爲空,且必須是字符串類型"); } int size = partitionInfos.size(); if (size <= 1) { return size; } else { String keyString = (String) key; //key值爲sms的消息分配最後一個分區 if ("sms".equals(keyString)) { return size - 1; } return Math.abs(Utils.murmur2(keyBytes) % (size - 1)); } } @Override public void close() { //關閉分區,主要爲了關閉那些創建分區時初始化的系統資源等 } @Override public void configure(Map<String, ?> configs) { } }
-
自定義分區案例
/** * 1. 創建topic * bin/kafka-topics.sh --create --zookeeper zookeeper:2181 --replication-factor 3 --partitions 5 --topic testTopic * 2. 自定義分區 * 3. 運行後分區的消息數 * bin/kafka-run-class.sh kafka.tools.GetOffsetShell --broker-list kafka-master:9092 -topic testTopic --time -1 * testTopic:2:0 * testTopic:4:100 * testTopic:1:0 * testTopic:3:0 * testTopic:0:200 */ @Test public void testPartition() { Properties props = new Properties(); props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka-master:9092,kafka-slave1:9093,kafka-slave2:9094"); props.put(ProducerConfig.ACKS_CONFIG, "all"); props.put(ProducerConfig.COMPRESSION_TYPE_CONFIG, "lz4"); props.put(ProducerConfig.RETRIES_CONFIG, "10"); props.put(ProducerConfig.RETRY_BACKOFF_MS_CONFIG, "1000"); props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer"); props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer"); props.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, "cn.jannal.kafka.partition.SmsPartition"); KafkaProducer<Integer, String> producer = new KafkaProducer<>(props); String key = "sms"; String topic = "testTopic"; for (int i = 200; i < 300; i++) { try { String messageStr = "hello world " + i; ProducerRecord producerRecord = new ProducerRecord(topic, key, messageStr); Future<RecordMetadata> future = producer.send(producerRecord); List<PartitionInfo> partitionInfos = producer.partitionsFor(topic); for (PartitionInfo partitionInfo : partitionInfos) { logger.info(partitionInfo.toString()); } //同步調用 RecordMetadata recordMetadata = future.get(); logger.info(ToStringBuilder.reflectionToString(recordMetadata)); } catch (InterruptedException e) { Thread.currentThread().interrupt(); logger.error(e.getMessage(), e); } catch (ExecutionException e) { Thread.currentThread().interrupt(); logger.error(e.getMessage(), e); } } producer.close(); }
攔截器
-
interceptor主要用於實現 clients 端的定製化控制邏輯,對於 producer 而言, interceptor 使得用戶在消息發送前以及 producer 回調邏輯前對消息做一些定製化需求,比如修改消息、統計等,多個interceptor形成一個攔截器鏈
-
自定義攔截器需要實現
org.apache.kafka.clients.producer.ProducerInterceptor
接口 -
interceptor 可能運行在多個線程中,因此在具體實現時需要自行確保線程安全。另外,若指定了多個 interceptor,則 producer 將按照指定順序調用它們,同時把每個interceptor 中捕獲的異常記錄到錯誤日誌中而不是向上傳遞
public class CounterProducerlnterceptor implements ProducerInterceptor { private static AtomicInteger sendCounter = new AtomicInteger(0); private static AtomicInteger successCounter = new AtomicInteger(0); private static AtomicInteger failedCounter = new AtomicInteger(0); /** * 1. 消息被序列化以計算分區前調用該方法,該方法中可以對消息做任何操作, * 但最好保證不要修改消息所屬的topic和分區 * 2. 該方法運行在發送主線程中 */ @Override public ProducerRecord onSend(ProducerRecord record) { sendCounter.incrementAndGet(); return record; } /** * 1. 在消息被應答之前或消息發送失敗時調用 * 2. 該方法在producer的I/O線程中,儘量不要放入耗時的業務邏輯 */ @Override public void onAcknowledgement(RecordMetadata metadata, Exception exception) { if (exception == null) { successCounter.incrementAndGet(); } else { failedCounter.incrementAndGet(); } } /** * 關閉interceptor執行一些資源清理工作 * 關閉producer時,會調用此方法 */ @Override public void close() { System.out.println("發送個數" + sendCounter.get()); System.out.println("成功個數" + successCounter.get()); System.out.println("失敗個數" + failedCounter.get()); } @Override public void configure(Map<String, ?> configs) { } }
-
實際案例
@Test public void testProducerInterceptor() { Properties props = new Properties(); props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka-master:9092,kafka-slave1:9093,kafka-slave2:9094"); props.put(ProducerConfig.ACKS_CONFIG, "all"); props.put(ProducerConfig.COMPRESSION_TYPE_CONFIG, "lz4"); props.put(ProducerConfig.RETRIES_CONFIG, "10"); props.put(ProducerConfig.RETRY_BACKOFF_MS_CONFIG, "1000"); props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer"); props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer"); //構建攔截器鏈 List<String> interceptors = new ArrayList<>(); interceptors.add("cn.jannal.kafka.interceptor.CounterProducerlnterceptor"); props.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, interceptors); KafkaProducer<Integer, String> producer = new KafkaProducer<>(props); String topic = "testTopic"; for (int i = 0; i < 100; i++) { try { String messageStr = "hello world " + i; ProducerRecord producerRecord = new ProducerRecord(topic, null, messageStr); Future<RecordMetadata> future = producer.send(producerRecord); List<PartitionInfo> partitionInfos = producer.partitionsFor(topic); for (PartitionInfo partitionInfo : partitionInfos) { logger.info(partitionInfo.toString()); } //同步調用 RecordMetadata recordMetadata = future.get(); logger.info(ToStringBuilder.reflectionToString(recordMetadata)); } catch (InterruptedException e) { Thread.currentThread().interrupt(); logger.error(e.getMessage(), e); } catch (ExecutionException e) { logger.error(e.getMessage(), e); } } //關閉producer時,纔會調用interceptor的close方法 producer.close(); }
多線程多進程發送
- 實際環境中爲了達到最大的吞吐量,需要多線程或者多進程同時給kafka集羣發送消息
- 多線程單KafkaProducer 實例:因爲KafkaProducer是線程安全的,所以需要構造一個全局的KafkaProducer實例,在多線程中共享使用。所有線程共享一個內存緩衝區(需要調大),對於分區數量比較少的集羣環境,可以使用這種方式
- 多線程多 KafkaProducer實例:每個發送線程構造一個KafkaProducer實例,每個發送線程共享自己的KafkaProducer實例、緩衝區以及參數配置,不同線程之間的KafkaProducer實例相互獨立,互不影響。對於分區數量很多的集羣環境,可以使用這種方式,方便管理。
防止producer消息丟失
- producer丟失消息的場景:若 I/O線程發送之前 producer崩潰,則存儲緩衝區中的消息全部丟失
- producer需要的配置
max.block.ms=60000ms
:默認60000msacks=all或者-1
retries:Integer.MAX_VALUE
:無限重試可恢復的異常max.in.flight.requests.per.connection = 1
:防止消息亂序,但是可能會降低吞吐量
- broker端配置
unclean.leader.election.enable =false
:不允許非ISR中的副本被選舉爲leaderreplication.factor=3
:多副本備份min.insync.replicas=2
:控制某條消息至少被寫入到 ISR 中的多少個副本纔算成功。producer設置acks=-1此參數纔有意義replication.factor >min.insync.replicas
:如果設置相等,只要有一個副本掛掉,分區就無法正常工作。一般配置replication.factor = min.insync.replicas+1
- 防止producer宕機,而導致消息丟失
- 在發送消息前,先將消息寫入redis,然後再發送,發送成功後刪除redis中的消息。
- 單獨啓動一個job,對於長時間沒有被刪除的key做重試處理(有可能會產生重複消息,consumer來做冪等處理)