Spark


简介

目前流行的大数据处理场景,可将其分为如下三种:

  • 离线计算:复杂的批量数据处理,耗时数十分钟到数小时;
  • 准实时计算:基于历史数据的交互式查询,耗时数十秒到数分钟;
  • 实时计算:基于实时数据流的数据处理,耗时数百毫秒到数秒;

Spark生态的目标,是将以上三者融合到一个软件栈当中。Spark一般被视为与Hadoop并行的大数据框架;不同之处在,Spark长于实时计算,Hadoop长于批处理、离线计算。另外,Spark只有计算功能,其底层的数据存储,仍依托于Hadoop的HDFS来实现。

组成部件

Spark基于Spark Core扩展了四个核心组件,分别用于满足不同领域的计算需求。

  • Spark Core:基于RDD提供了丰富的接口,利用DAG进行统一的任务规划,是的使得Spark能更加灵活的处理MapReduce的批处理作业;
  • Spark SQL:实现交互式离线查询的分布式SQL引擎,兼容Hive,提供了比Hive高出10-100倍的查询速度;
  • Spark Streaming:准实时计算,是基于Spark Core开展的流处理计算;这部分是重点,也是生产中Spark的主要用途;
  • Structure Streaming:
  • MLlib:利用Spark Core的迭代式运算能力构建的机器学习库框架;
  • Graphx:图计算框架,兼容Pregel和GraphLab接口,增强了图构建以及图转换功能;

应用程序类型

在Spark的安装目录 ./spark/bin 下,有下列几种Spark程序:

  • spark-sql:执行离线批量计算;
  • spark-submit:用于执行Scala / Java / Python编写的Spark程序;
  • spark-shell:以scala为交互语言的shell;
  • pyspark:以python为交互语言的shell;
  • sparkR:以R为交互语言的shell;
  • run-example:运行Spark自带样例;

教程

  • 官网
  • 官方API文档
  • O’ Reilly系列:《Spark快速大数据分析》、《Spark高级数据分析》

命令

# 开发环境集群:[192.168.102.85]/ [192.168.102.86]/ [192.168.102.87]
# Spark version 2.3.1; Scala version 2.11.8; Java version 1.8.0
# 原生Apache Spark
# 安装路径:/home/devsa_dev/spark/bin/

# 生产环境[172.16.97.81]; Cloudera版集成安装
# Spark version 1.6.0; Scala version 2.10.5; Java version 1.8.0
# 安装路径:/opt/cloudera/parcels/SPARK2-2.3.0.cloudera2-1.cdh5.13.3.p0.316101/

# 启动Spark console;同时还可查看Spark版本,还可显示Scala/ Java版本
# Local模式是最简单的一种运行方式,它采用单节点多线程方式运行,不用部署,开箱即用,适合日常测试开发
# 其中,local:只启动一个线程;local[k]:启动k个工作线程;local[*]:启动跟CPU数相同的工作线程;
spark-shell  # 启动Spark console,交互语言为Scala;进入之后,即创建了SparkContext对象
spark-shell --master local[2]  # 指定启动方式
pyspark  # 启动Spark console,交互语言为python
pyspark --master local[2]  # 指定启动方式
sparkR  # 启动Spark console,交互语言为R
sparkR --master local[2]  # 指定启动方式
spark-sql  # 启动spark-sql console,可进行交互式批处理,兼容Hive

# 运行Spark自带example,用于检查Spark是否安装成功
run-example SparkPi 10  # 计算圆周率近似值
run-example <class> [params]  # 通用语法
run-example org.apache.spark.examples.streaming.JavaNetworkWordCount localhost 9999

# 运行Spark自带Python例子;需要安装python或者R才能运行
spark-submit /home/devsa_dev/spark/examples/src/main/python/pi.py 10
spark-submit /home/devsa_dev/spark/examples/src/main/r/dataframe.R 10

# 运行Spark程序的jar包
spark-submit --class "java_class_name" <jar_linux_path>
spark-submit --class "SimpleApp" /home/spark/SimpleApp.jar  # demo

# idea将Spark程序打成jar包
# https://jingyan.baidu.com/article/15622f24d673befdfdbea557.html

