스파크 코드의 가독성과 성능 최적화 - 팔 개 예시 (서비스 로직 솔루션의 다양한)

스파크 코드의 가독성과 성능 최적화 - 팔 개 예시 (서비스 로직 솔루션의 다양한)

1. 주부

  • 에서 일곱의 예를 만든 수요의 최종 종류에서 "통계 테이블의 총 가치의 총 수에 해당하는 모든 필드 드 - 중복, 그리고 비어 있지 않은 해당 필드 값을 요구하면서." 당신은 세븐의 예를 본 적이 있다면, 분명히 당신은 해결하는 방법을 알고 있어야합니다.
  • 다음과 같이이 글을 쓰는 목적 :
    • 그리고 오해를 피하기 위해, 세부 사업의 요구 사항을 설명
    • 사업의 문제의 분석을 제공
    • 다양한 솔루션의 예를 표시

2. 수요 쇼

  • 다음과 같이 테이블에 앞서 tb_express, 예는 다음과 같습니다
이름 주소 무역 고치다 표현하다
왕 우 청두 (成都), 사천 d72_network ty002 ZTO
없는 Yubei z03_locker bk213 SF-명시
리 레이 창사, 후난 없는 없는 SF-명시
...... ...... ...... ...... ......
신원 미상 광저우, 광동 t92_locker tu87 테이블
  • 표 설명
    • 5 도시 여기 50 개 전체 테이블 필드의 합계 (이하, 편의상 표시, 예로서만이 다섯)
    • 10 억 개 데이터의 전체 표 A의 총
    • 데이터 소스의 문제 때문에, 필드 널 (null) 값 상황이 많이있을 것입니다. (최근 통계에 따르면 알아야 할 사항 수정 필드에게 9 억 5000 만 null이 아닌 값, 200-300000000의 범위에서 다른 분야에서 null이 아닌 값의 총 수의 총)
  • 비즈니스 요구 :
    • 모든 계산하기 위해 필요 분야의 총 가치중복 제거 후 필드 값의 총 수 및 요청 필드가 비어 있지 않습니다

3. 분석

3.1 하나의 문제 (낮은 성능의 SQL)

  • 사실, 사업 자체가 첫번째 생각 될 수있다 첫째는 SQL로 처리되어야하는 매우 간단한 요구입니다, 예는 다음과 같습니다 :
    SELECT count(name), count(distinct name)
    FROM tb_express
    WHERE name IS NOT NULL AND name != '';
    
  • SQL은 한 번에 모든 필드를 카운트 할 수 있도록 각 필드의 널 (null)이 다르기 때문에 그러나, 50 개 필드는 다시 실행했다. 이 단점을 볼 수 있습니다 클러스터 자원 소비, 그것은 시간이 오래 걸립니다.

