Hadoop生態圈(九):kafka

目錄

1  Kafka概述

1.1 消息隊列

1.2 爲什麼需要消息隊列

1.3 什麼是Kafka

1.4 Kafka架構

2 Kafka集羣部署

2.1 環境準備

2.1.1 集羣規劃

2.1.2 下載

2.2 Kafka集羣部署

2.3 Kafka命令行操作

3 Kafka工作流程分析

3.1 Kafka生產過程分析

3.1.1 寫入方式

3.1.2 分區(Partition)

3.1.3 副本(Replication)

3.1.4 寫入流程

3.2 Broker 保存消息

3.2.1 存儲方式

3.2.2 存儲策略(消息刪除策略)

3.3 Kafka消費過程分析

3.3.1 高級API

3.3.2 低級API[瞭解]

3.3.3 消費者組

3.3.4 消費方式

3.3.5 消費者組案例

4 Kafka的API實戰

4.1 環境準備

4.2 Kafka生產者javaAPI

4.2.1 創建生產者

4.2.2  創建帶回調函數的生產者

4.2.3 自定以分區生產者

4.3 Kafka消費者javaAPI

4.3.1 高級API

4.3.2 低級API

5 kafka product攔截器(interceptor)

5.1 攔截器原理

5.2 攔截器案例

6 Flume與kafka集成


1  Kafka概述

1.1 消息隊列

(1)點對點模式(一對一,消費者主動拉取數據,消息收到後消息清除)

點對點模型通常是一個基於拉取或者輪詢的消息傳送模型,這種模型從隊列中請求信息,而不是將消息推送到客戶端。這個模型的特點是發送到隊列的消息被一個且只有一個接收者接收處理,即使有多個消息監聽者也是如此

(2)發佈/訂閱模式(一對多,數據生產後,推送給所有訂閱者)

發佈訂閱模型則是一個基於推送的消息傳送模型。發佈訂閱模型可以有多種不同的訂閱者,臨時訂閱者只在主動監聽主題時才接收消息,而持久訂閱者則監聽主題的所有消息,即使當前訂閱者不可用,處於離線狀態。

1.2 爲什麼需要消息隊列

1)解耦:

允許你獨立的擴展或修改兩邊的處理過程,只要確保它們遵守同樣的接口約束。

2)冗餘:

消息隊列把數據進行持久化直到它們已經被完全處理,通過這一方式規避了數據丟失風險。許多消息隊列所採用的"插入-獲取-刪除"範式中,在把一個消息從隊列中刪除之前,需要你的處理系統明確的指出該消息已經被處理完畢,從而確保你的數據被安全的保存直到你使用完畢。

3)擴展性:

因爲消息隊列解耦了你的處理過程,所以增大消息入隊和處理的頻率是很容易的,只要另外增加處理過程即可。

4)靈活性&峯值處理能力:

在訪問量劇增的情況下,應用仍然需要繼續發揮作用,但是這樣的突發流量並不常見。如果爲以能處理這類峯值訪問爲標準來投入資源隨時待命無疑是巨大的浪費。使用消息隊列能夠使關鍵組件頂住突發的訪問壓力,而不會因爲突發的超負荷的請求而完全崩潰。

5)可恢復性:

系統的一部分組件失效時,不會影響到整個系統。消息隊列降低了進程間的耦合度,所以即使一個處理消息的進程掛掉,加入隊列中的消息仍然可以在系統恢復後被處理。

6)順序保證:

在大多使用場景下,數據處理的順序都很重要。大部分消息隊列本來就是排序的,並且能保證數據會按照特定的順序來處理。(Kafka保證一個Partition內的消息的有序性)

7)緩衝:

有助於控制和優化數據流經過系統的速度,解決生產消息和消費消息的處理速度不一致的情況

8)異步通信:

很多時候,用戶不想也不需要立即處理消息。消息隊列提供了異步處理機制,允許用戶把一個消息放入隊列,但並不立即處理它。想向隊列中放入多少消息就放多少,然後在需要的時候再去處理它們。

1.3 什麼是Kafka

一個開源消息系統,一個分佈式消息隊列;目的提供一個統一、高通量、低等待的平臺;依賴於Zookeeper來保證系統的可用性