# 修改Spark master端口/ Spark WebUI端口
./spark/sbin/start-master.sh

# 修改Spark在Console上打印的日志等级:将下列配置文件中的INFO改为ERROR
./spark/conf/log4j.properties

# Spark所有模式均使用spark-submit命令提交作业,其格式如下:
./bin/spark-submit \
  --class <main-class> \        # 应用程序主入口类
  --master <master-url> \       # 集群的Master Url
  --deploy-mode <deploy-mode> \ # 部署模式
  --conf <key>=<value> \        # 可选配置       
  ... # other options    
  <application-jar> \           # Jar包路径 
  [application-arguments]       #传递给主入口类的参数  

# Local模式提交作业,形式最简单,不需要任何配置。以下为提交Spark测试代码的例子
spark-submit \
--class org.apache.spark.examples.SparkPi \
--master local[2] \
/usr/app/spark-2.4.0-bin-hadoop2.6/examples/jars/spark-examples_2.11-2.4.0.jar \
100   # 传给SparkPi的参数

# 设置在任务结束后还能在SparkUI查看信息
set spark.yarn.historyServer.address = xxx;
set spark.eventLog.dir = xxx;
set spark.eventLog.enabled = true;

Spark Core

Demo:算子的三种写法

Spark 的大部分Transformations和一部分Actions,都需要依赖传递函数来进行。这个是很好理解的:你需要发送指令来告诉Spark,需要在这一堆杂乱的数据中,筛选哪些数据,生成怎么样的数据。因此,经常需要传入一个匿名内部类的函数function;需要org.apache.spark.api.java.function包中的任一函数接口的对象来传递该功能。

/*
 * Creator: YangSong
 * Date:    2020-4-28 16:34:39
 * Url:     http://spark.apache.org/docs/2.3.1/rdd-programming-guide.html#initializing-spark
 * Desc:    Spark2.3.1官方文档demo
 * Result:  
 * Tips:    
*/

import org.apache.spark.api.java.JavaSparkContext;
import org.apache.spark.api.java.JavaRDD;
import org.apache.spark.SparkConf;

public class SparkDemo {
    public static void main(String[] args) {
        // 设置Spark程序的名称为SparkDemo,并以本地模式运行
        SparkConf conf = new SparkConf().setAppName("SparkDemo").setMaster("local");
        // SparkContext对象,是在集群上运行Spark程序的入口
        JavaSparkContext sc = new JavaSparkContext(conf);

        // 创建一个dataset数据集,distData
        List<Integer> data = Arrays.asList(1, 2, 3, 4, 5);
        JavaRDD<Integer> distData = sc.parallelize(data);

        // 根据文本文件创建RDD
        JavaRDD<String> distFile = sc.textFile("data.txt");

        // 第一种写法:根据transformations类函数,map()来创建RDD;使用lambda表达式,更简洁
        JavaRDD<Integer> lineLengths = lines.map(s -> s.length());
        int totalLength = lineLengths.reduce((a, b) -> a + b);

		// 持久化lineLengths,便于重复利用
		lineLengths.persist(StorageLevel.MEMORY_ONLY());

		// 第二种写法:传递函数,使用匿名内部类,更便于阅读
		JavaRDD<Integer> lineLengths = lines.map(new Function<String, Integer>() {
			public Integer call(String s) {
				return s.length();
			}
		});
		int totalLength = lineLengths.reduce(new Function2<Integer, Integer, Integer>() {
			public Integer call(Integer a, Integer b) {
				return a + b;
			}
		});

		// 第三种写法:新建继承类,实现传递函数的功能;比较笨重
		class GetLength implements Function<String, Integer> {
		    public Integer call(String s) {
		    	return s.length();
		    }
		}
		class Sum implements Function2<Integer, Integer, Integer> {
			public Integer call(Integer a, Integer b) {
				return a + b;
			}
		}
		JavaRDD<Integer> lineLengths = lines.map(new GetLength());
		int totalLength = lineLengths.reduce(new Sum());

		// 针对k-v对的RDD,进行的Transformations/ Actions
		JavaPairRDD<String, Integer> pairs = lines.mapToPair(s -> new Tuple2(s, 1));
		JavaPairRDD<String, Integer> counts = pairs.reduceByKey((a, b) -> a + b);
		
    }
}

