一: RDD
RDD是一个数据结构,是一个对象,是一个抽象类
RDD是一个数据模型,封装了计算逻辑
RDD内部使用了分区
Hbase中的Range为了解决高表查询效率慢 宽表(列组)
Kafka中的Range为了提高消费者组的消费效率
RDD中的分区适合并行计算(封装了最小功能,便于重复使用)
RDD实现方式采用了装饰者设计模式
RDD(Dataset)不保留数据,类似于管道,转移数据,但是不保留数据
1.1:既然有分区数据应该放到哪里
kafka中有个partitioner分区器
hash表类似于分区(哈希定位 路由定位)
1.2:问题引入(哈希表 中的链表极限情况下什么时候会变成红黑二叉树)
(11个:) 首先变成红黑二叉树的前提条件是顺序表长度达到64并且链表长度达到8,因此,极限情况,就往一个哈希位置的链表上一直加数据当加到第9个 的时候,会进行扩容,16->32,到第10个的时候继续扩容32->64,第11个的时候,就会变成红黑二叉树。
此种情况,属于数据倾斜,解决数据倾斜的基本操作是分区;当扩容的不能解决问题的时候,就会改变存储的数据结构。
1.2.1:路由算法(为什么扩容是2倍)
1.2.2:小Demo(封装了最小功能,便于重复使用)
public class TestFlow {
public static void main(String[] args) {
String s = "hello";
// System.out.println(headerUpper(s));
String string = split(transform(s.substring(0, 1)), s.substring(1));
System.out.println(string);
}
public static String split(String prex , String sufix){
return prex + sufix;
}
public static String transform(String s){
return s.toUpperCase();
}
/*public static String headerUpper(String s ){
return s.substring(0,1).toLowerCase() + s.substring(1);
}
public static String headerLower(String s ){
return s.substring(0,1).toLowerCase() + s.substring(1);
}*/
}
二:RDD执行原理(将计算和资源整合到一起才能进行计算)
2.1:
从计算的角度来讲,数据处理过程中需要计算资源(内存 & CPU)和计算模型(逻辑)。执行时,需要将计算资源和计算模型进行协调和整合。
Spark框架在执行时,先申请资源,然后将应用程序的数据处理逻辑分解成一个一个的计算任务。然后将任务发到已经分配资源的计算节点上, 按照指定的计算模型进行数据计算。最后得到计算结果。
-
启动Yarn集群环境
2.Spark通过申请资源创建调度节点和计算节点
3.Spark框架根据需求将计算逻辑根据分区划分成不同的任务
2.1.2: yarn 调度 VS RDD调度
yarn调度 调度的是job ,RDD调度的是Task
学习RDD要写逻辑,底层会自动调度Task,然后送到Executor上进行计算
2.2:什么时候考虑使用tuple,容器的选取?
当数据之间没有关系的时候,可以使用tuple。因为没有关系,也不可能求和等运算。
三:RDD的创建(RDD类似于管道)
3.1:从集合(内存)中创建:将内存的数据作为数据模型的处理
// TODO 使用Spark时,必须先从创建RDD数据模型
// 构建RDD的方式:
// 1. 从集合(内存)中创建:将内存的数据作为数据模型的处理【数据源】
// 2. 从文件(磁盘)中创建:将文件的数据作为数据模型的处理【数据源】
// 3. 从旧的RDD创建新的(框架调用)
// 4. 直接创建新的(框架调用)
从集合(内存)中创建:将内存的数据作为数据模型的处理【数据源】
val seqRDD: RDD[Int] = sc.parallelize(Seq(1, 2, 3, 4)) val seqRDD: RDD[Int] = sc.makeRDD(Seq(1, 2, 3, 4))
def main(args: Array[String]): Unit = {
val spakConf = new SparkConf().setMaster("local").setAppName("WordCount").set("spark.testing.memory","2147480000")
val sc = new SparkContext(spakConf)
//从集合(内存)中创建:将内存的数据作为数据模型的处理【数据源】
val seqRDD: RDD[Int] = sc.parallelize(Seq(1, 2, 3, 4))
seqRDD.collect().foreach(println)
sc.stop()
}
3.1:从磁盘中创建:将内存的数据作为数据模型的处理
val fileRDD : RDD[String] = sc.textFile("data")(不能获取哪个文件中的数据)一行一行的取数据
wholeTextFiles方法返回的数据的泛型为元组,第一个元素为文件路径,第二个元素为文件内容(能获取哪个文件中的数据)缺点:一次性把数据全部拿过来,因此,当文件的能容数据过大的时候不能使用这个方法;
def main(args: Array[String]): Unit = {
val spakConf = new SparkConf().setMaster("local").setAppName("WordCount").set("spark.testing.memory","2147480000")
val sc = new SparkContext(spakConf)
//按行进行读取 从文件(磁盘)中创建:将文件的数据作为数据模型的处理【数据源】
// 从文件中读取数据,文件路径可以使用通配符
// 从文件中读取数据,文件路径可以为文件目录
// val fileRDD: RDD[String] = sc.textFile("datas/word.txt")
//val fileRDD: RDD[String] = sc.textFile("datas")
// val fileRDD: RDD[String] = sc.textFile("datas/word*.txt")
//fileRDD.collect().foreach(println)
// wholeTextFiles方法返回的数据的泛型为元组,第一个元素为文件路径,第二个元素为文件内容
val fileRDD: RDD[(String, String)] = sc.wholeTextFiles("datas")
fileRDD.collect().foreach(println)
sc.stop()
}
四:分区的方式将数据保存
查看源码:
①:val seqRDD: RDD[Int] = sc.makeRDD(Seq(1, 2, 3, 4),2)
(
makeRDD方法可以 传递两个参数
// 第一个参数表示数据源
// 第二个参数表示分区(切片)的数量, 存在默认值def makeRDD[T: ClassTag]( seq: Seq[T], numSlices: Int = defaultParallelism): RDD[T] = withScope { parallelize(seq, numSlices) }
)
②:taskScheduler.defaultParallelism
(
override def defaultParallelism(): Int = backend.defaultParallelism()
)
③:(
override def defaultParallelism(): Int =
scheduler.conf.getInt("spark.default.parallelism", totalCores))
④:(
private[spark] def conf: SparkConf = _conf (总核心数)
)
⑤:(
val spakConf = new SparkConf().setMaster("local[*]").setAppName("WordCount").set("spark.testing.memory","2147480000")
)
4.1:分区的计算
// 数据源为文件时的分区数量
// textFile方法可以传递2个参数
// 第一个参数表示文件路径
// 第二个参数表示【最小】分区数量 : math.min(defaultParallelism, 2)
// Spark中没有读取文件的能力,读取文件采用的就是Hadoop(
hadoopFile(path, classOf[TextInputFormat], classOf[LongWritable], classOf[Text], minPartitions).map(pair => pair._2.toString).setName(path)
)
def defaultMinPartitions: Int = math.min(defaultParallelism, 2)
Hadoop中文件的切分数量的计算:
long totalSize = ; (文件总的字节数)
long goalSize(预计平均每个分区的字节数) = totalSize / (numSplits == 0 ? 1 : numSplits);
10% (也就是hadoop中切片的1.1倍 切片平均之后 剩余的那个数如果占其余每个分区的百分之10以内就不用新的分区)
比如:7个字节 val fileRDD: RDD[String] = sc.textFile("datas/word1.txt",3) 设置3个分区
每个分区2个字节还剩一个字节 1/2 = 50% > 10% 因此实际运行之后为4个分区。
Hadoop 本地环境 32 M 切块,集群128M 企业256M
4.2:内存每个分区放什么数据(每个分区的存放形式)
数据切分的源码形式:
def main(args: Array[String]): Unit = {
val spakConf = new SparkConf().setMaster("local[*]").setAppName("WordCount").set("spark.testing.memory","2147480000")
val sc = new SparkContext(spakConf)
// TODO 使用Spark时,必须先从创建RDD数据模型
// 【1,2】【3,4, 5】
// 【1,2, 3】【4, 5】
// 【1】【2,3】【4,5】
val seqRDD: RDD[Int] = sc.makeRDD(Seq(1, 2, 3, 4, 5), 3)
//分区保存数据
seqRDD.saveAsTextFile("output")
}
val slices = ParallelCollectionRDD.slice(data, numSlices).toArray
[1,2,3,4,5] 3
源码:
case _ => val array = seq.toArray // To prevent O(n^2) operations for List etc positions(array.length, numSlices).map { case (start, end) => array.slice(start, end).toSeq }.toSeq
def positions(length: Long, numSlices: Int): Iterator[(Int, Int)] = { (0 until numSlices).iterator.map { i => val start = ((i * length) / numSlices).toInt val end = (((i + 1) * length) / numSlices).toInt (start, end) } }
0 until numSlices 也就是 [0,3)
0 => [0,1)=>1
1 => [1,3 ) =>2 ,3
2 => [3,5) => 4,5
positions(array.length, numSlices).map { case (start, end) => array.slice(start, end).toSeq }.toSeq
切分数组
override def getPartitions: Array[Partition] = { val slices = ParallelCollectionRDD.slice(data, numSlices).toArray slices.indices.map(i => new ParallelCollectionPartition(id, i, slices(i))).toArray }
4.3:分区数据存储原理-磁盘
def main(args: Array[String]): Unit = {
val spakConf = new SparkConf().setMaster("local[*]").setAppName("WordCount").set("spark.testing.memory","2147480000")
val sc = new SparkContext(spakConf)
val fileRDD: RDD[String] = sc.textFile("datas/word1.txt",2)
fileRDD.saveAsTextFile("output")
sc.stop()
}
TODO Hadoop中分区的计算规则和分区数据存储规则不一样。
计算分区是按照字节计算7 => 3 (按照偏移量 预计的偏移量为:)
[0, 3]
[3, 6]
[6, 7]读取数据不能按照字节算,按行算,一读就是一行
读取数据是按照偏移量计算
相同的偏移量不会重复读取
五:算子
区分方法如果返回是一个RDD,则是转换算子,如果转换的是某个结果,则是行动算子。
5.1:算子转换
RDD根据数据处理方式的不同将算子整体上分为单Value类型、双Value类型(比如scala中的zip和zipwithIndex)和Key-Value类型
5.1:map()算子
def main(args: Array[String]): Unit = {
val spakConf = new SparkConf().setMaster("local[*]").setAppName("WordCount").set("spark.testing.memory","2147480000")
val sc = new SparkContext(spakConf)
//算子 转换
// 所谓的转换算子,其实就是通过调用RDD对象的方法,将旧的RDD转换为新的RDD
// 通过转换,将多个功能组合在一起.
// TODO map -> 转换,映射 (Key => Value)
// rdd的map算子可以将数据集中的每一条数据进行转换后返回
val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4))
// rdd的map算子可以将数据集中的每一条数据进行转换后返回
// map算子需要传递一个参数,参数类型为 : Int => U(不确定)
def mapFunction(num : Int) : Int = {
num * 2
}
rdd.map(mapFunction)
val newRDD: RDD[Int] = rdd.map(_ * 2)
newRDD.collect().foreach(println)
sc.stop()
}
}
函数签名 def map[U: ClassTag](f: T => U): RDD[U]
这里的转换可以是类型的转换,也可以是值的转换。
flatmap()与map()的主要区别:
flatmap()旨在把一条数据的整体,拆成个体,所有的数据还保留;
map()旨在把一条长数据转成短数据;
5.2:map()算子 分区
def main(args: Array[String]): Unit = {
val spakConf = new SparkConf().setMaster("local[*]").setAppName("WordCount").set("spark.testing.memory","2147480000")
val sc = new SparkContext(spakConf)
val dataRDD: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4),2)
val resultRDD: RDD[Int] = dataRDD.map(
num => {
println("*******" + num)
num * 2
}
)
resultRDD.collect().foreach(println)
sc.stop()
}
def main(args: Array[String]): Unit = {
val spakConf = new SparkConf().setMaster("local[*]").setAppName("WordCount").set("spark.testing.memory","2147480000")
val sc = new SparkContext(spakConf)
val dataRDD: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4),2)
val resultRDD: RDD[Int] = dataRDD.map(
num => {
println("*******" + num)
num * 2
}
)
val lastRDD: RDD[Int] = resultRDD.map(
num => {
println("$$$$$$$$" + num)
num * 2
}
)
lastRDD.collect()
sc.stop()
}
5.3:性能比较
public class TestString {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
for (String s : list) {
test(s);
}
}
public static void test(String s){
System.out.println("获取数据库连接");
System.out.println(s);
System.out.println("关闭数据库连接");
}
public static void testList(List<String> list){
System.out.println("获取数据库连接");
for (String s : list) {
test(s);
}
System.out.println("关闭数据库连接");
}
}
一个栈空间默认是1M,一个栈中有许多栈帧,如果一个栈帧1K, 那么改栈最多能放1024个
栈帧,如果我们设置栈为5M=》5*1024
一个线程对应一个栈 如果500个线程,每个线程10M则会给栈分配5G,内存被占的越多,可用的就越少,因此服务器被访问的太多,性能就会下降,资源不足等等。栈
栈内存溢出,如果栈大小10M,来一个线程new Thread() 线程多了没办法创建了,此时是资源不够了,
栈内存溢出,一般与线程相关 VS 栈溢出 一般与压栈相关
5.3:算子转换 mappartitions()
Map算子是分区内一个数据一个数据的执行,类似于串行操作()。
一个一个操作数据
而mapPartitions算子是以分区为单位进行批处理操作。
功能的角度
Map算子主要目的将数据源中的数据进行转换和改变。但是不会减少或增多数据。
MapPartitions算子需要传递一个迭代器,返回一个迭代器,没有要求的元素的个数保持不变,所以可以增加或减少数据
public static void main(String[] args) {
User user = new User();
ArrayList<User> userList = new ArrayList<User>();
userList.add(user);
ArrayList<User> userList1 = (ArrayList<User>)userList.clone();
User user1 = userList1.get(0);
user1.name = "lisi";
System.out.println(userList);
System.out.println(userList1);
}
}
class User {
public String name;
@Override
public String toString() {
return "User["+name+"]";
}
}
案例一 mappartitions()(求每个分区的最大值)
def main(args: Array[String]): Unit = {
val spakConf = new SparkConf().setMaster("local").setAppName("WordCount").set("spark.testing.memory","2147480000")
val sc = new SparkContext(spakConf)
val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4), 2)
// mapPartitions算子需要传递一个参数,类型为函数类型: Iterator => Iterator
rdd.mapPartitions(
list =>{
//包装成一个迭代器
List(list.max).iterator
}
)
sc.stop()
}
案例二 mapPartitionsWithIndex()(只保留某一个分区)
mapPartitionsWithIndex算子需要传递参数,参数的一个值为分区索引,从0开始
def main(args: Array[String]): Unit = {
val spakConf = new SparkConf().setMaster("local").setAppName("WordCount").set("spark.testing.memory","2147480000")
val sc = new SparkContext(spakConf)
val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4), 2)
// mapPartitions算子需要传递一个参数,类型为函数类型: Iterator => Iterator
rdd.mapPartitionsWithIndex(
(index,list) => {
if (index == 1){
list
}else{
Nil.iterator
}
}
)
sc.stop()
}
案例三 groupBy()
def main(args: Array[String]): Unit = {
val spakConf = new SparkConf().setMaster("local").setAppName("WordCount").set("spark.testing.memory","2147480000")
val sc = new SparkContext(spakConf)
val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4), 2)
// groupBy算子需要传递一个参数,参数类型为函数类型:Int => K
/*val groupRDD: RDD[(String, Iterable[Int])] = rdd.groupBy(
num => {
if (num % 2 == 0) {
"偶数"
} else {
"奇数"
}
}
)*/
/*val resRDD: RDD[(Int, Iterable[Int])] = rdd.groupBy(
num => {
if (num % 2 == 0) {
0
} else {
1
}
}
)*/
/*val groupRDD: RDD[(Int, Iterable[Int])] = rdd.groupBy(
num => num % 2
)*/
val groupRDD: RDD[(Int, Iterable[Int])] = rdd.groupBy(_ % 2)
groupRDD.collect().foreach(println)
sc.stop()
}
案例三 Shuffle
所有包含shuffle的算子都有改变分区的能力,默认是不改变。