Spark手稿

一、简介

Spark集批处理、实时流处理、交互式查询、机器学习与图计算于一体。大多数现有的集群计算系统都是基于非循环的数据流模型。即从稳定的物理存储(分布式文件系统)中加载记录,
记录被传入由一组确定性操作构成的DAG(有向无环图),然后写回稳定存储。DAG数据流图能够在运行时自动实现任务调度和故障恢复。基于数据流的框架没有明确支持工作集,所以需要将数据输出到磁盘,
然后在每次查询时重新加载,这回带来较大的开销。针对以上问题,Spark实现了一种分布式的内存抽象,称为弹性分布式数据集(RDD),它支持基于工作集的应用,同时具有数据流模型的特点:自动容错、
位置感知性调度和可伸缩。
RDD允许用户在执行多个查询时显式地将工作集缓存在内存中,后续的查询能够重用工作集,这极大地提升了查询速度。
Spark 本身作为平台也开发了 streaming 处理框架 spark streaming, SQL 处理框架 Dataframe, 机器学习库 MLlib, 和图处理库 GraphX

二、Spark的数据抽象RDD

特点:可以并行操作并且是容错的。
如何得到RDD:(1)执行Transform操作(变换操作)(2)读取外部存储系统的数据集

三、Spark容错的实现原理:

RDD不同的依赖关系导致Spark对不同的依赖关系有不同的处理方式。
对于宽依赖而言,由于宽依赖实质是指父RDD的一个分区会对应一个子RDD的多个分区,在   此情况下出现  部 分计算结果丢失,单一计算丢失的数据无法达到效果,便采用重新计算该步骤中的所有数据,从而会导致计   算数据重复;对于窄依赖而言,由于窄依赖实质是指父RDD的分区最多被一个子RDD使用,在此情况下出现部  分计算的错误,由于计算结果的数据只与依赖的父RDD的相关数据有关,所以不需要重新计算所有数据,只重  新计算出错部分的数据即可。Spark框架层面的容错机制,主要分为三大层面(调度层、RDD血统层、Checkpoint层),在这三大层面中包括Spark RDD容错四大核心要点。	
(1)Stage输出失败,上层调度器DAGScheduler重试。
(2)Spark计算中,Task内部任务失败,底层调度器重试。
(3)RDD Lineage血统中窄依赖、宽依赖计算。
(4)Checkpoint缓存。

四、RDD的依赖关系(窄依赖,宽依赖)

1)窄依赖是指每一个父RDD的Partition最多被子RDD的一个Partition使用,不会引入shuffle
2)宽依赖指的是父RDD的一个Partiton被子RDD的多个Partiton依赖,子RDD的Partiton是父   RDD的所有Partiton shuffle的结果。 Shuffle的本质就是将数据汇总后重新分发的过程。

五、DAG的生成与Stage的划分(Job与Task划分):

原始RDD通过一系列转换形成了DAG。
Spark的Stage:Spark在执行任务(job)时,首先会根据依赖关系,将DAG划分为不同的阶段 (Stage),也就是在  DAG最后一个RDD上会触发动作并生成一个Job。
也就是说一个DAG就是一个Job
Spark的Task分为两种:
1)org.apache.sppark.scheduler.ShuffleMapTask
2)org.apache.spark.scheduler.ResultTask
简单来说,DAG的最后一个阶段会为每个结果的Partition生成一个ResultTask,其余所有的阶段都会生成ShuffleMapTask。
Spark的Job和Task:
原始的RDD经过一系列转换后(一个DAG),会在最后一个RDD上触发一个动作,这个动作会生成一个Job,而DAG中被划分的每个Stage就是Job中的一个task。Job被划分成一批task后这批task会被提交到集群上的计算节点上。

六、Spark在集群中的运行架构:

1)Driver Program
2)Cluster Manager(负责work node中的cpu 内存等资源的管理,以及资源使用情况的监控)
3)Worker Node)(Excutor进程,Excutor负责任务的调度和计算结果的处理以及向Manager汇报资源使用情况)
当代码写好打成jar包提交到集群之后Driver中的SparkContext对象会联系cluster manager让她为work node分配 CPU 内存等资源 并在每个 work node上启动一个执行器
Executor负责任务的调度分配。程序在运行时SparkContext对象是直接与Excutor交互的,cluster manager 只是负责资源的管理和调度,而task的分配和结果的处理它是不管的。
Cluster Manager >>>>> Master 进程
work Node >>>>>worker

