1. Spark与Scala的版本问题
- 官网会给出
- Maven Repository上可以查到
2. RDD(Resilent Distributed DataSet)
- 一组Partition,每个分片都被一个计算任务处理,未指定的话默认是程序分配的CPU core的数目
- 计算每个Paritition的函数
每个Partition上的数据都有一个函数进行计算
- RDD之间的依赖关系
Rdd每次转换会生成一个新的RDD,RDD之间存在流水线一样的前后依赖关系,当一部分分区数据丢失,Spark可通过依赖关系重新计算丢失的分区数据
- 一个Partitioner(分区器)
只有对于Key-Value类型的RDD,才存在Partitioner,其他的是None。Spark中存在两种:一种是基于HashPartitioner,另一种是RangePartitioner。
- 一个列表(存取每个Partition的优先位置)
3. 窄依赖和宽依赖
RDD之间的依赖关系有窄依赖和宽依赖,窄依赖指一个父RDD的分区只被一个子RDD的分区使用,比如map, filter, union, join(父RDD是hash-partitioned ), mapPartitions, mapValues;
宽依赖指一个父RDD的分区可能被多个子RDD的分区使用,比如groupByKey, join(父RDD不是hash-partitioned ), partitionBy等,具体如官网图所示
对比:
1.窄依赖的每个父RDD的分区只会传入到一个子RDD分区中,每个分区里的数据都被加载到机器的内存里,依次调用 map, filter函数到分区,形成流水线操作,一个父RDD的分区传入到不同的子RDD分区中,中间可能有多个节点之间的数据传输,增加了网络和磁盘IO;
2.当RDD分区丢失时,spark会进行重算:
对于窄依赖,只需重算子RDD分区对应的父RDD分区即可,所以这个重算对数据的利用率是100%的,对于宽依赖,重算的父RDD分区对应多个子RDD分区,父RDD重算会有多余的计算,如下图,b1丢失重算,b2的数据就是冗余计算的。
4. 血统和Stage
血统:记录RDD的元数据和转换行为,便于恢复丢失的分区;
Stage:区分宽窄依赖, 为了生成任务。
5. RDD的获取方式
Spark Core为我们提供了三种创建RDD的方式,包括:
- 使用程序中的集合创建RDD,应用场景:自己使用构造的集合测试数据;
Spark会调用SparkContext中的parallelize()方法,将集合中的数据拷贝到集群上去,形成一个分布式的数据集合,也就是一个RDD。
1 val array = Array(1,2,3,4,5)
2 val rdd = sc.parallelize(array)
3 val sum = rdd.reduce(_ + _)
在调用parallelize()方法时,可以指定将集合切分成多少个partition,Spark会为每一个partition运行一个task来进行处理。
1 val rdd = sc.parallelize(array,10)
Spark官方的建议是,为集群中的每个CPU创建2-4个partition。Spark默认会根据集群的情况来设置partition的数量。
- 使用本地文件创建RDD,应用场景:在本地临时性地处理一些存储了大量数据的文件;
- 使用HDFS文件创建RDD,应用场景:可以针对HDFS上存储的大数据,进行离线批处理操作,最常用;
Spark是支持使用任何Hadoop支持的存储系统上的文件创建RDD的,比如说HDFS、Cassandra、HBase以及本地文件。通过调用SparkContext的textFile()方法,可以针对本地文件或HDFS文件创建RDD。
1 // textFile()方法中,输入本地文件路径或是HDFS路径
2 // HDFS:hdfs://spark1:9000/data.txt
3 // local:/home/hadoop/data.txt
4 val rdd = sc.textFile(“/home/hadoop/data.txt”)
5 val wordCount = rdd.map(line => line.length).reduce(_ + _)
Spark默认会为hdfs文件的每一个block创建一个partition,但是也可以通过textFile()的第二个参数手动设置分区数量,只能比block数量多,不能比block数量少。
该部分主要参考:Rdd的几种创建方式
6. DataFrame
DataFrame是一种以RDD为基础的分布式数据集,类似于传统数据库中的二位表格。DataFrame使用Sql的话必须先注册成一张表,例如:
1 df.registerTempTable("表名")
2 sqlContext.sql("Sql语句")
RDD转换DataFrame有两种方法:
- rdd.toDF
- 通过Schema
7. DStream
DStream由以下三部分组成:
- 对其他流的依赖;
- 产生RDD的时间间隔;
- 每个时间间隔产生RDD的函数。
8. groupByKey 、aggregateByKey、reduceByKey的区别
官方建议尽量使用aggregateByKey和reduceByKey,减少计算量和网络传输(不需要所有键值对都被传输)。
groupByKey()是对RDD中的所有数据做shuffle,根据不同的Key映射到不同的partition中再进行aggregate。
aggregateByKey()是先对每个partition中的数据根据不同的Key进行aggregate,然后将结果shuffle,完成各个partition之间的aggregate。因此,和groupByKey()相比,运算量小了很多。
distinct()也是对RDD中的所有数据做shuffle进行aggregate后再去重。
reduceByKey()也是先在单台机器中计算,再将结果进行shuffle,减小运算量。
参考下,举个例子:
有一系列元组(用户ID,用户访问的站点),要对每个用户访问的站点去重,groupByKey运算量较大,可选方案有reduceByKey,aggregateByKey。
1 val mapedUserAccess = userAccesses.map(userSite => (userSite._1, Set(userSite._2)))
2 val distinctSite = mapedUserAccess.reduceByKey(_++_)
reduceByKey的问题是,RDD的每个值都创建一个Set,如果处理一个巨大的RDD,这些对象将大量吞噬内存,并对垃圾回收造成压力。
1 val zeroValue = collecyion.mutable.set[String]()
2 val aggregated = userAccesses.aggregateByKey(zeroValue)((set,v) => set += v, (setOne, setTwo) => setOne ++= setTwo)
aggregateByKey可以解决内存问题,它有三个参数:
- 零值(zero):即聚合的初始值
- 函数f:(U, V):把值V合并到数据结构U, 该函数在分区内合并值时使用
- 函数 g:(U, V):合并两个数据结构U,在分区间合并值时调用此函数。
9. Cluster和Client模式
cluster模式:Driver程序在YARN上运行,应用的运行结果不能在客户端显示,所以最好将运行结果保存在外部介质(如HDFS、Redis、Mysql)而非stdout输出的应用程序,客户端的终端显示的仅是YARN上job的简单运行情况;
client模式:Driver运行在Client上,应用程序运行结果会在客户端显示,所以适合运行结果有输出的应用程序(如spark-shell)
10. Spark开发环境搭建
主要是:
- scala插件的安装
- 全局JDK和Library的设置
- 配置全局的Scala SDK
11. Spark内存管理
这块完全照抄Spark1.6内存管理,只是怕丢了。
从1.6.0版本开始,Spark内存管理模型发生了变化。旧的内存管理模型由StaticMemoryManager类实现,现在称为“legacy(遗留)”。默认情况下,“Legacy”模式被禁用,这意味着在Spark 1.5.x和1.6.0上运行相同的代码会导致不同的行为。为了兼容,您可以使用spark.memory.useLegacyMode参数启用“旧”内存模型。
1.6.0及以后版本,使用的统一内存管理器,由UnifiedMemoryManager实现,先看一张图。
图中主要有3个区域,分别用3种不同颜色表示。
1、Reserved Memory
系统保留内存。从Spark 1.6.0起,它的值默认为300MB(被硬编码到spark代码中,代码private val RESERVED_SYSTEM_MEMORY_BYTES = 300 * 1024 * 1024) ),这意味着这个300MB的RAM不参与Spark内存区域大小的计算。它的值可以通过参数spark.testing.reservedMemory来调整,但不推荐使用,从参数名字可以看到这个参数仅供测试使用,不建议在生产环境中使用。虽然是保留内存,但并不意味这这部分内存没有使用,实际上,它会存储大量的Spark内部对象。而且,executor在执行时,必须保证至少有1.5 *保留内存= 450MB大小的堆,否则程序将失败,并抛出一个异常“Please use a larger heap size”。参考代码:
1 val minSystemMemory = reservedMemory * 1.5
2 if (systemMemory < minSystemMemory) {
3 throw new IllegalArgumentException(s"System memory $systemMemory must " +
4 s"be at least $minSystemMemory. Please use a larger heap size.")
5 }
2、User Memory
这是在Spark Memory分配后仍然保留的内存,使用方式完全取决于你。你可以存储RDD转换中用到的数据结构。例如,您可以在mapPartitions算子时,维护一个HashMap(这将消耗所谓的用户内存)。在Spark 1.6.0中,此内存池的大小可以计算为(“executor-memory” - “保留内存”)(1.0 - spark.memory.fraction),默认值等于(“Java Heap”) - 300MB ) 0.25。例如,使用4GB堆,您将拥有949MB的用户内存。在这部分内存中,将哪些东西存储在内存中以及怎么存,完全取决你。Spark也完全不会关心你在那里做什么,以及是否保证容量不超限。如果超限,可能会导致程序OOM错误。
3、Spark Memory
这是由Spark管理的内存池。它的大小可以计算为(“executor-memory” - “保留内存”)* spark.memory.fraction,并使用Spark 1.6.0默认值给我们(“executor-memory” - 300MB)* 0.75。例如,使用4GB executor-memory,这个池的大小将是2847MB。
这个整个池分为2个区域 -Storage Memory(存储内存) 和Execution Memory(执行内存) ,它们之间的边界由spark.memory.storageFraction参数设置,默认为0.5。
UnifiedMemoryManager内存管理方案的优点是,该边界不是静态的。在某部分内存有压力的情况下,边界将被移动,即一个区域将通过从另一个借用空间而增长。稍后会讨论“移动”这个边界,现在让我们来关注这个内存如何被使用。
3.1 Storage Memory
这一块内存用作Spark缓存数据(cache,persist)和序列化数据”unroll”临时空间。另外所有的”broadcast”广播变量都以缓存块存在这里。其实并不需要内存中有足够的空间来存unrolled块- 如果没有足够的内存放下所有的unrolled分区,如果设置的持久化level允许的话,Spark将会把它直接放进磁盘。所有的broadcast变量默认用MEMORY_AND_DISK持久化level缓存。
3.2 Execution Memory
这一块内存是用来存储Spark task执行需要的对象。比如用来存储Map阶段的shuffle临时buffer,此外还用来存储hash合并用到的hash table。当没有足够可用内存时,这块内存同样可以溢写到磁盘,但是这块内存不能被其他线程(tasks)强行剥夺(该内存相对于Storage Memory更重要。Storage Memory内存如果被剥夺,可以通过重算来获得数据。而 Execution Memory一旦剥夺,可能会引起程序失败)。
Spark很大一部分内存用于storage和execution,即存储和执行任务。execution memory指的是在shuffle,join,sorts,aggregation操作中使用的内存,storage memory指的是用于整个集群缓存数据和传播中间数据的内存。spark中storage和execution分享一个统一的区域(M),当没有execution memory被使用,storage可以占用全部可用内存,反之亦然。storage占用execution memory时且有必要时execution可以驱逐storage,直到storage memory使用量低于一个阈值(R)。也就是说R描述了一个小于M的缓存区域,绝不会被驱逐。execution占用storage memory时因为实现复杂所以storage不会驱逐execution。
还有一个问题:
Spark管理页面中Storage Memory是如何计算的呢?参考Spark1.6内存管理(二) 实例讲解。
12. Spark读取Redis
可以自定义source、或者one connection/partition方式或者Spark-Redis方式。老大整理的,我在这里记录下,
1 <?xml version="1.0" encoding="UTF-8"?>
2 <project xmlns="http://maven.apache.org/POM/4.0.0"
3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5 <modelVersion>4.0.0</modelVersion>
6
7 <groupId>com.w3sun.data</groupId>
8 <artifactId>spark-redis</artifactId>
9 <version>1.0-SNAPSHOT</version>
10
11 <properties>
12 <maven.compiler.source>1.8</maven.compiler.source>
13 <maven.compiler.target>1.8</maven.compiler.target>
14 <spark.version>2.2.0</spark.version>
15 <type.safe.version>1.3.0</type.safe.version>
16 <joda.time.version>2.9.4</joda.time.version>
17 <fast.json.version>1.2.10</fast.json.version>
18 <spark.redis.version>0.3.2</spark.redis.version>
19 <encoding>UTF-8</encoding>
20 </properties>
21
22
23 <repositories>
24 <!-- list of RedisLabs repositories -->
25 <repository>
26 <id>SparkPackagesRepo</id>
27 <url>http://dl.bintray.com/spark-packages/maven</url>
28 </repository>
29 </repositories>
30
31 <dependencies>
32 <dependency>
33 <groupId>org.apache.spark</groupId>
34 <artifactId>spark-core_2.11</artifactId>
35 <version>${spark.version}</version>
36 </dependency>
37 <dependency>
38 <groupId>org.apache.spark</groupId>
39 <artifactId>spark-streaming_2.11</artifactId>
40 <version>${spark.version}</version>
41 </dependency>
42
43 <!--Configuration for configuration reader-->
44 <dependency>
45 <groupId>com.typesafe</groupId>
46 <artifactId>config</artifactId>
47 <version>${type.safe.version}</version>
48 </dependency>
49
50 <!--Configuration for Joda time to generate timestamp-->
51 <dependency>
52 <groupId>joda-time</groupId>
53 <artifactId>joda-time</artifactId>
54 <version>${joda.time.version}</version>
55 </dependency>
56
57 <dependency>
58 <groupId>com.alibaba</groupId>
59 <artifactId>fastjson</artifactId>
60 <version>${fast.json.version}</version>
61 </dependency>
62
63 <!--ConfigConfiguration for Spark && Redis RDD generation transformation -->
64 <dependency>
65 <groupId>RedisLabs</groupId>
66 <artifactId>spark-redis</artifactId>
67 <version>${spark.redis.version}</version>
68 </dependency>
69 </dependencies>
70
71 <build>
72 <pluginManagement>
73 <plugins>
74 <plugin>
75 <groupId>net.alchim31.maven</groupId>
76 <artifactId>scala-maven-plugin</artifactId>
77 <version>3.2.2</version>
78 </plugin>
79 <plugin>
80 <groupId>org.apache.maven.plugins</groupId>
81 <artifactId>maven-compiler-plugin</artifactId>
82 <version>3.5.1</version>
83 </plugin>
84 </plugins>
85 </pluginManagement>
86 <plugins>
87 <plugin>
88 <groupId>net.alchim31.maven</groupId>
89 <artifactId>scala-maven-plugin</artifactId>
90 <executions>
91 <execution>
92 <id>scala-compile-first</id>
93 <phase>process-resources</phase>
94 <goals>
95 <goal>add-source</goal>
96 <goal>compile</goal>
97 </goals>
98 </execution>
99 <execution>
100 <id>scala-test-compile</id>
101 <phase>process-test-resources</phase>
102 <goals>
103 <goal>testCompile</goal>
104 </goals>
105 </execution>
106 </executions>
107 </plugin>
108
109 <plugin>
110 <groupId>org.apache.maven.plugins</groupId>
111 <artifactId>maven-compiler-plugin</artifactId>
112 <executions>
113 <execution>
114 <phase>compile</phase>
115 <goals>
116 <goal>compile</goal>
117 </goals>
118 </execution>
119 </executions>
120 </plugin>
121
122 <plugin>
123 <groupId>org.apache.maven.plugins</groupId>
124 <artifactId>maven-shade-plugin</artifactId>
125 <version>2.4.3</version>
126 <executions>
127 <execution>
128 <phase>package</phase>
129 <goals>
130 <goal>shade</goal>
131 </goals>
132 <configuration>
133 <filters>
134 <filter>
135 <artifact>*:*</artifact>
136 <excludes>
137 <exclude>META-INF/*.SF</exclude>
138 <exclude>META-INF/*.DSA</exclude>
139 <exclude>META-INF/*.RSA</exclude>
140 </excludes>
141 </filter>
142 </filters>
143 </configuration>
144 </execution>
145 </executions>
146 </plugin>
147 </plugins>
148 </build>
149 </project>
View Code
1 import java.util
2
3 import com.redislabs.provider.redis.rdd.RedisKeysRDD
4 import com.typesafe.config.{ConfigFactory, ConfigValue}
5 import org.apache.log4j.{Level, Logger}
6 import org.apache.spark.{SparkConf, SparkContext}
7
8
9 object App {
10 //调整日志输出级别
11 Logger.getLogger("org.apache.spark").setLevel(Level.WARN)
12 def main(args: Array[String]): Unit = {
13 val sparkConf = new SparkConf()
14 .setMaster("local[*]")
15 .setAppName(s"${this.getClass.getSimpleName}")
16
17 //设置Redis服务器信息,导入操作Redis隐式转换
18 initRedis(sparkConf)
19 import com.redislabs.provider.redis._
20 val sparkContext = new SparkContext(sparkConf)
21 //根据指定pattern获取所有符合条件的Key所对应的RDD
22 val keysRDD: RedisKeysRDD = sparkContext.fromRedisKeyPattern("1002010011540185*", 4)
23 keysRDD.foreach(println)
24 sparkContext.stop()
25 }
26
27 private def initRedis(sparkConf: SparkConf): Unit = {
28 import scala.collection.JavaConverters._
29 ConfigFactory.load()
30 .entrySet().asScala
31 .foreach {
32 entry: util.Map.Entry[String, ConfigValue] =>
33 sparkConf.set(entry.getKey, entry.getValue.unwrapped().toString)
34 }
35 }
36 }
View Code
Redis相关的配置信息存储在文件中,通过TypeSafe读取:
1 redis.host = xx.xxx.xx.x
2 redis.db = 0
3 redis.port = 6399
4 redis.max.total = 500
5 redis.pool.maxidle = 500
6 redis.pool.testonborrow = true
7 redis.pool.testonreturn = true
8 redis.pool.maxwaitmillis = 1000
9 redis.protocol.timeout = 48000
13. Spark Metric Reporter和Meric API
14. Spark读取文件
spark sql 读取文件,文件中存在 date 和 Date 字段,下面的配置默认 False 解析查询不区分大小写,改为 True 之后就可以区分大小写,不报错了
spark.conf.set("spark.sql.caseSensitive", "true")
15. Spark导入Mysql增加自增主键id
spark DataFrame写入mysql的时候添加一个自增主键id有两种方法,窗口函数和zipWithIndex算子,具体参见:链接
16. --files理解
spark-submit --files可以用来加载外部资源文件,比如加载log4j.properties:
--filesread-first-day-test.properties,log4j.properties
17. 修改日志级别
spark修改默认log4j.properties,比如
spark-submit \
--files log4j-driver.properties,log4j-executor.properties \
--driver-java-options "-Dlog4j.configuration=log4j-driver.properties" \
--conf spark.executor.extraJavaOptions="-Dlog4j.configuration=log4j-executor.properties" \
# Set everything to be logged to the console
log4j.rootCategory=FATAL, console
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.target=System.err
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=%d{yy/MM/dd HH:mm:ss} %p %c{1}: %m%n
# Set the default spark-shell log level to WARN. When running the spark-shell, the
# log level for this class is used to overwrite the root logger's log level, so that
# the user can have different defaults for the shell and regular Spark apps.
log4j.logger.org.apache.spark.repl.Main=FATAL
# Settings to quiet third party logs that are too verbose
log4j.logger.org.spark_project.jetty=FATAL
log4j.logger.org.spark_project.jetty.util.component.AbstractLifeCycle=FATAL
log4j.logger.org.apache.spark.repl.SparkIMain$exprTyper=FATAL
log4j.logger.org.apache.spark.repl.SparkILoop$SparkILoopInterpreter=FATAL
log4j.logger.org.apache.parquet=FATAL
log4j.logger.parquet=FATAL
# SPARK-9183: Settings to avoid annoying messages when looking up nonexistent UDFs in SparkSQL with Hive support
log4j.logger.org.apache.hadoop.hive.metastore.RetryingHMSHandler=FATAL
log4j.logger.org.apache.hadoop.hive.ql.exec.FunctionRegistry=FATAL
View Code
具体参见:
18. 分组TopN
- 第一种方法基于Spark SQL的开窗函数实现,
- 第二种方法基于原生的RDD接口实现。
比如目标:取出每个科目下成绩排名前三的学生,先我们构造一份班级的成绩数据,这份数据有三列组成,第一列是考试科目category,第二列是学生的名字name,第三列是学生的成绩score。如下:
val df = spark.createDataFrame(Seq(
("A", "Tom", 78),
("B", "James", 47),
("A", "Jim", 43),
("C", "James", 89),
("A", "Lee", 93),
("C", "Jim", 65),
("A", "James", 10),
("C", "Lee", 39),
("B", "Tom", 99),
("C", "Tom", 53),
("B", "Lee", 100),
("B", "Jim", 100)
)).toDF("category", "name", "score")
df.show(false)
View Code
第一种方法基于Spark SQL的开窗函数
val N = 3
val window = Window.partitionBy(col("category")).orderBy(col("score").desc)
val top3DF = df.withColumn("topn", row_number().over(window)).where(col("topn") <= N)
top3DF.show(false)
也可以用sql查询的方式
df.createOrReplaceTempView("grade")
val sql = "select category, name, score from (select category, name, score, row_number() over (partition by category order by score desc ) rank from grade) g where g.rank <= 3".stripMargin
val top3DFBySQL = spark.sql(sql)
top3DFBySQL.show(false)
第二种方法基于原生的RDD接口实现
主要有以下三个步骤:
- 将数据按指定的标准分组。比如在本例中需要按“category”分组
- 对每个分组中的元素进行排序,然后取TopN个元素
- 将以上数据Flat展开,恢复为原有格式
// 使用RDD取Top
// step 1: 分组
val groupRDD = df.rdd.map(x => (x.getString(0), (x.getString(1), x.getInt(2)))).groupByKey()
// step 2: 排序并取TopN
val N = 3
val sortedRDD = groupRDD.map(x => {
val rawRows = x._2.toBuffer
val sortedRows = rawRows.sortBy(_._2.asInstanceOf[Integer])
// 取TopN元素
if (sortedRows.size > N) {
sortedRows.remove(0, (sortedRows.length - N))
}
(x._1, sortedRows.toIterator)
})
// step 3: 展开
val flatRDD = sortedRDD.flatMap(x => {
val y = x._2
for (w <- y) yield (x._1, w._1, w._2)
})
flatRDD.toDF("category", "name", "score").show(false)
View Code
+--------+-----+-----+
|category|name |score|
+--------+-----+-----+
|B |Lee |100 |
|B |Jim |100 |
|B |Tom |99 |
|C |James|89 |
|C |Jim |65 |
|C |Tom |53 |
|A |Lee |93 |
|A |Tom |78 |
|A |Jim |43 |
+--------+-----+-----+
19. Spark算子
- na.fill
两个数据表如A,B JOIN的时候,其结果往往会出现NULL值的出现。Spark为此提供了一个高级操作,就是:na.fill的函数可实现对NULL值的填充。