在流式計算中。Kafka一般用於緩存數據,Storm或者spark通過消費Kafka的數據進行計算。

1)Apache Kafka是一個開源消息系統,由Scala寫成。是由Apache軟件基金會開發的一個開源消息系統項目。

2)Kafka最初是由LinkedIn公司開發,並於2011年初開源。2012年10月從Apache Incubator畢業。該項目的目標是爲處理實時數據提供一個統一、高通量、低等待的平臺。

3)Kafka是一個分佈式消息隊列Kafka對消息保存時根據Topic進行歸類,發送消息者稱爲Producer,消息接受者稱爲Consumer,此外kafka集羣有多個kafka實例組成,每個實例(server)稱爲broker。

4)無論是kafka集羣,還是consumer都依賴於zookeeper集羣保存一些meta信息,來保證系統可用性。

1.4 Kafka架構

Kafka整體架構:

Kafka詳細架構:

組件介紹:

1)Producer :消息生產者,就是向kafka broker發消息的客戶端;

2)Consumer :消息消費者,向kafka broker取消息的客戶端;

3)Topic :可以理解爲一個隊列(就是同一個業務的數據放在一個topic下)

4) Consumer Group (CG):這是kafka用來實現一個topic消息的廣播(發給所有的consumer)和單播(發給任意一個consumer)的手段。一個topic可以有多個CG。topic的消息會複製(不是真的複製,是概念上的)到所有的CG,但每個partion只會把消息發給該CG中的一個consumer。如果需要實現廣播,只要每個consumer有一個獨立的CG就可以了。要實現單播只要所有的consumer在同一個CG。用CG還可以將consumer進行自由的分組而不需要多次發送消息到不同的topic;

5)Broker :一臺kafka服務器就是一個broker。一個集羣由多個broker組成。一個broker可以容納多個topic;

6)Partition:爲了實現擴展性,一個非常大的topic可以分佈到多個broker(即服務器)上,一個topic可以分爲多個partition,每個partition是一個有序的隊列。partition中的每條消息都會被分配一個有序的id(offset)。kafka只保證按一個partition中的順序將消息發給consumer,不保證一個topic的整體(多個partition間)的順序;

7)Offset:偏移量。

2 Kafka集羣部署

2.1 環境準備

2.1.1 集羣規劃

hadoop101

hadoop102 hadoop103
zk zk zk
Kafka Kafka Kafka

2.1.2 下載

官網下載:http://kafka.apache.org/downloads.html

網盤鏈接:請點這裏     提取碼:lm8k 

2.2 Kafka集羣部署

1)解壓安裝包

[root@hadoop101 software]$ tar -zxvf kafka_2.11-0.11.0.0.tgz -C /opt/module/

2)修改解壓後的文件名稱

[root@hadoop101 module]$ mv kafka_2.11-0.11.0.0/ kafka

3)在/opt/module/kafka目錄下創建logs文件夾

[root@hadoop101 kafka]$ mkdir logs

4)修改配置文件

[root@hadoop101 kafka]$ cd config/

[root@hadoop101 config]$ vim server.properties

#broker全局唯一編號,不能重複

broker.id=1

#刪除topic功能使能

delete.topic.enable=true

#kafka運行日誌存放的路徑

log.dirs=/opt/module/kafka/logs

#配置連接Zookeeper集羣地址

zookeeper.connect=hadoop101:2181,hadoop102:2181,hadoop103:2181

5)分發安裝包

6)分別在hadoop102和hadoop103上修改配置文件/opt/module/kafka/config/server.properties中的broker.id=2、broker.id=3

注:broker.id不得重複

7)啓動集羣

依次在hadoop101、hadoop102、hadoop103節點上啓動kafka

[root@hadoop101 kafka]$ bin/kafka-server-start.sh config/server.properties &

[root@hadoop102 kafka]$ bin/kafka-server-start.sh config/server.properties &

[root@hadoop103 kafka]$ bin/kafka-server-start.sh config/server.properties &

8)關閉集羣

[root@hadoop101 kafka]$ bin/kafka-server-stop.sh stop

[root@hadoop102 kafka]$ bin/kafka-server-stop.sh stop

[root@hadoop103 kafka]$ bin/kafka-server-stop.sh stop

