十一、Spark Streaming和Kafaka

@Author : By Runsen
@Date : 2020/6/21

作者介紹:Runsen目前大三下學期,專業化學工程與工藝,大學沉迷日語,Python, Java和一系列數據分析軟件。導致翹課嚴重,專業排名中下。.在大學60%的時間,都在CSDN。

在一月到四月都沒怎麼寫博客,因爲決定寫書,結果出書方說大學生就是一個菜鳥,看我確實還是一個菜鳥,就更新到博客算了。

我把第九章更新到博客上。

9.6 Spark

9.6.4 Spark Streaming

Spark Streaming 是一套優秀的實時計算框架。其良好的可擴展性、高吞吐量以及容錯機制能夠滿足我們很多的場景應用。Kafka 是一個分佈式的基於發佈,訂閱模式的消息隊列(Message Queue),主要應用於
大數據實時處理領域,可以處理消費者在網站中的所有動作流數據。因此,很多企業採用spark streaming流式處理kafka中的數據。

(1)Kafaka基本架構和安裝

Kafaka採用的是拓撲結構,拓撲結構是指網絡中各個站點相互連接的形式,Kafaka基本架構如下圖9-20所示。

  • producer:消息生產者,發佈消息到 kafka 集羣的終端。
  • broker:kafka 集羣中包含的服務器。
  • topic:每條發佈到 kafka 集羣的消息屬於的類別,即 kafka 是面向 topic 的。一個 broker
    可以容納多個 topic。
  • partition:每個 topic 包含一個或多個 partition。kafka 分配的單位是 partition,每個 partition 是一個有序的隊列。
  • consumer:消息消費者,從 kafka 集羣中消費消息的終端。
  • Consumer group:消費者組,由多個 consumer 組成。消費者組內每個消費者負
    責消費不同分區的數據,一個分區只能由一個組內消費者消費;消費者組之間互不影響。
  • replica:partition 的副本,保障 partition 的高可用。
  • leader: 每個分區多個副本的“主”,producer 和 consumer 只跟 leader 交互。生產者發送數據的對象,以及消費者消費數據的對象都是 leader。
  • follower::每個分區多個副本中的“從”,實時從 leader 中同步數據,保持和 leader 數據的同步。
  • controller:kafka 集羣中的其中一個服務器,用來進行 leader election 以及 各種 failover。
  • zookeeper:kafka 通過 zookeeper 來存儲集羣的 meta 信息。

下面搭建Kafka分佈式集羣,官方下載鏈接:http://kafka.apache.org/downloads

[root@node01 ~]# mkdir -p opt/module/kafaka
[root@node01 ~]# cd opt/module/kafaka/
[root@node01 kafaka]# wget http://mirrors.tuna.tsinghua.edu.cn/apache/kafka/2.4.0/kafka_2.13-2.4.0.tgz
[root@node01 kafaka]# tar -zxvf  kafka_2.13-2.4.0.tgz 
[root@node01 kafaka]# mv kafka_2.13-2.4.0/ kafaka
[root@node01 kafaka]# cd kafaka
[root@node01 kafaka]# mkdir logs 
[root@node01 kafaka]# cd config
[root@node01 config]# vim server.properties 
##########
#broker 的全局唯一編號,
broker.id=0 
#kafka 運行日誌存放的路徑 
log.dirs=/root/opt/module/kafaka/kafaka/logs
#配置連接 Zookeeper 集羣地址 
zookeeper.connect=node01:2181,node02:2181,node03:2181 
[root@node01 config]# vim /etc/profile 
##########
export KAFKA_HOME=/opt/module/kafka/kafaka
export PATH=$PATH:$KAFKA_HOME/bin 
[root@node01 config]# source /etc/profile 
[root@node01 config]# cd ../../../
[root@node01 module]# scp -rp kafaka/ root@node02:/root/opt/module/kafaka/
[root@node01 module]# scp -rp kafaka/ root@node03:/root/opt/module/kafaka/

[root@node01 ]# scp -rp /etc/profile node02:/etc/profile
[root@node01 ]# scp -rp /etc/profile node03:/etc/profile

[root@node02]# source /etc/profile
[root@node03]# source /etc/profile

在node02 和 node02上修改配置文件server.properties中的 broker.id=1、broker.id=2

[root@node02 kafaka]# vim config/server.properties 
broker.id=1
[root@node03 kafaka]# vim config/server.properties 
broker.id=2

