在文件流的应用场景中,需要编写Spark Streaming程序,一直对文件系统中的某个目录进行监听,一旦发现有新的文件生成,Spark Streaming就会自动把文件内容读取过来,使用用户自定义的处理逻辑进行处理。
在Linux系统中打开第一个终端(为了便于区分多个终端,这里称为“数据源终端”),创建一个logfile目录
$ cd /usr/local/spark/mycode
$ mkdir streaming
$ cd streaming
$ mkdir logfile
在Linux系统中打开第二个终端(为了便于区分多个终端,这里称为“流计算终端”),启动进入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")
scala> val words = lines.flatMap(_.split(" "))
scala> val wordCounts = words.map(x => (x,1)).reduceByKey(_ + _)
scala> wordCounts.print()
scala> ssc.start()
scala> ssc.awaitTermination()
ssc.textFileStream()语句用于创建一个“文件流”类型的输入源。接下来的lines.flatMap()、words.map()和wordCounts.print()是流计算处理过程,负责对文件流中发送过来的文件内容进行词频统计。ssc.start()语句用于启动流计算过程,实际上,当在spark-shell中输入ssc.start()并按Enter键后,Spark Streaming就开始进行循环监听,下面的ssc.awaitTermination()是无法输入到屏幕上的,但是,为了程序完整性,这里还是给出ssc.awaitTermination()。可以使用组合键Ctrl+C,在任何时候手动停止这个流计算过程。
在spark-shell中输入ssc.start()以后,程序就开始自动进入循环监听状态。
//这里省略若干屏幕信息
-------------------------------------------
Time: 1479431100000 ms
-------------------------------------------
//这里省略若干屏幕信息
-------------------------------------------
Time: 1479431120000 ms
-------------------------------------------
//这里省略若干屏幕信息
-------------------------------------------
Time: 1479431140000 ms
-------------------------------------------
这时可以切换到第一个Linux终端(即“数据源终端”),在“/usr/local/spark/mycode/streaming/logfile”目录下新建一个log.txt文件,在文件中输入一些英文语句后保存并退出文件编辑器。然后,切换到第二个Linux终端(即“流计算终端”),最多等待20秒,就可以看到词频统计结果。
首先,创建代码目录和代码文件TestStreaming.scala,在Linux系统中,重新打开一个终端(为了便于区分多个终端窗口,这里称为“流计算终端”)。
$ cd /usr/local/spark/mycode
$ mkdir streaming
$ cd streaming
$ mkdir file
$ cd file
$ mkdir -p src/main/scala
$ cd src/main/scala
$ vim TestStreaming.scala
TestStreaming.scala文件内容:
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]")
//设置为本地运行模式,两个线程,一个监听,另一个处理数据
val ssc = new StreamingContext(sparkConf, Seconds(2)) // 时间间隔为2秒
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()
}
}
在“/usr/local/spark/mycode/streaming/file”目录下创建一个simple.sbt文件,内容如下:
name := "Simple Project"
version := "1.0"
scalaVersion := "2.11.8"
libraryDependencies += "org.apache.spark" % "spark-streaming_2.11" % "2.1.0"
使用sbt工具对代码进行编译打包,命令如下:
$ cd /usr/local/spark/mycode/streaming/file
$ /usr/local/sbt/sbt package
打包成功以后,就可以输入以下命令启动这个程序:
$ cd /usr/local/spark/mycode/streaming/file
$ /usr/local/spark/bin/spark-submit \
> --class "WordCountStreaming" \
> ./target/scala-2.11/simple-project_2.11-1.0.jar
在“流计算终端”内执行上面命令后,程序就进入了监听状态。新建另一个Linux终端(这里称为“数据源终端”),在“/usr/local/spark/mycode/streaming/logfile”目录下再新建一个log2.txt文件,文件里面输入一些单词,保存文件并退出文件编辑器。再次切换回“流计算终端”,最多等待20秒以后,按组合键Ctrl+C或者Ctrl+D停止监听程序,就可以看到“流计算终端”的屏幕上会打印出单词统计信息。
Spark Streaming可以通过Socket端口监听并接收数据。
在网络编程中,大量的数据交换都是通过Socket实现的。Socket工作原理和日常生活的电话交流非常类似。在日常生活中,用户A要打电话给用户B,首先,用户A拨号,用户B听到电话铃声后提起电话,这时A和B就建立起了连接,二者之间就可以通话了。等交流结束以后,挂断电话结束此次交谈。Socket工作过程也是类似的,即“open(拨电话)—write/read(交谈)—close(挂电话)”模式。服务器端先初始化Socket,然后与端口绑定(Bind),对端口进行监听(Listen),调用accept()方法进入阻塞状态,等待客户端连接。客户端初始化一个Socket,然后连接服务器(Connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。
创建代码目录和代码文件NetworkWordCount.scala。关闭Linux系统中已经打开的所有终端,新建一个终端(为了便于区分,这里称为“流计算终端”),在该终端里执行如下命令:
$ cd /usr/local/spark/mycode
$ mkdir streaming #如果已经存在该目录,则不用创建
$ cd streaming
$ mkdir socket
$ cd socket
$ mkdir -p src/main/scala #如果已经存在该目录,则不用创建
$ cd /usr/local/spark/mycode/streaming/socket/src/main/scala
$ vim NetworkWordCount.scala #这里使用vim编辑器创建文件
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 " )
System.exit(1)
}
StreamingExamples.setStreamingLogLevels()
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)
val words = lines.flatMap(_.split(" "))
val wordCounts = words.map(x => (x, 1)).reduceByKey(_ + _)
wordCounts.print()
ssc.start()
ssc.awaitTermination()
}
}
StreamingExamples.setStreamingLogLevels()用于设置log4j的日志级别,从而使得在程序运行过程中,wordCounts.print()语句的打印信息能够得到正确显示;StreamingExamples是在另一个代码文件StreamingExamples.scala中定义的。ssc.socketTextStream()用于创建一个“套接字流”类型的输入源。ssc.socketTextStream()有3个输入参数,其中,args(0)提供了主机地址,args(1).toInt提供了通信端口号,Socket客户端使用该主机地址和端口号与服务器端建立通信,StorageLevel.MEMORY_AND_DISK_SER表示Spark Streaming作为客户端,在接收到来自服务器端的数据以后,采用的存储方式为MEMORY_AND_DISK_SER,即使用内存和磁盘作为存储介质。lines.flatMap()、words.map()和wordCounts.print()是自定义的处理逻辑,用于实现对源源不断到达的流数据执行词频统计。
在与代码文件NetworkWordCount.scala相同的目录下,新建一个代码文件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)
}
}
}
StreamingExamples.scala代码中定义了一个单例对象StreamingExamples,它继承自org.apache.spark.internal.Logging类,在该单例对象中定义了一个方法setStreamingLogLevels(),它会把log4j日志的级别设置为WARN。由于单例对象中的方法都是静态方法,因此,在NetworkWordCount.scala中可以直接调用StreamingExamples.setStreamingLogLevels()。
在“/usr/local/spark/mycode/streaming/socket”目录下创建一个simple.sbt文件:
name := "Simple Project"
version := "1.0"
scalaVersion := "2.11.8"
libraryDependencies += "org.apache.spark" % "spark-streaming_2.11" % "2.1.0"
使用sbt工具对代码进行编译打包,命令如下:
$ cd /usr/local/spark/mycode/streaming/socket
$ /usr/local/sbt/sbt package
打包成功以后,就可以输入以下命令启动这个程序:
$ cd /usr/local/spark/mycode/streaming/socket
$ /usr/local/spark/bin/spark-submit \
> --class "org.apache.spark.examples.streaming.NetworkWordCount" \
> ./target/scala-2.11/simple-project_2.11-1.0.jar \
> localhost 9999
执行上面命令以后,就在当前的Linux终端(即“流计算终端”)内顺利启动了Socket客户端。现在,再打开一个新的Linux终端(这里称为“数据源终端”),启动一个Socket服务器端,让该服务器端接收客户端的请求,并给客户端不断发送数据流。通常,Linux发行版中都带有NetCat(简称nc),可以使用如下nc命令生成一个Socket服务器端:
$ nc -lk 9999
在上面的nc命令中,-l这个参数表示启动监听模式,也就是作为Socket服务器端,nc会监听本机(localhost)的9999号端口,只要监听到来自客户端的连接请求,就会与客户端建立连接通道,把数据发送给客户端;-k参数表示多次监听,而不是只监听1次。
由于之前已经在“流计算终端”内运行了NetworkWordCount程序,该程序扮演了Socket客户端的角色,会向本地(localhost)主机的9999号端口发起连接请求,所以,“数据源终端”内的nc程序就会监听到本地(localhost)主机的9999号端口有来自客户端(NetworkWordCount程序)的连接请求,于是就会建立服务器端(nc程序)和客户端(NetworkWordCount程序)之间的连接通道。连接通道建立以后,nc程序就会把我们在“数据源终端”内手动输入的内容,全部发送给“流计算终端”内的NetworkWordCount程序进行处理。为了测试程序运行效果,在“数据源终端”内执行上面的nc命令后,可以通过键盘输入一行英文句子后按Enter键,反复多次输入英文句子并按Enter键,nc程序会自动把一行又一行的英文句子不断发送给“流计算终端”的NetworkWordCount程序。在“流计算终端”内,NetworkWordCount程序会不断接收到nc发来的数据,每隔1秒就会执行词频统计,并打印出词频统计信息。
在“流计算终端”的屏幕上出现类似如下的结果:
-------------------------------------------
Time: 1479431100000 ms
-------------------------------------------
(hello,1)
(world,1)
-------------------------------------------
Time: 1479431120000 ms
-------------------------------------------
(hadoop,1)
-------------------------------------------
Time: 1479431140000 ms
-------------------------------------------
(spark,1)
采用自己编写的程序产生Socket数据源。
新建一个代码文件DataSourceSocket.scala:
$ cd /usr/local/spark/mycode/streaming/socket/src/main/scala
$ vim DataSourceSocket.scala
DataSourceSocket.scala内容:
package org.apache.spark.examples.streaming
import java.io.{PrintWriter} import java.net.ServerSocket
import scala.io.Source
object DataSourceSocket {
def index(length: Int) = { //返回位于0到length-1之间的一个随机数
val rdm = new java.util.Random
rdm.nextInt(length)
}
def main(args: Array[String]) {
if (args.length != 3) {
System.err.println("Usage: " )
System.exit(1)
}
val fileName = args(0) //获取文件路径
val lines = Source.fromFile(fileName).getLines.toList //读取文件中的所有行的内容
val rowCount = lines.length //计算出文件的行数
val listener = new ServerSocket(args(1).toInt) //创建监听特定端口的ServerSocket对象
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) // 每隔多长时间发送一次数据
val content = lines(index(rowCount)) //从lines列表中取出一个元素
println(content)
out.write(content + '\n') //写入要发送给客户端的数据
out.flush() //发送数据给客户端
}
socket.close()
}
}.start()
}
}
}
从一个文件中读取内容,把文件的每一行作为一个字符串,每次随机选择文件中的一行,源源不断发送给客户端(即NetworkWordCount程序)。DataSourceSocket程序在运行时,需要为该程序提供3个参数,即、和,其中,表示作为数据源头的文件的路径,表示Socket通信的端口号,表示Socket服务器端(即DataSourceSocket程序)每隔多长时间向客户端(即NetworkWordCount程序)发送一次数据。
val lines = Source.fromFile(fileName).getLines.toList语句执行后,文件中的所有行的内容都会被读取到列表lines中。val listener = new ServerSocket(args(1).toInt)语句用于在服务器端创建监听特定端口(端口是args(1).toInt)的ServerSocket对象,ServerSocket负责接收客户端的连接请求。val socket =listener.accept()语句执行后,listener会进入阻塞状态,一直等待客户端(即NetworkWordCount程序)的连接请求。一旦listener监听到在特定端口(如9999)上有来自客户端的请求,就会执行new Thread(),生成新的线程,负责和客户端建立连接,并发送数据给客户端。
DataSourceSocket程序需要把一个文本文件作为输入参数,所以,在启动这个程序之前,需要首先创建一个文本文件“/usr/local/spark/mycode/streaming/socket/word.txt”并随便输入几行英文语句。然后,就可以在当前终端(数据源终端)内执行如下命令启动DataSourceSocket程序:
$ cd /usr/local/spark/mycode/streaming/socket
$ /usr/local/spark/bin/spark-submit \
> --class "org.apache.spark.examples.streaming.DataSourceSocket" \
> ./target/scala-2.11/simple-project_2.11-1.0.j ar \
> ./word.txt 9999 1000
DataSourceSocket程序启动后,会一直监听9999端口,一旦监听到客户端的连接请求,就会建立连接,每隔1000毫秒(1秒)向客户端源源不断发送数据。
启动客户端,即NetworkWordCount程序。新建一个终端(这里称为“流计算终端”),输入以下命令启动NetworkWordCount程序:
$ cd /usr/local/spark/mycode/streaming/socket
$ /usr/local/spark/bin/spark-submit \
> --class "org.apache.spark.examples.streaming.NetworkWordCount" \
> ./target/scala-2.11/simple-project_2.11-1.0.jar \
> localhost 9999
执行上面命令以后,就在当前的Linux终端(即“流计算终端”)内顺利启动了Socket客户端,它会向本机(Localhost)的9999号端口发起Socket连接。在另外一个终端(数据源终端)内正在运行的DataSourceSocket程序,一直在监听9999端口,一旦监听到NetworkWordCount程序的连接请求,就会建立连接,每隔1000毫秒(1秒)向NetworkWordCount源源不断发送数据。流计算终端内的NetworkWordCount程序收到数据后,就会执行词频统计,打印出类似如下的统计信息:
-------------------------------------------
Time: 1479431100000 ms
-------------------------------------------
(hello,1) (world,1)
-------------------------------------------
Time: 1479431120000 ms
-------------------------------------------
(hadoop,1)
-------------------------------------------
Time: 1479431140000 ms
-------------------------------------------
(spark,1)
在编写Spark Streaming应用程序的时候,可以调用StreamingContext对象的queueStream()方法来创建基于RDD队列的DStream。例如,streamingContext.queueStream(queueOfRDD),其中,queueOfRDD是一个RDD队列。
在“/usr/local/spark/mycode/streaming/rddqueue”目录下,新建一个TestRDDQueueStream.scala代码文件
文件内容:
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]")
val ssc = new StreamingContext(sparkConf, Seconds(2))
val rddQueue =new scala.collection.mutable.SynchronizedQueue[RDD[Int]]()
val queueStream = ssc.queueStream(rddQueue)
val mappedStream = queueStream.map(r => (r % 10, 1))
val reducedStream = mappedStream.reduceByKey(_ + _)
reducedStream.print()
ssc.start()
for (i <- 1 to 10){
rddQueue += ssc.sparkContext.makeRDD(1 to 100,2)
Thread.sleep(1000)
}
ssc.stop()
}
}
代码释义:
val queueStream = ssc.queueStream(rddQueue)语句用于创建一个“RDD队列流”类型的数据源。在该程序中,Spark Streaming会每隔两秒从rddQueue这个队列中取出数据(即若干个RDD)进行处理。
val mappedStream = queueStream.map(r => (r % 10, 1))语句会把queueStream中的每个RDD元素进行转换。例如,如果取出的RDD元素是67,就会被转换成一个元组(7,1)。
val reducedStream = mappedStream.reduceByKey(_ + _)语句负责统计每个余数的出现次数。reducedStream.print()负责打印输出统计结果。
ssc.start()语句执行以后,流计算过程就开始了,Spark Streaming会每隔两秒从rddQueue这个队列中取出数据(即若干个RDD)进行处理。但是,这时的RDD队列rddQueue中没有任何RDD存在,所以,下面通过一个for (i <- 1 to 10)循环,不断向rddQueue中加入新生成的RDD。ssc.sparkContext.makeRDD(1 to 100,2)的功能是创建一个RDD,这个RDD被分成两个分区,RDD中包含100个元素,即1,2,3,…,99,100。
for循环执行10次以后,ssc.stop()语句被执行,整个流计算过程停止。
在“/usr/local/spark/mycode/streaming/rddqueue”目录下创建一个simple.sbt文件,然后使用sbt工具进行编译打包,并执行如下命令运行该程序:
$ cd /usr/local/spark/mycode/streaming/rddqueue
$ /usr/local/spark/bin/spark-submit \
> --class "org.apache.spark.examples.streaming.QueueStream" \
> ./target/scala-2.11/simple-project_2.11-1.0.jar
运行结果:
-------------------------------------------
Time: 1479522100000 ms
-------------------------------------------
(4,10)
(0,10)
(6,10)
(8,10)
(2,10)
(1,10)
(3,10)
(7,10)
(9,10)
Spark Streaming是用来进行流计算的组件,可以把Kafka(或Flume)作为数据源,让Kafka(或Flume)产生数据发送给Spark Streaming应用程序,Spark Streaming应用程序再对接收到的数据进行实时处理,从而完成一个典型的流计算过程。这里仅以Kafka为例进行介绍,Spark和Flume的组合使用也是类似的,这里不再赘述,可以参考本教材官网的“实验指南”栏目的“使用Flume作为Spark的数据源”。
文章来源:《Spark技术内幕:深入解析Spark内核架构设计与实现原理》 作者:张安站
文章内容仅供学习交流,如有侵犯,联系删除哦!