2.3 Kafka命令行操作

1)查看當前服務器中的所有topic

[root@hadoop101 kafka]$ bin/kafka-topics.sh --zookeeper hadoop101:2181 --list

2)創建topic

[root@hadoop101 kafka]$ bin/kafka-topics.sh --zookeeper hadoop101:2181 --create --replication-factor 3 --partitions 1 --topic first

選項說明:

--topic 定義topic名

--replication-factor  定義副本數

--partitions  定義分區數

3)刪除topic

[root@hadoop101 kafka]$ bin/kafka-topics.sh --zookeeper hadoop101:2181 --delete --topic first

需要server.properties中設置delete.topic.enable=true否則只是標記刪除或者直接重啓。

4)發送消息

創建second主題

bin/kafka-topics.sh --zookeeper hadoop101:2181 --create --replication-factor 3 --partitions 3 --topic second

[root@hadoop101 kafka]$ bin/kafka-console-producer.sh --broker-list hadoop101:9092 --topic second

>hello world

>bigdata  bigdata

5)消費消息

[root@hadoop102 kafka]$ bin/kafka-console-consumer.sh --zookeeper hadoop101:2181 --topic second --from-beginning

--from-beginning會把first主題中以往所有的數據都讀取出來。根據業務場景選擇是否增加該配置。

6)查看某個Topic的詳情

[root@hadoop101 kafka]$ bin/kafka-topics.sh --zookeeper hadoop101:2181 --describe --topic second

3 Kafka工作流程分析

Create topic(創建topic的時候指定該topic分區數、副本數);此處對比create table  Producer向kafka的topic發送消息

3.1 Kafka生產過程分析

3.1.1 寫入方式

producer採用推(push)模式將消息發佈到broker,每條消息都被追加(append)到分區(patition)中,屬於順序寫磁盤(順序寫磁盤效率比隨機寫內存要高,保障kafka吞吐率)。

3.1.2 分區(Partition)

消息發送時都被髮送到一個topic,其本質就是一個目錄,而topic是由一些Partition Logs(分區日誌)組成,其組織結構如下圖所示:

我們可以看到,每個Partition中的消息都是有序的,生產的消息被不斷追加到Partition log上,其中的每一個消息都被賦予了一個唯一的offset值

1)分區的原因

(1)方便在集羣中擴展,每個Partition可以通過調整以適應它所在的機器,而一個topic又可以有多個Partition組成,因此整個集羣就可以適應任意大小的數據了;

(2)可以提高併發,因爲可以以Partition爲單位讀寫了。

2)分區的原則

(1)指定了patition,則直接使用;    <1,  “我是中國人”>

(2)未指定patition但指定key,通過對key的值進行hash出一個patition;

(3)patition和key都未指定,使用輪詢選出一個patition。

DefaultPartitioner類
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;
        }
    }

3.1.3 副本(Replication)

副本是針對分區的副本

同一個partition可能會有多個replication(對應 server.properties 配置中的 default.replication.factor=N)。沒有replication的情況下,一旦broker 宕機,其上所有 patition 的數據都不可被消費,同時producer也不能再將數據存於其上的patition。引入replication之後,同一個partition可能會有多個replication,而這時需要在這些replication之間選出一個leader,producer和consumer只與這個leader交互,其它replication作爲follower從leader 中複製數據。

3.1.4 寫入流程

producer寫入消息流程如下

1)producer先從zookeeper的 "/brokers/.../state"節點找到該partition的leader

2)producer將消息發送給該leader

3)leader將消息寫入本地log

4)followers從leader pull消息,寫入本地log後向leader發送ACK

5)leader收到所有ISR中的replication的ACK後向producer發送ACK

3.2 Broker 保存消息

3.2.1 存儲方式

物理上把topic分成一個或多個patition(對應 server.properties 中的num.partitions=3配置),每個patition物理上對應一個文件夾(該文件夾存儲該patition的所有消息和索引文件),如下:

[root@hadoop101 logs]$ ll

drwxrwxr-x. 2 root root 4096 8月   6 14:37 first-0

drwxrwxr-x. 2 root root 4096 8月   6 14:35 first-1

drwxrwxr-x. 2 root root 8月   6 14:37 first-2

