@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-python
和pykafaka
兩個第三方庫,我們可以通過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();
}
}