任务调度

任务调度模块主要包括两大部分:
1)DAGScheduler:主要负责分析依赖关系,将DAG划分为不同的阶段,阶段划分出来后提交到TaskScheduler。
2)TaskScheduler:通过ClusterManager 申请计算资源(包括在WorkNode上启动Excutor进程,CPU,内存等),然后在Excutor中运行Task任务并将计算的结果回传到Driver。
3)SchedulerBackend:负责分配已经申请到的当前可用的资源给等待资源的Task,并将Task提交到Excutor上执行。

七、Spark Shuffle:

本质就是将数据重新打乱然后汇聚到不同节点的过程。
特点是<1>数据量大(为节省宽带需要对数据压缩)<2>发生多次磁盘的续写<3>网络传输数据故而数据的序列化和反序列化复杂。
一般来说,每个Task处理的数据可以完全载入内存(如果不能,可以减小每个Partition的大小),因此Task可以做到在内存中计算。但是对于Shuffle来说,如果不持久化这个中间结果,
一旦数据丢失,就需要重新计算依赖的全部RDD,因此有必要持久化这个中间结果。所以这就是为什么Shuffle过程会产生文件的原因。	
Shuffle Write:
 数据是如何持久化到文件中,以使得下游的Task可以获取到其需要处理的数据(即Shuffle Read)。
 Hash Based Shuffle(org.apache.spark.shuffle.hash.HashShuffleManager):上游的每个MapTask 会为下游的每一个reducerTask都生成一个文件,故产生的文件数=N*M。但不会排序
 Sort Based Shuffle(org.apache.spark.shuffle.sort.SortShuffleManager):每个Shuffle Map Task不会为每个Reducer生成一个单独的文件;相反,它会将所有的结果写到一个文件里,同时会生成一个Index文件Reducer可以通过这个Index文件取得它需要处理的数据。

Shuffle 调优

1)spark.shuffle.manager
   如果需要Hash Based Shuffle,只需将spark.shuffle.manager设置成“hash”即可
 2)spark.shuffle.spill
   这个参数的默认值是true,用于指定Shuffle过程中如果内存中的数据超过阈值(参考spark.shuffle.memoryFraction的设置)时是否需要将部分数据临时写入外部存储。
   如果设置为false,那么这个过程就会一直使用内存,会有内存溢出的风险。
 3)spark.shuffle.memoryFraction
   在启用spark.shuffle.spill的情况下,spark.shuffle.memoryFraction决定了当Shuffle过程中使用的内存达到总内存多少比例的时候开始spill
 4)spark.shuffle.sort.bypassMergeThreshold
   这个配置的默认值是200,用于设置在Reducer的Partition数目少于多少的时候,Sort Based Shuffle内部不使用归并排序的方式处理数据,而是直接将每个Partition写入单独的文件
 5)spark.shuffle.compress和spark.shuffle.spill.compress
   这两个参数的默认配置都是true。spark.shuffle.compress和spark.shuffle.spill.compress都是用来设置Shuffle过程中是否对Shuffle数据进行压缩。
   其中,前者针对最终写入本地文件系统的输出文件;后者针对在处理过程需要写入到外部存储的中间数据,即针对最终的shuffle输出文件。
 6)spark.reducer.maxMbInFlight
   这个参数用于限制一个Reducer Task向其他的Executor请求Shuffle数据时所占用的最大内存数,尤其是如果网卡是千兆和千兆以下的网卡时。
   默认值是48MB。设置这个值需要综合考虑网卡带宽和内存。

八、Spark调优

