Kafka API詳解

Producer API

消息發送流程

Kafka的Producer發送消息採用的是異步發送的方式。在消息發送的過程中,涉及到了兩個線程——main線程和Sender線程,以及一個線程共享變量——RecordAccumulator。main線程將消息發送給RecordAccumulator,Sender線程不斷從RecordAccumulator中拉取消息發送到Kafka broker。
image.png
相關參數:
batch.size:只有數據積累到batch.size之後,sender纔會發送數據。
linger.ms:如果數據遲遲未達到batch.size,sender等待linger.time之後就會發送數據。

異步發送API

導入依賴

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

編寫代碼

需要用到的類:
KafkaProducer:需要創建一個生產者對象,用來發送數據
ProducerConfig:獲取所需的一系列配置參數
ProducerRecord:每條數據都要封裝成一個ProducerRecord對象

  1. 不帶回調函數的API
import org.apache.kafka.clients.producer.*;
import java.util.Properties;
import java.util.concurrent.ExecutionException;
public class CustomProducer {
 public static void main(String[] args) throws ExecutionException, InterruptedException {
     Properties props = new Properties();
     props.put("bootstrap.servers", "hadoop102:9092");//kafka集羣,broker-list
     props.put("acks", "all");
     props.put("retries", 1);//重試次數
     props.put("batch.size", 16384);//批次大小
     props.put("linger.ms", 1);//等待時間
     props.put("buffer.memory", 33554432);//RecordAccumulator緩衝區大小
     props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
     props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
     Producer<String, String> producer = new KafkaProducer<>(props);
     for (int i = 0; i < 100; i++) {
         producer.send(new ProducerRecord<String, String>("first", Integer.toString(i), Integer.toString(i)));
     }
     producer.close();
 }
}
  1. 帶回調函數的API
    回調函數會在producer收到ack時調用,爲異步調用,該方法有兩個參數,分別是RecordMetadata和Exception,如果Exception爲null,說明消息發送成功,如果Exception不爲null,說明消息發送失敗。
    注意:消息發送失敗會自動重試,不需要我們在回調函數中手動重試
import org.apache.kafka.clients.producer.*;
import java.util.Properties;
import java.util.concurrent.ExecutionException;
public class CustomProducer {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Properties props = new Properties();
        props.put("bootstrap.servers", "hadoop102:9092");//kafka集羣,broker-list
        props.put("acks", "all");
        props.put("retries", 1);//重試次數
        props.put("batch.size", 16384);//批次大小
        props.put("linger.ms", 1);//等待時間
        props.put("buffer.memory", 33554432);//RecordAccumulator緩衝區大小
        props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        Producer<String, String> producer = new KafkaProducer<>(props);
        for (int i = 0; i < 100; i++) {
            producer.send(new ProducerRecord<String, String>("first", Integer.toString(i), Integer.toString(i)), new Callback() {
                //回調函數,該方法會在Producer收到ack時調用,爲異步調用
                @Override
                public void onCompletion(RecordMetadata metadata, Exception exception) {
                    if (exception == null) {
                        System.out.println("success->" + metadata.offset());
                    } else {
                        exception.printStackTrace();
                    }
                }
            });
        }
        producer.close();
    }
}

同步發送API

同步發送的意思就是,一條消息發送之後,會阻塞當前線程,直至返回ack。
由於send方法返回的是一個Future對象,根據Futrue對象的特點,我們也可以實現同步發送的效果,只需在調用Future對象的get方發即可

import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.Producer;
import org.apache.kafka.clients.producer.ProducerRecord;
import java.util.Properties;
import java.util.concurrent.ExecutionException;
public class CustomProducer {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Properties props = new Properties();
        props.put("bootstrap.servers", "hadoop102:9092");//kafka集羣,broker-list
        props.put("acks", "all");
        props.put("retries", 1);//重試次數
        props.put("batch.size", 16384);//批次大小
        props.put("linger.ms", 1);//等待時間
        props.put("buffer.memory", 33554432);//RecordAccumulator緩衝區大小
        props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        Producer<String, String> producer = new KafkaProducer<>(props);
        for (int i = 0; i < 100; i++) {
            producer.send(new ProducerRecord<String, String>("first", Integer.toString(i), Integer.toString(i))).get();
        }
        producer.close();
    }
}

