目录
1. Spark简介
Spark是为大规模分布式数据处理而设计的一站式引擎(分布式数据处理引擎),为中间计算结果提供了基于内存的存储。
2. Spark的相关术语
Master 是 Spark 的 主控节点,Worker 是 Spark 的工作节点,向 Master 汇报自身的资源、Executor 执行状态的改变,并接受 Master 的命令启动 Executor 或 Driver。Driver 是应用程序的驱动程序,每个应用包括许多小任务,Driver 负责推动这些小任务的有序执行。Executor 是 Spark 的工作进程,由 Worker 监管,负责具体任务的执行。
2.1 master和worker节点
整个 Spark 集群中,分为 Master 节点与 worker 节点,同时一个集群有多个master节点和多个worker节点。
-
master:主节点,该节点负责管理worker节点,我们从master节点提交应用,负责将串行任务变成可并行执行的任务集Tasks,同时还负责出错问题处理等;
-
worker:从节点,该节点与master节点通信,负责执行任务并管理executor进程。它为集群中任何可以运行Application代码的节点,在Standalone模式中指的是通过slave文件配置的Worker节点,在Spark on Yarn模式下就是NodeManager节点。
一台机器可以同时作为master和worker节点,比如有四台机器,可以选择一台设置为master节点,然后剩下三台设为worker节点,也可以把四台都设为worker节点,这种情况下,有一个机器既是master节点又是worker节点。
2.2 Application
Application都是指用户编写的Spark应用程序,其中包括一个Driver功能的代码和分布在集群中多个节点上运行的Executor代码。
2.3 driver和executor进程
-
Driver的功能是创建 SparkContext,负责执行用户写的 Application 的 main 函数进程,创建SparkContext的目的是为了准备Spark应用程序的运行环境,在Spark中有SparkContext负责与Cluster Manager通信,进行资源申请、任务的分配和监控等,当Executor部分运行完毕后,Driver同时负责将SparkContext关闭,通常用SparkContext代表Driver。不同的模式可能会将 Driver 调度到不同的节点上执行。
-
executor:执行器,为某个Application运行在worker节点上的一个进程,该进程负责运行某些Task,并且负责将数据存到内存或磁盘上,每个Application都有各自独立的一批Executor进程。executor宿主在worker节点上,每个 Worker 上存在一个或多个 Executor 进程,每个executor持有一个线程池,每个线程可以执行一个task。根据 Executor 上 CPU-core 的数量,其每个时间可以并行多个跟 core 一样数量的 task。task 任务即为具体执行的 Spark 程序的任务。executor执行完task以后将结果返回给driver,每个executor执行的task都属于同一个应用。此外executor还有一个功能就是为应用程序中要求缓存的 RDD 提供内存式存储,RDD 是直接缓存在executor进程内的,因此任务可以在运行时充分利用缓存数据加速运算。
通常Executor的内存主要分为三块:第一块是让task执行我们自己编写的代码时使用,默认是占Executor总内存的20%;第二块是让task通过shuffle过程拉取了上一个stage的task的输出后,进行聚合等操作时使用,默认也是占Executor总内存的20%;第三块是让RDD持久化时使用,默认占Executor总内存的60%。
2.4 Cluster Manager
集群管理器,指的是在集群上获取资源的外部服务。目前有三种类型:
-
Standalone : spark原生的资源管理,由Master负责资源的分配,易于构建集群
-
Apache Mesos:通用的集群管理,与hadoop MR兼容性良好的一种资源调度框架,可以在其上运行Hadoop MapReduce和一些服务应用
-
Hadoop Yarn: 主要是指Yarn中的ResourceManager
在集群不是特别大,并且没有MapReduce和Spark同时运行的需求的情况下,用Standalone模式效率最高。
2.5 Task
被送到某个Executor上的工作单元,是运行Application的基本单位,多个Task组成一个Stage,而Task的调度和管理等是由TaskScheduler负责。
2.6 Job
包含多个Task组成的并行计算,往往由Spark Action(行动算子)触发生成, 一个Application中往往会产生多个Job。总之Job=多个stage。
2.7 Stage
每个Job会被拆分成多组Task, 作为一个TaskSet, 其名称为Stage,Stage的划分和调度是有DAGScheduler来负责的,Stage有非最终的Stage(Shuffle Map Stage)和最终的Stage(Result Stage)两种,Stage的边界就是发生shuffle的地方。总之Stage=多个同种task。
2.8 DAGScheduler
根据Job构建基于Stage的DAG(Directed Acyclic Graph有向无环图),并提交Stage给TASkScheduler。 其划分Stage的依据是RDD之间的依赖的关系找出开销最小的调度方法。
2.9 TASKScheduler
将TaskSET提交给worker运行,每个Executor运行什么Task就是在此处分配的。TaskScheduler维护所有TaskSet,当Executor向Driver发生心跳时,TaskScheduler会根据资源剩余情况分配相应的Task。另外TaskScheduler还维护着所有Task的运行标签,重试失败的Task。
3. 运行原理
第一步:当我们提交一个Spark作业之后,这个作业就会启动一个对应的Driver进程。driver进程就是应用的main()函数并且构建sparkContext对象,根据使用的部署模式不同,Driver进程可能在本地启动,也可能在集群中某个工作节点上启动。driver本身会根据我们设置的参数占有一定的资源(主要指cpu core和memory)。
第二步:Driver进程首先会向集群管理器(standalone、yarn,mesos)申请spark应用所需的资源,这里的资源指的就是Executor进程。然后集群管理器会根据spark应用所设置的参数在各个worker上分配一定数量的executor,每个executor都占用一定数量的cpu和memory。
第三步:在得到申请的应用所需资源以后,driver就开始调度和执行我们编写的应用代码。driver进程会将我们编写的spark应用代码拆分成多个stage,每个stage执行一部分代码片段,并为每个stage创建一批task,然后将这些tasks分配到各个executor中执行,task是最小的计算单元,负责执行一模一样的计算逻辑(也就是我们自己编写的某个代码片段),只是每个task处理的数据不同而已。
第四步:一个stage的所有task都执行完毕之后,会在各个节点本地的磁盘文件中写入计算中间结果,然后Driver就会调度运行下一个stage。下一个stage的task的输入数据就是上一个stage输出的中间结果。如此循环往复,直到将我们自己编写的代码逻辑全部执行完,并且计算完所有的数据,得到我们想要的结果为止。运行完成后,会释放所有资源。
4. 任务提交
4.1 使用spark submit启动应用程序
./bin/spark-submit \ --class <main-class> \ --master <master-url> \ --deploy-mode <deploy-mode> \ --conf <key>=<value> \ ... # other options <application-jar> \ [application-arguments]
4.1.1 参数详解
参数名 | 简介 |
---|---|
class | spark程序的主类,仅针对 java 或 scala 应用,注意用 全包名+类名。 |
name | 应用程序的名称。 |
master | 与deploy-mode合起来表名程序提交到哪个资源管理框架,以何种方式部署。 表示要连接的集群管理器,可 以 是 spark://host:port、mesos://host:port、yarn、yarn-cluster、yarn-client、local。其中local和local[K].这种是本地run的模式,不会提交到YARN,便于测试使用。这两者之间的区别,带K的是多K个线程,不带K的是单个线程。如果是*的话就是尽可能多的线程数。 |
deploy-mode | 在本地 (client) 启动 driver 或在 cluster 上启动,默认是client。client是指driver在提交任务的服务器上执行,cluster是driver和exectutor都在集群内执行。 |
conf | 指定 spark 配置属性的值。 |
executor-memory | 每个执行器(executor)的内存 ,以字节为单位。可以使用后缀指定更大的单位,比如“512m”(512MB),或“15g”(15GB)。默认是1G,最大不超过30G,yarn模式下其内存加上container要使用的内存(默认值是1G)不要超过NM可用内存,不然分配不到container来运行executor。 |
executor-cores | 每个执行器(executor)的核数 ,即单个executor能并发执行task数,根据job设置。在yarn或者standalone下使用。Spark on Yarn 默认为 1,推荐值2-16;standalone 默认为 worker 上所有可用的 core。 |
Executor-memoryOverheadM | 设置执行器的堆外内存 ,执行器的内存=executor-memory+Executor-overheadMemory。 |
driver-memory | 驱动器(Driver)进程使用的内存量 (例如:1000M,5G)。以字节为单位。可以使用后缀指定更大的单位,比如“512m”(512MB),或“15g”(15GB)。默认是1G,推荐值2-6G,不宜太大。 |
driver-cores | 驱动器(Driver)的核数 ,默认是1。在 yarn 或者 standalone 下使用。 |
driver-memoryOverheadM | 设置driver的堆外内存 。driver的内存=driver-memory+driver-overheadMemory |
num-executors | 设置执行器(executor)的数量;默认为2。在 yarn 下使用。但是目前CDH和FusionInsight都支持动态分配(dynamic allocat)模式。在这种模式下,无论怎么设置num-executors其实都会被忽略的。 |
queue | 执行队列池;通常生产环境都会为特定的租户分配资源池。这个参数便是用于指定跑批租户资源池名称的。 |
Jars | 用逗号分隔的本地 jar 包,指定本次程序(Driver 和 executor)依赖的jar包。需要上传并放到应用的CLASSPATH中的JAR包的列表。如果应用依赖于少量第三方JAR包,可以把他们放在这个参数里,有两种方式: 1、把依赖包打入执行程序包,这样会造成包体较大,每次打包上传耗时间比较大; 2、不打入依赖包,把依赖包提前传到服务器,再通过jars指令指出依赖包的文件路径 |
示例:
# 本地运行应用程序,8核 ./bin/spark-submit \ --class org.apache.spark.examples.SparkPi \ --master local[8] \ /path/to/examples.jar \ 100 # 在Spark独立群集上以客户端部署模式运行 ./bin/spark-submit \ --class org.apache.spark.examples.SparkPi \ --master spark://207.184.161.138:7077 \ --executor-memory 20G \ --total-executor-cores 100 \ /path/to/examples.jar \ 1000 # 在Spark独立集群上以集群部署模式运行 ./bin/spark-submit \ --class org.apache.spark.examples.SparkPi \ --master spark://207.184.161.138:7077 \ --deploy-mode cluster \ --supervise \ --executor-memory 20G \ --total-executor-cores 100 \ /path/to/examples.jar \ 1000 # 在YARN上以cluster模式运行 export HADOOP_CONF_DIR=XXX ./bin/spark-submit \ --class org.apache.spark.examples.SparkPi \ --master yarn \ --deploy-mode cluster \ # can be client for client mode --executor-memory 20G \ --num-executors 50 \ /path/to/examples.jar \ 1000 # 在Spark独立集群上运行python脚本 ./bin/spark-submit \ --master spark://207.184.161.138:7077 \ examples/src/main/python/pi.py \ 1000 # 以集群部署模式在Mesos集群上运行 ./bin/spark-submit \ --class org.apache.spark.examples.SparkPi \ --master mesos://207.184.161.138:7077 \ --deploy-mode cluster \ --supervise \ --executor-memory 20G \ --total-executor-cores 100 \ http://path/to/examples.jar \ 1000 # 以集群部署模式在Kubernetes集群上运行 ./bin/spark-submit \ --class org.apache.spark.examples.SparkPi \ --master k8s://xx.yy.zz.ww:443 \ --deploy-mode cluster \ --executor-memory 20G \ --num-executors 50 \ http://path/to/examples.jar \ 1000
yarn-client用于学习及测试,开发不用,因为driver运行在本地客户端,负责调度application,会与yarn集群产生大量的网络通信,从而导致网卡流量激增。好处在于,应用程序运行结果会在客户端显示,直接执行时,本地可以看到所有的log,方便调试。 yarn-cluster用于生产环境,因为driver运行在YARN集群NodeManager中,没有网卡流量激增的问题,缺点在于调试不方便,本地用spark-submit提交之后,看不到log,应用的运行结果也不能在客户端显示,查看不方便。
4.2 使用sparkLauncher
示例:
import org.apache.spark.launcher.SparkAppHandle; import org.apache.spark.launcher.SparkLauncher; import java.io.IOException; public class Launcher { public static void main(String[] args) throws IOException { SparkAppHandle handler = new SparkLauncher() .setAppName("hello-world") .setSparkHome(args[0]) .setMaster(args[1]) .setConf("spark.driver.memory", "2g") .setConf("spark.executor.memory", "1g") .setConf("spark.executor.cores", "3") .setAppResource("/home/xinghailong/launcher/launcher_test.jar") //此处应写类的全限定名 .setMainClass("HelloWorld") .addAppArgs("I come from Launcher") .setDeployMode("cluster") .startApplication(new SparkAppHandle.Listener(){ @Override public void stateChanged(SparkAppHandle handle) { System.out.println("********** state changed **********"); } @Override public void infoChanged(SparkAppHandle handle) { System.out.println("********** info changed **********"); } }); while(!"FINISHED".equalsIgnoreCase(handler.getState().toString()) && !"FAILED".equalsIgnoreCase(handler.getState().toString())){ System.out.println("id "+handler.getAppId()); System.out.println("state "+handler.getState()); try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } } } }
具体配置信息可以类比spark-submit。
5. RDD
5.1 什么是RDD
RDD(Resilient Distributed Dataset)叫做弹性分布式数据集,是Spark中最基本的数据抽象,它代表一个不可变、可分区、里面的元素可并行计算的集合。RDD具有数据流模型的特点:自动容错、位置感知性调度和可伸缩性。RDD允许用户在执行多个查询时显式地将工作集缓存在内存中,后续的查询能够重用工作集,这极大地提升了查询速度。
5.2 RDD的属性
(1)一组分片(Partition),即数据集的基本组成单位。对于RDD来说,每个分片都会被一个计算任务处理,并决定并行计算的粒度。用户可以在创建RDD时指定RDD的分片个数,如果没有指定,那么就会采用默认值。默认值就是程序所分配到的CPU Core的数目。
(2)一个计算每个分区的函数。Spark中RDD的计算是以分片为单位的,每个RDD都会实现compute函数以达到这个目的。compute函数会对迭代器进行复合,不需要保存每次计算的结果。
(3)RDD之间的依赖关系。RDD的每次转换都会生成一个新的RDD,所以RDD之间就会形成类似于流水线一样的前后依赖关系。在部分分区数据丢失时,Spark可以通过这个依赖关系重新计算丢失的分区数据,而不是对RDD的所有分区进行重新计算。
(4)一个Partitioner,即RDD的分片函数。当前Spark中实现了两种类型的分片函数,一个是基于哈希的HashPartitioner,另外一个是基于范围的RangePartitioner。只有对于于key-value的RDD,才会有Partitioner,非key-value的RDD的Parititioner的值是None。Partitioner函数不但决定了RDD本身的分片数量,也决定了parent RDD Shuffle输出时的分片数量。
(5)一个列表,存储存取每个Partition的优先位置(preferred location)。对于一个HDFS文件来说,这个列表保存的就是每个Partition所在的块的位置。按照“移动数据不如移动计算”的理念,Spark在进行任务调度的时候,会尽可能地将计算任务分配到其所要处理数据块的存储位置。
5.3 RDD的创建方式
5.3.1 通过读取文件生成的
sc.textFile("/spark/hello.txt")
5.3.2 通过并行化的方式创建RDD
JavaRDD<Integer> parallelize = javaSparkContext.parallelize(Arrays.asList(1, 2, 3, 4, 5), 4);
5.4 RDD编程API
Spark支持两个类型(算子)操作: Transformation和Action
5.4.1 Transform
主要做的是就是将一个已有的RDD生成另外一个RDD。Transformation具有lazy特性(延迟加载)。Transformation算子的代码不会真正被执行。只有当我们的程序里面遇到一个action算子的时候,代码才会真正的被执行。这种设计让Spark更加有效率地运行。
常用的Transformation:
动作 | 含义 |
---|---|
map(func) | 返回一个新的RDD,该RDD由每一个输入元素经过func函数转换后组成 |
filter(func) | 返回一个新的RDD,该RDD由经过func函数计算后返回值为true的输入元素组成 |
flatMap(func) | 类似于map,但是每一个输入元素可以被映射为0或多个输出元素(所以func应该返回一个序列,而不是单一元素) |
mapPartitions(func) | 类似于map,但独立地在RDD的每一个分片上运行,因此在类型为T的RDD上运行时,func的函数类型必须是Iterator[T] => Iterator[U] |
mapPartitionsWithIndex(func) | 类似于mapPartitions,但func带有一个整数参数表示分片的索引值,因此在类型为T的RDD上运行时,func的函数类型必须是(Int, Interator[T]) => Iterator[U] |
sample(withReplacement, fraction, seed) | 根据fraction指定的比例对数据进行采样,可以选择是否使用随机数进行替换,seed用于指定随机数生成器种子 |
union(otherDataset) | 对源RDD和参数RDD求并集后返回一个新的RDD |
intersection(otherDataset) | 对源RDD和参数RDD求交集后返回一个新的RDD |
distinct([numTasks])) | 对源RDD进行去重后返回一个新的RDD |
groupByKey([numTasks]) | 在一个(K,V)的RDD上调用,返回一个(K, Iterator[V])的RDD |
reduceByKey(func, [numTasks]) | 在一个(K,V)的RDD上调用,返回一个(K,V)的RDD,使用指定的reduce函数,将相同key的值聚合到一起,与groupByKey类似,reduce任务的个数可以通过第二个可选的参数来设置 |
aggregateByKey(zeroValue)(seqOp, combOp, [numTasks]) | 先按分区聚合 再总的聚合 每次要跟初始值交流 例如:aggregateByKey(0)(+,+) 对k/y的RDD进行操作 |
sortByKey([ascending], [numTasks]) | 在一个(K,V)的RDD上调用,K必须实现Ordered接口,返回一个按照key进行排序的(K,V)的RDD |
sortBy(func,[ascending], [numTasks]) | 与sortByKey类似,但是更灵活 第一个参数是根据什么排序 第二个是怎么排序 false倒序 第三个排序后分区数 默认与原RDD一样 |
join(otherDataset, [numTasks]) | 在类型为(K,V)和(K,W)的RDD上调用,返回一个相同key对应的所有元素对在一起的(K,(V,W))的RDD 相当于内连接(求交集) |
cogroup(otherDataset, [numTasks]) | 在类型为(K,V)和(K,W)的RDD上调用,返回一个(K,(Iterable,Iterable))类型的RDD |
cartesian(otherDataset) | 两个RDD的笛卡尔积 的成很多个K/V |
pipe(command, [envVars]) | 调用外部程序 |
coalesce(numPartitions) | 重新分区 第一个参数是要分多少区,第二个参数是否shuffle 默认false 少分区变多分区 true 多分区变少分区 false |
repartition(numPartitions) | 重新分区 必须shuffle 参数是要分多少区 少变多 |
repartitionAndSortWithinPartitions(partitioner) | 重新分区+排序 比先分区再排序效率高 对K/V的RDD进行操作 |
foldByKey(zeroValue)(seqOp) | 该函数用于K/V做折叠,合并处理 ,与aggregate类似 第一个括号的参数应用于每个V值 第二括号函数是聚合例如:+ |
combineByKey | 合并相同的key的值 rdd1.combineByKey(x => x, (a: Int, b: Int) => a + b, (m: Int, n: Int) => m + n) |
partitionBy(partitioner) | 对RDD进行分区 partitioner是分区器 |
cache | RDD缓存,可以避免重复计算从而减少时间,区别:cache内部调用了persist算子,cache默认就一个缓存级别MEMORY-ONLY ,而persist则可以选择缓存级别 |
persist | RDD缓存,可以避免重复计算从而减少时间,区别:cache内部调用了persist算子,cache默认就一个缓存级别MEMORY-ONLY ,而persist则可以选择缓存级别 |
Subtract(rdd) | 返回前rdd元素不在后rdd的rdd |
leftOuterJoin | leftOuterJoin类似于SQL中的左外关联left outer join,返回结果以前面的RDD为主,关联不上的记录为空。只能用于两个RDD之间的关联,如果要多个RDD关联,多关联几次即可。 |
rightOuterJoin | rightOuterJoin类似于SQL中的有外关联right outer join,返回结果以参数中的RDD为主,关联不上的记录为空。只能用于两个RDD之间的关联,如果要多个RDD关联,多关联几次即可 |
subtractByKey | substractByKey和基本转换操作中的subtract类似只不过这里是针对K的,返回在主RDD中出现,并且不在otherRDD中出现的元素 |
5.4.2 Action
触发代码的运行,一段Spark代码中至少有一个action操作
常用的Action操作
|动作|
动作 | 含义 |
---|---|
reduce(func) | 通过func函数聚集RDD中的所有元素,这个功能必须是课交换且可并联的 |
collect() | 在驱动程序中,以数组的形式返回数据集的所有元素 |
count() | 返回RDD的元素个数 |
first() | 返回RDD的第一个元素(类似于take(1)) |
take(n) | 返回一个由数据集的前n个元素组成的数组 |
takeSample(withReplacement,num, [seed]) | 返回一个数组,该数组由从数据集中随机采样的num个元素组成,可以选择是否用随机数替换不足的部分,seed用于指定随机数生成器种子 |
takeOrdered(n, [ordering]) | |
saveAsTextFile(path) | 将数据集的元素以textfile的形式保存到HDFS文件系统或者其他支持的文件系统,对于每个元素,Spark将会调用toString方法,将它装换为文件中的文本 |
saveAsSequenceFile(path) | 将数据集中的元素以Hadoop sequencefile的格式保存到指定的目录下,可以使HDFS或者其他Hadoop支持的文件系统。 |
saveAsObjectFile(path) | |
countByKey() | 针对(K,V)类型的RDD,返回一个(K,Int)的map,表示每一个key对应的元素个数。 |
foreach(func) | 在数据集的每一个元素上,运行函数func进行更新。 |
aggregate | 先对分区进行操作,在总体操作 |
reduceByKeyLocally | |
lookup | |
top | |
fold | |
foreachPartition | 针对分区做foreach |
6. Spark SQL
Spark SQL是spark套件中一个模板,它将数据的计算任务通过SQL的形式转换成了RDD的计算,类似于Hive通过SQL的形式将数据的计算任务转换成了MapReduce。
特点:
-
和Spark Core的无缝集成,可以在写整个RDD应用的时候,配置Spark SQL来完成逻辑实现。
-
统一的数据访问方式,Spark SQL提供标准化的SQL查询。
-
Hive的继承,Spark SQL通过内嵌的hive或者连接外部已经部署好的hive案例,实现了对hive语法的继承和操作。
-
标准化的连接方式,Spark SQL可以通过启动thrift Server来支持JDBC、ODBC的访问,将自己作为一个BI Server使用
6.1 什么是DataFrame
与RDD类似,DataFrame也是一个分布式数据容器。然而DataFrame更像传统数据库的二维表格,除了数据以外,还记录数据的结构信息,即schema。同时,与Hive类似,DataFrame也支持嵌套数据类型(struct、array和map)。从API易用性的角度上看,DataFrame API提供的是一套高层的关系操作,比函数式的RDD API要更加友好,门槛更低。
6.2 什么是DataSet
1)是Dataframe API的一个扩展,是Spark最新的数据抽象。 2)用户友好的API风格,既具有类型安全检查也具有Dataframe的查询优化特性。 3)Dataset支持编解码器,当需要访问非堆上的数据时可以避免反序列化整个对象,提高了效率。 4)样例类被用来在Dataset中定义数据的结构信息,样例类中每个属性的名称直接映射到DataSet中的字段名称。 5) Dataframe是Dataset的特列,DataFrame=Dataset[Row] ,所以可以通过as方法将Dataframe转换为Dataset。Row是一个类型,跟Car、Person这些的类型一样,所有的表结构信息我都用Row来表示。 6)DataSet是强类型的。比如可以有Dataset[Car],Dataset[Person]. 7)DataFrame只是知道字段,但是不知道字段的类型,所以在执行这些操作的时候是没办法在编译的时候检查是否类型失败的,比如你可以对一个String进行减法操作,在执行的时候才报错,而DataSet不仅仅知道字段,而且知道字段类型,所以有更严格的错误检查。就跟JSON对象和类对象之间的类比。
6.3 SparkSQL编程
6.3.1 SparkSession新的起点
在老的版本中,SparkSQL提供两种SQL查询起始点:一个叫SQLContext,用于Spark自己提供的SQL查询;一个叫HiveContext,用于连接Hive的查询。 SparkSession是Spark最新的SQL查询起始点,实质上是SQLContext和HiveContext的组合,所以在SQLContext和HiveContext上可用的API在SparkSession上同样是可以使用的。SparkSession内部封装了sparkContext,所以计算实际上是由sparkContext完成的。
6.3.2 DataFrame
在Spark SQL中SparkSession是创建DataFrame和执行SQL的入口,创建DataFrame有三种方式:通过Spark的数据源进行创建;从一个存在的RDD进行转换;还可以从Hive Table进行查询返回。
spark.read.json("/opt/module/spark/examples/src/main/resources/people.json") df.createOrReplaceTempView("people") // df.createGlobalTempView("people") spark.sql("SELECT * FROM people") df.groupBy("age").count().show()
6.3.3 DataSet
Dataset是具有强类型的数据集合,需要提供对应的类型信息。
6.3.4 RDD、DataFrame、DataSet
在SparkSQL中Spark为我们提供了两个新的抽象,分别是DataFrame和DataSet。他们和RDD有什么区别呢?首先从版本的产生上来看: RDD (Spark1.0) —> Dataframe(Spark1.3) —> Dataset(Spark1.6) 如果同样的数据都给到这三个数据结构,他们分别计算之后,都会给出相同的结果。不同是的他们的执行效率和执行方式。 在后期的Spark版本中,DataSet会逐步取代RDD和DataFrame成为唯一的API接口。
6.3.4.1 三者的共性
1、RDD、DataFrame、Dataset全都是spark平台下的分布式弹性数据集,为处理超大型数据提供便利 2、三者都有惰性机制,在进行创建、转换,如map方法时,不会立即执行,只有在遇到Action如foreach时,三者才会开始遍历运算。 3、三者都会根据spark的内存情况自动缓存运算,这样即使数据量很大,也不用担心会内存溢出。 4、三者都有partition的概念 5、三者有许多共同的函数,如filter,排序等 6、在对DataFrame和Dataset进行操作许多操作都需要这个包进行支持。
6.3.4.2 三者的区别
RDD: 1)RDD一般和spark mlib同时使用 2)RDD不支持sparksql操作 DataFrame: 1)与RDD和Dataset不同,DataFrame每一行的类型固定为Row,每一列的值没法直接访问,只有通过解析才能获取各个字段的值,如:
testDF.foreach{ line => val col1=line.getAs[String]("col1") val col2=line.getAs[String]("col2") }
2)DataFrame与Dataset一般不与spark mlib同时使用 3)DataFrame与Dataset均支持sparksql的操作,比如select,groupby之类,还能注册临时表/视窗,进行sql语句操作,如:
dataDF.createOrReplaceTempView("tmp") spark.sql("select ROW,DATE from tmp where DATE is not null order by DATE").show(100,false)
4)DataFrame与Dataset支持一些特别方便的保存方式,比如保存成csv,可以带上表头,这样每一列的字段名一目了然
//保存 val saveoptions = Map("header" -> "true", "delimiter" -> "\t", "path" -> "hdfs://hadoop102:9000/test") datawDF.write.format("com.atguigu.spark.csv").mode(SaveMode.Overwrite).options(saveoptions).save() //读取 val options = Map("header" -> "true", "delimiter" -> "\t", "path" -> "hdfs://hadoop102:9000/test") val datarDF= spark.read.options(options).format("com.atguigu.spark.csv").load()
利用这样的保存方式,可以方便的获得字段名和列的对应,而且分隔符(delimiter)可以自由指定。
Dataset: 1)Dataset和DataFrame拥有完全相同的成员函数,区别只是每一行的数据类型不同。 2)DataFrame也可以叫Dataset[Row],每一行的类型是Row,不解析,每一行究竟有哪些字段,各个字段又是什么类型都无从得知,只能用上面提到的getAS方法或者共性中的第七条提到的模式匹配拿出特定字段。而Dataset中,每一行是什么类型是不一定的,在自定义了case class之后可以很自由的获得每一行的信息
6.4 SparkSQL数据源
6.4.1 通用加载/保存方法
val peopleDF = spark.read.format("json").load("examples/src/main/resources/people.json") peopleDF.write.format("parquet").save("hdfs://hadoop102:9000/namesAndAges.parquet") val sqlDF = spark.sql("SELECT * FROM parquet.`hdfs://hadoop102:9000/namesAndAges.parquet`") sqlDF.show()
6.4.2 文件保存选项
可以采用SaveMode执行存储操作,SaveMode定义了对数据的处理模式。需要注意的是,这些保存模式不使用任何锁定,不是原子操作。此外,当使用Overwrite方式执行时,在输出新数据之前原数据就已经被删除。SaveMode详细介绍如下表:
Scala/Java Any Language Meaning SaveMode.ErrorIfExists(default) “error”(default) 如果文件存在,则报错 SaveMode.Append “append” 追加 SaveMode.Overwrite “overwrite” 覆写 SaveMode.Ignore “ignore” 数据存在,则忽略
6.4.3 JSON文件
Spark SQL 能够自动推测 JSON数据集的结构,并将它加载为一个Dataset[Row]. 可以通过SparkSession.read.json()去加载一个 一个JSON 文件。 {“name”:“Michael”} {“name”:“Andy”, “age”:30} {“name”:“Justin”, “age”:19}
6.4.4 JDBC
6.4.4.1 数据源选项
属性名称 | 说明 |
---|---|
Url | 表示连接的JDBC URL,可以在URL中指定特定源的连接属性,例如jdbc:postgresql://localhost/test?user=fred&password=secret |
dbtable | 表示要读取的JDBC 表,请注意, 可以使用SQL查询的FROM子句中任何有效内容,例如你可以在圆括号中使用子查询,而不是全表查询 |
driver | 用于连接到此URL的JDBC驱动器的类名 |
partitionColumn,lowerBound,upperBound | 如果指定了这些选项中的任何一个,则必须设置其他所有选项。另外,还必须指定numPartition。这些属性描述了如何在从多个worker并行读取时对表格进行划分。partitionColumn是要分区的列,必须是整类型。请注意,lowerBound和upperBound仅用于确定分区跨度,而不用于过滤表中的行。因此表中所有的行都将被划分返回 |
numPartitions | 在读取和写入数据表时,数据表可用于并行的最大分区数,这也决定了并发JDBC连接的最大数目。如果要写入的分区数超过此限制,则通过在写入时调用coalesce(numPartitions)来将分区数降到符合此限制 |
fetchsize | 表示JDBC每次读取多少条记录。这个设置与JDBC驱动器的性能有关系,JDBC驱动器默认值低获取行数。该选项仅适用于读操作 |
batchsize | 表示JDBC批处理大小。用于指定每次写入多少条记录。这个选项与JDBC驱动器性能有关系,该选项仅适用于写操作,默认值为1000 |
isolationLevel | 表示数据库的事务隔离级别(适用于当前连接)。它可以取值NONE,READ_COMMITTED,READ_UNCOMMITTED,REPEATABLE_READ或SERIALIZABLE,分别对应于JDBC的Connection对象定义的标准事务隔离级别。默认值为READ_UNCOMMITTED,此选项仅适用于写操作 |
truncate | 这是一个和JDBC写入相关的选项。当Spark要执行覆盖表操作时,即启用SaveMode.Overwrite,Spark将截取现有表,而不是删除之后再重新创建它。这样可以提高效率,并防止表元数据被删除。但是,在某些情况下,例如新数据具有不同的schema时,它并不起作用。默认值为false,该选项仅用于写操作。 |
createTableOptions | 这是一个JDBC写入相关的选项。用于在创建表时设置特定数据库表和分区选项。例如,CREATE TABLE t (name string) ENGINE=InnoDB。该选项仅适用于写操作 |
createTableColumnTypes | 表示创建表时使用的数据库列数据类型,而不是默认值。应该使用与CREATE TABLE列语法相同(例如,"name char(64), comments varchar(1024)")来指定数据类型信息,指定的类型应该是有效的SparkSQL数据类型。该选项仅适用于写操作 |
6.4.4.2 从sql读取数据
spark.read .format("jdbc") .option("driver", "org.postgresql.Driver") .option("url", "jdbc:postgresql://database_server") .option("dbtable", "schema.tablename") .option("user", "username") .option("password", "my-secret-password") .load() pgDf.select("DEST_COUNTRY_NAME").distinct().show(5)
并行度数据库,可以通过指定最大分区数量来限制并行读写的最大数量
// 当数据不多时,仍然会作为一个分区,但是此配置可帮助你确保在读取和写入数据时不会导致数据库过载 spark.read.format("jdbc") .option("driver", "org.postgresql.Driver") .option("url", "jdbc:postgresql://database_server") .option("dbtable", "tablename") .option("numPartitions", 10) .load()
基于滑动窗口的分区,现在基于谓词进行分区,基于数值型的count进行分区,我们为第一个分区和最后一个分区分别指定一个最小值和一个最大值,超出该范围的数据将存放到第一个分区或最后一个分区。接下来指定分区总数
val props = new java.util.Properties props.setProperty("driver", "org.sqlite.JDBC") val colName = "count" val lowerBound = 0L val upperBound = 348113L val numPartitions = 10 // 根据count列数值,从小到大均匀划分10个间隔区间的数据,之后每个区间的数据被分到一个分区 spark.read.jdbc( "url", "tablename", colName, lowerBound, upperBound, numPartitions, props ).count()
其他数据源以及使用选项留待后续补充,接下来介绍重头戏,自定义外部数据源。
6.5 自定义外部数据源
目前Spark datasource接口经历了V1到V2的版本,但是V2版本的接口处于@Evolving状态,可以在生产中使用中,可能会再新版本中迭代,Spark 2.X 主要还是使用Data Source V2。 iotdb使用的Spark DataSource V1的接口开发iotdb的Spark接口,Seatunnel主要使用的是DataSourceV2的接口,Spark 3.x的数据源接口需要再研究一下,所以本次主要还是针对DataSourceV2。
DataSource V2下有如下的接口:
public class MyJdbcDataSource implements ReadSupport, DataSourceV2, DataSourceRegister { /** * 批处理格式下的数据读取 * @param dataSourceOptions 选项 * @return */ @Override public DataSourceReader createReader(DataSourceOptions dataSourceOptions) { return new MyJdbcReader(dataSourceOptions); } @Override public String shortName() { return "myJdbc"; } }
ReadSupport: 创建读取器接口
DataSourceV2: 是一个标记接口
DataSourceRegister: 这里就是给定自定义数据源的名称,也就是spark.read.format(""),format中填入的东西
public class MyJdbcReader implements DataSourceReader { private final String schema; public MyJdbcReader(DataSourceOptions dataSourceOptions) { schema = dataSourceOptions.get("schema").orElse(""); } @Override public StructType readSchema() { StructField[] fields = new StructField[12]; fields[0] = DataTypes.createStructField("id", DataTypes.IntegerType, true); fields[1] = DataTypes.createStructField("point_name", DataTypes.StringType, true); fields[2] = DataTypes.createStructField("point_describe", DataTypes.StringType, true); fields[3] = DataTypes.createStructField("mold", DataTypes.IntegerType, true); fields[4] = DataTypes.createStructField("period", DataTypes.IntegerType, true); fields[5] = DataTypes.createStructField("unit", DataTypes.StringType, true); fields[6] = DataTypes.createStructField("implacable", DataTypes.DoubleType, true); fields[7] = DataTypes.createStructField("source_name", DataTypes.StringType, true); fields[8] = DataTypes.createStructField("calc_function", DataTypes.StringType, true); fields[9] = DataTypes.createStructField("calc_type", DataTypes.IntegerType, true); fields[10] = DataTypes.createStructField("parameters", DataTypes.StringType, true); fields[11] = DataTypes.createStructField("cycleperiod", DataTypes.IntegerType, true); return DataTypes.createStructType(fields); } /** * 这个函数如何返回需要在看下 * @return */ @Override public List<InputPartition<InternalRow>> planInputPartitions() { return Collections.singletonList(new MyInputPartition()); } }
这是在定义读取器,主要是给定数据模式和创建读取分区,读取分区是实际进行读的类,这里只定义了一个分区。
public class MyInputPartition implements InputPartition<InternalRow>, Serializable { @Override public InputPartitionReader<InternalRow> createPartitionReader() { return new MyInputPartitionReader(); } }
创建分区读取器
public class MyInputPartitionReader implements InputPartitionReader<InternalRow>, Serializable { private final String url = "jdbc:postgresql://ip:5432/database?serverTimezone=GMT%2B8&allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8"; private final String driver = "org.postgresql.Driver"; private final String username = "用户名"; private final String password = "密码"; private ResultSet resultSet; { init(); } private void init() { try { Class.forName(driver); Connection connection = DriverManager.getConnection(url, username, password); PreparedStatement preparedStatement = connection.prepareStatement("select * from t_thunder_calc_info"); resultSet = preparedStatement.executeQuery(); } catch (ClassNotFoundException | SQLException e) { e.printStackTrace(); } } // 是否还有数据 @Override public boolean next() { try { return resultSet.next(); } catch (SQLException e) { e.printStackTrace(); } return false; } // 获取单条记录 @Override public InternalRow get() { ArrayList<Object> record = new ArrayList<>(); try { record.add(resultSet.getInt(1)); record.add(UTF8String.fromBytes(resultSet.getString(2).getBytes(StandardCharsets.UTF_8))); record.add(UTF8String.fromBytes(resultSet.getString(3).getBytes(StandardCharsets.UTF_8))); record.add(resultSet.getInt(4)); record.add(resultSet.getInt(5)); record.add(UTF8String.fromBytes(resultSet.getString(6).getBytes(StandardCharsets.UTF_8))); record.add(resultSet.getDouble(7)); record.add(UTF8String.fromBytes(resultSet.getString(8).getBytes(StandardCharsets.UTF_8))); record.add(UTF8String.fromBytes(resultSet.getString(9).getBytes(StandardCharsets.UTF_8))); record.add(resultSet.getInt(10)); record.add(UTF8String.fromBytes(resultSet.getString(11).getBytes(StandardCharsets.UTF_8))); record.add(resultSet.getInt(12)); } catch (SQLException e) { e.printStackTrace(); } return new GenericInternalRow(record.toArray(new Object[0])); } // 关闭连接 @Override public void close() throws IOException { try { resultSet.close(); } catch (SQLException e) { e.printStackTrace(); } } }
测试用例:
public class TestReader { public static void main(String[] args) { SparkSession spark = SparkSession.builder().appName("testReader").master("local[1]").getOrCreate(); Dataset<Row> myTxt = spark.read().format("myJdbc").load(); myTxt.show(); // Dataset<Row> data = spark.read().format("defaultSource").load(); // // data.show(); spark.close(); } }
这里只是简单的针对Spark的自定义数据源给出了一些简单的介绍,但利用这些已经可以对Seatunnel的连接器原理进行源码级解析了。后续再展开关于Seatunnel 2.3.x以上的连接器的实现原理。
7. Spark StructedStreaming
public class KafkaStructuredStreamingDemo { public static void main(String[] args) throws StreamingQueryException { SparkSession spark = SparkSession.builder().master("local[*]").appName("SparkSQL").getOrCreate(); SparkContext sc = spark.sparkContext(); sc.setLogLevel("WARN"); Dataset<Row> dataDF = spark.readStream().format("kafka").option("kafka.bootstrap.servers", "ip:9092") .option("subscribe", "spark_kafka").load(); Dataset<Row> result = dataDF.selectExpr("CAST(value AS STRING)").as("new_value"); StreamingQuery sink = result.writeStream().format("console").outputMode(OutputMode.Update()) .trigger(Trigger.ProcessingTime(0)).option("numRows", 10) .option("truncate", false).start(); sink.awaitTermination(); sink.stop(); } }
Spark Streaming已经不怎么用了,目前主要还是使用的StructedStreaming,目前主要针对的还是结构化的数据,针对无法结构化的数据,比如视频数据、文本流数据,有时候可能会用到RDD和Spark Streaming。所以目前主要关注的还是结构化处理。上面给出的是一个读取kafka的示例。