[root@hadoop101 logs]$ cd first-0

[root@hadoop101 first-0]$ ll

-rw-rw-r--. 1 root root 10485760 8月   6 14:33 00000000000000000000.index

-rw-rw-r--. 1 root root 219 8月   6 15:07 00000000000000000000.log

-rw-rw-r--. 1 root root 10485756 8月   6 14:33 00000000000000000000.timeindex

-rw-rw-r--. 1 root root 8 8月   6 14:37 leader-epoch-checkpoint

3.2.2 存儲策略(消息刪除策略)

無論消息是否被消費,kafka都會保留所有消息。有兩種策略可以刪除舊數據:

1)基於時間:log.retention.hours=168

2)基於大小:log.retention.bytes=1073741824

需要注意的是,因爲Kafka讀取特定消息的時間複雜度爲O(1),即與文件大小無關,所以這裏刪除過期文件與提高 Kafka 性能無關。

3.3 Kafka消費過程分析

kafka提供了兩套consumer API:高級Consumer API和低級Consumer API。

3.3.1 高級API

1)高級API優點

高級API 寫起來簡單

不需要自行去管理offset,系統通過zookeeper自行管理

不需要管理分區,副本等情況,.系統自動管理

消費者斷線會自動根據上一次記錄在zookeeper中的offset去接着獲取數據(默認設置1分鐘更新一下zookeeper中存的offset)

可以使用group來區分對同一個topic 的不同程序訪問分離開來(不同的group記錄不同的offset,這樣不同程序讀取同一個topic纔不會因爲offset互相影響)

2)高級API缺點

不能自行控制offset(對於某些特殊需求來說)

不能細化控制如分區、副本、zk等

3.3.2 低級API[瞭解]

1)低級 API 優點

能夠開發者自己控制offset,想從哪裏讀取就從哪裏讀取。

自行控制連接分區,對分區自定義進行負載均衡

對zookeeper的依賴性降低(如:offset不一定非要靠zk存儲,自行存儲offset即可,比如存在文件或者內存中)

2)低級API缺點

太過複雜,需要自行控制offset,連接哪個分區,找到分區leader 等。

3.3.3 消費者組

消費者是以consumer group消費者組的方式工作,由一個或者多個消費者組成一個組,共同消費一個topic。每個分區在同一時間只能由group中的一個消費者讀取,但是多個group可以同時消費這個partition。在圖中,有一個由三個消費者組成的group,有一個消費者讀取主題中的兩個分區,另外兩個分別讀取一個分區。某個消費者讀取某個分區,也可以叫做某個消費者是某個分區的擁有者。

在這種情況下,消費者可以通過水平擴展的方式同時讀取大量的消息。另外,如果一個消費者失敗了,那麼其他的group成員會自動負載均衡讀取之前失敗的消費者讀取的分區。

3.3.4 消費方式

Consumer採用pull(拉)模式從broker中讀取數據。

push(推)模式很難適應消費速率不同的消費者,因爲消息發送速率是由broker決定的。它的目標是儘可能以最快速度傳遞消息,但是這樣很容易造成consumer來不及處理消息,典型的表現就是拒絕服務以及網絡擁塞。而pull模式則可以根據consumer的消費能力以適當的速率消費消息。

對於Kafka而言,pull模式更合適,它可簡化broker的設計,consumer可自主控制消費消息的速率,同時consumer可以自己控制消費方式。

pull模式不足之處是,如果kafka沒有數據,消費者可能會陷入循環中,一直等待數據到達。

3.3.5 消費者組案例

消費者組效果

1)需求:測試同一個消費者組中的消費者,同一時刻只能有一個消費者消費。

2)案例實操

(1)在hadoop102、hadoop103上修改/opt/module/kafka/config/consumer.properties配置文件中的group.id屬性爲任意組名。

[root@hadoop102 config]$ vim consumer.properties

group.id=hadoop_kafka

(2)在hadoop102、hadoop103上分別啓動消費者

[root@hadoop102 kafka]$ bin/kafka-console-consumer.sh --zookeeper hadoop101:2181 --topic second --consumer.config config/consumer.properties

[root@hadoop103 kafka]$ bin/kafka-console-consumer.sh --zookeeper hadoop101:2181 --topic second --consumer.config config/consumer.properties