转换算子Transformation

对RDD进行转换,生成一个新的RDD;在真实的开发中,数据大多是json格式的。具体的java实现方式,可见Git

// 第一类:输入为value型RDD,且输入RDD与输出RDD为一对一型的算子
RDD_result = RDD.map(func)  // map是对RDD中的每个元素都执行一个指定的函数,来产生一个新的RDD。任何原RDD中的元素,在新RDD中都有且只有一个元素与之对应;map是一对一的
RDD_result = RDD.flatMap(func.iterator())  // 在map的基础上,进行扁平化
RDD_result = RDD.filter(func)  // 接受一个函数作为参数,筛选RDD中符合条件的元素
RDD_result = RDD.distinct()  // 将RDD中的元素去重
RDD_result = RDD.sample(withReplacement, fraction, seed)  // 随机采样,生成新的RDD。withReplacement:true-可重复,false-不可重复;fraction:采样比例;seed:采样初始值
RDD_result<List<T>> = RDD.glom()  // 将同一个分区内的元素合并到一个Array中
pairRDD_result = RDD.mapToPair(func)  // 将RDD元素按func生成k-v形式的新RDD
pairRDD_result = RDD.groupby(func)  // 将RDD的元素按func分组,生成k-v类型的RDD;其中,func的返回值为key,RDD的元素为value

// 第二类:输入为value型RDD,且输入RDD与输出RDD为多对一型的算子
RDD_result = RDD1.union(RDD2)  // 合并两个RDD,且不去重,生成新的RDD
RDD_result = RDD1.intersection(RDD2)  // 合并两个RDD,且去重,生成新的RDD
RDD_result = RDD1.subtract(RDD2)  // 取两个RDD的差集
pairRDD_result = RDD1.cartesian(RDD2)  // 对两个RDD取笛卡尔积,生成新的<k, v>形式的RDD

// 第三类:输入为key-value数据类型,且输入RDD与输出RDD为一对一型的算子
RDD_result = pairRDD.keys()  // 返回一个仅包含key的RDD
RDD_result = pairRDD.values()  // 返回一个仅包含value的RDD
pairRDD_result = pairRDD.mapValues(func)  // 将pairRDD的value,按func改变
pairRDD_result = pair_RDD.groupByKey([num])  // 将RDD中的算子按照key进行分组操作,所有的value形成一个Iterable;num为设置的并行度

// 第四类:key-value数据类型,连接算子
pairRDD_result<T1, Tuple2<T2, T3>> = pairRDD1.join(pairRDD2, [numTasks])  // 按照key将两个RDD中进行汇总操作,会对每个key所对应的两个RDD中的数据进行笛卡尔积计算
pairRDD_result<T1, Tuple2<T2, Optional<T3>>> = pairRDD1.leftOuterJoin(pairRDD2, [numTasks])  // 以pairRDD1为主key将两个RDD中进行汇总操作,会对每个key所对应的两个RDD中的数据进行笛卡尔积计算
pairRDD_result<T1, Tuple2<Optional<T2>, T3>> = pairRDD1.rightOuterJoin(pairRDD2, [numTasks])  // 以pairRDD2为主key将两个RDD中进行汇总操作,会对每个key所对应的两个RDD中的数据进行笛卡尔积计算
pairRDD_result<T1, Tuple2<Optional<T2>, Optional<T3>>> = pairRDD1.fullOuterJoin(pairRDD2, [numTasks])  // 类似Hive-ql的full join

// 第五类:持久化算子
RDD.cache()  // 将数据持久化到内存中,便于复用;cache()后面不能再接算子;
RDD.persist(StorageLevel.MEMORY_ONLY)  // 等同于RDD.cache()
RDD.persist(StorageLevel.MEMORY_AND_DISK)  // RDD先持久化到内存,放不下的话,就放到磁盘中
RDD.unpersist()  // 取消持久化

行动算子Actions

对RDD计算后,产生一个value。具体的java实现方式,可见Git

// 第一类:无输出的遍历算子
RDD.foreach(void_func)  // 遍历RDD中每个元素,运行无返回值的函数void_func

