SparkStreaming
sparkStreaming是基于spark的流式计算框架,支持可扩展、分布式、高吞吐的准实时流计算,可以支持kafaka、flume、scokets的数据来源,随后进行复杂的算子或者可以自己定义的逻辑处理数据,最后将数据或者结果存储到hdfs或者磁盘上。
sparkStreaming和strom的区别
- storm是纯实时流处理,spakstreaming是准实时(微批处理)的处理框架,吞吐量要比storm要高
- storm的事务处理要比saprkstreaming要好
- sparkstreaming可以进行复杂的计算处理,storm只能进行简单的汇总报告处理
- storm支持资源调度(spark1.2也支持)
sparkStreaming流程理解
客户端进行9999端口进行数据发送 ,sparkstreaming开启端口监听,receiver task来接收数据处理 receiver task是7*24小时一直在执行,一直接受数据,将一段时间内接收来的数据保存到batch中。假设batchInterval为5s,那么会将接收来的数据每隔5秒封装到一个batch中,batch没有分布式计算特性,这一个batch的数据又被封装到一个RDD中最终封装到一个DStream中。DStream的底层还是RDD,包括逻辑处理的算子也和RDD类似。经过一系列逻辑处理后最后要output Operator算子输出结果,相当于RDD中的action算子。
注意:batchinterval假如为5,每隔5秒通过SparkStreaming将得到一个DStream,在第6秒的时候计算这5秒的数据,假设执行任务的时间是3秒,那么第6到9秒在一边接收数据一边在计算,10秒只是在接收数据。然后在第11秒的时候重复上面的操作。
如果job执行的时间大于batchInterval,那么接受过来的数据设置的级别是仅内存,接收来的数据会越堆积越多,最后可能会导致OOM(如果设置StorageLevel包含disk, 则内存存放不下的数据会溢写至disk, 加大延迟 )。这在后面的优化会提到。
sparkStreaming代码运行
简单的wordcount代码
在这里插入代码片/**
* 1、local的模拟线程数必须大于等于2 因为一条线程被receiver(接受数据的线程)占用,另外一个线程是job执行
* 2、Durations时间的设置,就是我们能接受的延迟度,这个我们需要根据集群的资源情况以及监控每一个job的执行时间来调节出最佳时间。
* 3、 创建JavaStreamingContext有两种方式 (sparkconf、sparkcontext)
* 4、业务逻辑完成后,需要有一个output operator
* 5、JavaStreamingContext.start()straming框架启动之后是不能在次添加业务逻辑
* 6、JavaStreamingContext.stop()无参的stop方法会将sparkContext一同关闭,stop(false) 只会关闭SteamContext ,sparkcontext依然存在
* 7、JavaStreamingContext.stop()停止之后是不能在调用start
*/
public class WordCountOnline {
@SuppressWarnings("deprecation")
public static void main(String[] args) throws InterruptedException {
final SparkConf conf = new SparkConf().setMaster("local[2]").setAppName("WordCountOnline");
/**
* 在创建streaminContext的时候 设置batch Interval
*/
// final JavaStreamingContext jsc = new JavaStreamingContext(conf, Durations.seconds(5));
JavaSparkContext sc = new JavaSparkContext(conf);
JavaStreamingContext jsc = new JavaStreamingContext(sc, Durations.seconds(5));
// final JavaSparkContext sparkContext = jsc.sparkContext();
JavaReceiverInputDStream<String> lines = jsc.socketTextStream("node01", 9999);
JavaDStream<String> words = lines.flatMap(new FlatMapFunction<String, String>() {
private static final long serialVersionUID = 1L;
@Override
public Iterator<String> call(String s) {
return Arrays.asList(s.split(" ")).iterator();
}
});
// JavaPairDStream<String, Integer> ones = words.mapToPair(x->new Tuple2<>(x,1));
JavaPairDStream<String, Integer> ones = words.mapToPair(new PairFunction<String, String, Integer>() {
@Override
public Tuple2<String, Integer> call(String s) throws Exception {
return new Tuple2<>(s, 1);
}
});
JavaPairDStream<String, Integer> counts = ones.reduceByKey(new Function2<Integer, Integer, Integer>() {
private static final long serialVersionUID = 1L;
@Override
public Integer call(Integer i1, Integer i2) {
return i1 + i2;
}
});
//outputoperator类的算子
counts.print();
jsc.start();
//等待spark程序被终止
jsc.awaitTermination();
jsc.stop();
System.out.println("stop=====================");
}
}
sparkStreaming算子
- foreachRDD
output operation算子,必须对抽取出来的RDD执行action类算子,代码才能执行。类似的还有print算子。
public static void main(String[] args) {
SparkConf sparkConf = new SparkConf();
sparkConf.setMaster("local[2]").setAppName("sparkStream");
JavaSparkContext sparkContext = new JavaSparkContext(sparkConf);
JavaStreamingContext streamingContext = new JavaStreamingContext(sparkContext, Durations.seconds(5));
JavaReceiverInputDStream<String> dStream = streamingContext.socketTextStream("node01", 9999);
JavaPairDStream<String, Integer> counts = dStream.flatMap(new FlatMapFunction<String, String>() {
@Override
public Iterator<String> call(String s) throws Exception {
return Arrays.asList(s.split(" ")).iterator();
}
}).mapToPair(new PairFunction<String, String, Integer>() {
@Override
public Tuple2<String, Integer> call(String s) throws Exception {
return new Tuple2<>(s, 1);
}
}).reduceByKey(new Function2<Integer, Integer, Integer>() {
@Override
public Integer call(Integer v1, Integer v2) throws Exception {
return v1 + v2;
}
});
counts.foreachRDD(new VoidFunction<JavaPairRDD<String, Integer>>() {
@Override
public void call(JavaPairRDD<String, Integer> RDD) throws Exception {
System.out.println("**********lllllllll************");
RDD.foreach(new VoidFunction<Tuple2<String, Integer>>() {
@Override
public void call(Tuple2<String, Integer> tuple2) throws Exception {
System.out.println(tuple2);
}
});
}
});
// counts.print();
streamingContext.start();
try {
streamingContext.awaitTermination();
} catch (InterruptedException e) {
e.printStackTrace();
}
streamingContext.stop();
}
- transform算子
transformation类算子 可以通过transform算子,对Dstream做RDD到RDD的任意操作。
public static void main(String[] args) {
SparkConf sparkConf = new SparkConf();
sparkConf.setMaster("local[2]").setAppName("sparkStream");
JavaSparkContext sparkContext = new JavaSparkContext(sparkConf);
JavaStreamingContext streamingContext = new JavaStreamingContext(sparkContext, Durations.seconds(5));
JavaReceiverInputDStream<String> dStream = streamingContext.socketTextStream("node01", 9999);
JavaDStream<String> result = dStream.mapToPair(new PairFunction<String, String, String>() {
@Override
public Tuple2<String, String> call(String s) throws Exception {
return new Tuple2<>(s.split(" ")[1], s);
}
}).transform(new Function<JavaPairRDD<String, String>, JavaRDD<String>>() {
@Override
public JavaRDD<String> call(JavaPairRDD<String, String> stringStringJavaPairRDD) throws Exception {
return stringStringJavaPairRDD.map(new Function<Tuple2<String, String>, String>() {
@Override
public String call(Tuple2<String, String> stringStringTuple2) throws Exception {
return stringStringTuple2._2();
}
});
}
});
result.print();
streamingContext.start();
try {
streamingContext.awaitTermination();
} catch (InterruptedException e) {
e.printStackTrace();
}
streamingContext.stop();
}
需要注意的是虽然这个算子没有什么意思,但是注意里面重写方法的时候除了RDD逻辑处理以外的代码都是在Driver端执行的,根据需要使用。
- updateStateByKey
transformation算子
1>为SparkStreaming中每一个Key维护一份state状态,state类型可以是任意类型的,可以是一个自定义的对象,更新函数也可以是自定义的。
2> 通过更新函数对该key的状态不断更新,对于每个新的batch而言,SparkStreaming会在使用updateStateByKey的时候为已经存在的key进行state的状态更新。
3>使用到updateStateByKey要开启checkpoint机制和功能。
多久会将内存中的数据写入到磁盘一份?
如果batchInterval设置的时间小于10秒,那么10秒写入磁盘一份。如果batchInterval设置的时间大于10秒,那么就会batchInterval时间间隔写入磁盘一份。
public static void main(String[] args) {
SparkConf conf = new SparkConf().setMaster("local[2]").setAppName("UpdateStateByKeyDemo");
JavaStreamingContext jsc = new JavaStreamingContext(conf, Durations.seconds(5));
/**
* 设置checkpoint目录
*
* 多久会将内存中的数据(每一个key所对应的状态)写入到磁盘上一份呢?
* 如果你的batch interval小于10s 那么每格10s会将内存中的数据写入到磁盘上
* 如果bacth interval 大于10s,那么就以bacth interval为准
*
* 这样做是为了防止频繁的写HDFS
*/
// jsc.checkpoint("hdfs://shsxt/spark/checkpoint");
jsc.checkpoint("./checkpoint");
JavaReceiverInputDStream<String> lines = jsc.socketTextStream("node01", 9999);
JavaDStream<String> words = lines.flatMap(x-> Arrays.asList(x.split(" ")).iterator());
// JavaPairDStream<String, Integer> ones = words.mapToPair(x->new Tuple2<>(x,1));
JavaPairDStream<String, Integer> ones = words.mapToPair(new PairFunction<String, String, Integer>() {
@Override
public Tuple2<String, Integer> call(String s) throws Exception {
return new Tuple2<>(s, 1);
}
});
JavaPairDStream<String, Integer> counts =
ones.updateStateByKey(new Function2<List<Integer>, Optional<Integer>, Optional<Integer>>() {
private static final long serialVersionUID = 1L;
@Override
public Optional<Integer> call(List<Integer> values, Optional<Integer> state) throws Exception {
/**
* values:经过分组最后 这个key所对应的value [1,1,1,1,1]
* state:这个key在本次之前之前的状态
*/
Integer updateValue = 0;
if (state.isPresent()) {
updateValue = state.get();
}
System.out.println(updateValue + " ======== ");
for (Integer value : values) {
updateValue += value;
}
return Optional.of(updateValue);
}
});
//output operator
counts.print();
jsc.start();
try {
jsc.awaitTermination();
} catch (InterruptedException e) {
e.printStackTrace();
}
jsc.close();
}
- 窗口操作
假设每隔5s 1个batch,上图中窗口长度为15s,窗口滑动间隔10s。
窗口长度和滑动间隔必须是batchInterval的整数倍。如果不是整数倍会检测报错。并且在长度和华东间隔要设置连续,不然会少读数据
没优化时直接是假如是框三个,直接读取三批数据的数据,reducebykey操作,优化后是按窗口滑动,保存上一个窗口的数据状态,在读取新的一批数据加上,在减去之前丢出去的一批数据,优化后的window操作要保存状态所以要设置checkpoint路径,没有优化的window操作可以不设置checkpoint路径。
代码如下
public static void main(String[] args) {
SparkConf conf = new SparkConf()
.setMaster("local[2]")
.setAppName("WindowHotWord");
JavaStreamingContext jssc = new JavaStreamingContext(conf, Durations.seconds(5));
/**
* 设置日志级别为WARN
*
*/
jssc.sparkContext().setLogLevel("WARN");
/**
* 注意:
* 没有优化的窗口函数可以不设置checkpoint目录
* 优化的窗口函数必须设置checkpoint目录
*/
// jssc.checkpoint("hdfs://node1:9000/spark/checkpoint");
// jssc.checkpoint("./checkpoint");
JavaReceiverInputDStream<String> searchLogsDStream = jssc.socketTextStream("node01", 7777);
//word 1
JavaDStream<String> searchWordsDStream = searchLogsDStream.flatMap(new FlatMapFunction<String, String>() {
private static final long serialVersionUID = 1L;
@Override
public Iterator<String> call(String t) throws Exception {
System.out.println(t + "*************");
return Arrays.asList(t.split(" ")).iterator();
}
});
// 将搜索词映射为(searchWord, 1)的tuple格式
JavaPairDStream<String, Integer> searchWordPairDStream = searchWordsDStream.mapToPair(
new PairFunction<String, String, Integer>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2<String, Integer> call(String searchWord)
throws Exception {
return new Tuple2<String, Integer>(searchWord, 1);
}
});
/**
* 每隔10秒,计算最近60秒内的数据,那么这个窗口大小就是60秒,里面有12个rdd,在没有计算之前,这些rdd是不会进行计算的。
* 那么在计算的时候会将这12个rdd聚合起来,然后一起执行reduceByKeyAndWindow操作 ,
* reduceByKeyAndWindow是针对窗口操作的而不是针对DStream操作的。
*/
JavaPairDStream<String, Integer> searchWordCountsDStream =
searchWordPairDStream.reduceByKeyAndWindow(new Function2<Integer, Integer, Integer>() {
@Override
public Integer call(Integer v1, Integer v2) throws Exception {
System.out.println(v1 + " : " + v2);
return v1 + v2;
}
},Durations.seconds(15),Durations.seconds(5));
/**
* window窗口操作优化:
*/
// JavaPairDStream<String, Integer> searchWordCountsDStream =
// searchWordPairDStream.reduceByKeyAndWindow(new Function2<Integer, Integer, Integer>() {
// @Override
// public Integer call(Integer v1, Integer v2) throws Exception {
// System.out.println("v1:" + v1 + " v2:" + v2 + " ++++++++++");
// return v1 + v2;
// }
// }, new Function2<Integer, Integer, Integer>() {
// @Override
// public Integer call(Integer v1, Integer v2) throws Exception {
// System.out.println("v1:" + v1 + " v2:" + v2 + "------------");
// return v1 - v2;
// }
// }, Durations.seconds(15), Durations.seconds(5));
//
searchWordCountsDStream.print();
jssc.start();
try {
jssc.awaitTermination();
} catch (InterruptedException e) {
e.printStackTrace();
}
jssc.close();
}
- saveasFile
将DStream中的数据存储到本地或者hdfs中
public static void main(String[] args) {
final SparkConf conf = new SparkConf().setMaster("local[2]").setAppName("saveFile");
JavaSparkContext sc = new JavaSparkContext(conf);
JavaStreamingContext jsc = new JavaStreamingContext(sc, Durations.seconds(5));
JavaReceiverInputDStream<String> lines = jsc.socketTextStream("node01", 9999);
JavaDStream<String> words = lines.flatMap(new FlatMapFunction<String, String>() {
private static final long serialVersionUID = 1L;
@Override
public Iterator<String> call(String s) {
return Arrays.asList(s.split(" ")).iterator();
}
});
JavaPairDStream<String, Integer> ones = words.mapToPair(new PairFunction<String, String, Integer>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2<String, Integer> call(String s) {
return new Tuple2<String, Integer>(s, 1);
}
});
JavaPairDStream<String, Integer> counts = ones.reduceByKey((x,y)->x+y);
// 将数据存到hdfs上面
// counts.saveAsHadoopFiles();
DStream<Tuple2<String, Integer>> dstream = counts.dstream();
//将数据存到本地的文件目录下...
dstream.saveAsTextFiles("./data/prefix","suffix");
jsc.start();
try {
jsc.awaitTermination();
} catch (InterruptedException e) {
e.printStackTrace();
}
jsc.stop();
}
Driver HA(Standalone或者Mesos)
因为SparkStreaming是7*24小时运行,Driver只是一个简单的进程,有可能挂掉,所以实现Driver的HA就有必要(如果使用的Client模式就无法实现Driver HA ,这里针对的是cluster模式)。Yarn平台cluster模式提交任务,AM(AplicationMaster)相当于Driver,如果挂掉会自动启动AM。这里所说的DriverHA针对的是Spark standalone和Mesos资源调度的情况下。实现Driver的高可用有两个步骤:
第一:提交任务层面,在提交任务的时候加上选项 --supervise,当Driver挂掉的时候会自动重启Driver。
第二:代码层面,使用JavaStreamingContext.getOrCreate(checkpoint路径,JavaStreamingContextFactory)
Driver中元数据包括:
- 创建应用程序的配置信息。
- DStream的操作逻辑。
- job中没有完成的批次数据,也就是job的执行进度。
SparkStreaming 和kafaka在整合时的两种模式
- receiver模式
原理图
在spark中启动receiver task任务从kafaka中拉取数据,拉过来后启动持久化备份到其他节点,并将数据读取偏移量offset存储到zookeeper上,在节点上的数据位置会被汇报给Driver,Driver根据数据所在的位置来分发task到相应的节点计算,体现了计算向数据端移动的思想,task结果在汇报给driver端。
receiver模式中存在的问题
当Driver进程挂掉后,Driver下的Executor都会被杀掉,当更新完zookeeper消费偏移量的时候,Driver如果挂掉了,就会存在找不到数据的问题,相当于丢失数据。
如何解决这个问题?
开启WAL(write ahead log)预写日志机制,在接受过来数据备份到其他节点的时候,同时备份到HDFS上一份(我们需要将接收来的数据的持久化级别降级到MEMORY_AND_DISK 1),这样就能保证数据的安全性。不过,因为写HDFS比较消耗性能,要在备份完数据之后才能进行更新zookeeper以及汇报位置等,这样会增加job的执行时间,这样对于任务的执行提高了延迟度。这样总体来看的话就要看集群的稳定性了
但是这样会导致数据重复消费问题,先将数据存储到hdfs上后,再将数据偏移量存储到zookeeper上之前这个时间点,这样的话driver重启后,会先去hdfs上的数据做计算,然后再去zookeeper上读取偏移量,这样会造成重复消费。
receiver的并行度设置
receiver的并行度是由spark.streaming.blockInterval来决定的,默认为200ms,假设batchInterval为5s,那么每隔blockInterval就会产生一个block,这里就对应每批次产生RDD的partition,这样5秒产生的这个Dstream中的这个RDD的partition为25个,并行度就是25个task。如果想提高并行度可以减少blockInterval的数值,但是最好不要低于50ms。
public static void main(String[] args) {
SparkConf conf = new SparkConf().setAppName("SparkStreamingOnKafkaReceiver").setMaster("local[2]");
//开启预写日志 WAL机制
// conf.set("spark.streaming.receiver.writeAheadLog.enable", "true");
JavaStreamingContext jsc = new JavaStreamingContext(conf, Durations.seconds(10));
// jsc.checkpoint("./receivedata");
Map<String, Integer> topicConsumerConcurrency = new HashMap<String, Integer>();
/**
* 设置读取的topic和接受数据的线程数
*/
topicConsumerConcurrency.put("sk1", 1);
topicConsumerConcurrency.put("sk2", 1);
/**
* 第一个参数是StreamingContext
* 第二个参数是ZooKeeper集群信息(接受Kafka数据的时候会从Zookeeper中获得Offset等元数据信息)
* 第三个参数是Consumer Group 消费者组
* 第四个参数是消费的Topic以及并发读取Topic中Partition的线程数
*
* 注意:
* KafkaUtils.createStream 使用五个参数的方法,设置receiver的存储级别
*/
// JavaPairReceiverInputDStream<String,String> lines = KafkaUtils.createStream(
// jsc,
// "node3:2181,node4:2181,node5:2181",
// "MyFirstConsumerGroup",
// topicConsumerConcurrency,
// StorageLevel.MEMORY_AND_DISK());
JavaPairReceiverInputDStream<String, String> lines = KafkaUtils.createStream(
jsc,
"node01:2181,node02:2181,node03:2181",
"MyFirstConsumerGroup",
topicConsumerConcurrency);
JavaDStream<String> words = lines.flatMap(new FlatMapFunction<Tuple2<String, String>, String>() {
private static final long serialVersionUID = 1L;
public Iterator<String> call(Tuple2<String, String> tuple) throws Exception {
return Arrays.asList(tuple._2.split("\t")).iterator();
}
});
JavaPairDStream<String, Integer> pairs = words.mapToPair(new PairFunction<String, String, Integer>() {
private static final long serialVersionUID = 1L;
public Tuple2<String, Integer> call(String word) throws Exception {
return new Tuple2<String, Integer>(word, 1);
}
});
JavaPairDStream<String, Integer> wordsCount = pairs.reduceByKey((x,y)->x+y);
wordsCount.print();
jsc.start();
try {
jsc.awaitTermination();
} catch (InterruptedException e) {
e.printStackTrace();
}
jsc.close();
}
- Direct模式
SparkStreaming+kafka 的Driect模式就是将kafka看成存数据的一方,不是被动接收数据,而是主动去取数据。消费者偏移量也不是用zookeeper来管理,而是SparkStreaming内部对消费者偏移量自动来维护,默认消费偏移量是在内存中,当然如果设置了checkpoint目录,那么消费偏移量也会保存在checkpoint中。当然也可以实现用zookeeper来管理。
Direct模式的并行度是由读取的kafka中topic的partition数决定的。
public static void main(String[] args) {
SparkConf conf = new SparkConf().setMaster("local[2]").setAppName("SparkStreamingOnKafkaDirected");
// conf.set("spark.streaming.backpressure.enabled", "true");
// conf.set("spark.streaming.kafka.maxRatePerPartition", "100");
// conf.set("spark.streaming.stopGracefullyOnShutdown","true");
JavaStreamingContext jsc = new JavaStreamingContext(conf, Durations.seconds(5));
/**
* 可以不设置checkpoint 不设置不保存offset,offset默认在内存中有一份,如果设置checkpoint在checkpoint也有一份offset, 一般要设置。
*/
jsc.checkpoint("./checkpoint");
Map<String, String> kafkaParameters = new HashMap<String, String>();
kafkaParameters.put("metadata.broker.list", "node01:9092,node02:9092,node03:9092");
// kafkaParameters.put("auto.offset.reset", "smallest");
Set<String> topics = new HashSet<>();
topics.add("sk1");
topics.add("sk2");
JavaPairInputDStream<String, String> lines = KafkaUtils.createDirectStream(
jsc,
String.class,
String.class,
StringDecoder.class,
StringDecoder.class,
kafkaParameters,
topics
);
JavaDStream<String> words = lines.flatMap(new FlatMapFunction<Tuple2<String, String>, String>() {
@Override
public Iterator<String> call(Tuple2<String, String> tuple2) throws Exception {
return Arrays.asList(tuple2._2.split("\t")).iterator();
}
});
// JavaPairDStream<String, Integer> pairs = words.mapToPair(x->new Tuple2<>(x,1));
JavaPairDStream<String, Integer> pairs = words.mapToPair(new PairFunction<String, String, Integer>() {
@Override
public Tuple2<String, Integer> call(String s) throws Exception {
return new Tuple2<>(s, 1);
}
});
JavaPairDStream<String, Integer> wordsCount = pairs.reduceByKey((x,y)->x+y);
wordsCount.print();
jsc.start();
try {
jsc.awaitTermination();
} catch (InterruptedException e) {
e.printStackTrace();
}
jsc.close();
}
3.相关配置
预写日志:
spark.streaming.receiver.writeAheadLog.enable 默认false没有开启
blockInterval:
spark.streaming.blockInterval 默认200ms
反压机制:
spark.streaming.backpressure.enabled 默认false
反压机制是一个动态的过程,假如kafaka输出50000条数据每秒,而sparkStreaming只能处理10000条每秒,这样会有数据堆积,会造成oom,这样反压机制在处理速度小于输入速度时,会提高输入的速度,在处理速度大于输入数据速度的时候,会根据延迟度和自身资源来调制,会降低输入数据速度,上下限值不变。
接收数据速率:
Receiver模式:
spark.streaming.receiver.maxRate 默认没有设置
Direct模式:
spark.streaming.kafka.maxRatePerPartition
优雅的停止sparkstream :
spark.streaming.stopGracefullyOnShutdown 设置成true
kill -15/sigterm driverpid