(3)在hadoop101上啓動生產者

[root@hadoop101 kafka]$ bin/kafka-console-producer.sh \

--broker-list hadoop101:9092 --topic second

>hello world

(4)查看hadoop102和hadoop103的接收者。

同一時刻只有一個消費者接收到消息。

4 Kafka的API實戰

4.1 環境準備

1)啓動zk和kafka集羣,在kafka集羣中打開一個消費者

[bigdata@hadoop101 kafka]$ bin/kafka-console-consumer.sh --zookeeper hadoop101:2181 --topic second

2)導入下面的依賴

        <!-- https://mvnrepository.com/artifact/org.apache.kafka/kafka-clients -->
        <dependency>
            <groupId>org.apache.kafka</groupId>
            <artifactId>kafka-clients</artifactId	>
            <version>0.11.0.0</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.apache.kafka/kafka -->
        <dependency>
            <groupId>org.apache.kafka</groupId>
            <artifactId>kafka_2.12</artifactId>
            <version>0.11.0.0</version>
        </dependency>

4.2 Kafka生產者javaAPI

4.2.1 創建生產者

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;

public class NewProducer {
    public static void main(String[] args) {
        Properties props = new Properties();
        // Kafka服務端的主機名和端口號
        props.put("bootstrap.servers", "hadoop103:9092");
        // 等待所有副本節點的應答
        props.put("acks", "all");
        // 消息發送最大嘗試次數
        props.put("retries", 0);
        // 發送緩存區內存大小
        props.put("buffer.memory", 33554432);
        // key序列化
        props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        // value序列化
        props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");

        Producer<String, String> producer = new KafkaProducer<>(props);
        for (int i = 0; i < 50; i++) {
            producer.send(new ProducerRecord<String, String>("second", Integer.toString(i), "hello world-" + i));
        }
        producer.close();
    }
}

4.2.2  創建帶回調函數的生產者

import org.apache.kafka.clients.producer.*;
import java.util.Properties;

public class CallBackProducer {
	public static void main(String[] args) {
		Properties props = new Properties();
		// Kafka服務端的主機名和端口號
		props.put("bootstrap.servers", "hadoop103:9092");
		// 等待所有副本節點的應答
		props.put("acks", "all");
		// 消息發送最大嘗試次數
		props.put("retries", 0);
		// 發送緩存區內存大小
		props.put("buffer.memory", 33554432);
		// key序列化
		props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
		// value序列化
		props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");

		// 自定義分區
		props.put("partitioner.class", "com.bigdata.CustomPartitioner");

		Producer<String, String> kafkaProducer = new KafkaProducer<>(props);

		for (int i = 0; i < 50; i++) {
			kafkaProducer.send(new ProducerRecord<String, String>("second", "hello" + i), new Callback() {
				@Override
				public void onCompletion(RecordMetadata metadata, Exception exception) {
					if (metadata != null) {
						System.err.println(metadata.partition() + "---" + metadata.offset());
					}
				}
			});
		}
		kafkaProducer.close();
	}
}

4.2.3 自定以分區生產者

1)需求:將所有數據存儲到topic的第0號分區上

2)自定義分區

import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.common.Cluster;

import java.util.Map;

public class CustomPartitioner implements Partitioner {

	@Override
	public void configure(Map<String, ?> configs) {
	}
	@Override
	public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        // 控制分區
		return 0;
	}
	@Override
	public void close() {
	}
}

3)調用自定義分區

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;

public class PartitionerProducer {

	public static void main(String[] args) {
		
		Properties props = new Properties();
		// Kafka服務端的主機名和端口號
		props.put("bootstrap.servers", "hadoop103:9092");
		// 等待所有副本節點的應答
		props.put("acks", "all");
		// 消息發送最大嘗試次數
		props.put("retries", 0);
		// 發送緩存區內存大小
		props.put("buffer.memory", 33554432);
		// key序列化
		props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
		// value序列化
		props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
		// 自定義分區
		props.put("partitioner.class", "com.bigdata.CustomPartitioner");

		Producer<String, String> producer = new KafkaProducer<>(props);
		producer.send(new ProducerRecord<String, String>("second", "1", "bigdata"));

		producer.close();
	}
}