1)Shuffle调优
 2)配置合理的序列化类:org.apache.spark.serializer.KryoSerializer
   修改spark-defaults.conf配置文件
   启动spark-shell或者spark-submit时配置  --conf spark.serializer=org.apache.spark.serializer.KryoSerializer
   在代码中:val conf = new SparkConf()
            conf.set(“spark.serializer”,“org.apache.spark.serializer.KryoSerializer”)
 3)配置临时文件目录
   spark.local.dir参数。当shuffle、归并排序(sort、merge)时都会产生临时文件。这些临时文件都在这么指定的目录下。
   那这个文件夹有很多临时文件,如果都发生读写操作,有的线程在读这个文件,有的线程在往这个文件里写,IO性能就非常低。
   怎么解决呢?可以创建多个文件夹,每个文件夹都对应一个真实的硬盘。只需要在这个配置时配置多个路径就可以。中间用逗号分隔。
   spark.local.dir=/home/tmp,/home/tmp2
 4)启用推测执行机制
   可以设置spark.speculation true 
   开启后,spark会检测执行较慢的Task,并复制这个Task在其他节点运行,最后哪个节点先运行完,就用其结果,然后将慢Task 杀死
 5)设置合理的reducerTask数
   也就是在计算任务时设置合理的分区数以保证合理的并发数和每个任务的饱和度。
 6)有些情况下,RDD操作使用MapPartitions替代map
   map方法对RDD的每一条记录逐一操作。mapPartitions是对RDD里的每个分区操作
   rdd.map{ x=>conn=getDBConn.conn;
            write(x.toString);
			conn close;
		}
   这样频繁的链接、断开数据库,效率差。
   rdd.mapPartitions{(record:=>conn.getDBConn;
   for(item<-recorders;
   write(item.toString);conn close;
   }
   这样就一次链接一次断开,中间批量操作,效率提升。

SparkConf与SparkContext

1、sparkconf
 Configuration for a Spark application.Most of the time, you would create a SparkConf object with new SparkConf(), which will load values from any spark.* Java system properties set in your application as well. In this case, parameters you set directly on the SparkConf object take priority over system properties.
 译文:大概是说这是个spark应用的配置对象,可以通过new SparkConf()来得到对象,默认是从spark应用系统配置中去加载属性值,在创建conf对象时也可以直接设置,这样的优先级比系统高。
(1)创建conf对象以及对象的几个常用方法:
     new SparkConf():Create a SparkConf that loads defaults from system properties and the classpath
     new SparkConf(loadDefaults: Boolean): loadDefaults :whether to also load values from Java system properties
     def set(key: String, value: String): SparkConf   Set a configuration variable.
     def setAppName(name: String): SparkConf  Set a name for your application.
     def setMaster(master: String): SparkConf   The master URL to connect to, such as "local" to run locally with one thread, "local[4]" to run locally with 4  cores, or "spark://master:7077" to run on a Spark standalone cluster.
(2)动态加载Spark属性
     在一些情况下,你可能想在SparkConf中避免硬编码确定的配置。Spark允许你简单地创建一个空conf。
     val sc = new SparkContext(new SparkConf())
     然后你在运行时设置变量:
     ./bin/spark-submit --name "MyApp" --master local[3] --conf spark.shuffle.spill=false
     --conf "spark.executor.extraJavaOptions=-XX:+PrintGCDetails -XX:+PrintGCTimeStamps" myApp.jar
2. SparkContext
      Only one SparkContext may be active per JVM. You must stop() the active SparkContext before creating a new one.[在同一JVM中只能有一个sc存活,因此在创建一个新的sc之前需要先停掉存活的sc]
      new SparkContext(master: String, appName: String, conf: SparkConf)
          Alternative constructor that allows setting common Spark properties directly
master  Cluster URL to connect to (e.g. mesos://host:port, spark://host:port, local[4]).
appName A name for your application, to display on the cluster web UI
conf  a org.apache.spark.SparkConf object specifying other Spark parameters

RDD Operations

1. Transformations:
map(func):	Return a new distributed dataset formed by passing each element of the source through a   function func.
filter(func):	Return a new dataset formed by selecting those elements of the source on which func returns true.
flatMap(func):	Similar to map, but each input item can be mapped to 0 or more output items (so func should return a Seq rather than a single item).
mapPartitions(func):	Similar to map, but runs separately on each partition (block) of the RDD, so func must be of type Iterator<T> => Iterator<U> when running on an RDD of type T.
mapPartitionsWithIndex(func):	Similar to mapPartitions, but also provides func with an integer value representing the index of the partition, so func must be of type (Int, Iterator<T>) => Iterator<U> when running on an RDD of type T.
sample(withReplacement, fraction, seed):	Sample a fraction fraction of the data, with or without replacement, using a given random number generator seed.
union(otherDataset):	Return a new dataset that contains the union of the elements in the source dataset and the argument.
intersection(otherDataset):	Return a new RDD that contains the intersection of elements in the source dataset and the argument.
distinct([numTasks])):	Return a new dataset that contains the distinct elements of the source dataset.
groupByKey([numTasks]):	When called on a dataset of (K, V) pairs, returns a dataset of (K, Iterable<V>) pairs.
Note: If you are grouping in order to perform an aggregation (such as a sum or average) over each key, using reduceByKey or aggregateByKey will yield much better performance.
Note: By default, the level of parallelism in the output depends on the number of partitions of the parent RDD. You can pass an optional numTasks argument to set a different number of tasks.
 reduceByKey(func, [numTasks]):	When called on a dataset of (K, V) pairs, returns a dataset of (K, V) pairs where the values for each key are aggregated using the given reduce function func, which must be of type (V,V) => V. Like in groupByKey, the number of reduce tasks is configurable through an optional second argument.
 aggregateByKey(zeroValue)(seqOp, combOp, [numTasks]):	When called on a dataset of (K, V) pairs, returns a dataset of (K, U) pairs where the values for each key are aggregated using the given combine functions and a neutral "zero" value. Allows an aggregated value type that is different than the input value type, while avoiding unnecessary allocations. Like in groupByKey, the number of reduce tasks is configurable through an optional second argument.
sortByKey([ascending], [numTasks]):	When called on a dataset of (K, V) pairs where K implements Ordered, returns a dataset of (K, V) pairs sorted by keys in ascending or descending order, as specified in the boolean ascending argument.
join(otherDataset, [numTasks]):	When called on datasets of type (K, V) and (K, W), returns a dataset of (K, (V, W)) pairs with all pairs of elements for each key. Outer joins are supported through leftOuterJoin, rightOuterJoin, and fullOuterJoin.
cogroup(otherDataset, [numTasks]):	When called on datasets of type (K, V) and (K, W), returns a dataset of (K, (Iterable<V>, Iterable<W>)) tuples. This operation is also called groupWith.
cartesian(otherDataset)	When called on datasets of types T and U, returns a dataset of (T, U) pairs (all pairs of elements).
pipe(command, [envVars]):	Pipe each partition of the RDD through a shell command, e.g. a Perl or bash script. RDD elements are written to the process's stdin and lines output to its stdout are returned as an RDD of strings.
coalesce(numPartitions):	Decrease the number of partitions in the RDD to numPartitions. Useful for running operations more efficiently after filtering down a large dataset.
repartition(numPartitions):	Reshuffle the data in the RDD randomly to create either more or fewer partitions and balance it across them. This always shuffles all data over the network.
repartitionAndSortWithinPartitions(partitioner):	Repartition the RDD according to the given partitioner and, within each resulting partition, sort records by their keys. This is more efficient than calling repartition and then sorting within each partition because it can push the sorting down into the shuffle machinery.

2. Actions
reduce(func)	Aggregate the elements of the dataset using a function func (which takes two arguments and returns one). The function should be commutative and associative so that it can be computed correctly in parallel.
collect()	Return all the elements of the dataset as an array at the driver program. This is usually useful after a filter or other operation that returns a sufficiently small subset of the data.
count()	Return the number of elements in the dataset.
first()	Return the first element of the dataset (similar to take(1)).
take(n)	Return an array with the first n elements of the dataset.
takeSample(withReplacement, num, [seed])	Return an array with a random sample of num elements of the dataset, with or without replacement, optionally pre-specifying a random number generator seed.
takeOrdered(n, [ordering])	Return the first n elements of the RDD using either their natural order or a custom comparator.
saveAsTextFile(path)	Write the elements of the dataset as a text file (or set of text files) in a given directory in the local filesystem, HDFS or any other Hadoop-supported file system. Spark will call toString on each element to convert it to a line of text in the file.
saveAsSequenceFile(path)
(Java and Scala)	Write the elements of the dataset as a Hadoop SequenceFile in a given path in the local filesystem, HDFS or any other Hadoop-supported file system. This is available on RDDs of key-value pairs that     implement Hadoop's Writable interface. In Scala, it is also available on types that are implicitly convertible to Writable (Spark includes conversions for basic types like Int, Double, String, etc).
saveAsObjectFile(path)
(Java and Scala)	Write the elements of the dataset in a simple format using Java serialization, which can then be loaded using SparkContext.objectFile().
countByKey()	Only available on RDDs of type (K, V). Returns a hashmap of (K, Int) pairs with the count of each key.
foreach(func)	Run a function func on each element of the dataset. This is usually done for side effects such as updating an Accumulator or interacting with external storage systems.
Note: modifying variables other than Accumulators outside of the foreach() may result in undefined behavior. See Understanding closures for more details.

Spark集群的运行模式

Spark的运行模式基本取决于传递给SparkContext的deployMode和Master环境变量的值,目前Master有local、yarn-client、yarn-cluster、spark、mesos模式,
而deploy-mode必须是cluster、client中的一样。在这里我只讨论三种常见的部署模式,Spark Standalone、Spark on Yarn-Cluster、Spark on Yarn-Client。
Spark Standalone:需要部署Spark到相关节点,包括Master和Worker。
Yarn-Cluster:Driver和Executor都运行在Yarn集群。
Yarn-Client:Driver运行在本地,Executor运行在Yarn集群中。

基本流程:

DAGScheduler:解析DAG成stage,每个stage以TaskSet的形式提交给TaskScheduler。
TaskScheduler:负责具体启动任务,监控和汇报任务运行情况。
每个Spark Application都有自己的Executor进程,此进程的生命周期和整个Application的生命周期相同,此进程的内部维持着多个线程来并行地执行分配给它的Task。这种运行形式有利于进行不同Application之间的资源、调度隔离,但这也意味着不同Application之间难以做到相互通信和信息交换。

Standalone模式

这种模式下资源的调度是Spark框架自己实现的,其节点分为Master节点和Worker节点,
其中Driver运行在Master节点,并有常驻内存中的Master进程,Worker进程常驻在Worker节点,负责与Master的通信,
通过ExecutorRunner来控制运行在当前节点上的CoarseGrainedExecutorBackend。
每个Worker 上存在一个或多个CoarseGrainedExecutorBackend进程。
每个进程包含一个Executor对象,该对象持有一个线程池,每个线程可以执行一个Task。
submit提交:
1)向Master注册Driver。
2)Driver初始化SparkContext、DAGScheduler、TaskScheduler等。
3)DAGScheduler向Master注册Application,Master接受请求后,根据资源情况向Worker发送指令启动Executor,Executor启动后向Driver注册。
4)DAGScheduler进行Job的Stage划分(TaskSet)。
5)DAGScheduler提交作业给TaskScheduler。
6)TaskScheduler调用Executor运行任务。
7)Executor将任务反序列化并运行任务。
8)如果运行的ShuffleMapTask,将结果保存到本地文件中,汇报给Driver等待ResultTask或者其他的ShuffleTask任务。
9)ResultTask根据Driver中提供的信息进行结果获取和reduce,最后汇报给Driver。

