Kafka Api 練習

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 略

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