4)測試

      在hadoop101上監控/opt/module/kafka/logs/目錄下first主題3個分區的log日誌動態變化情況

[root@hadoop101 first-0]$ tail -f 00000000000000000000.log

[root@hadoop101 first-1]$ tail -f 00000000000000000000.log

[root@hadoop101 first-2]$ tail -f 00000000000000000000.log

4.3 Kafka消費者javaAPI

4.3.1 高級API

1)在控制檯創建生產者

[root@hadoop101 kafka]$ bin/kafka-console-producer.sh --broker-list hadoop101:9092 --topic second

>hello world

2)官方提供案例(自動維護消費情況)

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 CustomNewConsumer {

	public static void main(String[] args) {

		Properties props = new Properties();
		// 定義kakfa 服務的地址,不需要將所有broker指定上 
		props.put("bootstrap.servers", "hadoop101:9092");
		// 制定consumer group 
		props.put("group.id", "test");
		// 是否自動確認offset 
		props.put("enable.auto.commit", "true");
		// 自動確認offset的時間間隔 
		props.put("auto.commit.interval.ms", "1000");
		// key的序列化類
		props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
		// value的序列化類 
		props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
		// 定義consumer 
		KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
		
		// 消費者訂閱的topic, 可同時訂閱多個 
		consumer.subscribe(Arrays.asList("first", "second","third"));

		while (true) {
			// 讀取數據,讀取超時時間爲100ms 
			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());
		}
	}
}

4.3.2 低級API

實現使用低級API讀取指定的topic,指定partition,指定offset的數據

1)方法描述:

findLeader()

客戶端向種子節點發送主題元數據,將副本集加入備用節點

getLastOffset()

消費者客戶端發送偏移量請求,獲取分區最近的偏移量

run()

消費者低級AP I拉取消息的主要方法

findNewLeader()

當分區的主副本節點發生故障,客戶將要找出新的主副本

2)代碼:

import kafka.api.FetchRequest;
import kafka.api.FetchRequestBuilder;
import kafka.api.PartitionOffsetRequestInfo;
import kafka.cluster.BrokerEndPoint;
import kafka.common.ErrorMapping;
import kafka.common.TopicAndPartition;
import kafka.javaapi.*;
import kafka.javaapi.consumer.SimpleConsumer;
import kafka.message.MessageAndOffset;

import java.nio.ByteBuffer;
import java.util.*;

public class SimpleExample {
    private List<String> m_replicaBrokers = new ArrayList<>();

    public SimpleExample() {
        m_replicaBrokers = new ArrayList<>();
    }

    public static void main(String args[]) {
        SimpleExample example = new SimpleExample();
        // 最大讀取消息數量
        long maxReads = Long.parseLong("3");
        // 要訂閱的topic
        String topic = "second";
        // 要查找的分區
        int partition = Integer.parseInt("0");
        // broker節點的ip
        List<String> seeds = new ArrayList<>();
        seeds.add("hadoop101");
        seeds.add("hadoop102");
        seeds.add("hadoop103");
        // 端口
        int port = Integer.parseInt("9092");
        try {
            example.run(maxReads, topic, partition, seeds, port);
        } catch (Exception e) {
            System.out.println("Oops:" + e);
            e.printStackTrace();
        }
    }