啓動kafaka集羣前,需要啓動zookeeper集羣。

[root@node01 kafaka]# bin/kafka-server-start.sh -daemon config/server.properties 
[root@node01 kafaka]# jps
4579 Jps
3511 SecondaryNameNode
3226 NameNode
3772 ResourceManager
4140 QuorumPeerMain
4556 Kafka
[root@node02 kafaka]# bin/kafka-server-start.sh -daemon config/server.properties
[root@node02 kafaka]# jps
3360 QuorumPeerMain
3762 Kafka
3847 Jps
3066 DataNode
3196 NodeManager
[root@node03 kafaka]# bin/kafka-server-start.sh -daemon config/server.properties 
[root@node03 kafaka]# jps
3698 DataNode
3990 QuorumPeerMain
4519 Jps
3834 NodeManager
4442 Kafka

如果我們停止Kafaka集羣,只需要執行kafka-server-stop.sh stop命令

 [root@node01 kafaka]# bin/kafka-server-stop.sh stop

(2)Kafaka簡單使用

創建Topic

[root@node01 kafaka]# bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic Hello-Kafaka
Created topic Hello-Kafaka.

上述命令會創建一個名爲Hello-Kafaka的Topic,並指定了replication-factor和partitions分別爲1。其中replication-factor控制一個Message會被寫到多少臺服務器上,因此這個值必須小於或者等於Broker的數量。

列出Topic

[root@node01 kafaka]# bin/kafka-topics.sh --zookeeper localhost:2181 --list
Hello-Kafaka

發佈消息到指定的Topic

[root@node01 kafaka]# bin/kafka-console-producer.sh --broker-list localhost:9092 --topic Hello-Kafaka
>Hello
>Kafaka

消費指定Topic上的消息

[root@node01 kafaka]# bin/kafka-console-consumer.sh --zookeeper localhost:2181 --from-beginning --topic Hello-Kafaka
Hello
Kafaka

刪除指定Topic

[root@node01 kafaka]# bin/kafka-topics.sh --delete --zookeeper localhost:2181 --topic Hello-Kafaka
Topic Hello-Kafaka is marked for deletion.

更多的Kafaka教程查看官方文檔:http://kafka.apache.org/documentation/

下面通過Python簡單操控Kafaka,Python提供了kafka-pythonpykafaka兩個第三方庫,我們可以通過pip安裝。在這裏,我們使用kafka-python

Python操作Kafaka代碼如下:

import time
from kafka import KafkaProducer
from kafka import KafkaConsumer
from kafka.structs import TopicPartition

class OperateKafka:
    def __init__(self,bootstrap_servers,topic):
        self.bootstrap_servers = bootstrap_servers
        self.topic = topic

    """生產者"""
    def produce(self):
        producer = KafkaProducer(bootstrap_servers=self.bootstrap_servers)
        for i in range(5):
            msg = "msg%d" %i
            producer.send(self.topic,key=str(i),value=msg.encode('utf-8'))
        producer.close()

    """一個消費者消費一個topic"""
    def consumer(self):
        consumer = KafkaConsumer(self.topic,bootstrap_servers=self.bootstrap_servers)
        for message in consumer:
            print ("%s:%d:%d: key=%s value=%s" % (message.topic,message.partition,message.offset, message.key,message.value))

    """一個消費者訂閱多個topic """
    def consumer2(self):
        consumer = KafkaConsumer(bootstrap_servers=['192.168.124.201:9092'])
        consumer.subscribe(topics=('TEST','TEST1'))  #訂閱要消費的主題
        for message in consumer:
            print("%s:%d:%d: key=%s value=%s" % (message.topic, message.partition, message.offset, message.key, message.value))
                
    """消費者(手動拉取消息)"""
    def consumer3(self):
        consumer = KafkaConsumer(group_id="mygroup",max_poll_records=3,bootstrap_servers=['192.168.92.90:9092'])
        consumer.subscribe(topics=('TEST','TEST1'))
        while True:
            message = consumer.poll(timeout_ms=5)   #從kafka獲取消息
                if message:
                    print(message)
                    time.sleep(1)
                
def main():
    bootstrap_servers = ['192.168.92.90:9092']
    topic = "TEST"
    operateKafka = OperateKafka(bootstrap_servers,topic)
    operateKafka.produce()
    operateKafka.consumer()
    #operateKafka.consumer2()
    #operateKafka.consumer3()
    
