我们的目的是通过代码理解Spark Streaming的机理,不是让你自己就能写出这些代码,没有人会是神一样Scala啥都没学就能啥都写得出来 |
传统的数据库就是用户基于数据库中的历史数据来进行查询
流计算就是用户订阅了以后,当数据产生时,系统自动计算,然后推送给用户。具有实时性。
一、Spark Streaming设计
Spark Streaming可整合多种输入数据源,如Kafka、Flume、HDFS,甚至是普通的TCP套接字。经处理后的数据可存储至文件系统、数据库,或显示在仪表盘里。
Spark Streaming的基本原理是将实时输入数据流以时间片(秒级)为单位进行拆分,然后经Spark引擎以类似批处理的方式处理每个时间片数据,执行流程如下图所示。
SparkCore的数据抽象是RDD,Spark SQL的数据抽象是DataFrame,Spark Streaming的数据抽象是DStream
Spark Streaming的输入数据按照时间片(如1秒)分成一段一段的DStream,每一段数据转换为Spark中的RDD,并且对DStream的操作都最终转变为对相应的RDD的操作。
例如,下图展示了进行单词统计时,每个时间片的数据(存储句子的RDD)经flatMap操作,生成了存储单词的RDD。整个流式计算可根据业务的需求对这些中间的结果进一步处理,或者存储到外部设备中。
二、Spark Streaming程序基本步骤
编写Spark Streaming程序的基本步骤是:
- 通过创建输入DStream来定义输入源
- 通过对DStream应用转换操作和输出操作来定义流计算。
- 用streamingContext.start()来开始接收数据和处理流程。
- 通过streamingContext.awaitTermination()方法来等待处理结束(手动结束或因为错误而结束)。
- 可以通过streamingContext.stop()来手动结束流计算进程。
三、创建StreamingContext对象
方式一:
登录Linux系统,启动spark-shell。进入spark-shell以后,就已经获得了一个默认的SparkConext,也就是sc。因此,可以采用如下方式来创建StreamingContext对象:
scala> import org.apache.spark.streaming._
scala> val ssc = new StreamingContext(sc, Seconds(1)) //Seconds(1)表示每隔1秒钟就自动执行一次流计算,这个秒数可以自由设定。
方式二:
如果是编写一个独立的Spark Streaming程序,而不是在spark-shell中运行,则需要通过如下方式创建StreamingContext对象:
import org.apache.spark._
import org.apache.spark.streaming._
val conf = new SparkConf().setAppName("TestDStream").setMaster("local[2]") //setAppName(“TestDStream”)是用来设置应用程序名称,这里我们取名为“TestDStream”。setMaster(“local[2]”)括号里的参数”local[2]’字符串表示运行在本地模式下,并且启动2个工作线程。
val ssc = new StreamingContext(conf, Seconds(1))
输入源包括(1)基本输入源 和(2)高级输入源; |
以下是Spark Streaming的基本输入源
一、文件流
①用spark-shell来实现
为了能够演示文件流的创建,我们需要首先创建一个日志目录,并在里面放置两个模拟的日志文件。
cd /usr/local/spark/mycode
mkdir streaming
cd streaming
mkdir logfile
cd logfile
然后,在logfile中新建两个日志文件log1.txt和log2.txt,里面可以随便输入一些内容。
比如,我们在log1.txt中输入以下内容:
I love Hadoop
I love Spark
Spark is fast
下面我们就进入spark-shell创建文件流。
scala> import org.apache.spark.streaming._
scala> val ssc = new StreamingContext(sc, Seconds(20))
scala> val lines = ssc.textFileStream("file:///usr/local/spark/mycode/streaming/logfile") //输入Dstream定义的输入源
scala> val words = lines.flatMap(_.split(" "))
scala> val wordCounts = words.map(x => (x, 1)).reduceByKey(_ + _)
scala> wordCounts.print()
scala> ssc.start() //启动流计算
scala> ssc.awaitTermination() //遇错停止,否则会不断地监听
//这里省略若干屏幕信息
-------------------------------------------
Time: 1479431100000 ms
-------------------------------------------
//这里省略若干屏幕信息
-------------------------------------------
Time: 1479431120000 ms
-------------------------------------------
//这里省略若干屏幕信息
-------------------------------------------
Time: 1479431140000 ms
-------------------------------------------
②用打包程序实现
上面是用spark-shell交互式环境来操作的,往往我们需要一个完整的程序把代码打包然后来运行,其中需要的代码如下:
import org.apache.spark._
import org.apache.spark.streaming._
object WordCountStreaming {
def main(args: Array[String]) {
val sparkConf = new SparkConf().setAppName("WordCountStreaming").setMaster("local[2]")//设置为本地运行模式,2个线程,一个监听,另一个处理数据
val ssc = new StreamingContext(sparkConf, Seconds(20))// 时间间隔为20秒
val lines = ssc.textFileStream("file:///usr/local/spark/mycode/streaming/logfile") //这里采用本地文件,当然你也可以采用HDFS文件
val words = lines.flatMap(_.split(" "))
val wordCounts = words.map(x => (x, 1)).reduceByKey(_ + _)
wordCounts.print()
ssc.start()
ssc.awaitTermination()
}
}
二、套接字流
①使用NC程序产生数据
原理是:你启动NC程序来源源不断的发送一些英文字符,然后写两个代码程序负责把接收到的这些字符来统计处理。注意,重点是你如何用代码来处理那些数据,不要纠结NC程序的工作原理。
第一步,创建NetworkWordCount.scala代码文件,请在该文件中输入如下内容:
package org.apache.spark.examples.streaming
import org.apache.spark._
import org.apache.spark.streaming._
import org.apache.spark.storage.StorageLevel
object NetworkWordCount {
def main(args: Array[String]) {
if (args.length < 2) {
System.err.println("Usage: NetworkWordCount <hostname> <port>") //主机名和端口号两个参数
System.exit(1)
}
StreamingExamples.setStreamingLogLevels() //设置日志显示级别,设置以后你才能看到那些显示信息(StreamingExamples.setStreamingLogLevels()需要你在相同目录下再新建另外一个代码文件StreamingExamples.scala来写)
val sparkConf = new SparkConf().setAppName("NetworkWordCount").setMaster("local[2]")
val ssc = new StreamingContext(sparkConf, Seconds(1))
val lines = ssc.socketTextStream(args(0), args(1).toInt, StorageLevel.MEMORY_AND_DISK_SER) //定义一个输入数字流,arg(0)是hostname,arg(1)是端口号port,StorageLevel.MEMORY_AND_DISK_SER是以内存加磁盘的方法保存那些源源不断地输入的数字流
val words = lines.flatMap(_.split(" "))
val wordCounts = words.map(x => (x, 1)).reduceByKey(_ + _)
wordCounts.print()
ssc.start()
ssc.awaitTermination()
}
}
上面的代码,不能直接拿去sbt打包编辑,因为,里面有个 StreamingExamples.setStreamingLogLevels(),而StreamingExamples来自另外一个代码文件,请在相同目录下再新建另外一个代码文件StreamingExamples.scala,文件内容如下:(下面的代码是附属品,不要你看懂)
package org.apache.spark.examples.streaming
import org.apache.spark.internal.Logging
import org.apache.log4j.{Level, Logger}
/** Utility functions for Spark Streaming examples. */
object StreamingExamples extends Logging {
/** Set reasonable logging levels for streaming if the user has not configured log4j. */
def setStreamingLogLevels() {
val log4jInitialized = Logger.getRootLogger.getAllAppenders.hasMoreElements
if (!log4jInitialized) {
// We first log something to initialize Spark's default logging, then we override the
// logging level.
logInfo("Setting log level to [WARN] for streaming example." +
" To override add a custom log4j.properties to the classpath.")
Logger.getRootLogger.setLevel(Level.WARN)
}
}
}
第二步,将上面的两个代码文件打包。
第三步,用nc -lk 9999
启动NC程序,然后就可以在nc窗口中随意输入一些单词,监听窗口就会自动获得单词数据流信息,在监听窗口每隔1秒就会打印出词频统计信息,大概会再屏幕上出现类似如下的结果:
-------------------------------------------
Time: 1479431100000 ms
-------------------------------------------
(hello,1)
(world,1)
-------------------------------------------
Time: 1479431120000 ms
-------------------------------------------
(hadoop,1)
-------------------------------------------
Time: 1479431140000 ms
-------------------------------------------
(spark,1)
②使用Socket编程来实现自定义数据源
我们把数据源头的产生方式修改一下,不要使用nc程序,而是采用自己编写的程序产生Socket数据源,然后让之前写好的两个代码程序负责把接收到的这些字符来统计处理。
package org.apache.spark.examples.streaming
import java.io.{PrintWriter}
import java.net.ServerSocket
import scala.io.Source
object DataSourceSocket {
def index(length: Int) = {
val rdm = new java.util.Random
rdm.nextInt(length)
//这个代码的意思就是,如果你输入的length是10,那么就会产生0-9的随机整数
}
def main(args: Array[String]) {
if (args.length != 3) {
System.err.println("Usage: <filename> <port> <millisecond>") //filename是从文件中随机抓取数据传送给客户端,port是监听端口的端口号,millisecond是数据传输的时间间隔
System.exit(1)
}
val fileName = args(0) //也就是<filename>
val lines = Source.fromFile(fileName).getLines.toList //读取filename指向的文件,读取内容形成列表
val rowCount = lines.length //原来文本有几行,这个就有多大
//下面是监听过程
val listener = new ServerSocket(args(1).toInt) //生成ServerSocket对象,监听对应端口号的窗口(args(1)就是<port>端口号)
while (true) { //反复监听状态
val socket = listener.accept() //假如没有任何客户端发起请求,那么会进入阻塞状态,等待连接
new Thread() { //一旦客户端发起连接,就进行如下处理
override def run = {
println("Got client connected from: " + socket.getInetAddress) //打印客户端地址
val out = new PrintWriter(socket.getOutputStream(), true) /生成输出流
while (true) {
Thread.sleep(args(2).toLong) //args(2)是睡眠时间millisecond
val content = lines(index(rowCount)) //index()是之前写的一个生成随机数的方法,rowCount是文本的行数,lines(index(rowCount))就是随便抓取文本的一行内容
println(content)
out.write(content + '\n')
out.flush() //刷新
}
socket.close()
}
}.start()
}
}
}
上面这个程序的功能是,从一个文本文件中随机读取某行文本作为数据源,发送出去。
三、RDD队列流
就是你创建一个RDD队列,然后往该队列里面扔一个又一个的RDD,然后推给Spark Streaming来处理
程序任务:我们接下来每隔一秒创建一个RDD,然后扔给SparkStreaming,SparkStreaming是每隔两秒来进行一次处理
下面的代码旨在了解RDD队列流机理,不要求你会写出来
package org.apache.spark.examples.streaming
import org.apache.spark.SparkConf
import org.apache.spark.rdd.RDD
import org.apache.spark.streaming.StreamingContext._
import org.apache.spark.streaming.{Seconds, StreamingContext}
object QueueStream {
def main(args: Array[String]) {
val sparkConf = new SparkConf().setAppName("TestRDDQueue").setMaster("local[2]") //生成一个Spark Context对象
val ssc = new StreamingContext(sparkConf, Seconds(20)) //生成一个STreamingContext对象,处理数据的时间间隔是2秒
val rddQueue =new scala.collection.mutable.SynchronizedQueue[RDD[Int]]()//生成一个RDD队列,队列里面每一个是RDD,而每一个RDD又是Int类型
//下面就是对rdd队列流的处理过程
val queueStream = ssc.queueStream(rddQueue)
val mappedStream = queueStream.map(r => (r % 10, 1)) //每个数转变成(余数,1)(发送过来的数字是16,16%10=6也就是统计余数6出现了几次)
val reducedStream = mappedStream.reduceByKey(_ + _) //进行词频统计
reducedStream.print()
ssc.start()
//下面就是往RDD队列流里面扔RDD
for (i <- 1 to 10){
rddQueue += ssc.sparkContext.makeRDD(1 to 100,2) //生成的每个RDD都是包含1-100的,把这些RDD分两个区,加到rddQueue里面去
Thread.sleep(1000) //1000ms,也就是每隔一秒输送一个RDD
}
ssc.stop()
}
}
执行程序后就会看到如下结果:
-------------------------------------------
Time: 1479522100000 ms
-------------------------------------------
(4,10)
(0,10)
(6,10)
(8,10)
(2,10)
(1,10)
(3,10)
(7,10)
(9,10)
(5,10)
以下是Spark Streaming的高级输入源
一、Apache Kafka作为DStream数据源
原理是:你启动Kafka程序来源源不断的发送一些英文字符,然后写两个代码程序负责把接收到的这些字符来统计处理。
①准备工作:需要你事先安装Kafaka,配置spark
②编写SparkStreaming程序来接受Kafaka数据源
使用vim编辑器新建了KafkaWordProducer.scala,它是产生一系列字符串的程序,会产生随机的整数序列,每个整数被当做一个单词,提供给KafkaWordCount程序去进行词频统计。请在KafkaWordProducer.scala中输入以下代码:
package org.apache.spark.examples.streaming
import java.util.HashMap
import org.apache.kafka.clients.producer.{KafkaProducer, ProducerConfig, ProducerRecord}
import org.apache.spark.SparkConf
import org.apache.spark.streaming._
import org.apache.spark.streaming.kafka._
object KafkaWordProducer {
def main(args: Array[String]) {
if (args.length < 4) {
System.err.println("Usage: KafkaWordCountProducer <metadataBrokerList> <topic> " +
"<messagesPerSec> <wordsPerMessage>") //<metadataBrokerList>是Kafka的Broker的地址,<topic>指定往哪个topic里面扔东西,<messagesPerSec>是每秒钟发几条消息给topic,<wordsPerMessage>是每条消息要包含几个单词
System.exit(1)
}
val Array(brokers, topic, messagesPerSec, wordsPerMessage) = args //这是Scala提取器的原理,当args里面有内容时就会提取出来分别赋给(brokers, topic, messagesPerSec, wordsPerMessage)
//下面这段代码相当于设置参数,不用硬理解
val props = new HashMap[String, Object]() //HashMap里面是(key:value)的映射的形式,我们把运行Kafka的属性和值封装在HashMap里面去
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, brokers) //key是“ProducerConfig.BOOTSTRAP_SERVERS_CONFIG”,values是“Broker的地址”
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
"org.apache.kafka.common.serialization.StringSerializer") //key是“ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG”,value是“"org.apache.kafka.common.serialization.StringSerializer"”,value指明了ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG的可序列化的方式
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,
"org.apache.kafka.common.serialization.StringSerializer") //"org.apache.kafka.common.serialization.StringSerializer"指明了ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG的可序列化的方式
val producer = new KafkaProducer[String, String](props) //有了props就可以生成一个KafkaProducer对象了
//下面这段程序是 完成每秒钟发送messagesPerSec条消息,每条消息包括wordsPerMessage个单词
while(true) {
(1 to messagesPerSec.toInt).foreach { messageNum => //我们假设messagesPerSec是4,也就是每秒钟发送四条消息,那么就是对1-4进行遍历,遍历时就是把1-4取出来赋值给messageNum
val str = (1 to wordsPerMessage.toInt).map(x => scala.util.Random.nextInt(10).toString) //wordsPerMessage就是每条消息包含几个单词,我们假设wordsPerMessage是5,那么进行五次map后就会生成5个随机数,假如是“5” “7” “4” “9” “2”(我们用一个随机数代表一个单词)
.mkString(" ") //以空格的方式拼起来,随机数变成了“5 7 4 9 2”
print(str)
println()
val message = new ProducerRecord[String, String](topic, null, str) //Kafka只接受ProducerRecord对象,所以我们需要把str封装成ProducerRecord对象,而ProducerRecord里面需要指明往那个topic里面扔,其次还要以键值对的方式来传送你要传递的东西,由于没有键,所以用null,而值就是str
producer.send(message)
}
Thread.sleep(1000) //每隔1秒执行一次上面的while(true)语句
}
}
}
KafkaWordCount.scala是用于单词词频统计,它会把KafkaWordProducer发送过来的单词进行词频统计,代码内容如下:
package org.apache.spark.examples.streaming
import org.apache.spark._
import org.apache.spark.SparkConf
import org.apache.spark.streaming._
import org.apache.spark.streaming.kafka._
import org.apache.spark.streaming.StreamingContext._
import org.apache.spark.streaming.kafka.KafkaUtils
object KafkaWordCount{
def main(args:Array[String]){
StreamingExamples.setStreamingLogLevels()
val sc = new SparkConf().setAppName("KafkaWordCount").setMaster("local[2]")
val ssc = new StreamingContext(sc,Seconds(10))
ssc.checkpoint("file:///usr/local/spark/mycode/kafka/checkpoint") //设置检查点,如果存放在HDFS上面,则写成类似ssc.checkpoint("/user/hadoop/checkpoint")这种形式,但是,要启动hadoop
val zkQuorum = "localhost:2181" //Zookeeper服务器地址
val group = "1" //topic所在的group,可以设置为自己想要的名称,比如不用1,而是val group = "test-consumer-group"
val topics = "wordsender" //topics的名称
val numThreads = 1 //每个topic的分区数
val topicMap =topics.split(",").map((_,numThreads.toInt)).toMap
val lineMap = KafkaUtils.createStream(ssc,zkQuorum,group,topicMap)
val lines = lineMap.map(_._2)
val words = lines.flatMap(_.split(" "))
val pair = words.map(x => (x,1))
val wordCounts = pair.reduceByKeyAndWindow(_ + _,_ - _,Minutes(2),Seconds(10),2) //这行代码的含义在下一节的窗口转换操作中会有介绍
wordCounts.print
ssc.start
ssc.awaitTermination
}
}
然后,继续在当前目录下创建StreamingExamples.scala代码文件,用vim StreamingExamples.scala
用于设置log4j:
package org.apache.spark.examples.streaming
import org.apache.spark.internal.Logging
import org.apache.log4j.{Level, Logger}
/** Utility functions for Spark Streaming examples. */
object StreamingExamples extends Logging {
/** Set reasonable logging levels for streaming if the user has not configured log4j. */
def setStreamingLogLevels() {
val log4jInitialized = Logger.getRootLogger.getAllAppenders.hasMoreElements
if (!log4jInitialized) {
// We first log something to initialize Spark's default logging, then we override the
// logging level.
logInfo("Setting log level to [WARN] for streaming example." +
" To override add a custom log4j.properties to the classpath.")
Logger.getRootLogger.setLevel(Level.WARN)
}
}
}
这样,我们在“/usr/local/spark/mycode/kafka/src/main/scala”目录下,就有了如下三个代码文件:
KafkaWordProducer.scala
KafkaWordCount.scala
StreamingExamples.scala
打包编辑后,
执行上面命令后,屏幕上会不断滚动出现新的单词,如下:
7 5 0 7 3
2 8 2 1 3
0 1 2 9 2
8 0 9 0 9
9 0 0 6 8
6 6 1 6 5
8 3 6 7 7
3 3 2 6 8
4 5 8 1 5
3 8 8 4 8
2 7 6 3 6
5 7 0 3 6
8 2 9 4 8
2 6 7 6 7
8 8 9 4 5
3 3 2 6 7
0 1 5 8 4
6 1 1 9 0
9 5 6 6 6
2 4 4 2 9
2 0 1 8 8
3 8 4 2 5
运行上面命令以后,就启动了词频统计功能,屏幕上就会显示如下类似信息:
-------------------------------------------
Time: 1488156500000 ms
-------------------------------------------
(4,5)
(8,12)
(6,14)
(0,19)
(2,11)
(7,20)
(5,10)
(9,9)
(3,9)
(1,11)
-------------------------------------------
Time: 1488156510000 ms
-------------------------------------------
(4,18)
(8,24)
(6,21)
(0,39)
(2,31)
(7,33)
(5,27)
(9,27)
(3,21)
(1,29)