目录
一、Spark Streaming概述
1、概述
Spark Streaming类似于Apache Storm,用于流式数据的处理。根据其官方文档介绍,Spark Streaming有高吞吐量和容错能力强等特点,而且Spark Streaming易用、容错、易整合到Spark体系 。Spark Streaming支持的数据输入源很多,例如:Kafka、Flume、Twitter、ZeroMQ和简单的TCP套接字等等。数据输入后可以用Spark的高度抽象原语如:map、reduce、join、window等进行运算。而结果也能保存在很多地方,如HDFS,数据库等。另外Spark Streaming也能和MLlib(机器学习)以及Graphx完美融合。
2、什么是DStream
Spark Streaming使用离散化流(discretized stream)作为抽象表示,叫作DStream。DStream 是随时间推移而收到的数据的序列。在内部,每个时间区间收到的数据都作为 RDD 存在,而 DStream 是由这些 RDD 所组成的序列(因此 得名“离散化”)。
源码:
DStream 可以从各种输入源创建,比如 Flume、Kafka 或者 HDFS。创建出来的DStream 支持两种操作,一种是转化操作(transformation),会生成一个新的DStream,另一种是输出操作(output operation),可以把数据写入外部系统中。DStream 提供了许多与 RDD 所支持的操作相类似的操作支持,还增加了与时间相关的新操作,比如滑动窗口。
3、Spark与Storm对比
二、Spark Streaming整体架构
接收器是一直运行的,为了防止数据丢失,会将数据备份到另一工作节点上,通过Spark Streaming写的驱动程序来设置调度批次的时间间隔,假设设置为1秒,那Spark Streaming就会处理接收1秒内接收的所有数据,1秒调度一次,微批次的概念。实际上就是处理RDD的概念。
三、初始化Spark Streaming
1、Spark Streaming入口
第一种:可以通过ssc.sparkContext 来访问SparkContext(Seconds(1):设置处理1秒的数据)
val conf = new SparkConf().setAppName(appName).setMaster(master)
val ssc = new StreamingContext(conf, Seconds(1))
第二种:通过已经存在的SparkContext来创建StreamingContext(Seconds(1):设置处理1秒的数据)
val sc = ...
val ssc = new StreamingContext(sc, Seconds(1))
初始化完Context之后:
1) 定义消息输入源来创建DStreams.
2) 定义DStreams的转化操作和输出操作。
3) 通过 streamingContext.start()来启动消息采集和处理.
4) 等待程序终止,可以通过
streamingContext.awaitTermination()来设置
5) 通过streamingContext.stop()来手动终止处理程序。
四、运行Spark Streaming
1、IDEA编写WordCount代码
import org.apache.spark.SparkConf
import org.apache.spark.streaming.{Seconds, StreamingContext}
object WorldCount {
def main(args: Array[String]) {
// 需要新建一个sparkConf变量,来提供Spark的配置
val conf = new SparkConf().setMaster("local[2]").setAppName("NetworkWordCount")
// 新建一个StreamingContext入口
val ssc = new StreamingContext(conf, Seconds(1))
// 从master01机器上的9999端口不断的获取输入的文本数据
val lines = ssc.socketTextStream("master01", 9999)
// 将每一行文本通过空格分隔成多个单词
val words = lines.flatMap(_.split(" "))
// 每个单词转换为一个元祖
val pairs = words.map(word => (word, 1))
//根据单词来统计相同单词的数量
val wordCounts = pairs.reduceByKey(_ + _)
wordCounts.print() // 打印结果
ssc.start() // 启动流式处理程序
ssc.awaitTermination() // 等待停止信号
}
}
2、通过Netcat发送数据:
先打开9999端口
3、按照Spark Core中的方式进行打包,并将程序上传到Spark机器上。并运行:(这是无状态转换)
bin/spark-submit --class com.atguigu.streaming.WorldCount ~/wordcount-jar-with-dependencies.jar
在9999端口打开的终端窗口输入信息后Enter:
即可在程序运行的中断窗口接收到信息:
五、Spark Streaming的输入
1、基本数据源
1-1、文件件数据源(使用不多,一般测试使用)
文件数据流:能够读取所有HDFS API兼容的文件系统文件,通过fileStream方法进行读取
Spark Streaming 将会监控 dataDirectory 目录并不断处理移动进来的文件,记住目前不支持嵌套目录。
1) 文件需要有相同的数据格式
2) 文件进入 dataDirectory的方式需要通过移动或者重命名来实现。
3) 一旦文件移动进目录,则不能再修改,即便修改了也不会读取新数据。
如果文件比较简单,则可以使用 streamingContext.textFileStream(dataDirectoryPath)方法来读取文件。文件流不需要接收器,不需要单独分配CPU核。
HDFS读取实例:
提前需在HDFS上建好目录
代码:
scala> import org.apache.spark.streaming._
import org.apache.spark.streaming._
scala> val ssc = new StreamingContext(sc, Seconds(1))
ssc: org.apache.spark.streaming.StreamingContext = org.apache.spark.streaming.StreamingContext@4027edeb
scala> val lines = ssc.textFileStream("hdfs://master01:9000/data/")
lines: org.apache.spark.streaming.dstream.DStream[String] = org.apache.spark.streaming.dstream.MappedDStream@61d9dd15
scala> val words = lines.flatMap(_.split(" "))
words:org.apache.spark.streaming.dstream.DStream[String]=
org.apache.spark.streaming.dstream.FlatMappedDStream@1e084a26
scala> val wordCounts = words.map(x => (x, 1)).reduceByKey(_ + _)
wordCounts: org.apache.spark.streaming.dstream.DStream[(String, Int)] =
org.apache.spark.streaming.dstream.ShuffledDStream@8947a4b
scala> wordCounts.print()
scala> ssc.start()
-------------------------------------------
Time: 1504665716000 ms
-------------------------------------------
-------------------------------------------
Time: 1504665717000 ms
-------------------------------------------
上传文件到HDFS:
获取结算结果:
总结:
1、Spark Streaming通过streamingContext.fileStream[KeyClass, ValueClass, InputFormatClass](dataDirectory) 这个方法提供了对目录下文件数据源的支持。
2、如果你的文件是比较简单的文本文件,你可以使用 streamingContext.textFileStream(dataDirectory) 来代替。
3、文件数据源目前不支持嵌套目录:
1、 文件需要有相同的数据格式
2、文件进入 dataDirectory的方式需要通过移动或者重命名来实现
3、一旦文件移动进目录,则不能再修改,即便修改了也不会读取新数据。
1-2、自定义数据源(*****)
通过继承Receiver(自定义Receiver),并实现onStart、onStop方法来自定义数据源采集。
代码:
import java.io.{BufferedReader, InputStreamReader}
import java.net.Socket
import java.nio.charset.StandardCharsets
import org.apache.spark.SparkConf
import org.apache.spark.storage.StorageLevel
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.streaming.receiver.Receiver
class CustomReceiver (host: String, port: Int) extends Receiver[String](StorageLevel.MEMORY_AND_DISK_2) {
//Receiver启动的时候需要调用的方法
override def onStart(): Unit = {
//定义一个新的线程去运行我的receiver方法
new Thread("Socket Receiver") {
override def run() { receive() }
}.start()
}
//Receiver停止的时候需要调用的方法
override def onStop(): Unit = {
// There is nothing much to do as the thread calling receive()
// is designed to stop by itself if isStopped() returns false
}
//我的receiver方法
private def receive() {
var socket: Socket = null
var userInput: String = null
try {
//新建一个socket连接
socket = new Socket(host, port)
//获取socket的输入
val reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8))
//获取第一条输入
userInput = reader.readLine()
//如果你的Receiver没有停止并且userInput不是null
while(!isStopped && userInput != null) {
//传送出来
store(userInput)
//继续读取下一行
userInput = reader.readLine()
}
//关闭资源
reader.close()
socket.close()
// Restart in an attempt to connect again when server is active again
restart("Trying to connect again")
} catch {
case e: java.net.ConnectException =>
// restart if could not connect to server
restart("Error connecting to " + host + ":" + port, e)
case t: Throwable =>
// restart if there is any other error
restart("Error receiving data", t)
}
}
}
object CustomReceiver {
def main(args: Array[String]) {
val conf = new SparkConf().setMaster("local[2]").setAppName("NetworkWordCount")
val ssc = new StreamingContext(conf, Seconds(1))
// Create a DStream that will connect to hostname:port, like localhost:9999
val lines = ssc.receiverStream(new CustomReceiver("master01", 9999))
// Split each line into words
val words = lines.flatMap(_.split(" "))
//import org.apache.spark.streaming.StreamingContext._ // not necessary since Spark 1.3
// Count each word in each batch
val pairs = words.map(word => (word, 1))
val wordCounts = pairs.reduceByKey(_ + _)
// Print the first ten elements of each RDD generated in this DStream to the console
wordCounts.print()
ssc.start() // Start the computation
ssc.awaitTermination() // Wait for the computation to terminate
//ssc.stop()
}
}
启动9999端口发送数据:
打包运行:
端口发送数据后:
总结:
自定义Receiver
1、你需要新建一个Class去继承Receiver,并给Receiver传入一个类型参数,该类型参数是你需要接收的数据的类型。
2、你需要去复写Receiver的方法: onStart方法(在Receiver启动的时候调用的方法)、onStop方法(在Receiver正常停止的情况下调用的方法)
3、你可以在程序中通过streamingContext.receiverStream( new CustomeReceiver)来调用你定制化的Receiver。
1-3、RDD队列(了解)(day06==>第03集91:00-99:00)
测试过程中,可以通过使用streamingContext.queueStream(queueOfRDDs)来创建DStream,每一个推送到这个队列中的RDD,都会作为一个DStream处理。
代码:
import org.apache.spark.SparkConf
import org.apache.spark.rdd.RDD
import org.apache.spark.streaming.{Seconds, StreamingContext}
import scala.collection.mutable
object QueueRdd {
def main(args: Array[String]) {
val conf = new SparkConf().setMaster("local[2]").setAppName("QueueRdd")
val ssc = new StreamingContext(conf, Seconds(1))
//创建RDD队列
val rddQueue = new mutable.SynchronizedQueue[RDD[Int]]()
// 创建QueueInputDStream
val inputStream = ssc.queueStream(rddQueue)
//处理队列中的RDD数据
val mappedStream = inputStream.map(x => (x % 10, 1))
val reducedStream = mappedStream.reduceByKey(_ + _)
//打印结果
reducedStream.print()
//启动计算
ssc.start()
// Create and push some RDDs into
//for循环模拟插入数据
for (i <- 1 to 30) {
rddQueue += ssc.sparkContext.makeRDD(1 to 300, 10)
Thread.sleep(2000)
//通过程序停止StreamingContext的运行
//ssc.stop()
}
ssc.awaitTermination()
}
}
打包运行:
2、高级数据源
2-1、Spark Streaming和Kafka的集成(*****)
下面例子是为了演示SparkStreaming如何从Kafka读取消息,如果通过连接池方法把消息处理完成后再写回Kafka:
代码:
kafka Connection Pool程序:
import java.util.Properties
import org.apache.commons.pool2.impl.DefaultPooledObject
import org.apache.commons.pool2.{BasePooledObjectFactory, PooledObject}
import org.apache.kafka.clients.producer.{KafkaProducer, ProducerRecord}
/**
* @param brokerList:Kafka连接地址
* @param producerConfig:生产者配置
* @param defaultTopic:默认写出的队列名
* @param producer:KafkaProducer
*/
case class KafkaProducerProxy(brokerList: String,
producerConfig: Properties = new Properties,
defaultTopic: Option[String] = None,
producer: Option[KafkaProducer[String, String]] = None) {
type Key = String
type Val = String
require(brokerList == null || !brokerList.isEmpty, "Must set broker list")
private val p = producer getOrElse {
var props:Properties= new Properties();
props.put("bootstrap.servers", brokerList);
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
new KafkaProducer[String,String](props)
}
//把我的消息包装成了ProducerRecord
private def toMessage(value: Val, key: Option[Key] = None, topic: Option[String] = None): ProducerRecord[Key, Val] = {
val t = topic.getOrElse(defaultTopic.getOrElse(throw new IllegalArgumentException("Must provide topic or default topic")))
require(!t.isEmpty, "Topic must not be empty")
key match {
case Some(k) => new ProducerRecord(t, k, value)
case _ => new ProducerRecord(t, value)
}
}
def send(key: Key, value: Val, topic: Option[String] = None) {
//调用KafkaProducer的send方法发送消息
p.send(toMessage(value, Option(key), topic))
}
def send(value: Val, topic: Option[String]) {
send(null, value, topic)
}
def send(value: Val, topic: String) {
send(null, value, Option(topic))
}
def send(value: Val) {
send(null, value, None)
}
def shutdown(): Unit = p.close()
}
abstract class KafkaProducerFactory(brokerList: String, config: Properties, topic: Option[String] = None) extends Serializable {
def newInstance(): KafkaProducerProxy
}
class BaseKafkaProducerFactory(brokerList: String,config: Properties = new Properties,defaultTopic: Option[String] = None)
extends KafkaProducerFactory(brokerList, config, defaultTopic) {
override def newInstance() = new KafkaProducerProxy(brokerList, config, defaultTopic)
}
//继承一个基础的连接池,需要提供池化的对象类型
class PooledKafkaProducerAppFactory(val factory: KafkaProducerFactory)
extends BasePooledObjectFactory[KafkaProducerProxy] with Serializable {
//用于池来创建对象
override def create(): KafkaProducerProxy = factory.newInstance()
//用于池来包装对象
override def wrap(obj: KafkaProducerProxy): PooledObject[KafkaProducerProxy] = new DefaultPooledObject(obj)
//用于池来销毁对象
override def destroyObject(p: PooledObject[KafkaProducerProxy]): Unit = {
p.getObject.shutdown()
super.destroyObject(p)
}
}
KafkaStreaming main:
import org.apache.commons.pool2.impl.{GenericObjectPool, GenericObjectPoolConfig}
import org.apache.kafka.clients.consumer.ConsumerRecord
import org.apache.kafka.common.serialization.StringDeserializer
import org.apache.spark.SparkConf
import org.apache.spark.api.java.function.VoidFunction
import org.apache.spark.rdd.RDD
import org.apache.spark.streaming.kafka010.{ConsumerStrategies, KafkaUtils, LocationStrategies}
import org.apache.spark.streaming.{Seconds, StreamingContext}
//单例对象(保证了池只有一个)
object createKafkaProducerPool{
//用于返回真正的对象池
def apply(brokerList: String, topic: String): GenericObjectPool[KafkaProducerProxy] = {
val producerFactory = new BaseKafkaProducerFactory(brokerList, defaultTopic = Option(topic))
val pooledProducerFactory = new PooledKafkaProducerAppFactory(producerFactory)
//指定了你的kafka对象池的大小
val poolConfig = {
val c = new GenericObjectPoolConfig
val maxNumProducers = 10
c.setMaxTotal(maxNumProducers)
c.setMaxIdle(maxNumProducers)
c
}
//返回一个对象池
new GenericObjectPool[KafkaProducerProxy](pooledProducerFactory, poolConfig)
}
}
object KafkaStreaming{
def main(args: Array[String]) {
//设置sparkConf
val conf = new SparkConf().setMaster("local[4]").setAppName("NetworkWordCount")
//新建了streamingContext
val ssc = new StreamingContext(conf, Seconds(1))
//kafka地址
val brobrokers = "172.16.148.150:9092,172.16.148.151:9092,172.16.148.152:9092"
//kafka的队列名称
val sourcetopic="source";
val targettopic="target";
//创建消费者组
var group="con-consumer-group"
//Kafka消费者配置(具体应用时再去官网查配置信息)
val kafkaParam = Map(
"bootstrap.servers" -> brobrokers, //用于初始化链接到集群的地址
"key.deserializer" -> classOf[StringDeserializer],
"value.deserializer" -> classOf[StringDeserializer],
//用于标识这个消费者属于哪个消费团体
"group.id" -> group,
//如果没有初始化偏移量或者当前的偏移量不存在任何服务器上,可以使用这个配置属性
//可以使用这个配置,latest自动重置偏移量为最新的偏移量,为了获取最新的消息
"auto.offset.reset" -> "latest",
//如果是true,则这个消费者的偏移量会在后台自动提交
"enable.auto.commit" -> (false: java.lang.Boolean)
);
//ssc.sparkContext.broadcast(pool)
//创建DStream,返回接收到的输入数据
var stream=KafkaUtils.createDirectStream[String,String](ssc,
LocationStrategies.PreferConsistent,ConsumerStrategies.Subscribe[String,String](Array(sourcetopic),kafkaParam))
//每一个stream都是一个ConsumerRecord
stream.map(s =>("id:" + s.key(),">>>>:"+s.value())).foreachRDD(rdd => {
//对于RDD的每一个分区执行一个操作
rdd.foreachPartition(partitionOfRecords => {
//Kafka连接池
val pool = createKafkaProducerPool(brobrokers, targettopic)
//从连接池里面取出了一个Kafka的连接
val p = pool.borrowObject()
//发送当前分区里面每一个数据(send)
partitionOfRecords.foreach {message => System.out.println(message._2);p.send(message._2,Option(targettopic))}
//使用完了需要将kafka还回去
pool.returnObject(p)
})
})
ssc.start()
ssc.awaitTermination()
}
}
程序部署:
1、启动zookeeper和kafka(master、slave1、slave2三台机器都启动)
bin/kafka-server-start.sh -daemon ./config/server.properties
2、创建两个topic,一个为source,一个为target
bin/kafka-topics.sh --create --zookeeper 192.168.1.101:2181,192.168.1.102:2181,192.168.1.103:2181,192.168.1.104:2181 --replication-factor 2 --partitions 2 --topic source
bin/kafka-topics.sh --create --zookeeper 192.168.1.101:2181,192.168.1.102:2181,192.168.1.103:2181,192.168.1.104:2181 --replication-factor 2 --partitions 2 --topic targe
master节点下:
3、启动kafka console producer写入source topic
bin/kafka-console-producer.sh --broker-list 192.168.1.101:9092,192.168.1.102:9092, 192.168.1.103:9092,
192.168.1.104:9092 --topic source
4、启动kafka console consumer监听target topic(验证是否成功)
bin/kafka-console-consumer.sh --bootstrap-server 192.168.1.101:9092,192.168.1.102:9092, 192.168.1.103:9092,
192.168.1.104:9092 --topic source
验证:
5、启动kafkaStreaming程序:
[bigdata@master01 ~]$ ./hadoop/spark-2.1.1-bin-hadoop2.7/bin/spark-submit --class com.atguigu.streaming.KafkaStreaming ./kafkastreaming-jar-with-dependencies.jar
打包上传运行:
一个端口发送数据:将数据写入到source1
接收数据:KafkaStreaming给接收的数据前面加了:" >>>>: "
consumer监控target1队列,所以当KafkaStreaming把数据传入target1倍consumer监控到时,consumer就会直接将数据打印出来