スパークコードの読みやすさとパフォーマンスの最適化 - 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)
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)、例えば、ベース値(最大値、カウント値)、データセットの少量の複数の最終的な結果を低減する必要性
- ここで、例えば、重合セットの重量に対して、各フィールドの終わり、設定されたメモリフットプリントが増大内大量のデータ場合は、運転者が終了をハングすることができます