Consumer API

Consumer消費數據時的可靠性是很容易保證的,因爲數據在Kafka中是持久化的,故不用擔心數據丟失問題。
由於consumer在消費過程中可能會出現斷電宕機等故障,consumer恢復後,需要從故障前的位置的繼續消費,所以consumer需要實時記錄自己消費到了哪個offset,以便故障恢復後繼續消費。
所以offset的維護是Consumer消費數據是必須考慮的問題。

手動提交offset

  1. 導入依賴
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>0.11.0.0</version>
</dependency>
  1. 編寫代碼
    需要用到的類:
    KafkaConsumer:需要創建一個消費者對象,用來消費數據
    ConsumerConfig:獲取所需的一系列配置參數
    ConsuemrRecord:每條數據都要封裝成一個ConsumerRecord對象
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import java.util.Arrays;
import java.util.Properties;
public class CustomConsumer {
    public static void main(String[] args) {
        Properties props = new Properties();
        props.put("bootstrap.servers", "hadoop102:9092");
        props.put("group.id", "test");//消費者組,只要group.id相同,就屬於同一個消費者組
        props.put("enable.auto.commit", "false");//自動提交offset
        props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
        consumer.subscribe(Arrays.asList("first"));
        while (true) {
            ConsumerRecords<String, String> records = consumer.poll(100);
            for (ConsumerRecord<String, String> record : records) {
                System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
            }
            consumer.commitSync();
        }
    }
}
  1. 代碼分析:
    手動提交offset的方法有兩種:分別是commitSync(同步提交)和commitAsync(異步提交)。兩者的相同點是,都會將本次poll的一批數據最高的偏移量提交;不同點是,commitSync會失敗重試,一直到提交成功(如果由於不可恢復原因導致,也會提交失敗);而commitAsync則沒有失敗重試機制,故有可能提交失敗。
  2. 數據重複消費問題
    image.png

自動提交offset

爲了使我們能夠專注於自己的業務邏輯,Kafka提供了自動提交offset的功能。
自動提交offset的相關參數:
enable.auto.commit:是否開啓自動提交offset功能
auto.commit.interval.ms:自動提交offset的時間間隔
以下爲自動提交offset的代碼:

import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import java.util.Arrays;
import java.util.Properties;
public class CustomConsumer {
    public static void main(String[] args) {
        Properties props = new Properties();
        props.put("bootstrap.servers", "hadoop102:9092");
        props.put("group.id", "test");
        props.put("enable.auto.commit", "true");
        props.put("auto.commit.interval.ms", "1000");
        props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
        consumer.subscribe(Arrays.asList("first"));
        while (true) {
            ConsumerRecords<String, String> records = consumer.poll(100);
            for (ConsumerRecord<String, String> record : records)
                System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
        }
    }
}

自定義Interceptor

攔截器原理

Producer攔截器(interceptor)是在Kafka 0.10版本被引入的,主要用於實現clients端的定製化控制邏輯。
對於producer而言,interceptor使得用戶在消息發送前以及producer回調邏輯前有機會對消息做一些定製化需求,比如修改消息等。同時,producer允許用戶指定多個interceptor按序作用於同一條消息從而形成一個攔截鏈(interceptor chain)。Intercetpor的實現接口是org.apache.kafka.clients.producer.ProducerInterceptor,其定義的方法包括:

  1. configure(configs)
    獲取配置信息和初始化數據時調用。
  2. onSend(ProducerRecord):
    該方法封裝進KafkaProducer.send方法中,即它運行在用戶主線程中。Producer確保在消息被序列化以及計算分區前調用該方法。用戶可以在該方法中對消息做任何操作,但最好保證不要修改消息所屬的topic和分區,否則會影響目標分區的計算。
  3. onAcknowledgement(RecordMetadata, Exception):
    該方法會在消息從RecordAccumulator成功發送到Kafka Broker之後,或者在發送過程中失敗時調用。並且通常都是在producer回調邏輯觸發之前。onAcknowledgement運行在producer的IO線程中,因此不要在該方法中放入很重的邏輯,否則會拖慢producer的消息發送效率。
  4. close:
    關閉interceptor,主要用於執行一些資源清理工作
    如前所述,interceptor可能被運行在多個線程中,因此在具體實現時用戶需要自行確保線程安全。另外倘若指定了多個interceptor,則producer將按照指定順序調用它們,並僅僅是捕獲每個interceptor可能拋出的異常記錄到錯誤日誌中而非在向上傳遞。這在使用過程中要特別留意。