    public void run(long a_maxReads, String a_topic, int a_partition, List<String> a_seedBrokers, int a_port) throws Exception {
        // 獲取指定Topic partition的元數據
        PartitionMetadata metadata = findLeader(a_seedBrokers, a_port, a_topic, a_partition);
        if (metadata == null) {
            System.out.println("Can't find metadata for Topic and Partition. Exiting");
            return;
        }
        if (metadata.leader() == null) {
            System.out.println("Can't find Leader for Topic and Partition. Exiting");
            return;
        }
        String leadBroker = metadata.leader().host();
        String clientName = "Client_" + a_topic + "_" + a_partition;

        SimpleConsumer consumer = new SimpleConsumer(leadBroker, a_port, 100000, 64 * 1024, clientName);
        long readOffset = getLastOffset(consumer, a_topic, a_partition, kafka.api.OffsetRequest.EarliestTime(), clientName);
        int numErrors = 0;
        while (a_maxReads > 0) {
            if (consumer == null) {
                consumer = new SimpleConsumer(leadBroker, a_port, 100000, 64 * 1024, clientName);
            }
            FetchRequest req = new FetchRequestBuilder().clientId(clientName).addFetch(a_topic, a_partition, readOffset, 100000).build();
            FetchResponse fetchResponse = consumer.fetch(req);

            if (fetchResponse.hasError()) {
                numErrors++;
                // Something went wrong!
                short code = fetchResponse.errorCode(a_topic, a_partition);
                System.out.println("Error fetching data from the Broker:" + leadBroker + " Reason: " + code);
                if (numErrors > 5)
                    break;
                if (code == ErrorMapping.OffsetOutOfRangeCode()) {
                    // We asked for an invalid offset. For simple case ask for
                    // the last element to reset
                    readOffset = getLastOffset(consumer, a_topic, a_partition, kafka.api.OffsetRequest.LatestTime(), clientName);
                    continue;
                }
                consumer.close();
                consumer = null;
                leadBroker = findNewLeader(leadBroker, a_topic, a_partition, a_port);
                continue;
            }
            numErrors = 0;

            long numRead = 0;
            for (MessageAndOffset messageAndOffset : fetchResponse.messageSet(a_topic, a_partition)) {
                long currentOffset = messageAndOffset.offset();
                if (currentOffset < readOffset) {
                    System.out.println("Found an old offset: " + currentOffset + " Expecting: " + readOffset);
                    continue;
                }
                readOffset = messageAndOffset.nextOffset();
                ByteBuffer payload = messageAndOffset.message().payload();

                byte[] bytes = new byte[payload.limit()];
                payload.get(bytes);
                System.out.println(String.valueOf(messageAndOffset.offset()) + ": " + new String(bytes, "UTF-8"));
                numRead++;
                a_maxReads--;
            }

            if (numRead == 0) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException ie) {
                }
            }
        }
        if (consumer != null)
            consumer.close();
    }

    public static long getLastOffset(SimpleConsumer consumer, String topic, int partition, long whichTime, String clientName) {
        TopicAndPartition topicAndPartition = new TopicAndPartition(topic, partition);
        Map<TopicAndPartition, PartitionOffsetRequestInfo> requestInfo = new HashMap<TopicAndPartition, PartitionOffsetRequestInfo>();
        requestInfo.put(topicAndPartition, new PartitionOffsetRequestInfo(whichTime, 1));
        kafka.javaapi.OffsetRequest request = new kafka.javaapi.OffsetRequest(requestInfo, kafka.api.OffsetRequest.CurrentVersion(), clientName);
        OffsetResponse response = consumer.getOffsetsBefore(request);

        if (response.hasError()) {
            System.out.println("Error fetching data Offset Data the Broker. Reason: " + response.errorCode(topic, partition));
            return 0;
        }
        long[] offsets = response.offsets(topic, partition);
        return offsets[0];
    }


    private String findNewLeader(String a_oldLeader, String a_topic, int a_partition, int a_port) throws Exception {
        for (int i = 0; i < 3; i++) {
            boolean goToSleep = false;
            PartitionMetadata metadata = findLeader(m_replicaBrokers, a_port, a_topic, a_partition);
            if (metadata == null) {
                goToSleep = true;
            } else if (metadata.leader() == null) {
                goToSleep = true;
            } else if (a_oldLeader.equalsIgnoreCase(metadata.leader().host()) && i == 0) {
                // first time through if the leader hasn't changed give
                // ZooKeeper a second to recover
                // second time, assume the broker did recover before failover,
                // or it was a non-Broker issue
                //
                goToSleep = true;
            } else {
                return metadata.leader().host();
            }
            if (goToSleep) {
                    Thread.sleep(1000);
            }
        }
        System.out.println("Unable to find new leader after Broker failure. Exiting");
        throw new Exception("Unable to find new leader after Broker failure. Exiting");
    }

    private PartitionMetadata findLeader(List<String> a_seedBrokers, int a_port, String a_topic, int a_partition) {
        PartitionMetadata returnMetaData = null;
        loop:
        for (String seed : a_seedBrokers) {
            SimpleConsumer consumer = null;
            try {
                consumer = new SimpleConsumer(seed, a_port, 100000, 64 * 1024, "leaderLookup");
                List<String> topics = Collections.singletonList(a_topic);
                TopicMetadataRequest req = new TopicMetadataRequest(topics);
                kafka.javaapi.TopicMetadataResponse resp = consumer.send(req);

                List<TopicMetadata> metaData = resp.topicsMetadata();
                for (TopicMetadata item : metaData) {
                    for (PartitionMetadata part : item.partitionsMetadata()) {
                        if (part.partitionId() == a_partition) {
                            returnMetaData = part;
                            break loop;
                        }
                    }
                }
            } catch (Exception e) {
                System.out.println("Error communicating with Broker [" + seed + "] to find Leader for [" + a_topic + ", " + a_partition + "] Reason: " + e);
            } finally {
                if (consumer != null)
                    consumer.close();
            }
        }
        if (returnMetaData != null) {
            m_replicaBrokers.clear();
            for (BrokerEndPoint replica : returnMetaData.replicas()) {
                m_replicaBrokers.add(replica.host());
            }
        }
        return returnMetaData;
    }
}