if __name__ == '__main__':
    main()

在運行代碼時,可能會報以下錯誤:assert type(key_bytes) in (bytes, bytearray, memoryview, type(None)),這是由於async是python3.7版本的關鍵字引起的,解決方法使用Python 3.6版本,或者安裝對應版本的kafka-python,下載鏈接:https://github.com/dpkp/kafka-python/releases。

(3)Spark Streaming集成Kafaka

針對不同的spark、Kafka版本,集成處理數據的方式分爲兩種:Receiver based Approach和Direct Approach。z在Direct模式下,Steaming使用Kafaka原生API直接操作Kafaka集羣,不需要藉助zookeeper集羣。因此由於Spark Streaming 中 Direct 方式的優越性,現在可以說都使用 Direct 方式來獲取 Kafka 數據

下面我們將使用Direct模式對Kafaka寫入訂單數據,用Streaming處理數據。

我們創建Maven工程,在pom.xml配置文件中添加Kafaka和spark-streaming依賴。

<dependencies>
<dependency>
    <groupId>org.apache.spark</groupId>
    <artifactId>spark-streaming-kafka-0-8_2.11</artifactId>
    <version>2.4.5</version>
</dependency>
<dependency>
    <groupId>org.apache.spark</groupId>
    <artifactId>spark-streaming_2.12</artifactId>
    <version>2.4.5</version>
</dependency>
</dependencies>

定義一個OrderPrice類,通過Random隨時改變price。

import java.util.Random;
public class OrderPrice {
    public static int getRandomNum(int bound){
        Random random = new Random();
        return random.nextInt(bound);
    }
    public static void main(String[] args) throws InterruptedException{
        while(true){
            int randomNum = getRandomNum(20);
            System.out.println(randomNum);
            for(int i=0; i<randomNum;i++){
                System.out.println("random:" + getRandomNum(randomNum*10));
            }
            Thread.sleep(30000);
        }
    }
}

定義一個訂單系統Order類,實現get的方法。

import java.io.Serializable;
public class Order implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    private Float price;
    public Order(String name, Float price) {
        super();
        this.name = name;
        this.price = price;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public Float getPrice() {
        return price;
    }
    public void setPrice(Float price) {
        this.price = price;
    }
    @Override
    public String toString() {
        return "Order [name=" + name + ", price=" + price + "]";
    }
}

下面我們定義一個kafka消息生產者OrderProducer類,將訂單數據寫進Kafaka集羣

import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import com.fasterxml.jackson.core.JsonProcessingException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.databind.ObjectMapper;
import kafka.javaapi.producer.Producer;
import kafka.producer.KeyedMessage;
import kafka.producer.ProducerConfig;
/**
 * 訂單 kafka消息生產者
 */
public class OrderProducer {
    private static Logger logger = LoggerFactory.getLogger(OrderProducer.class);
    public static void main(String[] args) throws JsonProcessingException, InterruptedException {
        // set up the producer
        Producer<String, String> producer = null;
        ObjectMapper mapper = new ObjectMapper();
        Properties props = new Properties();
        // kafka集羣
        props.put("metadata.broker.list", "192.168.92.90:9092,192.168.92.90:9092,192.168.92.90:9092");
        // 配置value的序列化類
        props.put("serializer.class", "kafka.serializer.StringEncoder");
        // 配置key的序列化類
        props.put("key.serializer.class", "kafka.serializer.StringEncoder");
        ProducerConfig config = new ProducerConfig(props);
        producer = new Producer<String, String>(config);
        // 定義發佈消息體
        List<KeyedMessage<String, String>> messages = new ArrayList<KeyedMessage<String, String>>();
        // 每隔3秒生產隨機個訂單消息
        while (true) {
            int random = OrderPrice.getRandomNum(20);
            if (random == 0) {
                continue;
            }
            messages.clear();
            for (int i = 0; i < random; i++) {
                int orderRandom = OrderPrice.getRandomNum(random * 10);
                Order order = new Order("name" + orderRandom, Float.valueOf("" + orderRandom));
                // 訂單消息體:topic和消息
                KeyedMessage<String, String> message = new KeyedMessage<String, String>(
                        "My-Topic", mapper.writeValueAsString(order));
                messages.add(message);
            }
            producer.send(messages);
            logger.warn("orderNum:" + random + ",message:" + messages.toString());
            producer.close();
        }
    }
}

