scala语言网上自行学习
spark生态圈
其实Hadoop生态圈差不多,只不过Hadoop计算是交给yarn进行。而sql语句是有hive来完成,storm来完成流式计算。而spark把这些东西都集成到一起,用起来很方便。
一、什么是Spark?特点
(1)spark就是为大规模的数据处理过程的一个统一的数据分析引擎。
(2)特点:
1、快:基于内存,同时提供容错机制
2、兼容HDFS、兼容Yarn
3、易用:支持多种编程语言:Scala、Java、Python、R
4、部署在不同平台上:Standalone、Yarn、容器Docker(k8s)
5、通用性:完备的生态圈系统
二、Spark的体系架构:主从架构
spark也是主从架构,与其他主从架构一样,也是主节点管理从节点的信息。master节点主要负责接收客户端的请求,协调调度集群的资源,与我们前面所说的yarn差不多。worker则负责执行任务,当然,这些任务是交给executor去工作的。
客户端提交任务主要通过两种方式进行的:
1、spark-submit:提交的是我们写的jar包
2、spark-shell :直接连接master节点,当这个shell不关闭,资源就不释放。
三、搭建Spark的环境
(1)伪分布
1、解压
tar -zxvf spark-3.0.0-bin-hadoop3.2.tgz -C /usr/local/
由于spark的命令与Hadoop命令相似,因此spark与Hadoop的环境变量只能配置一个,我们已经配置过Hadoop环境变量,因此我们不在配置spark环境变量。我们所有的操作都在spark的home目录下执行。
2、核心的配置文件:conf/spark-env.sh
生成spark-env.sh
mv spark-env.sh.template spark-env.sh
进行配置 vi spark-env.sh
export JAVA_HOME=/usr/local/java/jdk1.8.0_251
export SPARK_MASTER_HOST=bigdata111
export SPARK_MASTER_PORT=7077
3、启动sbin/start-all.sh
查看web页面:Web Console:http://192.168.92.111:8080/
(2)全分布
部署在bigdata111(master)、bigdata222(work)、bigdata333(work)主机上
1、配置spark-env.sh
export JAVA_HOME=/usr/local/java/jdk1.8.0_251
export SPARK_MASTER_HOST=bigdata222
export SPARK_MASTER_PORT=7077
2、配置slaves
生成slaves
mv slaves.template slaves
配置slaves : vi slaves
bigdata444
bigdata333
3、复制到bigdata333、bigdata444
scp -r spark/ root@bigdata333:/usr/local/
scp -r spark/ root@bigdata444:/usr/local/
注意:在部署全分布的时候,进行复制到其他节点的时候,从节点目录必须保持和主节点目录保持一致。
4、在master节点启动sbin/start-all.sh
(3)HA模式
1、基于目录的单点恢复:开发测试、本质依然是一个单点,不是真正的HA。就是把元信息持久化本地磁盘上面,当master宕机时,重启master就可以恢复。这种方式我们不常用。
配置参数 | 参考值 |
spark.deploy.recoveryMode | 设置为FILESYSTEM开启单点恢复功能,默认值:NONE |
spark.deploy.recoveryDirectory | Spark 保存恢复状态的目录 |
- 配置spark-env.sh
export JAVA_HOME=/usr/local/java/jdk1.8.0_251
export SPARK_MASTER_HOST=bigdata222
export SPARK_MASTER_PORT=7077
export SPARK_DAEMON_JAVA_OPTS="-Dspark.deploy.recoveryMode=FILESYSTEM -Dspark.deploy.recoveryDirectory=/usr/local/spark/recovery"
- 配置slaves
bigdata444
bigdata333
- 复制到bigdata333、bigdata444
scp -r spark/ root@bigdata333:/usr/local/
scp -r spark/ root@bigdata444:/usr/local/
2、基于ZooKeeper:生产。这时master节点就交给zookeeper进行管理,因此我们不用去指定哪台主机是master,当master宕机时,zookeeper会自动选举出新的master,在master切换时,只会影响新的job的提交,不会影响正在进行job,zookeeper会去指定,当客户端提交任务时,也是连接zookeeper地址。
配置参数 | 参考值 |
spark.deploy.recoveryMode | 设置为ZOOKEEPER 开启单点恢复功能,默认值:NONE |
spark.deploy.zookeeper.url | Zookeeper集群的地址 |
spark.deploy.zookeeper.dir | spark信息在ZK中的保存目录,默认:/spark |
-配置spark-env.sh
注意:在这里不要指定master地址与端口,只需要配置zookeeper地址就行
export JAVA_HOME=/usr/local/java/jdk1.8.0_251
export SPARK_DAEMON_JAVA_OPTS="-Dspark.deploy.recoveryMode=ZOOKEEPER -Dspark.deploy.zookeeper.url=bigdata222:2181,bigdata333:2181,bigdata444:2181 -Dspark.deploy.zookeeper.dir=/spark"
- 配置slaves
配置在哪些节点启动work
bigdata444
bigdata333
- 复制到其他节点
scp -r spark/ root@bigdata333:/usr/local/
scp -r spark/ root@bigdata444:/usr/local/
- 启动
在任意一个节点的spark的home目录下,启动sbin/start-all.sh命令,首先会在这个节点启动一个主节点master,然后按照slaves文件启动从节点。
当然我们也可以在其他节点的spark的home目录下,启动sbin/start-master.sh命令,再启动一个主节点master,各个节点的元信息都是由zookeeper尽心维护。
当第一个启动的master宕机,后补master节点补上。
四、执行Spark的任务:客户端工具
(1)spark-submit:提交Spark的jar包
bin/spark-submit --master spark://masterIp:7077 --class main所在类 jar包目录
(2)spark-shell:类似Scala REPL命令行
1、本地模式local:把程序运行在本地的环境(相当于在开发工具中直接运行)
bin/spark-shell
日志:
Spark context available as 'sc' (master = local[*], app id = local-1603111126829).
Spark session available as 'spark'.
2、集群模式cluster
bin/spark-shell --master spark://bigdata111:7077
日志:
Spark context available as 'sc' (master = spark://bigdata111:7077, app id = app-20201019204212-0001).
Spark session available as 'spark'.
demo:WordCount程序
bin/spark-shell --master spark://bigdata111:7077
sc.textFile("hdfs://bigdata111:9000/input/data.txt").flatMap(_.split(" ")).map((_,1)).reduceByKey(_+_).collect
五、开发自己的WordCount程序
(1)开发Java版本
1、把SPARK_HOME/jar包全部添加到项目中
2、创建MyWordCountDemo 类
public class MyWordCountDemo {
public static void main(String[] args) {
// Javaspark传输数据也是基于元组tuple进行传输数据。而tuple是提前定义好的一个数据类型。
SparkConf conf = new SparkConf().setAppName("MyWordCountDemo").setMaster("local");
JavaSparkContext javaspark = new JavaSparkContext(conf);
JavaRDD<String> data = javaspark.textFile("word.txt");
// 进行切分单词压平操作
JavaRDD<String> words = data.flatMap(new FlatMapFunction<String, String>() {
@Override
public Iterator<String> call(String s) throws Exception {
return Arrays.asList(s.split(" ")).iterator();
}
});
// 单词返回(word , 1)
// 输入 输出 输出
JavaPairRDD<String, Integer> wordpair = words.mapToPair(new PairFunction<String, String, Integer>() {
@Override // 输出 输出 输入
public Tuple2<String, Integer> call(String word) throws Exception {
return new Tuple2<String, Integer>(word , 1);
}
});
// 进行按字段分组,进行计数
JavaPairRDD<String, Integer> wordcount = wordpair.reduceByKey(new Function2<Integer, Integer, Integer>() {
@Override
public Integer call(Integer integer, Integer integer2) throws Exception {
return integer + integer2;
}
});
// 进行输出
List<Tuple2<String, Integer>> collect = wordcount.collect();
for (Tuple2<String, Integer> tuple2 : collect) {
System.out.println(tuple2._1 + " "+tuple2._2);
}
//停止SparkContext
javaspark.stop();
}
}
(2)开发scala版本
和Java相似,只不过代码量比Java少了很多。
(3)分析WordCount数据处理的过程
sc.textFile(“hdfs://bigdata111:9000/input/data.txt”).flatMap(.split(" ")).map((,1)).reduceByKey(+).collect
过程和MapReduce相似。
六、RDD:弹性分布式数据集(当成是一个集合,由分区组成)
1、什么是RDD?特性、创建RDD
通俗的说,rdd可以就是一个分区,并且生成的分区分布在不同的worker上。
(1)弹性分布式数据集(当成是一个集合,由分区组成);每个分区运行在不同的Worker的从节点上
(2)RDD的特性:查看源码RDD.scala
- A list of partitions
一组分区 - A function for computing each split(分片)
函数(算子)执行计算 - A list of dependencies on other RDDs
RDD彼此具有依赖关系 - Optionally, a Partitioner for key-value RDDs (e.g. to say that the RDD is hash-partitioned)
自定义分区 - Optionally, a list of preferred locations to compute each split on (e.g. block locations for an HDFS file)
2、算子:函数、方法
(1)Transformation:延时计算
转换 | 含义 |
map | 返回一个新的rdd ,并且rdd的每个元素进过func函数转换后组成 |
filter | 返回新的rdd,每个元素经过func计算后返回true的元素组成新的rdd |
flatmap | 就是flat与map的组合,将多个集合合并成一个 |
mappartitions | 类似于map,但是,是作用于每个分区,获得的是整个分区的元素,而map是获得的是rdd每个元素 |
mappartitionswithindex | 和mappartitions一样,只不过带有每个分区的索引值 |
sample(withReplacement,fraction,seed) | 根据fraction指定的比例对数据进行采用,可以女选择是否使用随机数进行替换,seed是指定随机数的种子 |
union | 两个rdd的并集,并返回新的rdd |
intersection | 两个rdd的交集,并返回新的rdd |
distinct | 去重复,并返回的新的rdd |
groupbykey | 在一个key-value的rdd上调用,返回一个(k,iterarot[v] )rdd |
reducebykey | 类似 groupbykey,只不过value值直接进行计算 |
sortbykey | 进行排序 |
sortby | 也是进行排序 |
jion | 有两个rdd,返回这两个rdd的相同的key对应的所有元素,并对value尽心聚合 |
Cartesian | 笛卡尔积 |
注意:在执行上面的算子的时候,并不会执行,只有执行下面的action算子的时候才会触发执行。
(2)Action:触发计算
动作 | 含义 |
reduce | 聚集所有rdd中所有的元素 ,将rdd中所有元素进行累加 |
collect | 以数组的形式返回数据集的所有元素 |
count | 返回rdd中的元素个数 |
first | 返回rdd中的第一个元素个数 |
take(n) | 返回前n个元素组成的数组 |
takesample | 随机采集几个元素进行返回 |
saveastextfile(path) | 将数据集的元素以textfile的形式保存到hdfs文件系统或其他支持的文件系统,对于每个元素,spark将会调用tostring方法,将它转换为文件中的文本 |
saveassequencefile(path) | 将数据集以Hadoop sequencefile的格式保存到指定的目录,可以使hdfs或其他Hadoop的支持文件系统 |
saveasobjectfile | 以对象的形式进行保存 |
foreach | 在数据集的每个元素上,运行函数进行更新 |
3、RDD的功能
(1)RDD依赖关系:根据依赖关系,可以划分任务的阶段Stage -----> 划分任务阶段的标准:宽依赖
- 窄依赖:父RDD的每一分区,最多只被一个子RDD的分区使用
- 宽依赖:父RDD的每一分区,被多个子RDD的分区使用
(2)RDD的缓存机制:提高效率
默认缓存在内存中
参数设置(源码):object StorageLevel
val NONE = new StorageLevel(false, false, false, false)
val DISK_ONLY = new StorageLevel(true, false, false, false)
val DISK_ONLY_2 = new StorageLevel(true, false, false, false, 2)
val MEMORY_ONLY = new StorageLevel(false, true, false, true)
val MEMORY_ONLY_2 = new StorageLevel(false, true, false, true, 2)
val MEMORY_ONLY_SER = new StorageLevel(false, true, false, false)
val MEMORY_ONLY_SER_2 = new StorageLevel(false, true, false, false, 2)
val MEMORY_AND_DISK = new StorageLevel(true, true, false, true)
val MEMORY_AND_DISK_2 = new StorageLevel(true, true, false, true, 2)
val MEMORY_AND_DISK_SER = new StorageLevel(true, true, false, false)
val MEMORY_AND_DISK_SER_2 = new StorageLevel(true, true, false, false, 2)
val OFF_HEAP = new StorageLevel(true, true, true, false, 1)
查看RDD的源码
/**
* Persist this RDD with the default storage level (`MEMORY_ONLY`).
*/
def persist(): this.type = persist(StorageLevel.MEMORY_ONLY)
/**
* Persist this RDD with the default storage level (`MEMORY_ONLY`).
*/
def cache(): this.type = persist()
Demo例子:使用RDD读取大的文本
val rdd1 = sc.textFile("hdfs://bigdata111:9000/sh/sales")
rdd1.count ----> 第一次执行,没有缓存
rdd1.cache ---->标识该RDD能够被缓存
rdd1.count ----> 第二次执行,(1)执行计算 (2)将结果缓存
rdd1.count ----> 第三次执行,不执行计算,直接从缓存中取出结果
== Java代码同上 ==
(3)RDD的容错机制:检查点checkpoint
复习:关于检查点
- HDFS的检查点:触发日志合并
- Oracle的检查点:会内存中的脏数据写到数据文件上
- Spark和Flink中的检查点:都是容错机制、本质就是一个目录(本地、HDFS)
设置Spark的检查点,检查点与linesage(血统)有关:就是任务执行的过程(生命周期)
- 本地目录:用于测试,用于开发和测试,如果使用spark shell测试,需要运行在本地模式上
- hdfs目录:用于生产
scala> sc.setCheckpointDir("hdfs://bigdata111:9000/spark/ckpt/1021")
scala> val myrdd = sc.textFile("hdfs://bigdata111:9000/input/data.txt").flatMap(_.split(" ")).map((_,1)).reduceByKey(_+_)
myrdd: org.apache.spark.rdd.RDD[(String, Int)] = ShuffledRDD[23] at reduceByKey at <console>:24
#表明myrdd是可以进行检查点容错的
scala> myrdd.checkpoint
scala> myrdd.collect
== Java代码同上 ==
4、RDD的高级算子
(1)都是针对每一个分区进行操作
- mapPartitions(func) map是有返回值
- foreachPartition foreach没有返回值
- mapPartitionsWithIndex(func) 重点
API的文档
mapPartitionsWithIndex[U: ClassTag](f: (Int, Iterator[T]) => Iterator[U],
preservesPartitioning: Boolean,
isOrderSensitive: Boolean)
f的参数类型:匿名函数
(Int, Iterator[T]) => Iterator[U]
Int:表示分区号,从0开始
Iterator[T]:分区中的元素
Iterator[U]:操作完成后的结果
例子:操作分区中的元素,返回:分区号+元素
创建一个RDD,有两个分区
val rdd1 = sc.parallelize(Array(1,2,3,4,5,6),2)
创建一个函数,作为f的参数值
def func1(index:Int, iter:Iterator[Int]):Iterator[String] ={
iter.toList.map( x => "[PartID:" + index + ", Value:" + x + "]" ).iterator
}
调用
rdd1.mapPartitionsWithIndex(func1).collect
结果
分区0:[PartID:0, Value:1], [PartID:0, Value:2], [PartID:0, Value:3],
分区1:[PartID:1, Value:4], [PartID:1, Value:5], [PartID:1, Value:6]
[PartID:0, Value:1], [PartID:0, Value:2], [PartID:0, Value:3],
[PartID:1, Value:4], [PartID:1, Value:5], [PartID:1, Value:6], [PartID:1, Value:7]
(2)aggregate、aggregateByKey 聚合
含义:先局部(分区)进行聚合,再全局(RDD)进行聚合
Demo:
- aggregate的处理过程如下:
Java代码实现:
public static void main(String[] args) {
Logger.getLogger("org.apache.spark").setLevel(Level.ERROR);
Logger.getLogger("org.eclipse.jetty.server").setLevel(Level.OFF);
SparkConf conf = new SparkConf().setAppName("MyWordCountDemo").setMaster("local");
JavaSparkContext sc = new JavaSparkContext(conf);
Integer[] a = new Integer[]{8,2,9,1,10};
Integer[] b = new Integer[]{5,6,4,2,3};
List<Integer> list1 = Arrays.asList(a);
List<Integer> list2 = Arrays.asList(b);
JavaRDD<Integer> rdd1 = sc.parallelize(list1);
JavaRDD<Integer> rdd2 = sc.parallelize(list2);
JavaRDD<Integer> rdd3 = rdd1.union(rdd2);
Integer aggregate = rdd3.aggregate(0, new Function2<Integer, Integer, Integer>() {
@Override
public Integer call(Integer integer, Integer integer2) throws Exception {
return integer >= integer2 ? integer:integer2;
}
}, new Function2<Integer, Integer, Integer>() {
@Override
public Integer call(Integer integer, Integer integer2) throws Exception {
return integer + integer2;
}
});
System.out.println(aggregate);
sc.stop();
}
- aggregateByKey 的处理过程如下:
必须讲JavaADD转化为JavaPairADD
(3)coalesce与repartition
区别:
coalesce ----> 执行shuffle(默认:false) 不会真正的进行分区
repartition—> 执行shuffle(默认:true)
val rdd1 = sc.parallelize(Array(1,2,3,4,5,6),2)
val rdd2 = rdd1.coalesce(3) -----> 还是2个分区
val rdd3 = rdd1.repartition(3) -----> 是3个分区
val rdd4 = rdd1.coalesce(3,true) -----> 和repartition一样
七、编程案例:Spark Core离线计算
1、求网站的PV(Page View):通过分析访问日志
2、创建自定义分区
主函数
public class WordCount {
public static void main(String[] args) {
Logger.getLogger("org.apache.spark").setLevel(Level.ERROR);
Logger.getLogger("org.eclipse.jetty.server").setLevel(Level.OFF);
SparkConf conf = new SparkConf().setAppName("MyWordCountDemo").setMaster("local");
JavaSparkContext sc = new JavaSparkContext(conf);
// "192.168.88.1 - - [30/Jul/2017:12:54:40 +0800] \"GET /cle.jsp MyDemoWeb/oracle.jsp HTTP/cle.jsp1.1\" 200 242" ;
JavaRDD<Tuple2<String, String>> jspList = sc.textFile("src/resources/localhost_access_log.2017-07-30.txt").map(line -> {
String stName;
Pattern pattern = Pattern.compile("\\w*.jsp");
Matcher matcher = pattern.matcher(line);
if (matcher.find()) {
stName = matcher.group();
} else {
stName = null;
}
return new Tuple2<String, String>(stName, line);
});
//得到所有的JSP的名字(去重)
List<String> jspname = jspList.map(x -> x._1).distinct().collect();
//创建分区规则
MyPartitioner myPartitioner = new MyPartitioner(jspname);
//创建分区------报错,Java好像没有partitionBy这个函数
JavaRDD<Tuple2<String, String>> partitionList = jspList.partitionBy(myPartitioner);
//输出
partitionList.saveAsTextFile("d:\\download\\partitionout");
sc.stop();
}
}
创建MyPartitioner类
import org.apache.spark.Partitioner;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class MyPartitioner extends Partitioner {
Map<String , Integer> partitionMap = new HashMap<String, Integer>();
public MyPartitioner(List<String> jspname) {
int partitionID =0;
for (String s : jspname) {
partitionMap.put(s,partitionID++);
}
}
@Override
public int getPartition(Object key) {
return partitionMap.getOrDefault(key,0);
}
//分区数量
@Override
public int numPartitions() {
return partitionMap.size();
}
}
3、访问数据库Oracle:一定要针对分区进行操作
基于JDBC,略