3.2 번째 문제 (데이터 스큐)

  • 그래서, 당신은 문제를 해결하기 위해 코드를 작성하려고 할 수도 있습니다. 다음과 같이 일반 예는 준비 :
    import org.apache.spark.SparkConf
    import org.apache.spark.sql.SparkSession
    import org.apache.spark.sql.types.{StringType, StructField, StructType}
    
    /**
      * Description: '字段值总数'与'字段值去重后的总数'的统计(错误示例)
      * <br/>
      * Date: 2019/12/2 16:57
      *
      * @author ALion
      */
    object CountDemo {
    
      val expressSchema: StructType = StructType(Array(
        StructField("name", StringType),
        StructField("address", StringType),
        StructField("trade", StringType),
        StructField("fix", StringType),
        StructField("express", StringType)
      ))
    
      def main(args: Array[String]): Unit = {
        val conf = new SparkConf()
          .setAppName("CountDemo")
        val spark = SparkSession.builder()
          .config(conf)
          .getOrCreate()
    
        val expressDF = spark.read.schema(expressSchema).table("tb_express")
    
        val resultRDD = expressDF.rdd
    //      .flatMap { row =>
    //        val name = row.get(row.fieldIndex("name"))
    //        val address = row.get(row.fieldIndex("address"))
    //        val trade = row.get(row.fieldIndex("trade"))
    //        val fix = row.get(row.fieldIndex("fix"))
    //        val express = row.get(row.fieldIndex("express"))
    //
    //        val buffer = ArrayBuffer[(String, String)]()
    //        // 去除null值
    //        // 字段名设置为key,字段值设置为value
    //        if (name != null) buffer.append(("name", name.toString))
    //        if (address != null) buffer.append(("address", address.toString))
    //        if (trade != null) buffer.append(("trade", trade.toString))
    //        if (fix != null) buffer.append(("fix", fix.toString))
    //        if (express != null) buffer.append(("express", express.toString))
    //
    //        buffer
    //      }
          .flatMap { row =>
            // 更函数式的写法,也更简短
            Array("name", "address", "trade", "fix", "express")
              .flatMap { name =>
                Option(row.get(row.fieldIndex(name))) match {
                  case Some(v) => Some((name, v.toString))
                  case None => None
                }
              }
          }.groupByKey()
          .mapValues { iter => (iter.size, iter.toSet.size) } // 此处计算'字段值总数'与'字段值去重后的总数'
    
        // 拉取数据,打印结果
        resultRDD.collect()
          .foreach { case (fieldName, (count, distinctCount)) =>
            println(s"字段名 = $fieldName, 字段值总数 = $count, 字段值去重后的总数 = $distinctCount")
          }
    
        spark.stop()
      }
    
    }
    
  • 쓰기 코드는 일반적으로 첫 번째 생각은 이런 식으로, 각 그룹화 필드 이름이며, 필드 값은 통계이다.
  • 그러나, 여기서 문제는 상기 우리에서 언급 된 코드 데이터 리드가 경사 GroupByKey에서이다 "수정 필드 합계 950 000 000 비 - 널 값 200-300 만 범위 내의 다른 필드의 비 - 널 값의 총 수." 필드가 핵심한다면, 그것은 GroupByKey에서이며, 셔플 노드 데이터로 이어질 것입니다 것은 다른 노드보다 훨씬 더 크다.

3.3 문제 트리스 (데이터 내의 데이터 비스듬히 경사)

  • 당신은 아마, 원하는 것이 시간 "그것을가 기울어지지 않도록 내가 필드 이름 이외에 키를 넣어, 보증금 값을, 필드 값을 추가?" 몇 가지 예는 신체 있습니다 :
        val resultRDD = expressDF.rdd
          .flatMap { row =>
            // 更函数式的写法,也更简短
            Array("name", "address", "trade", "fix", "express")
              .flatMap { name =>
                Option(row.get(row.fieldIndex(name))) match {
                  case Some(v) => Some(((name, v.toString), 1))
                  case None => None
                }
              }
          }.groupByKey()
          .map { case ((name, value), iter) => (name, (value, iter.size)) }
          .groupByKey() // 第二次groupByKey虽然还是以字段名为key,但是因为数据量很小,所以会很快处理完
          .map {case (name, iter) =>
            // (字段名, 字段值总数, 字段值去重后的总数)
            (name, iter.map(_._2).sum, iter.size)
          }
    
  • 그러나 실행 그래서 한 번 위로, "이러한 비즈니스 요구 사항의 값을 배제 할 수는 없지만 수정 필드 억 5000 만 null이 아닌 값의 총 80 %가, 같은 값이다"고 숨겨진 비밀의 전면에서, 데이터는 것이다 여전히 틸트, 카드 위의 점. (하지만 당신은 분명히 reduceByKey 해결하여 생각해야)
  • 또한, 메모리 소비를 줄이기 위해, 필드 이름 (String)를 나타내는 디지털 번호 (INT)를 사용할 수 있습니다. 그러나 이후 쉽게 볼 예, 또는 필드 이름을 사용.

해결책의 실시 예 4. 다양한

  • 분명히 우리는 데이터에 문제가 거짓말 왜곡 것을 알고, 이전 수요 분석을 읽고, 필요가 그것을 해결하는 방법을 찾을 수 있습니다. 그런 다음 일반적인 솔루션은 reduceByKey이지만, 우리는 또한 여기에 내가 참조를 위해 몇 가지 예를 쓸 것, 다른 방법을 생각할 수있다.

4.1 난수의 사용은 데이터의 스큐 문제를 해결하는 방법으로 키를 첨가