// 第二类:RDD保存算子
RDD.saveAsTextFile(file_path)  // 输出RDD为文本文件,到指定目录下
RDD.saveAsHadoopFile(file_path)  // 输出RDD为文件,保存到HDFS目录下
RDD.saveAsSequenceFile(file_path)  // 输出RDD为Sequence,到HDFS指定目录下
RDD.saveAsObjectFile(file_path)  // 输出RDD为序列化对象,到指定目录下

// 第三类:输入为value类型的RDD,集合操作算子
List<T> list_result = RDD.collect()  // 将RDD转化为List
Integer result = RDD.reduce(func)  // 每次传入两个参数通过func得到一个返回值,该返回值继续与后面的参数进行调用func,直到所有的数据计算完成,最后返回一个计算结果值
List<T> list_result = RDD.top(k)  // 返回最大的k个元素组成的List
List<T> list_result = RDD.take(k)  // 返回最小的k个元素组成的List
List<T> list_result = RDD.takeOrdered(k)  // 返回最小的k个元素组成的List,并按照一定顺序排序
List<T> list_result = RDD.takeSample(withReplacement, num, seed)  // 随机采样,再选择元素;返回一个List
Integer result = RDD.first()  // 返回整个RDD中的第一个元素
long result = RDD.count()  // 返回RDD的元素数量,返回类型为Long
int result = RDD.fold([zero_value], func)  // 对RDD的每个分区,分别执行func操作,同时分别加zero_value
RDD.aggregate()  // 

// 第四类:输入为<k, v>形式的RDD,集合操作算子
List<T> list_result = pair_RDD.lookup(key)  // 返回RDD中特定key对应的value形成的List
Map<T1, Long> = pair_RDD.countByKey()  // 统计每个key的数量,返回(key, Long)类型的Map数据
Map<Tuple2<T1, T2>, Long>pair_RDD.countByValue()  // 统计每个元素的数量,返回(value, count)类型的Map数据
pair_RDD.collectAsMap()  // 将k-v形式的RDD转化为Map
pairRDD.reduceByKey(func)  // 以func的业务逻辑,合并具有相同key的value
pairRDD.groupByKey()  // 对具有相同key的value进行分组
pairRDD.combineByKey(createCombiner, mergeValue, mergeCombiners, partitioner)  // 使用不同的返回类型,合并具有相同key的value
pairRDD.mapValues(func)  // 对pairRDD中每个value执行func,但不改变key
pairRDD.flatMapValues(func)  // 对pairRDD中每个value执行func,对返回的每个元素都生成一个对应原来key的k-v
pairRDD.sortByKey()  // 返回一个根据键排序的RDD

Spark Streaming

Spark Streaming的本质是微批处理,它将数据流进行极小粒度的拆分,拆分为多个批处理,从而达到接近于流处理的效果。

Spark Streaming是Spark核心API的扩展,可以实现高吞吐量的、具备容错机制的实时流数据处理。Spark Streaming接收Kafka、Flume、HDFS等各种来源的实时输入数据,进行处理后,处理结构保存在HDFS、DataBase等各种地方。其中,使用的最多的是kafka + Spark Streaming。

Spark SQL处理的是批量的数据(离线数据),Spark Streaming实际上处理并不是像Strom一样来一条处理一条数据,而是对外部数据流按照时间切分,批处理一个个切分后的文件,和Spark处理逻辑是相同的。

Spark Streaming将接收到的实时流数据,按照一定时间间隔,对数据进行拆分,交给Spark Engine引擎,最终得到一批批的结果。即:将流当做批来处理。因此,又被称之为微批处理。

结构与原理

Spark Streaming,是一种准实时计算框架,也是Apache Spark项目的核心。其结构为:

  • 数据源:Kafka/ Flume/ HDFS/ S3/ Kinesis/ TCP
  • 数据接收器:HDFS/ database/ dashboard

计算原理:输入数据流进入Spark Streaming,划分为数据块batch;然后Spark Engine对batch进行计算,输出结果;

Discretized Streams (DStreams,离散的数据流):Spark Streaming的基本计算对象,由输入数据或其他的DStreams生成;其本质上是RDD的队列;对DStreams的API操作,本质上是对组成这个DStream的RDD,所分别进行的函数操作;SparkStreaming基于DStream来进行编程,处理的是一个流,这个流什么时候切成一个rdd,是根据batchinterval来决定何时切割成一个RDD。

