kafka的事務
Producer的事務
爲了實現跨分區跨會話的事務,需要引入一個全局唯一的TransactionID,並將Producer獲取的PID和TransactionId,這樣Producer重啓後就可以通過正在進行的TransactionID獲取原來的PID
爲了管理事務,Kafka引入了一個新的組件Transaction Coordinator.Producer就是通過和Transaction Coordinator交互獲得Transaction ID對應的任務狀態,Transaction Coordinator還負責將事務所有寫入kafka的內部Topic,這樣整個服務重啓,由於事務狀態得到保存,進行中的事務狀態可以得到恢復,從而繼續進行
Consumer事務
上述事務是對於Producer而言,事務的保證就會相對較弱,尤其無法保證Commit的信息被精準消費,這是由於Consumer可以通過offset訪問任意信息,而且不同SegmentFile聲明週期不同,同一事務的信息可能會出現重啓後被刪除的情況
Kafka API
消息發送流程
kafka的Producer發送消息採用的是異步發送的方式。在消息發送的過程中,涉及到了兩個線程----main線程和Sender線程,以及一個線程共享變量----RecordAccumulator.mian線程將消息發送給RecoderAccumulator,Sender線程不斷從RecordAccumulator中拉取消息發送到Kafka broker.
相關參數
batch.size:只有數據積累到batch.size之後,sender纔會發送數據
linger.ms:如果數據遲遲未達到batch.size,sender等待linger.time之後就會發送數據
Kafka API
caution:同一個消費者組裏的消費者不能消費同一個topic中的同一個partion
導入依賴
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>0.11.0.0</version>
</dependency>
生產者
package com.song.producer;
import org.apache.kafka.clients.KafkaClient;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import java.util.Properties;
public class MyProducer {
public static void main(String[] args) {
//1.創建kafka生產者的配置信息
Properties properties = new Properties();
//2.指定連接的kafka集羣 broker-list----->參數可以不用記參考
properties.put( ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "hadoop01:9092");
//3.ack應答級別
properties.put("acks", 3);
//4.重試次數
properties.put("retries", 3);
//5.批次的大小
properties.put("batch.size", 16384); //16k
//6.等待時間
properties.put("linger.ms", 1);
//7.RecordAccumulator緩衝區大小
properties.put("buffer.memory", 33554432);
//8.kv 序列化
properties.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
properties.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
//9.創建producer
KafkaProducer<String, String> producer = new KafkaProducer<>(properties);
//10.發送數據-->輪詢發送兩個分區
for (int i = 0; i < 10; i++) {
producer.send(new ProducerRecord<>("first","song---"+i));
}
//11.關閉資源
producer.close();
}
//測試,需要先創建一個topic[first]: bin/kafka-console-consumer.sh --zookeeper hadoop02:2181 --topic first
}
帶回調信息的producer
package com.song.producer;
import org.apache.kafka.clients.producer.*;
import java.util.Properties;
public class CallBackProducer {
public static void main(String[] args) {
//1.創建配置信息
Properties properties = new Properties();
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"hadoop01:9092");
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");
//2.創建生產者對象
KafkaProducer<String, String> producer = new KafkaProducer<>(properties);
//3.發送數據
for (int i = 0; i < 10; i++) {
producer.send(new ProducerRecord<>("first", "song-->" + i), (recordMetadata, e) -> {
if (e==null){
System.out.println(recordMetadata.partition());
System.out.println(recordMetadata.offset());
}else {
e.printStackTrace();
}
});
//4.關閉資源
producer.close();
}
}
}
自定義分區器
package com.song.partitioner;
import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.common.Cluster;
import org.apache.kafka.common.PartitionInfo;
import org.apache.kafka.common.internals.Topic;
import java.util.List;
import java.util.Map;
//自定義的組件不是繼承就是實現
//自定義分區可以參考DefaultPartitioner
public class MyPartitioner implements Partitioner {
//k,v已經是序列化的數據了
@Override
public int partition(String topic, Object key, byte[] bytes, Object value, byte[] bytes1, Cluster cluster) {
//確定了存活的partitioner
List<PartitionInfo> partitionInfos = cluster.availablePartitionsForTopic(topic);
/**
Integer count = cluster.partitionCountForTopic(topic);
return key.toString().hashCode()%count;
*/
//這裏簡單點,都寫到1號分區
return 1;
}
@Override
public void close() {
}
@Override
public void configure(Map<String, ?> map) {
}
}
使用自定義分區器,添加到producer配置裏面,使用send裏面的回調函數查看結果
....
properties.put(ProducerConfig.PARTITIONER_CLASS_CONFIG,"com.song.partitioner.MyPartitioner");
....
for (int i = 0; i < 10; i++) {
producer.send(new ProducerRecord<>("first-topic", "song-key","hehehe-value" + i), new Callback() {
@Override
public void onCompletion(RecordMetadata recordMetadata, Exception e) {
if (e==null){
System.out.println("分區 :"+recordMetadata.partition());
}else {
e.printStackTrace();
}
}
});
}
...
同步發送 API
同步發送的意思就是,一條消息發送之後,會阻塞當前線程,直至返回 ack。 由於 send 方法返回的是一個 Future 對象,根據 Futrue 對象的特點【futrue.get()獲取的返回值會阻塞線程】,我們也可以實現同步發送的效果,只需在調用 Future 對象的 get 方發即可。
Consumer API
Consumer 消費數據時的可靠性是很容易保證的,因爲數據在 Kafka 中是持久化的,故
不用擔心數據丟失問題。
由於 consumer 在消費過程中可能會出現斷電宕機等故障,consumer 恢復後,需要從故
障前的位置的繼續消費,所以 consumer 需要實時記錄自己消費到了哪個 offset,以便故障恢
復後繼續消費。
所以 offset 的維護是 Consumer 消費數據是必須考慮的問題。
package com.song.consumer;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.common.serialization.StringDeserializer;
import java.util.Arrays;
import java.util.Properties;
public class MyConsumer {
public static void main(String[] args) {
Properties properties = new Properties();
properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "hadoop01:9092");
//開啓自動提交offset
properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, true);
//自動提交的延遲
properties.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "1000"); // 默認自動提交
//k,v的反序列化
properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer");
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer");
//自定義的消費者組
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "bigdata");
//重置消費者offset[earliest,later],換組 或者 offSet失效 時這個參數生效
properties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(properties);
//訂閱主題,也可以是多個主題
consumer.subscribe(Arrays.asList("first", "second"));
while (true) {
//獲取數據,
ConsumerRecords<String, String> consumerRecords = consumer.poll(100);
//解析並打印
for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
String key = consumerRecord.key();
String value = consumerRecord.value();
System.out.println(key + "--->" + value);
}
}
}
}
notice : 當offset不設置爲自動提交時,只有在每一次啓動時纔會讀取保存在__consumer或者保存在zk裏面的舊的offset,否則consumer會讀取維護在內存中的offset
手動提交offset(瞭解)
由於自動提交是基於時間提交的,開發人員難以把握offset提交的時機,因此kafka提供了手動提交offset的API
手動提交有兩種:commitAsync(異步) commitSync(同步)
//設置手動提交
....
properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
...
while(true){
...
//異步提交
consumer.commitAsync(new OffsetCommitCallback() { @Override public void onComplete(Map<TopicPartition, OffsetAndMetadata> offsets, Exception exception) { if (exception != null) { System.err.println("Commit failed for" + offsets); } } });
}
官方提供了一種自定義offset(consumer 最終解決方案)
Kafka 0.9 版本之前,offset 存儲在 zookeeper,0.9 版本及之後,默認將 offset 存儲在 Kafka的一個內置的 topic 中。除此之外,Kafka 還可以選擇自定義存儲 offset。 offset 的維護是相當繁瑣的,因爲需要考慮到消費者的 Rebalance。 當有新的消費者加入消費者組、已有的消費者推出消費者組或者所訂閱的主題的分區發生變化,就會觸發到分區的重新分配,重新分配的過程叫做 Rebalance。 消費者發生 Rebalance 之後,每個消費者消費的分區就會發生變化。因此消費者要首先獲取到自己被重新分配到的分區,並且定位到每個分區最近提交的 offset 位置繼續消費。
要實現自定義存儲 offset,需要藉助 ConsumerRebalanceListener,以下爲示例代碼,其中提交和獲取 offset 的方法,需要根據所選的 offset 存儲系統自行實現。
package com.song.consumer;
import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.common.TopicPartition;
import java.util.*;
import java.util.function.Consumer;
public class CustomerConsumer {
private static Map<TopicPartition, Long> currentOffset = new HashMap<>();
public static void main(String[] args) {
//創建配置信息
Properties properties = new Properties();
properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "hadoop01:9092");
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "bigdata");
properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
//k,v的反序列化
properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer");
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer");
//創建消費者
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(properties);
//訂閱主題,也可以是多個主題
consumer.subscribe(Arrays.asList("first", "second"), new ConsumerRebalanceListener() {
//before rebanlce
@Override
public void onPartitionsRevoked(Collection<TopicPartition> collection) {
commitOffset(currentOffset);
}
//after rebanlce
@Override
public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
partitions.clear();
for (TopicPartition partition : partitions) {
//定位到最近提交offset位置繼續
consumer.seek(partition, getOffset(partition));
}
}
});
while (true) {
ConsumerRecords<String, String> consumerRecords = consumer.poll(100);
for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
System.out.printf("offset = %d, key = %s, value = %s%n", consumerRecord.offset(), consumerRecord.key(), consumerRecord.value());
currentOffset.put(new TopicPartition(consumerRecord.topic(), consumerRecord.partition()), consumerRecord.offset());
}
//異步提交
commitOffset(currentOffset);
}
}
//獲取某分區最新的offset
private static long getOffset(TopicPartition partition) {
return 0;
}
//提交該消費者所有分區的offset
private static void commitOffset(Map<TopicPartition, Long> currentOffset) {
}
}
自定義攔截器
攔截器原理
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,主要用於執行一些資源清理工作
攔截器案例
1)需求:
實現一個簡單的雙 interceptor 組成的攔截鏈。第一個 interceptor 會在消息發送前將時間
戳信息加到消息 value 的最前部;第二個 interceptor 會在消息發送後更新成功發送消息數或
失敗發送消息數。
package com.song.interceptor;
import org.apache.kafka.clients.producer.ProducerInterceptor;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
import java.util.Map;
public class TimeInterceptor implements ProducerInterceptor {
@Override
public void configure(Map<String, ?> map) {
}
@Override
public ProducerRecord onSend(ProducerRecord producerRecord) {
//取出數據
Object value = producerRecord.value();
//創建一個對象
return new ProducerRecord(producerRecord.topic(), producerRecord.partition(), producerRecord.timestamp(), producerRecord.key(), System.currentTimeMillis() + "," + value);
}
@Override
public void onAcknowledgement(RecordMetadata recordMetadata, Exception e) {
}
@Override
public void close() {
}
}
package com.song.interceptor;
import org.apache.kafka.clients.producer.ProducerInterceptor;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
import java.util.Map;
public class CounterInterceptor implements ProducerInterceptor {
int success;
int error;
@Override
public ProducerRecord onSend(ProducerRecord producerRecord) {
return producerRecord;
}
@Override
public void onAcknowledgement(RecordMetadata recordMetadata, Exception e) {
if (recordMetadata!=null){
success++;
}else {
error++;
}
}
@Override
public void close() {
System.out.println("success"+success);
System.out.println("error"+error);
}
@Override
public void configure(Map<String, ?> map) {
}
}
攔截器和分區器一樣 ,在配置文件中添加
....
interceptors.add("com.song.interceptor.TimeInterceptor");
interceptors.add("com.song.interceptor.CounterInterceptor");
properties.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG,interceptors);
...
Kafka監控
Kafka Eagle
1.修改 kafka 啓動命令 修改 kafka-server-start.sh 命令中
if [ "x$KAFKA_HEAP_OPTS" = "x" ]; then
export KAFKA_HEAP_OPTS="-Xmx1G -Xms1G"
fi
爲
if [ "x$KAFKA_HEAP_OPTS" = "x" ]; then
export KAFKA_HEAP_OPTS="-server -Xms2G -Xmx2G -XX:PermSize=128m -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:ParallelGCThreads=8 XX:ConcGCThreads=5 -XX:InitiatingHeapOccupancyPercent=70"
export JMX_PORT="9999"
#export KAFKA_HEAP_OPTS="-Xmx1G -Xms1G"
fi
注意:修改之後在啓動 Kafka 之前要分發之其他節點
2.上傳壓縮包 kafka-eagle-bin-1.3.7.tar.gz 到集羣/opt/software 目錄
3.解壓到本地
tar -zxvf kafka-eagle-bin1.3.7.tar.gz
#進入到解壓的目錄
#將 kafka-eagle-web-1.3.7-bin.tar.gz 解壓至/opt/module
tar -zxvf kafka-eagleweb-1.3.7-bin.tar.gz -C /opt/module/
#修改名稱
mv kafka-eagle-web-1.3.7/ eagle
#.給啓動文件執行權限
bin]chmod 777 ke.sh
4.修改配置文件
kafka.eagle.zk.cluster.alias=cluster1
cluster1.zk.list=hadoop01:2181,hadoop02:2181,hadoop03:2181,
cluster1.kafka.eagle.offset.storage=kafka
kafka.eagle.metrics.charts=true
kafka.eagle.sql.fix.error=false
kafka.eagle.driver=com.mysql.jdbc.Driver
kafka.eagle.url=jdbc:mysql://hadoop01:3306/ke?useUnicode=true&ch aracterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull
kafka.eagle.username=root
kafka.eagle.password=000000
5.添加環境變量
export KE_HOME=/opt/module/eagle
export PATH=$PATH:$KE_HOME/bin
source /etc/profile
6.啓動(注意:啓動之前需要先啓動 ZK 以及 KAFKA )
eagle]$ bin/ke.sh start
7.登錄頁面查看監控數據
http://hadoop01:8084/ke
Kafka對接flume 略