最後定義OrderSparkStreaming類來統計訂單量和訂單總值

import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;
import org.apache.spark.SparkConf;
import org.apache.spark.api.java.JavaRDD;
import org.apache.spark.api.java.function.Function;
import org.apache.spark.api.java.function.Function2;
import org.apache.spark.api.java.function.VoidFunction;
import org.apache.spark.streaming.Duration;
import org.apache.spark.streaming.Durations;
import org.apache.spark.streaming.api.java.JavaDStream;
import org.apache.spark.streaming.api.java.JavaPairInputDStream;
import org.apache.spark.streaming.api.java.JavaStreamingContext;
import org.apache.spark.streaming.kafka.KafkaUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.util.concurrent.AtomicDouble;
import kafka.serializer.StringDecoder;
import scala.Tuple2;

/**
 * spark streaming統計訂單量和訂單總值
 */
public class OrderSparkStreaming {
	public static JavaStreamingContext getJavaStreamingContext(String appName, String master,String logLeverl, Duration batchDuration) {
	
		SparkConf sparkConf = new SparkConf().setAppName(appName).setMaster(master);
		return new JavaStreamingContext(sparkConf,batchDuration);
	}

	private static Logger logger = LoggerFactory.getLogger(OrderSparkStreaming.class);
	private static AtomicLong orderCount = new AtomicLong(0);
	private static AtomicDouble totalPrice = new AtomicDouble(0);

	public static void main(String[] args) throws InterruptedException {

		// 2秒處理上下文
		JavaStreamingContext jssc = getJavaStreamingContext("JavaDirectKafkaWordCount",
				"local[2]", null, Durations.seconds(20));

		Set<String> topicsSet = new HashSet<>(Arrays.asList("My-topic".split(",")));
		Map<String, String> kafkaParams = new HashMap<>();
		kafkaParams.put("metadata.broker.list", "192.168.92.90:9092,192.168.92.90:9092,192.168.92.90:9092");
		kafkaParams.put("auto.offset.reset", "smallest");

		// Create direct kafka stream with brokers and topics
		JavaPairInputDStream<String, String> orderMsgStream = KafkaUtils.createDirectStream(jssc,String.class, String.class, StringDecoder.class, StringDecoder.class, kafkaParams,topicsSet);

		// json與對象映射對象
		final ObjectMapper mapper = new ObjectMapper();
		JavaDStream<Order> orderDStream = orderMsgStream
				.map(new Function<Tuple2<String, String>, Order>() {

					private static final long serialVersionUID = 1L;

					@Override
					public Order call(Tuple2<String, String> t2) throws Exception {
						return mapper.readValue(t2._2, Order.class);
					}
				}).cache();

		// 對DStream中的每一個RDD進行操作
		orderDStream.foreachRDD(new VoidFunction<JavaRDD<Order>>() {
			
			private static final long serialVersionUID = 1L;

			@Override
			public void call(JavaRDD<Order> orderJavaRDD) throws Exception {
				long count = orderJavaRDD.count();
				if (count > 0) {
					// 累加訂單總數
					orderCount.addAndGet(count);
					// 對RDD中的每一個訂單,首先進行一次Map操作,產生一個包含了每筆訂單的價格的新的RDD
					// 然後對新的RDD進行一次Reduce操作,計算出這個RDD中所有訂單的價格衆合
					Float sumPrice = orderJavaRDD.map(new Function<Order, Float>() {
						
						private static final long serialVersionUID = 1L;

						@Override
						public Float call(Order order) throws Exception {
							return order.getPrice();
						}
					}).reduce(new Function2<Float, Float, Float>() {
						
						private static final long serialVersionUID = 1L;

						@Override
						public Float call(Float a, Float b) throws Exception {
							return a + b;
						}
					});
					// 然後把本次RDD中所有訂單的價格總和累加到之前所有訂單的價格總和中。
					totalPrice.getAndAdd(sumPrice);

					// 數據訂單總數和價格總和,生產環境中可以寫入數據庫
					logger.warn("-------Total order count : " + orderCount.get()
							+ " with total price : " + totalPrice.get());
				}
			}
		});
		orderDStream.print();
		jssc.start();
		jssc.awaitTermination();
	}
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章