下面我们通过一段在线黑名单过滤的代码实验剖析SparkStreaming的运行本质
import java.util.Arrays;
import java.util.List;
import org.apache.spark.SparkConf;
import org.apache.spark.api.java.JavaPairRDD;
import org.apache.spark.api.java.JavaRDD;
import org.apache.spark.api.java.function.Function;
import org.apache.spark.api.java.function.PairFunction;
import org.apache.spark.streaming.Durations;
import org.apache.spark.streaming.api.java.JavaPairDStream;
import org.apache.spark.streaming.api.java.JavaReceiverInputDStream;
import org.apache.spark.streaming.api.java.JavaStreamingContext;
import com.google.common.base.Optional;
import scala.Tuple2;
/**
* 背景描述:在广告点击计费系统中,我们在线过滤掉黑名单的点击,进而保护广告商的利益,只进行有效的广告点击计费
* 或者在防刷评分(或者流量)系统,过滤掉无效的投票或者评分或者流量;
* 实现技术:使用transform Api直接基于RDD编程,进行join操作
*新浪微博:http://weibo.com/ilovepains/
*/
public class OnlineBlackListFilter {
public static void main(String[] args) {
/*
* 第一步:配置SparkConf,SparkConf主要用于配置运行时的配置信息,
* 比如说:通过setAppName来设置appName,通过setMaster来设置Spark集群的Master的URL
*/
SparkConf conf = new SparkConf().setMaster("spark://Master:7077,Worker1:7077").setAppName("OnlineBlackListFilter");
/*
* 第二步:创建SparkStreamingContext:
* SparkStreaming应用程序所有功能的起始点和程序调度的核心
*/
JavaStreamingContext jsc = new JavaStreamingContext(conf, Durations.seconds(300));
/*
* 第三步:创建Spark Streaming输入数据来源,此处我们采用socket网络通信为输入来源
*/
JavaReceiverInputDStream adsClickStream = jsc.socketTextStream("Master",9999);
/**
* 黑名单数据准备,实际上黑名单一般都是动态的,例如在Redis或者数据库中,黑名单的生成往往有复杂的业务
* 逻辑,具体情况算法不同,但是在Spark Streaming进行处理的时候每次都能工访问完整的信息
*/
List> blackList = Arrays.asList(new Tuple2("hadoop",true),new Tuple2("mahout",true));
final JavaPairRDD blackListRDD = jsc.sparkContext().parallelizePairs(blackList);
/**
* 此处模拟的广告点击的每条数据的格式为:time、name
* 此处map操作的结果是name、(time,name)的格式
*/
JavaPairDStream adsClickStreamFormatted = adsClickStream.mapToPair(new PairFunction() {
@Override
public Tuple2 call(String line) throws Exception {
// TODO Auto-generated method stub
String[] words = line.split(" ");
return new Tuple2(words[1],line);
}
});
adsClickStreamFormatted.transform(new Function, JavaRDD>() {
@Override
public JavaRDD call(JavaPairRDD userClickRDD) throws Exception {
// TODO Auto-generated method stub
//通过leftOuterJoin操作既保留了左侧用户广告点击内容的RDD的所有内容,又获得了相应点击内容是否在黑名单中
JavaPairRDD>> joinedBlackListRDD = userClickRDD.leftOuterJoin(blackListRDD);
/**
* 进行filter过滤的时候,其输入元素是一个Tuple:(name,((time,name), boolean))
* 其中第一个元素是黑名单的名称,第二元素的第二个元素是进行leftOuterJoin的时候是否存在在值
* 如果存在的话,表面当前广告点击是黑名单,需要过滤掉,否则的话则是有效点击内容;
*/
JavaPairRDD>> validClicked = joinedBlackListRDD.filter(new Function>>, Boolean>() {
@Override
public Boolean call(Tuple2>> v1) throws Exception {
// TODO Auto-generated method stub
if(v1._2._2.isPresent()){
return false;
}
return true;
}
});
return validClicked.map(new Function>>, String>() {
@Override
public String call(Tuple2>> validClick) throws Exception {
// TODO Auto-generated method stub
return validClick._2._1;
}
});
}
}).print();
jsc.start();
jsc.awaitTermination();
jsc.close();
}
}
将代码编写完成之后,接下来我们需要做的是将程序打成jar的方式放到我们的spark集群上,当然我们是要先启动我们的spark集群的,在我们这边的实验中为了更好的查看Job的运行情况,我们还启动了history-server
由于我们的程序是通过socket网络通信作为数据的输入来源,下面我们在shell界面上先执行nc
[oracle@Master ~]$ nc -lk 9999
接下来我们通过spark-submit提交我们的程序
/home/oracle/soft/spark-1.6.0-bin-hadoop2.6/bin/spark-submit --class com.xxj.spark.SparkApps.sparkstreaming.OnlineBlackListFilter --master spark://Master:7077,Worker1:7077 /home/oracle/data/SparkApps-0.0.1-SNAPSHOT.jar
下面我们在打开netcat,填一些数据,如下:
[oracle@Master ~]$ nc -lk 9999
hello spark
hello hadoop
adsa adas
hello mahout
gg asd
erte 3453
yuyu ert
运行完程序之后,我们可以在history-server的web页面上,看到如下信息
我们点击App ID进入查看job的运行情况:
从上面的图片我们可以看到个程序在运行期间,启动了4个Job。
下面我们先看一下 Job Id 0,点击进入详细页面
从Job Id 0的DAG图中执行了reduceBykey的算子操作,但我们在实际的代码中并没有reduceBykey的代码操作,由此可见在程序执行jsc.start()的时候框架本身帮我们额外的执行了一个job
我们再看看Job Id 1的页面
从上图我们可以看出这个的运行时间长达3.5 min,我们整个程序的执行才3.9min,从job的运行情况图上start at OnlineBlackListFilter.java:99中我们可以看出在StreamingContext start的时候,这个job也启动了,这个Job主要做什么呢?
是这样的:StreamingContext start的时候会通过启动一个Job的方式来启动Receiver,而且Receiver只会在一个Worker节点中Executor进程中执行,且以一个Task线程去接受我们的数据
下图是Job1的具体详细页面:
从上图可以看出Receiver在接收的数据的时候,默认是存放在内存当中的,当内存放不下时才会放在磁盘当中,当然对于这点我们从以下源码当中也可以出
def socketTextStream(
hostname: String,
port: Int,
storageLevel: StorageLevel = StorageLevel.MEMORY_AND_DISK_SER_2
): ReceiverInputDStream[String] = withNamedScope("socket text stream") {
socketStream[String](hostname, port, SocketReceiver.bytesToLines, storageLevel)
}
接下来我们再看看job2详细
从Job 2的DAG图我们可以我们程序的主要业务逻辑,在Stage 3、Stage 4、Stage 5得到了充分的体现,stage3、stage4接受到了两种不同的数据来源然后在stage5中进行transform操作
接下面我们再看看job3的详细
从Job3中我们也可以看到,其实Job2、Job3对于的DAG图是一样的,并且也都体现了我们程序的业务逻辑,不同的是在Job3中stage6、stage7是skipped的而已
从这4个Job我们可以看出在Spark应用程序中往往可以启动多个Job,不同的Job之间可以相互配合共同完成一个作业
探讨Spark Streaming本质
1、从官网的这张图我们可以看出Spark Streaming可以接受不同的数据来源,且处理之后也可以保存到多种介质中去
2、Spark Streaming提供了用于表示连续数据流的、高度抽象的被称为离散流的DStream。Dsream其本质上表示RDD的序列,也就是在以一个固定的时间间隔切割的一段RDD的数据流,所以对DStream的操作最终都会转变为对Spark Code RDD的操作,这样也就意味着DStream实际上是一种逻辑级别上的概念,相当于我们人为的把实时流进来的数据切为一段一段的(以固定的时间间隔),从空间的角度来看实际上这一段一段的数据实际上是固定不变的,这样的话也就是满足了Spark Code RDD只能对静态数据操作的要求,但时间的维度来看其实数据是流动的,这也是Spark Straming的精髓所在