スパークコードの読みやすさとパフォーマンスの最適化 - 8つの模範的な(サービス・ロジック、ソリューションの品種)

スパークコードの読みやすさとパフォーマンスの最適化 - 8つの模範的な(サービス・ロジック、ソリューションの品種)

1.主婦

  • 7例からなる需要の端種類の「統計表の合計値つつ重複除外の総数に対応する全てのフィールド、及び対応するフィールドの値が空でない必要があります。」あなたは七の例を見てきた場合は、明らかにあなたが解決する方法を知っている必要があります。
  • 次のようにこの記事を書くの目的:
    • そして、誤解を避けるために、詳細にビジネスのニーズを記述する
    • ビジネスの問題の分析を提供
    • 様々なソリューションの例を表示

2.オンデマンドショー

  • テーブル前tb_express次のように、一例です。
名前 住所 トレード 修正 急行
王呉 成都、四川省 d72_network ty002 ZTO
ヌル 渝北 z03_locker bk213 SF-急行
リーレイ 長沙、湖南省 ヌル ヌル SF-急行
...... ...... ...... ...... ......
ジョン・ドウ 広州、広東省 t92_locker tu87 テーブル
  • 表の説明
    • わずか5に示す、ここで50のフルテーブルフィールドの合計(以下、便宜的に示し、例として意志のみに5)
    • 全テーブル10億個のデータの合計
    • データソースの問題ので、フィールドはnull値の事情がたくさんあるでしょう。(:、修正フィールドを9.5億非ヌル値の合計を200から300000000の範囲内の他のフィールドに非ヌル値の総数を知るために、最後の統計によります)
  • ビジネスニーズ:
    • すべてカウントする必要がフィールドの合計値を重複排除後のフィールド値の合計数、および要求されたフィールドが空ではありません

3.分析

3.1一つの問題(性能の低いSQL)

  • 実際には、ビジネス自体がまず最初に考えたことがあり、非常に単純なニーズであるSQLで処理することで、以下のように、例を示します。
    SELECT count(name), count(distinct name)
    FROM tb_express
    WHERE name IS NOT NULL AND name != '';
    
  • SQLは、一度にすべてのフィールドを数えることができないように、各フィールドのヌルが、異なっているのでしかし、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)
          }
    
  • しかし、実行しているので、一度アップして、「これらのビジネス要件の値を除外することはできませんしながら、修正フィールド9.5億null以外の値の合計、および80%は、同じ値である」という隠された秘密の前、中、データは意志まだ傾き、カード上の点。(しかし、あなたは明らかにreduceByKeyが解決使用と考えなければなりません)
  • また、あなたは、メモリ消費量を削減するために、フィールド名(文字列)を表すために、デジタル数(int)を使用することができます。しかし、その後の見やすい例、またはフィールド名が使用されます。

ソリューションの例4 A品種

  • 明らかに、我々はデータスキューに問題のあることを知って、以前の需要分析を読んで、それを解決する方法を見つける必要があります。そして、一般的な解決策は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アグリゲータを調製、キーデータ構造を変更しません

  • アグリゲータクラス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は、キーのデータ構造を変更しない、値記憶(設定<フィールド値> 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