Spark之多线程安全问题 ,RDD的错误示范,累加器(分布式计算器) ,shuffle算子特殊情况 ,调用别人的API怎么操作(9)

一  多线程安全问题

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 计算这个分区对应的数据 .

 

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值