val resultRDD = expressDF.rdd
  .flatMap { row =>
    val random = new Random()
    // 更函数式的写法,也更简短
    Array("name", "address", "trade", "fix", "express")
      .flatMap { name =>
        Option(row.get(row.fieldIndex(name))) match {
          // 随机范围取100,最终会导致数据分成100份。根据当前集群启动的节点数合理取值,可以达到更好的效果。
          case Some(v) => Some((name + "_" + random.nextInt(100), v.toString))
          case None => None
        }
      }
  }.groupByKey()
  .map { case (k, v) =>
    // 完成本次聚合,并去掉随机数
    (k.split("_")(0), (v.size, v.toSet))
  }.groupByKey() // 同样的,你也可以写reduceByKey,不过此处几乎没有效率影响
    .mapValues {iter =>
      // (字段值总数, 字段值去重后的总数)
      (iter.map(_._1).sum, iter.map(_._2).reduce(_ ++ _).size)
    }

4.2 사용 reduceByKey, 주요 데이터 구조를 수정 한 다음 이후의 처리 방법 변경

val resultRDD = expressDF.rdd
  .flatMap { row =>
    // 更函数式的写法,也更简短
    Array("name", "address", "trade", "fix", "express")
      .flatMap { name =>
        Option(row.get(row.fieldIndex(name))) match {
          case Some(v) => Some(((name, v.toString), 1))
          case None => None
        }
      }
  }.reduceByKey(_ + _) // 将问题分析最后示例中的groupByKey替换为reduceByKey即可解决
  .map { case ((name, value), count) => (name, (value, count)) }
  // 第二次groupByKey虽然还是以字段名为key,但是因为数据量很小,所以会很快处理完。
  // 当然你这里也可以使用reduceByKey。
  .groupByKey()  
  .map { case (name, iter) =>
    // (字段名, 字段值总数, 字段值去重后的总数)
    (name, iter.map(_._2).sum, iter.size)
  }

4.3, 키 데이터 구조를 수정 애그리 게이터 (aggregator) 준비하지 않습니다

  • 그리 게이터 클래스 CountAggregator
    class CountAggregator(var count: Int, var countSet: mutable.HashSet[String]) {
      
      def +=(element: (Int, String)) : CountAggregator = {
        this.count += element._1
        this.countSet += element._2
    
        this
      }
      
      def ++=(that: CountAggregator): CountAggregator = {
        this.count += that.count
        this.countSet ++= that.countSet
    
        this
      }
    
    }
    
    object CountAggregator {
    
      def apply(): CountAggregator =
        new CountAggregator(0, mutable.HashSet[String]())
    
    }
    
  • 스파크 대상 코드
    val resultRDD = expressDF.rdd
      .flatMap { row =>
        // 更函数式的写法,也更简短
        Array("name", "address", "trade", "fix", "express")
          .flatMap { name =>
            Option(row.get(row.fieldIndex(name))) match {
              case Some(v) => Some((name, (1, v.toString)))
              case None => None
            }
          }
      }.aggregateByKey(CountAggregator())(
        (agg, v) => agg += v,
        (agg1, agg2) => agg1 ++= agg2
      ).mapValues { aggregator =>
        // (字段值总数, 字段值去重后的总数)
        (aggregator.count, aggregator.countSet.size)
      }
    

4.4 키 데이터 구조 <번호 필드 값 필드 값> 값 저장된 맵을 수정하지

 val resultRDD = expressDF.rdd
   .flatMap { row =>
     // 更函数式的写法,也更简短
     Array("name", "address", "trade", "fix", "express")
       .flatMap { name =>
         Option(row.get(row.fieldIndex(name))) match {
           case Some(v) => Some((name, (v.toString, 1)))
           case None => None
         }
       }
   }.aggregateByKey(mutable.HashMap[String, Int]())(
     (map, kv) => map += (kv._1 -> (map.getOrElse(kv._1, 0) + kv._2)),
     (map1, map2) => {
       for ((k, v) <- map2) {
         map1 += (k -> (map1.getOrElse(k, 0) + v))
       }
       map1
     }
   ).mapValues { map => 
      // 字段值总数, 字段值去重后的总数
        (map.values.sum, map.keySet.size)
   }