代码提交

# Run application locally on 8 cores
./bin/spark-submit \
  --class org.apache.spark.examples.SparkPi \
  --master local[8] \
  /path/to/examples.jar \
  100
# Run on a Spark standalone cluster in client deploy mode
./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
# Run on a Spark standalone cluster in cluster deploy mode with supervise
./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
# Run on a 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
# Run a Python application on a Spark standalone cluster
./bin/spark-submit \
  --master spark://207.184.161.138:7077 \
  examples/src/main/python/pi.py \
  1000
# Run on a Mesos cluster in cluster deploy mode with supervise
./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
# Run on a Kubernetes cluster in cluster deploy mode
./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
./bin/spark-submit \
  --class <main-class> \
  --master <master-url> \
  --deploy-mode <deploy-mode> \
  --conf <key>=<value> \
  ... # other options
  <application-jar> \
  [application-arguments]

一些常用的 options(选项)有 :
–class : 您的应用程序的入口点(例如。org.apache.spark.examples.SparkPi)。
–master : 集群的 Master URL(例如。spark://23.195.26.187:7077)。
–deploy-mode : 是在 worker 节点(cluster)上还是在本地作为一个外部的客户端(client)部署您的 driver(默认 : client)†。
–conf : 按照 key=value 格式任意的 Spark 配置属性。对于包含空格的 value(值)使用引号包 “key=value” 起来。
application-jar : 包括您的应用以及所有依赖的一个打包的 Jar 的路径。该 URL 在您的集群上必须是全局可见的,例如,一个 hdfs:// path 或者一个 file:// path 在所有节点是可见的。
application-arguments : 传递到您的 main class 的 main 方法的参数,如果有的话。