一 多线程安全问题
1 出现该问题的原因
一个 Executor 是一个进程 ,一个进程中可以同时运行多个Task ,如果多个 Task 使用了共享的变量 ,就会出现线程不安全的问题 .
2 案例
2.1 需求 : 使用 spark 将日期字符串转换成 long 类型时间戳
2.2 样例数据
2019-11-06 15:59:50
2019-11-06 15:59:51
2019-11-06 15:59:52
2019-11-06 15:59:53
2019-11-06 15:59:54
2019-11-06 15:59:55
2019-11-06 15:59:56
2019-11-06 15:59:57
2019-11-06 15:59:58
2.3 出现多线程安全的示范案例(多个线程访问共享数据)
1) 多个线程访问共享数据 ,这里的多个线程是指每一行数据 ,当一行数据未处理完成未格式化完全时 ,就被下一行数据抢走了
2) 比如说,第一个线程进来格式化到 "yyyy-MM-dd HH ,然后就被下一个线程抢走了 ;
下一个线程从 :mm:ss" 开始格式化 ,将 : 当成年来格式化 , 这个时候就会出现 java.lang.NumberFormatException: For input string: "" 异常
2.3.1 自定义一个日期格式化实例
object DateUtils02 { ---单例对象
--定义一个日期格式化实例 ,将数据拿过来,一个字节一个字节的进行格式化
private val sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
def parse(line: String): Long = { 一个线程进来使用这个方法,下一个线程进来还是使用这个方法
--将传的参数格式化并获取值
sdf.parse(line).getTime
}
}
2.3.1 逻辑代码实现
object ThreadSafe02 {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName(this.getClass.getSimpleName).setMaster("local[*]")
--创建 sparkcontext
val sc = new SparkContext(conf)
--创建原始化RDD
val lines: RDD[String] = sc.textFile(args(0))
val res = lines.map(line => {
--在Executor端初始化一个实例object类 ,该object不需要实现序列化接口
---单例对象,以后多个Task都会调用这个单例对象 ,产生了多个线程访问共享数据现象
val ts: Long = DateUtils02.parse(line)
(line, ts)
}).collect()
println(res.toBuffer)
--有时候该程序运行正常 ,有时候会出现线程安全问题就会抛出异常
}
}
2.4 代码实现(最佳实现方案)
2.4.1 自定义一个日期格式转化的class类
class DateUtilsClass01 extends Serializable {
private val dataFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
def parse(dt:String):Long ={
dataFormat.parse(dt).getTime
}
}
2.4.2 逻辑代码实现
object ThreadSafe01 {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName(this.getClass.getSimpleName).setMaster("local[*]")
val sc = new SparkContext(conf)
val lines: RDD[String] = sc.textFile("C:\\Users\\123\\Desktop\\date.txt")
--一个分区一个分区的数据进行处理
val result: Array[(String, Long)] = lines.mapPartitions(it => {
--一个分区一个class类的实例
val dateUtils = new DateUtilsClass01
it.map(dt => {
val ts = dateUtils.parse(dt)
(dt, ts)
})
}).collect()
println(result.toBuffer)
/**
* 结果为 :ArrayBuffer((2019-11-06 15:59:50,1573027190000),
* (2019-11-06 15:59:51,1573027191000),
* (2019-11-06 15:59:52,1573027192000),
* .....
*/
}
}
二 RDD的错误示范
1 RDD嵌套RDD ,使得被嵌套的RDD没有SparkContext ,运行时抛出"This RDD lacks a SparkContext.(这个RDD缺少SparkContext)" 这个异常
2 RDD调用算子,其括号里面的函数是在Executor端执行的(被嵌套的RDD就得在Executor端才能执行 ) ,但Executor端是没有 SparkContext的 ,如果有RDD被嵌套在里面 ,RDD没有 SparkContext 的话是无法执行的,那么就会抛出异常 ,查询无法正常运行 .
object ErrorStyle {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName("ErrorStyle").setMaster("local[*]")
--创建sparkcontext ,简称sc ,可以认为就是Driver
--负责将用户编写的代码转成Task ,然后调度到Executor中执行
val sc = new SparkContext(conf)
--创建原始的RDD
val rdd1: RDD[Int] = sc.parallelize(List(1, 2, 3, 4, 5, 6))
val rdd2: RDD[String] = sc.parallelize(List("A", "B", "C"))
---发生这种RDD嵌套RDD的情况
val rdd3: RDD[RDD[(Int, String)]] = rdd1.map(i => {
--括号里面的函数是在Executor端执行的 ,也就是说这里的rdd2是在executor端执行
--但是executor端没有 SparkContext ,没有Sparkcontext的话RDD是无法执行的
rdd2.map(s => {
(i, s)
})
})
println(rdd3.collect().toBuffer)
--这个程序会报"This RDD lacks a SparkContext."--即RDD缺失SparkContext
}
}
三 累加器
累加器是用来统计数据处理的条数 ,在Task局部聚合 ,再到Driver端sum
在Driver端定义累加器 ,有以下三种形式 :
1) collectionAccumulator : 将异常数据保存到集合里面
2) longAccumulator("累加器的名字")
3) DoubleAccumulator 无参
1) 累加器相当于Task 中的一个变量 ,一个状态 ;accumulator 只触发一次Action时 ,只有一个 job , 在Action这个程序孩子运行时 ,可以在web端看到数据运行完成情况(比如说1亿条数据需要10min才能处理完 ,在第5min的时候 ,就可以看到已经处理了多少条数据了 ,因为处理了一条数据就累加1 ,所以可以看到数据处理的完成情况/完成状态) ,可以通过网络端显示出action的进程状态
2) 没有触发Action 时没有数据 ;
3) foreachPartitionAsync : 异步Action方法 ,把Task给Executor端继续执行 ,Driver端也能继续往下执行
4) accumulator 触发两次 action 时 ,第一次 action 时累加一次得到结果 ,第二次action时还是用原来的accumulator ,又读了一次数据 ,产生两个job ,第二次 action 结果是累加的
5) 为了解决累加两次的问题 ,可以将第一次action结果cache到内存中 ,即使第二次执行action ,也是直接/优先到内存中获取数据 ,就不会出现有重复读取数据从头再算一次导致结果累加 .
1 使用spark的普通算子(count ,sum ,reduce)进行数据的累加/聚合(使用这种方式计数累加会触发多次action)
1.1 案例一 :
1) 对RDD中的数据过滤 ,然后计算出符合过滤条件的数据的总条数
2) 对原始RDD里面的数据进行累加
object FilterTest01 {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName("FilterTest01").setMaster("local[*]")
val sc = new SparkContext(conf)
--创建原始的RDD
val rdd1: RDD[Int] = sc.parallelize(List(1, 2, 3, 4, 5))
val rdd2: RDD[Int] = rdd1.filter(int => int % 2 == 0)
println(rdd2.count()) -- 2 模除2等于0的数据只有2条
val sum = rdd1.reduce(_ + _)
println(sum) -- 15 rdd1中的数据进行累加
println(rdd1.sum()) -- 15.0 sum的初始值是0.0 ,先局部聚合使用一次初始化值 ,再全局聚合又使用一次初始值 ,返回浮点数
}
}
1.2 案例二 :
1) 解析json数据 ,统计无法解析的数据的条数
2) 统计无法解析的数据占总条数的比率
--定义一个样例类(类似javabean) ,里面的属性名字与json数据的字段名字一样
case class OrderBeanClass(
cid : String,
oid : String,
money : Double,
longitude : Double,
latitude : Double
)
--记录json数据解析错误或者不能解析的数据共有多少条
object FilterTest02 {
private val logger: Logger = LoggerFactory.getLogger(this.getClass)
def main(args: Array[String]): Unit = {
--创建sparkconf
val conf = new SparkConf().setAppName(this.getClass.getSimpleName).setMaster("local[*]")
---创建 sparkcontext ,简称sc
val sc = new SparkContext(conf)
--创建原始RDD
val lines = sc.textFile(args(0))
--解析数据
val beanRDD: RDD[OrderBeanClass] = lines.map(line => {
var beanClass: OrderBeanClass = null
try {
--解析json数据,将数据封装到 orderbean 对象里面
beanClass = JSON.parseObject(line, classOf[OrderBeanClass])
} catch {
case e: JSONException => {
--记录错误的数据
logger.error("parse json error =>" + line)
/**
* 无法解析的脏数据如下 :
* 20/10/05 13:02:24 ERROR FilterTest02$: parse json error =>"oid":"o112", "cid": 3, "money": 200.0, "longitude":118.396128,"latitude":35.916527}
* 20/10/05 13:02:24 ERROR FilterTest02$: parse json error =>"oid":"o112", "cid": 3, "money": 200.0, "longitude":118.396128,"latitude":35.916527}
* 20/10/05 13:02:24 ERROR FilterTest02$: parse json error =>"oid":"o112", "cid": 3, "money": 200.0, "longitude":118.396128,"latitude":35.916527}
* 20/10/05 13:02:24 ERROR FilterTest02$: parse json error =>"oid":"o112", "cid": 3, "money": 200.0, "longitude":118.396128,"latitude":35.916527}
*/
}
}
--将beanClass对象返回
beanClass
})
val error = beanRDD.filter(_ == null).count() --错误数据共4条
val total = beanRDD.count() ---总数据共14条
println(error + "--" + total) -- 4--14
println(error.toDouble / total) -- 0.2857142857142857 --脏数据占总数据的比率
}
}
2 使用累加器(Accumulator)进行累加(不会触发多次action)
2.1 longAccumulator (long类型的累加器)
统计符合过滤条件的数据有多少条
val conf = new SparkConf().setAppName("AccumulatorTest01").setMaster("local[*]")
val sc = new SparkContext(conf)
--创建原始的RDD
val rdd1: RDD[Int] = sc.parallelize(List(1, 3, 2, 4, 6, 5, 7, 8), 2)
---在Driver端定义/初始化了一个累加器Accumulator ,伴随着Task发送到Executor端
val arrumulator = sc.longAccumulator("even_acc")
val rdd2 = rdd1.map(it =>{
if(it%2 == 0){
---这里的Executor端使用了闭包(函数内部使用了外部的引用-accumulator)
---因为使用了闭包的缘故 ,使得executor端每个Task中都有了自己的Accumulator实例 ,每个Task都持有计数器的引用
accumulator.add(1L) ---来一条数据就加 1 ,如果来一条数据加10 ,那累加器value值为 40
}
it*10
})
--这个action算子是一个异步的方法;对一个区一个区的数据进行操作
rdd2.foreachPartition(it =>{
it.foreach(t => println(t)) ---60 10 50 30 70 20 80 40
})
Thread.sleep(5000) --让当前线程睡眠5s
val value = accumulator.value --累加器里面加了多少东西 1+1+1+1=4
val count = accumulator.count --累加器调用了多少次,加了多少次1,来一条数据调用一次
val avg = accumulator.avg
println(value +"--"+ count +"--"+ avg) -- 4--4--1.0
2.2 collectionAccumulator (collection类型的累加器)
2.2.1 数据
{"cid": 1, "money": 600.0, "longitude":116.397128,"latitude":39.916527,"oid":"o123", }
"oid":"o112", "cid": 3, "money": 200.0, "longitude":118.396128,"latitude":35.916527}
{"oid":"o124", "cid": 2, "money": 200.0, "longitude":117.397128,"latitude":38.916527}
{"oid":"o125", "cid": 3, "money": 100.0, "longitude":118.397128,"latitude":35.916527}
{"oid":"o127", "cid": 1, "money": 100.0, "longitude":116.395128,"latitude":39.916527}
{"oid":"o128", "cid": 2, "money": 200.0, "longitude":117.396128,"latitude":38.916527}
{"oid":"o129", "cid": 3, "money": 300.0, "longitude":115.398128,"latitude":35.916527}
{"oid":"o130", "cid": 2, "money": 100.0, "longitude":116.397128,"latitude":39.916527}
{"oid":"o131", "cid": 1, "money": 100.0, "longitude":117.394128,"latitude":38.916527}
{"oid":"o132", "cid": 3, "money": 200.0, "longitude":118.396128,"latitude":35.916527}
{"cid": 1, "money": 600.0, "longitude":116.397128,"latitude":39.916527,"oid":"o123", }
"oid":"o112", "cid": 3, "money": 200.0, "longitude":118.396128,"latitude":35.916527}
"oid":"o112", "cid": 3, "money": 200.0, "longitude":118.396128,"latitude":35.916527}
"oid":"o112", "cid": 3, "money": 200.0, "longitude":118.396128,"latitude":35.916527}
2.2.2 定义一个case class ,将解析的json数据封装到这个样例类中
--定义一个样例类(类似javabean) ,里面的属性名字与json数据的字段名字一样
case class OrderBeanClass(
cid : String,
oid : String,
money : Double,
longitude : Double,
latitude : Double
)
2.2.3 将无法解析的json数据添加到 collection类型的累加器里面(将异常数据保存到集合里面)
import com.alibaba.fastjson.{JSON, JSONException}
object AccumulatorTest03 {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName("AccumulatorTest03").setMaster("local[*]")
val sc = new SparkContext(conf)
--在Driver端定义一个集合型的累加器 ,并指定集合里面的数据类型
val collectionAcc: CollectionAccumulator[String] = sc.collectionAccumulator[String]("error-acc")
--创建原始的RDD
val lines: RDD[String] = sc.textFile(args(0))
val beanRDD: RDD[OrderBeanClass] = lines.map(line => {
var beanClass: OrderBeanClass = null
try {
--解析json数据时可能会有脏数据 ,所以需要调用try/catch对脏数据进行捕获
--否则一遇到不能解析的数据代码就会终止运行
--将解析的数据封装到 classbean 对象里面
beanClass = JSON.parseObject(line, classOf[OrderBeanClass])
} catch {
case e: JSONException => {
--将无法解析的数据添加到累计器集合里面
collectionAcc.add(line)
}
}
beanClass
})
--计算出数据有多少条
val result: Long = beanRDD.filter(_ != null).count()
println(result) //10
--返回异常的数据(这里的是java的集合)
val lst: util.List[String] = collectionAcc.value
--调用隐式转换,将java的list集合转为scala的集合进行for循环遍历
import scala.collection.JavaConverters._
--将返回的异常数据转换为scala模式遍历出来
for(e <- lst.asScala){
println(e)
}
/**
* "oid":"o112", "cid": 3, "money": 200.0, "longitude":118.396128,"latitude":35.916527}
* "oid":"o112", "cid": 3, "money": 200.0, "longitude":118.396128,"latitude":35.916527}
* "oid":"o112", "cid": 3, "money": 200.0, "longitude":118.396128,"latitude":35.916527}
* "oid":"o112", "cid": 3, "money": 200.0, "longitude":118.396128,"latitude":35.916527}
*/
sc.stop()
}
}
四 shuffle算子的特殊情况
1 什么是 shuffle ?
指上游的数据按照一定的规律(由分区器决定的规律,可以是自定义的分区器 ,也可以是默认的HashPartition) ,将数据给下游的分区 ,严格的说是下游的 Task 到上游拉取数据 .shuffle 不一定走网络 ,走网络也不一定是 shuffle ,只有在 transformation 时才有 shuffle . 上游的一个分区的数据要给下游的一到多个分区 ,严格说是下游的 Task 要到上游的多个分区拉取数据 ,只要存在这种现象/可能就是 shuffle .
2 调用 groupByKey /reduceByKey 或者 join 等shuffle算子就一定会产生shuffle 吗 ?
不一定会产生 shuffle .但是不 shuffle 的前提条件是跟上一次 shuffle 时使用的分区器是相同的分区器 ,并且不改变分区的数量 ,这样的效果就是上游的一个分区的数据给到下游的一个分区 .
3 shuffle 逻辑图
3.1 调用 shuffle 算子产生 shuffle 现象的逻辑图
3.2 调用 shuffle 算子不产生 shuffle 现象的逻辑图
五 如何使用别人私有的 API
1 将包名该为别人同名的包名 ,就可以调用别人所有话的API
比如使用 apache 私有化的 MapPartitionsRDD 时 ,因为该API 是用 private 修饰的 ,如果想要使用的话一般情况下是不被允许的, 但是如果自己创建一个与 MapPartitionsRDD 所在的 apache 的包相同的包名 ,那么就可以在创建的这个包下使用了 .
2 案例 :代码实现(大部分的窄依赖算子都可以通过使用 MapPartitionsRDD 这种方式实现)
2.1 使用 MapPartitionsRDD 实现将RDD里面的元素取出来 ,然后全部乘以10
object MyMapTest {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName("MyMapTest").setMaster("local[*]")
val sc = new SparkContext(conf)
--创建原始的RDD
val rdd1: RDD[Int] = sc.parallelize(List(1, 2, 3, 4, 5, 6), 2)
--遍历rdd1中每个元素,让每个元素都乘以10
val result1: RDD[Int] = rdd1.map(it => it * 10)
println(result1.collect().toBuffer) --ArrayBuffer(10, 20, 30, 40, 50, 60)
--使用MapPartitionsRDD ,实现将rdd1中的每个元素都乘以10 的功能
val f = (x:Int) => x * 10
val result2: MapPartitionsRDD[Int, Int] = new MapPartitionsRDD[Int, Int](
rdd1,
--前两个参数没有使用到 ,直接用下划线代替
(_, _, iterator) => iterator.map(f) --(TaskContext,Int,Iterator[T])=>Iterator[U]
)
println(result2.collect().toBuffer) --ArrayBuffer(10, 20, 30, 40, 50, 60)
}
}
2.2 使用 MapPartitionsRDD 实现将RDD里面的元素进行过滤
object MyFilterTest {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName(this.getClass.getSimpleName).setMaster("local[*]")
--创建sparkcontext ,可以认为sparkcontext是Driver
--负责将编写的代码转成Task ,然后调度到Executor端执行
val sc = new SparkContext(conf)
--创建原始的RDD
val rdd1: RDD[Int] = sc.parallelize(List(1, 2, 3, 4, 5, 6),2)
--调用filter ,对rdd1进行过滤
val result1: RDD[Int] = rdd1.filter(_ % 2 == 0)
println(result1.collect().toBuffer) --ArrayBuffer(2, 4, 6)
----使用MapPartitionsRDD实现以上相同的功能--------
val f =(x: Int) => x % 2 == 0
val result2: MapPartitionsRDD[Int, Int] = new MapPartitionsRDD[Int, Int](
rdd1,
(_, _, it) => it.filter(f) --(TaskContext,Int,Iterator[T])=>Iterator[U]
)
println(result2.collect().toBuffer) --ArrayBuffer(2, 4, 6)
}
}
总结 :
1) 对 RDD 调用 map/filter 方法 ,本质上是对 RDD 里面的每个分区调用 map/filter 方法;
2) 在一个 Stage 里面 ,一个分区生成一个 Task (一个分区一个迭代器 ,一个迭代器对应一个 Task) ,那么这个分区对应的 Task 记录着以后从哪里读取数据 ,该怎么计算 ;
3) 我们对每一个分区进行操作 ,以后这个分区就生成 Task ,Task 计算这个分区对应的数据 .