攔截器案例

需求:

實現一個簡單的雙interceptor組成的攔截鏈。第一個interceptor會在消息發送前將時間戳信息加到消息value的最前部;第二個interceptor會在消息發送後更新成功發送消息數或失敗發送消息數。
image.png

案例實操

  1. 增加時間戳攔截器
import java.util.Map;
import org.apache.kafka.clients.producer.ProducerInterceptor;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
public class TimeInterceptor implements ProducerInterceptor<String, String> {
	@Override
	public void configure(Map<String, ?> configs) {
	}
	@Override
	public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) {
		// 創建一個新的record,把時間戳寫入消息體的最前部
		return new ProducerRecord(record.topic(), record.partition(), record.timestamp(), record.key(),
				System.currentTimeMillis() + "," + record.value().toString());
	}
	@Override
	public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
	}
	@Override
	public void close() {
	}
}
  1. 統計發送消息成功和發送失敗消息數,並在producer關閉時打印這兩個計數器
import java.util.Map;
import org.apache.kafka.clients.producer.ProducerInterceptor;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
public class CounterInterceptor implements ProducerInterceptor<String, String>{
    private int errorCounter = 0;
    private int successCounter = 0;
	@Override
	public void configure(Map<String, ?> configs) {
	}
	@Override
	public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) {
		 return record;
	}
	@Override
	public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
		// 統計成功和失敗的次數
        if (exception == null) {
            successCounter++;
        } else {
            errorCounter++;
        }
	}
	@Override
	public void close() {
        // 保存結果
        System.out.println("Successful sent: " + successCounter);
        System.out.println("Failed sent: " + errorCounter);
	}
}
  1. producer主程序
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.Producer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
public class InterceptorProducer {
	public static void main(String[] args) throws Exception {
		// 1 設置配置信息
		Properties props = new Properties();
		props.put("bootstrap.servers", "hadoop102:9092");
		props.put("acks", "all");
		props.put("retries", 0);
		props.put("batch.size", 16384);
		props.put("linger.ms", 1);
		props.put("buffer.memory", 33554432);
		props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
		props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
		// 2 構建攔截鏈
		List<String> interceptors = new ArrayList<>();
		interceptors.add("com.liujh.kafka.interceptor.TimeInterceptor"); 	interceptors.add("com.liujh.kafka.interceptor.CounterInterceptor"); 
		props.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, interceptors);
		String topic = "first";
		Producer<String, String> producer = new KafkaProducer<>(props);
		// 3 發送消息
		for (int i = 0; i < 10; i++) {
		    ProducerRecord<String, String> record = new ProducerRecord<>(topic, "message" + i);
		    producer.send(record);
		}
		// 4 一定要關閉producer,這樣纔會調用interceptor的close方法
		producer.close();
	}
}

測試

  1. 在kafka上啓動消費者,然後運行客戶端java程序
[liujh@hadoop102 kafka]$ bin/kafka-console-consumer.sh \
--bootstrap-server hadoop102:9092 --from-beginning --topic first
1501904047034,message0
1501904047225,message1
1501904047230,message2
1501904047234,message3
1501904047236,message4
1501904047240,message5
1501904047243,message6
1501904047246,message7
1501904047249,message8
1501904047252,message9

關注微信公衆號
簡書:https://www.jianshu.com/u/0278602aea1d
CSDN:https://blog.csdn.net/u012387141

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