RDD:最基本的操作单元;在spark中,我们要进行计算,就需要通过分布式数据集RDD进行操作,以表达我们的计算意图;这些操作会自动的在集群上并行计算,这样的数据集就叫做RDD(resilient distributed dataset)弹性分布式数据集。RDD就是spark对分布式数据和计算的基本抽象;我们可以在这个RDD上运行各种并行操作,比如count()和first();spark程序都是围绕着sparkcontext和RDD来进行操作的,RDD是spark对数据的核心抽象,在spark中,对于数据的操作,基本是创建RDD.转换RDD.使用RDD操作进行数据的计算,而spark的作用就是自动将RDD的数据分发到集群上,将操作并行化执行

DataSet是在DataFrame的基础上,增加了泛型和一些优化。
DataFrame = RDD - 泛型 + Schema + SQL操作 + 优化;
DataSet = RDD + Schema + SQL操作 + 优化;
DataSet = DataFrame + 泛型 + 优化;

Reference:Spark Streaming2.3.1官方文档


Demo:Spark Core读取Linux本地文件

本例只用到了Spark Core的RDD方法,本质上是一种离线处理,而非Spark Streaming的流式处理。在实际生产中,离线处理直接使用Spark SQL即可,并不需要使用这种方法;这里只是展示用SparkRDD读取本地文件的一种方法。
另外,Spark不同版本之间的API差别非常大,一定要注意集群中的软件版本,然后选择适当的maven依赖。否则极大概率会发生执行失败!


Demo:Spark Streaming读取Linux本地的文件

以Spark Streaming读取Linux本地文件,是将该文件视为流式数据,因此只有在该文件的内容发生改变的情况下,才会触发程序,将其内容打印出来。具体的方式为,不断的将修改好的file:///home/yangsong/test.txt文件,粘贴、覆盖到该目录下,从而人为的造成内容的修改,以形成流数据。但在生产中其实这类模式其实很少见,这里的demo只是权当演示而已。
运行环境:Spar version / Java version / Kafka version


Demo:SparkStreaming读取TCP

来自Spark官方文档的例子,让初学者身体力行的感知Spark Streaming的功能;实际上是几乎没有这样的使用场景的,
运行环境:Spar version / Java version / Kafka version


Demo:SparkStreaming读取Kafka-Receiver方式

实际生产中,最经典的模式就是Kafka+Spark Streaming;数据源为Kafka的某个topic,Spark Streaming作为Consumer获得Kafka传来的数据,然后添加业务逻辑进行实时计算;最后,将计算结果写入另外的Kafka、HBase、Hive等存储介质当中。
注意,本例的方法依赖于org.apache.spark.streaming.kafka包,这类新建DStream的方法仅适用于Spark 2.3.0之前的版本,在之后的版本中下架了这种方法。
运行环境:Spar version / Java version / Kafka version

// spark-submit --class demo /home/yangsong/demo.jar
// Receiver方式

import org.apache.spark.SparkConf;
import org.apache.spark.streaming.Durations;
import org.apache.spark.streaming.api.java.JavaPairReceiverInputDStream;
import org.apache.spark.streaming.api.java.JavaStreamingContext;
import org.apache.spark.streaming.kafka.KafkaUtils;

import java.util.Map;
import java.util.HashMap;

public class demo {
    public static void main(String[] args)
    {
        // Spark Streaming的基本配置,每两秒读取一次Kafka
        SparkConf sparkConf = new SparkConf().setMaster("local").setAppName("MySpark");
        JavaStreamingContext javaStreamingContext = new JavaStreamingContext(sparkConf, Durations.seconds(2));
        // javaStreamingContext.checkpoint("./check_out");  // 设置检查点

        // Kafka的参数配置
        String broker = "172.16.97.72:9092";
        String group = "yangsong";
        String topic = "yangsong.test";  // 每个topic以,分隔
        int num_thread = 1;  // 每个topic的分片数

        Map<String, Integer> topic_map = new HashMap<String, Integer>();
        String[] topic_array = topic.split(",");
        for (String x : topic_array)  // 存储topic和partition的映射关系
        {
            topic_map.put(x, num_thread);
        }
        
        // 通过Receiver方式,将Kafka的消息转化为RDD
        JavaPairReceiverInputDStream<String, String> message;
        message = KafkaUtils.createStream(javaStreamingContext, broker, group, topic_map);

        // 以下补充业务逻辑
        message.print();

        javaStreamingContext.start();

        try
        {
            javaStreamingContext.awaitTermination();
        } catch (Exception e)
        {
            e.printStackTrace();
        }

        javaStreamingContext.close();
    }
}