5 kafka product攔截器(interceptor)

5.1 攔截器原理

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):

該方法會在消息被應答或消息發送失敗時調用,並且通常都是在producer回調邏輯觸發之前。onAcknowledgement運行在producer的IO線程中,因此不要在該方法中放入很重的邏輯,否則會拖慢producer的消息發送效率

(4)close:

關閉interceptor,主要用於執行一些資源清理工作

如前面所描述的,interceptor可能被運行在多個線程中,因此在具體實現時用戶需要自行確保線程安全。另外倘若指定了多個interceptor,則producer將按照指定順序調用它們,並僅僅是捕獲每個interceptor可能拋出的異常記錄到錯誤日誌中而非在向上傳遞。

5.2 攔截器案例

1)需求:

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

2)案例

    (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() {

	}
}

    (2)統計發送消息成功和發送失敗消息數,並在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);
	}
}

    (3)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", "hadoop101:9092");
		props.put("acks", "all");
		props.put("retries", 0);
		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.bigdata.kafka.interceptor.TimeInterceptor");
                interceptors.add("com.bigdata.kafka.interceptor.CounterInterceptor"); 
		props.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, interceptors);
		 
		String topic = "second";
		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();
	}
}

3)測試

(1)在kafka上啓動消費者,然後運行客戶端java程序。

[bigdata@hadoop101 kafka]$ bin/kafka-console-consumer.sh --zookeeper hadoop101:2181 --from-beginning --topic second

1501904047034,message0

1501904047225,message1

1501904047230,message2

1501904047234,message3

1501904047236,message4

1501904047240,message5

1501904047243,message6

1501904047246,message7

1501904047249,message8

1501904047252,message9

(2)觀察java平臺控制檯輸出數據如下:

Successful sent: 10

Failed sent: 0

6 Flume與kafka集成

1)配置flume(flume-kafka.conf)

# define

a1.sources = r1

a1.sinks = k1

a1.channels = c1

 

# source

a1.sources.r1.type = exec

a1.sources.r1.command = tail -F  /opt/module/datas/flume.log

a1.sources.r1.shell = /bin/bash -c

 

# sink

a1.sinks.k1.type = org.apache.flume.sink.kafka.KafkaSink

a1.sinks.k1.kafka.bootstrap.servers = hadoop101:9092,hadoop102:9092,hadoop103:9092

a1.sinks.k1.kafka.topic = second

a1.sinks.k1.kafka.flumeBatchSize = 20

a1.sinks.k1.kafka.producer.acks = -1

 

# channel

a1.channels.c1.type = memory

a1.channels.c1.capacity = 1000

a1.channels.c1.transactionCapacity = 100

 

# bind

a1.sources.r1.channels = c1

a1.sinks.k1.channel = c1

 

2) 啓動kafkaIDEA消費者

3) 進入flume根目錄下,啓動flume

$ bin/flume-ng agent -c conf/ -n a1 -f jobs/flume-kafka.conf

4) 向 /opt/module/datas/flume.log裏追加數據,查看kafka消費者消費情況

$ echo hello > /opt/module/datas/flume.log

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