4.5 키 데이터 구조를 변경하지 않는 저장 값 (SET <필드 값> 1)

 val resultRDD = expressDF.rdd
   .flatMap { row =>
     // 更函数式的写法,也更简短
     Array("name", "address", "trade", "fix", "express")
       .flatMap { name =>
         Option(row.get(row.fieldIndex(name))) match {
           case Some(v) => Some((name, (v.toString, 1)))
           case None => None
         }
       }
   }.aggregateByKey((mutable.HashSet[String](), 0))(
     (agg, v) => (agg._1 += v._1, agg._2 + v._2),
     (agg1, agg2) => (agg1._1 ++= agg2._1, agg1._2 + agg2._2)
   )
   .mapValues { case (set, count)  =>
     // (字段值总数, 字段值去重后的总数)
     (count, set.size)
   }

4.6 다른 프로그램 treeAggregate를 사용

  • 샘플 코드는 다음과 같습니다 (현재의 비즈니스 로직을 위해,하지 treeAggregate을 권장)
  • 어 그리 게이터
    import scala.collection.mutable
    
    case class Counter(var count: Int, set: mutable.HashSet[String])
    
    class CountAggregator2(var counter1: Counter,
                           var counter2: Counter,
                           var counter3: Counter,
                           var counter4: Counter,
                           var counter5: Counter) {
    
      def +=(element: (Any, Any, Any, Any, Any)): CountAggregator2 = {
        def countFunc(counter: Counter, e: Any): Unit = {
          if (e != null) {
            counter.count += 1
            counter.set += e.toString
          }
        }
    
        countFunc(counter1, element._1)
        countFunc(counter2, element._2)
        countFunc(counter3, element._3)
        countFunc(counter4, element._4)
        countFunc(counter5, element._5)
    
        this
      }
    
      def ++=(that: CountAggregator2): CountAggregator2 = {
        this.counter1.count += that.counter1.count
        this.counter1.set ++= that.counter1.set
        this.counter2.count += that.counter2.count
        this.counter2.set ++= that.counter2.set
        this.counter3.count += that.counter3.count
        this.counter3.set ++= that.counter3.set
        this.counter4.count += that.counter4.count
        this.counter4.set ++= that.counter4.set
        this.counter5.count += that.counter5.count
        this.counter5.set ++= that.counter5.set
    
        this
      }
    
    }
    
    
    object CountAggregator2 {
    
      def apply(): CountAggregator2 =
        new CountAggregator2(
          Counter(0, mutable.HashSet[String]()),
          Counter(0, mutable.HashSet[String]()),
          Counter(0, mutable.HashSet[String]()),
          Counter(0, mutable.HashSet[String]()),
          Counter(0, mutable.HashSet[String]())
        )
    
    }
    
  • 스파크 대상 코드
     val result = expressDF.rdd
       .map { row =>
         val rowAny = (name: String) => row.get(row.fieldIndex(name))
         
         (rowAny("name"), rowAny("address"), rowAny("trade"), rowAny("fix"), rowAny("express"))
       }
       // 请根据业务、数据量,来调整treeReduce的深度depth,默认为2
       .treeAggregate(CountAggregator2())(
         (agg, v) => agg += v,
         (agg1, agg2) => agg1 ++= agg2
       )
    
  • 이점
    • treeAggregate 각 노드에서 수행 줄이기 위해 병렬 계산을 이용하여, 최종적으로 감소 노드 하나로 병합
    • 각 필드 이름에 대한 값을 생성 할뿐만 앞의 예와 같이 수행이 메모리 사용량을 줄이고, 중합 키로서 사용된다 (데이터 라인의 수의 필드, 필드의 수는 거의 이름을 생성한다)
  • 결점
    • 데이터가 작을수록 필요가 최종 결과를 줄이기 위해 예를 들면, 복수의 염기 값 (최대 값, 카운트 값), (정렬 전에 10) 데이터 세트 소량
    • 여기서의 예는, 중합 세트의 중량에 각 필드의 끝에는 설정 메모리 용량 증가 내에 많은 양의 데이터는, 운전자가 끝 걸어 일으킬 수 있다면
게시 된 128 개 원래 기사 · 원 찬양 45 ·은 15 만 + 조회수

추천

출처blog.csdn.net/alionsss/article/details/103349983