Demo:SparkStreaming读取Kafka-Direct方式(推荐)

本例的方法仅适用于Spark 2.3.0及之后的版本。Spark的API经常变化,要注意依赖的jar包的版本,这是一个大坑。注意,Saprk Streaming的Maven版本很多,这里采用的是下面demo代码中的依赖。
运行环境:Spar version / Java version / Kafka version


Spark Streaming + Kafka

在实际生产中,Spark Streaming用于准实时计算的时候,绝大多数情况下,其数据源都是Kafka;因此Spark Streaming和Kafka的集成,是数据开发者必须熟练掌握的一门技巧。

Spark的API在历次版本变迁中,屡次发生了重大变更,因此在实际编写Spark Streaming与Kafka的集成代码时,要根据生产集群中Spark/ Kafka/ Scala版本的不同,选择合适的API,否则很容易报错!

  • Spark version在2.3.0及之后,Kafka版本在0.10.0及之后,只能引入spark-streaming-kafka-0-10_2.11版本的jar包进行开发;只能使用Direct方式连接kafka,且不能向旧版本兼容!
  • Spark version在2.3.0之前,Kafka版本在0.8.2.1及之后,只能引入spark-streaming-kafka-0-8_2.11版本的jar包进行开发;可以使用Receiver-BasesDirect方式连接Kafka,且可以向高版本兼容!

另外,由于spark-streaming-kafka-0-10_2.11中已经包含了Kafka相关的API, 因此不用在pom.xml中再次引入org.apache.kafka相关的jar包;如果引入,反而容易引起报错!!!推荐根据Spark + Kafka版本选择合适的Spark Streaming的jar包,且采用Direct方式连接Kafka;由于在高版本的Spark中,Receive-Based方式逐渐被遗弃,因此不推荐使用这类API连接Kafka!!!

Reference : http://spark.apache.org/docs/2.3.1/streaming-kafka-integration.html


Spark SQL

Spark SQL主要用于计算HDFS中的结构化数据,与Hive的功能有极大的相似之处。其具有以下特点:

  • 能够将SQL查询与Spark程序无缝混合,允许使用SQL或DataFrame API对结构化数据进行查询。但在实际生产当中,大多直接使用Spark SQL进行开发,很少有使用DataFrame API的;
  • 支持多种数据源,包括Hive/ Avro/ Parquet/ ORC/ JSON/ JDBC/ s3/ HBase/ MongoDB,支持标准的JDBC和ODBC连接;
  • 支持HiveQL语法以及用户自定义函数,允许访问现有的Hive仓库。即是说,Spark SQL和Hive可以共用同一套元数据;
  • 支持优化器,列式存储和代码生成等特性,以提高查询效率;

Spark SQL与Hive相比,将底层的计算引擎,从MapReduce替换为了Spark。Spark SQL将SQL转化为RDD,然后提交到集群上执行,因此效率比较高;根据Spark的官方文档,其速度比Hive高10-100倍。但Spark会出现内存溢出的问题,因此稳定性不如Hive。

# 用Spark SQL执行文件查询语句,可见,语法与Hive非常类似
spark-sql -e 'Hive-ql'  # 例如:spark-sql -e 'select * from table_tmp'
sparl-sql -f [file_path]  # 例如:spark-sql -f /home/hadoop/xx.hql 

Structure Streaming


MLlib (Machine Learning Lib)

MLlib是Spark生态的机器学习模块。在数据量较小的单节点情况下,我们可以使用Python生态的sklearn等包进行机器学习的算法训练,但当数据量极大的时候,就需要基于Spark MLlib进行分布式的训练。


GraphX

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值