[ターン]のSparkCoreを学んで味を調整・チューニング・スパーク開発の道(8)

序文

コンピューティング、ビッグデータでは、スパークは、ますます人気コンピューティング・プラットフォームの1、ますます人気となっています。スパーク関数データ算出動作の様々なタイプのコンピューティングオフラインバッチの大フィールド、SQL処理クラス、ストリーミング/リアルタイム計算を、機械学習、図、アプリケーションとの見通しの非常に広い範囲を包含する。米国のミッション•パブリックコメントでは、様々なプロジェクトにスパークを使用しようとする多くの学生がありました。(筆者含む)理由ほとんどの学生は、もともと、主に速いジョブ実行速度を計算するビッグデータを作るために、より高い性能をスパークの非常に単純なを使用して開始しました。

しかし、仕事を計算する高性能なビッグデータを開発するスパークによって、それはそれほど単純ではありません。あなたはスパーク合理的なチューニングのための作業を行わないと、スパークジョブの実行速度が遅くなることがありますので、完全に優位に高速な大規模データ計算エンジンとしてスパーク具現化しません。そのため、あなたはスパークを十分に活用したい、それが合理的なパフォーマンスを最適化する必要があります。

スパークパフォーマンスのチューニングは、実際にいくつかのパラメータは、即時のリフティング動作性能を調整することができない、多くの部分で構成されています。私たちは、総合的な分析スパーク仕事を引き受けるために、さまざまなデータや状況に基づいてビジネスシナリオを必要として、最高のパフォーマンスを得るために、さまざまな側面を調整し、最適化します。

パフォーマンスの最適化スパークジョブのセットを総括する前に、蓄積された著者のスパークジョブ開発経験と実践によります。プログラム開発は、いくつかの部分をシャッフルチューニング、チューニングリソース、データスキュー調整、チューニングのパッケージに分割されています。開発のチューニングおよびリソースのチューニングは、すべてのスパークのジョブが従うことを注意していくつかの基本的な原則が必要である、高性能スパーク操作のための基礎であり、データスキュー調整、主にジョブデータの完全なセットに解決するために傾斜してスパークを解決するためにプログラム、シャッフルの曲、主にどのようにスパークジョブ曲のシャッフル操作手順や詳細に研究学生とマスタースパークの原則のより深いレベルがあるため。

主にチューニングおよびリソースのチューニングの開発に、本明細書のパフォーマンスの最適化の基本ガイドをスパーク。

開発のチューニング

チューニングの概要

最初のステップスパーク性能の最適化は、スパークジョブを開発する過程でいくつかの基本的な原則とアプリケーションのパフォーマンスの最適化に注意を払うことです。RDD系統の設計、サブ最適化オペレーションの合理的な利用やその他の特別なオペレータ:開発のチューニングは、誰もが含む以下のスパークの基本的な開発の原則のいくつかを、知っているようにです。開発プロセスでは、と常に上記の原則、および特定のビジネスだけでなく、実用的なアプリケーションシナリオに応じてこれらの原則は、柔軟性が自分のスパークジョブに適用することに留意すべきです。

原則:重複RDDを作成しないようにしてください

我々はスパークの開発で作業するとき、一般的に言えば、第一(例えばテーブルやハイブHDFSファイルとして)データソースに基づいて初期RDDを作成し、次にオペレータRDDの操作を実行し、次のRDDを得ます。ように、無限に、我々は、最終的な結果を計算する必要があるまで。一緒にこのプロセスでは、異なるRDD複数の算術演算子(例えばマップとしては、などを低減)される文字列に、「RDD文字列は」RDD系統、すなわち、ある「RDDの親族チェーン。」

同じデータを表現するために、複数のRDDを作成することはできません、同じデータについては、しかし、RDDを作成する必要があります。私たちは、開発プロセスのに注意を払う必要があります。

スパークジョブの開発の先頭にいくつかのスパーク初心者かは、すでにRDD作成したデータのコピーの前に、自分自身を忘れて、RDD系譜非常に長いスパークジョブの開発の経験豊富なエンジニアであり、につながります同じデータが、複数のRDDを作成します。この手段は、私たちのスパークの仕事は、このように、パフォーマンス・オーバーヘッド・オペレーションの増加、複数のRDDが同じデータを表す作成するための計算を繰り返すことになるということ。

簡単な例

// 需要对名为“hello.txt”的HDFS文件进行一次map操作,再进行一次reduce操作。也就是说,需要对一份数据执行两次算子操作。

// 错误的做法:对于同一份数据执行多次算子操作时,创建多个RDD。
// 这里执行了两次textFile方法,针对同一个HDFS文件,创建了两个RDD出来,然后分别对每个RDD都执行了一个算子操作。
// 这种情况下,Spark需要从HDFS上两次加载hello.txt文件的内容,并创建两个单独的RDD;第二次加载HDFS文件以及创建RDD的性能开销,很明显是白白浪费掉的。
val rdd1 = sc.textFile("hdfs://192.168.0.1:9000/hello.txt")
rdd1.map(...)
val rdd2 = sc.textFile("hdfs://192.168.0.1:9000/hello.txt")
rdd2.reduce(...)

// 正确的用法:对于一份数据执行多次算子操作时,只使用一个RDD。
// 这种写法很明显比上一种写法要好多了,因为我们对于同一份数据只创建了一个RDD,然后对这一个RDD执行了多次算子操作。
// 但是要注意到这里为止优化还没有结束,由于rdd1被执行了两次算子操作,第二次执行reduce操作的时候,还会再次从源头处重新计算一次rdd1的数据,因此还是会有重复计算的性能开销。
// 要彻底解决这个问题,必须结合“原则三:对多次使用的RDD进行持久化”,才能保证一个RDD被多次使用时只被计算一次。
val rdd1 = sc.textFile("hdfs://192.168.0.1:9000/hello.txt")
rdd1.map(...)
rdd1.reduce(...)

第2の原理:できるだけ再利用と同じRDD

加えて、開発プロセスにおいて正確に同じデータをオペレータに異なるデータはRDDを再利用することも可能で操作を実行する場合よりもRDDの作成を回避します。例えば、データフォーマットRDDキーと値のタイプがあり、他のタイプは、単一の値であり、これら二つのデータRDDの値が全く同じです。それは既に他のデータが含まれているため、そのため、この時点で我々は、RDDのキーと値のタイプを使用することができます。同様のデータRDDは、そのような重複を複数有しているか、含まれて、我々は、このように可能な実行オペレータの数を削減することができるようRDDの数を減らす、RDDを再利用しようとしなければなりません。

簡単な例

// 错误的做法。

// 有一个<Long, String>格式的RDD,即rdd1。
// 接着由于业务需要,对rdd1执行了一个map操作,创建了一个rdd2,而rdd2中的数据仅仅是rdd1中的value值而已,也就是说,rdd2是rdd1的子集。
JavaPairRDD<Long, String> rdd1 = ...
JavaRDD<String> rdd2 = rdd1.map(...)

// 分别对rdd1和rdd2执行了不同的算子操作。
rdd1.reduceByKey(...)
rdd2.map(...)

// 正确的做法。

// 上面这个case中,其实rdd1和rdd2的区别无非就是数据格式不同而已,rdd2的数据完全就是rdd1的子集而已,却创建了两个rdd,并对两个rdd都执行了一次算子操作。
// 此时会因为对rdd1执行map算子来创建rdd2,而多执行一次算子操作,进而增加性能开销。

// 其实在这种情况下完全可以复用同一个RDD。
// 我们可以使用rdd1,既做reduceByKey操作,也做map操作。
// 在进行第二个map操作时,只使用每个数据的tuple._2,也就是rdd1中的value值,即可。
JavaPairRDD<Long, String> rdd1 = ...
rdd1.reduceByKey(...)
rdd1.map(tuple._2...)

// 第二种方式相较于第一种方式而言,很明显减少了一次rdd2的计算开销。
// 但是到这里为止,优化还没有结束,对rdd1我们还是执行了两次算子操作,rdd1实际上还是会被计算两次。
// 因此还需要配合“原则三:对多次使用的RDD进行持久化”进行使用,才能保证一个RDD被多次使用时只被计算一次。

原理III:永続化のためのRDD複数回使用

あなたが繰り返しおめでとう後スパークコードで動作RDD演算子を行うにはときに、あなたは遠く可能リユースRDDなどとして、あるスパークジョブを、最適化の最初のステップを達成しています。この基準に関連して、この時点で、第二のステップは、RDDオペレータが複数の操作を実行するときに、RDD自体は一度だけカウントされることを確実にするために、すなわち、最適化されます。

デフォルトRDD事業者が複数のサブ原則を実行するためにあるスパークはこれです:あなたは1人のRDDオペレーターの操作を実行するたびに、RDDを計算するために、ソースで再び再計算した後、RDDを実行しますあなたのオペレータ操作。このようにパフォーマンスが悪いです。

したがって、この場合には、私たちのアドバイスは次のとおりです。永続化のために複数の使用のためにRDD。このスパークで、メモリやディスクにRDD中のデータを保存するために、あなたの永続化戦略に基づいて行われます。RDDの後続の各時間は、メモリまたはディスク永続RDDデータから直接抽出した後、オペレータが実行され、算術演算を実行し、再びこれをソースRDDから再計算されず、オペレータの操作を行います。

サンプルコードのRDD繰り返し使用が持続性であります

// 如果要对一个RDD进行持久化,只要对这个RDD调用cache()和persist()即可。

// 正确的做法。
// cache()方法表示:使用非序列化的方式将RDD中的数据全部尝试持久化到内存中。
// 此时再对rdd1执行两次算子操作时,只有在第一次执行map算子时,才会将这个rdd1从源头处计算一次。
// 第二次执行reduce算子时,就会直接从内存中提取数据进行计算,不会重复计算一个rdd。
val rdd1 = sc.textFile("hdfs://192.168.0.1:9000/hello.txt").cache()
rdd1.map(...)
rdd1.reduce(...)

// persist()方法表示:手动选择持久化级别,并使用指定的方式进行持久化。
// 比如说,StorageLevel.MEMORY_AND_DISK_SER表示,内存充足时优先持久化到内存中,内存不充足时持久化到磁盘文件中。
// 而且其中的_SER后缀表示,使用序列化的方式来保存RDD数据,此时RDD中的每个partition都会序列化成一个大的字节数组,然后再持久化到内存或磁盘中。
// 序列化的方式可以减少持久化的数据对内存/磁盘的占用量,进而避免内存被持久化数据占用过多,从而发生频繁GC。
val rdd1 = sc.textFile("hdfs://192.168.0.1:9000/hello.txt").persist(StorageLevel.MEMORY_AND_DISK_SER)
rdd1.map(...)
rdd1.reduce(...)

()メソッドを保持するために、我々は、異なるビジネスシナリオに応じて異なる永続性レベルを選択することができます。

の持続的なレベルをスパーク

持続性レベル 意味説明
MEMORY_ONLY 非Javaオブジェクト直列化フォーマットを使用して、データがメモリに格納されています。十分でないメモリはすべてのデータを格納する場合は、データを永続化されないことがあります。したがって、このRDDは、永続的なデータされていないオペレータの操作を実行する次回は、ソースから再び再計算する必要があります。これはデフォルトの永続化戦略、実際の永続化戦略を使用することですキャッシュ()メソッドの使用です。
MEMORY_AND_DISK メモリに記憶されている優先度データにしようと、非直列化Javaオブジェクトの形式を使用します。十分でないメモリはすべてのデータを格納する場合は、ディスクファイルにデータを書き込み、RDDの次の実行回数の深夜には、永続ディスク・ファイル・データは、使用から読み出されます。
MEMORY_ONLY_SER 同じMEMORY_ONLYの基本的な意味。唯一の違いは、データが各パーティションRDDは、バイト配列にシリアライズされ、RDDにシリアル化されることです。このアプローチは、それによって頻繁にGCにあまりにも多くのメモリの結果を使用する永続的なデータを避け、より多くのメモリ保存です。
MEMORY_AND_DISK_SER 同じMEMORY_AND_DISKの基本的な意味。唯一の違いは、データが各パーティションRDDは、バイト配列にシリアライズされ、RDDにシリアル化されることです。このアプローチは、それによって頻繁にGCにあまりにも多くのメモリの結果を使用する永続的なデータを避け、より多くのメモリ保存です。
DISK_ONLY 非直列化Javaオブジェクトフォーマットは、ディスク・ファイルに書き込まれたすべてのデータを使用してください。
MEMORY_ONLY_2、MEMORY_AND_DISK_2、等等。 これらの永続的なポリシーのいずれか、_2接尾場合について、各永続データに代わって、他のノードにコピーを保存して送って、単一のコピーです。主にフォールトトレランスに使用のコピーに基づいて、この永続化メカニズム。あなたはノードがハングアップした場合、ノードのメモリやディスク永続データは失われ、その後、RDD・コンピューティングのその後の使用は、他のノード上のデータのコピーを持つことができます。それの何のコピーが存在しない場合、それだけで再びそれでこれらのデータソースから再計算することができます。

最も適切な永続化戦略を選択する方法

  • デフォルトではもちろん、最高のパフォーマンス、MEMORY_ONLYが、あなたは全体RDD内のすべてのデータを保存するために、より十分なよりもするのに十分なメモリ十分な大きさを持っている場合のみ。ノーシリアライズとデシリアライズ動作は、この部分のパフォーマンスのオーバーヘッドを避けるために、RDDオペレータのその後の操作は、あなたが純粋なメモリの操作に基づいてディスク・ファイル・データからデータを読み取るために必要とされていません、高い性能が、他のノードへのデータのコピー、及び遠隔伝送をコピーする必要がありません。しかし、ここでこの永続性レベルを直接使用する、実際の運用環境では、私はデータRDD比較的長い時間であれば(例えば、数十億など)、この戦略は、直接、まだ限られたシーンを使用することができることが怖い、ということだろう注意しなければなりませんOOMは、JVMのメモリオーバーフロー例外につながります。

  • MEMORY_ONLYレベルを用いてメモリオーバーフローがある場合は、MEMORY_ONLY_SERレベルを使用しようとすることをお勧めします。このレベルはその後RDDメモリに格納されたデータをシリアル化し、その後、各パーティションは、わずかだけバイト配列であるオブジェクトの数を減らし、メモリフットプリントを低減します。MEMORY_ONLY、主シリアライゼーションおよびデシリアライゼーションのオーバーヘッドよりもパフォーマンスオーバーヘッドのこの余分なレベル。全体的なパフォーマンスは依然として比較的高いのでしかし、オペレータは、後続のメモリベースを操作することができます。また、問題は、データRDDの量が多すぎると、その後、彼はメモリオーバーフローOOM例外が発生することがあり、上記の発生する可能性があります。

  • 純粋なメモリレベルが利用できない場合、むしろMEMORY_AND_DISK戦略よりも、MEMORY_AND_DISK_SER戦略を使用することをお勧めします。私たちは、この段階に来ているので、それはメモリが完全に下に置くことができない、データRDDの大規模な量を示しています。データのシリアル化が比較的小さい、それはメモリとディスクスペースのオーバーヘッドを保存することができます。一方、戦略がディスクに書き込まれるメモリキャッシュよりも劣らず、メモリにデータをキャッシュするためにしようしようとしないように優先させて頂きます。

  • 一般DISK_ONLYと接尾_2レベルはお勧めしません:完全なディスクベースのファイルため、読み取りおよび書き込みデータを、それがすべてのRDD一度再計算されません良いなどとして、時には、急激な減少のパフォーマンスにつながります。_2サフィックスレベルは、あなたがそれ以外の場合はお勧めできません、高可用性の操作を除いて、他のノード、データ・レプリケーションおよびネットワーク伝送に送信されたすべてのデータのコピーが頭上に大きなパフォーマンスにつながることができますする必要があります。

原理IV:シャッフルクラス演算子を使用しないようにしてください

可能であれば、シャッフルクラスの演算子を使用しないようにしてみてください。スパークジョブプロセスが実行されているため、場所はほとんどの消費シャッフルプロセス性能です。シャッフル処理は、簡単に言えば、重合または他の操作を参加するために、同じノード上に引っ張って、キーを持つクラスタの複数のノードにわたって分散されています。例のreduceByKey、参加して、他の事業者の場合は、シャッフル操作をトリガーします。

シャッフルプロセスは、キーは、ローカルディスクに書き込まれた最初のファイル内の各ノードにネットワークを介して各ノードの同じキーにディスクファイルを引くために必要な、他のノードと同じになります。ときに、同じキーとストアには十分でないメモリで、その結果、ノード上であまりにも多くの重要なプロセスがあるかもしれないので、同じノードの集約操作にドラッグして、ディスクファイルに溢れます。だから、シャッフルプロセス、ディスク・ファイルの多数のネットワーク送信動作は、読み取りおよび書き込みIO操作、およびデータが発生することがあります。ディスクIOやネットワークのデータ伝送は、パフォーマンスの低下のシャッフルの主な理由です。

だから、私たちの開発プロセスでは、参加し、遠く可能回避などとして使用reduceByKeyを避けるために、明確な、配分及びその他の演算子は、非マップクラスを利用するためにシャッフル、シャッフルオペレータだろう。この場合、小さなシャッフル操作スパークジョブシャッフル操作がないかだけではありません、あなたは大幅にパフォーマンス上のオーバーヘッドを減らすことができます。

放送は、マップコードサンプルのために参加します

// 传统的join操作会导致shuffle操作。
// 因为两个RDD中,相同的key都需要通过网络拉取到一个节点上,由一个task进行join操作。
val rdd3 = rdd1.join(rdd2)

// Broadcast+map的join操作,不会导致shuffle操作。
// 使用Broadcast将一个数据量较小的RDD作为广播变量。
val rdd2Data = rdd2.collect()
val rdd2DataBroadcast = sc.broadcast(rdd2Data)

// 在rdd1.map算子中,可以从rdd2DataBroadcast中,获取rdd2的所有数据。
// 然后进行遍历,如果发现rdd2中某条数据的key与rdd1的当前数据的key是相同的,那么就判定可以进行join。
// 此时就可以根据自己需要的方式,将rdd1当前数据与rdd2中可以连接的数据,拼接在一起(String或Tuple)。
val rdd3 = rdd1.map(rdd2DataBroadcast...)

// 注意,以上操作,建议仅仅在rdd2的数据量比较少(比如几百M,或者一两G)的情况下使用。
// 因为每个Executor的内存中,都会驻留一份rdd2的全量数据。

原則V:マップ側予備重合シャッフル操作を使用して

ビジネスニーズのために、我々はシャッフル操作を使用する必要があり、場合、オペレータはマップベースで置き換えることができない、それは予備マップ側の演算子を利用することができます。

いわゆるマップ側予備重合は、各ノードでローカルに、ローカルコンバイナのMapReduceと同様の重合操作一度同じキーを、と言うことです。同じキー個まで重合されるので、予備マップ側の後、各ローカルノードは、1つのキーだけ同じです。すべてのノードで同じキー内の他のノードを引く、それは非常にディスクIO、ネットワーク伝送のオーバーヘッドを減らすれる、プルする必要があるデータの量を減少させます。可能な場合は一般的に言えば、失われたgroupByKey演算子を置き換えるためにreduceByKeyまたはaggregateByKey演算子を使用することをお勧めします。reduceByKey aggregateByKeyオペレータとユーザ定義関数は、ローカル予備の各ノードに同じ鍵を使用するため。オペレータが予備重合されていないGroupByKeyは、データの総量は、クラスタのノードとの間に比較的性能差を送信し、分配されます。

以下の2つの図、例えば、典型的な例であり、ワードカウントとreduceByKey groupByKeyに基づいていました。最初のグラフは、実行されたすべてのローカル凝集することなく、概略groupByKeyを見ることができるクラスタノード間のすべてのデータ転送である。図は、第二の概略reduceByKeyを見ることができるされ、各ローカルノード同じ鍵データは、グローバルに他のノードに送信重合前に予備重合を行いました。

原則VI:高性能な演算子の使用

オペレータの最適化の原則に加えてシャッフル関連、他のオペレータは、対応する最適化の原則を持っています。

  • 使用reduceByKey / aggregateByKey groupByKey代替
    の詳細は、「5つの原則を:マップ側がシャッフル操作を予備重合使用」を参照してください。
  • 使用mapPartitions替代普通map
    mapPartitions类的算子,一次函数调用会处理一个partition所有的数据,而不是一次函数调用处理一条,性能相对来说会高一些。但是有的时候,使用mapPartitions会出现OOM(内存溢出)的问题。因为单次函数调用就要处理掉一个partition所有的数据,如果内存不够,垃圾回收时是无法回收掉太多对象的,很可能出现OOM异常。所以使用这类操作时要慎重!
  • 使用foreachPartitions替代foreach
    原理类似于“使用mapPartitions替代map”,也是一次函数调用处理一个partition的所有数据,而不是一次函数调用处理一条数据。在实践中发现,foreachPartitions类的算子,对性能的提升还是很有帮助的。比如在foreach函数中,将RDD中所有数据写MySQL,那么如果是普通的foreach算子,就会一条数据一条数据地写,每次函数调用可能就会创建一个数据库连接,此时就势必会频繁地创建和销毁数据库连接,性能是非常低下;但是如果用foreachPartitions算子一次性处理一个partition的数据,那么对于每个partition,只要创建一个数据库连接即可,然后执行批量插入操作,此时性能是比较高的。实践中发现,对于1万条左右的数据量写MySQL,性能可以提升30%以上。
  • 使用filter之后进行coalesce操作
    通常对一个RDD执行filter算子过滤掉RDD中较多数据后(比如30%以上的数据),建议使用coalesce算子,手动减少RDD的partition数量,将RDD中的数据压缩到更少的partition中去。因为filter之后,RDD的每个partition中都会有很多数据被过滤掉,此时如果照常进行后续的计算,其实每个task处理的partition中的数据量并不是很多,有一点资源浪费,而且此时处理的task越多,可能速度反而越慢。因此用coalesce减少partition数量,将RDD中的数据压缩到更少的partition之后,只要使用更少的task即可处理完所有的partition。在某些场景下,对于性能的提升会有一定的帮助。
  • 使用repartitionAndSortWithinPartitions替代repartition与sort类操作
    repartitionAndSortWithinPartitions是Spark官网推荐的一个算子,官方建议,如果需要在repartition重分区之后,还要进行排序,建议直接使用repartitionAndSortWithinPartitions算子。因为该算子可以一边进行重分区的shuffle操作,一边进行排序。shuffle与sort两个操作同时进行,比先shuffle再sort来说,性能可能是要高的。

原则七:广播大变量

有时在开发过程中,会遇到需要在算子函数中使用外部变量的场景(尤其是大变量,比如100M以上的大集合),那么此时就应该使用Spark的广播(Broadcast)功能来提升性能。

在算子函数中使用到外部变量时,默认情况下,Spark会将该变量复制多个副本,通过网络传输到task中,此时每个task都有一个变量副本。如果变量本身比较大的话(比如100M,甚至1G),那么大量的变量副本在网络中传输的性能开销,以及在各个节点的Executor中占用过多内存导致的频繁GC,都会极大地影响性能。

因此对于上述情况,如果使用的外部变量比较大,建议使用Spark的广播功能,对该变量进行广播。广播后的变量,会保证每个Executor的内存中,只驻留一份变量副本,而Executor中的task执行时共享该Executor中的那份变量副本。这样的话,可以大大减少变量副本的数量,从而减少网络传输的性能开销,并减少对Executor内存的占用开销,降低GC的频率。

广播大变量的代码示例

// 以下代码在算子函数中,使用了外部的变量。
// 此时没有做任何特殊操作,每个task都会有一份list1的副本。
val list1 = ...
rdd1.map(list1...)

// 以下代码将list1封装成了Broadcast类型的广播变量。
// 在算子函数中,使用广播变量时,首先会判断当前task所在Executor内存中,是否有变量副本。
// 如果有则直接使用;如果没有则从Driver或者其他Executor节点上远程拉取一份放到本地Executor内存中。
// 每个Executor内存中,就只会驻留一份广播变量副本。
val list1 = ...
val list1Broadcast = sc.broadcast(list1)
rdd1.map(list1Broadcast...)

原则八:使用Kryo优化序列化性能

在Spark中,主要有三个地方涉及到了序列化:

  • 在算子函数中使用到外部变量时,该变量会被序列化后进行网络传输(见“原则七:广播大变量”中的讲解)。
  • 将自定义的类型作为RDD的泛型类型时(比如JavaRDD,Student是自定义类型),所有自定义类型对象,都会进行序列化。因此这种情况下,也要求自定义的类必须实现Serializable接口。
  • 使用可序列化的持久化策略时(比如MEMORY_ONLY_SER),Spark会将RDD中的每个partition都序列化成一个大的字节数组。

对于这三种出现序列化的地方,我们都可以通过使用Kryo序列化类库,来优化序列化和反序列化的性能。Spark默认使用的是Java的序列化机制,也就是ObjectOutputStream/ObjectInputStream API来进行序列化和反序列化。但是Spark同时支持使用Kryo序列化库,Kryo序列化类库的性能比Java序列化类库的性能要高很多。官方介绍,Kryo序列化机制比Java序列化机制,性能高10倍左右。Spark之所以默认没有使用Kryo作为序列化类库,是因为Kryo要求最好要注册所有需要进行序列化的自定义类型,因此对于开发者来说,这种方式比较麻烦。

以下是使用Kryo的代码示例,我们只要设置序列化类,再注册要序列化的自定义类型即可(比如算子函数中使用到的外部变量类型、作为RDD泛型类型的自定义类型等):

// 创建SparkConf对象。
val conf = new SparkConf().setMaster(...).setAppName(...)
// 设置序列化器为KryoSerializer。
conf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
// 注册要序列化的自定义类型。
conf.registerKryoClasses(Array(classOf[MyClass1], classOf[MyClass2]))

原则九:优化数据结构

ava中,有三种类型比较耗费内存:

  • 对象,每个Java对象都有对象头、引用等额外的信息,因此比较占用内存空间。
  • 字符串,每个字符串内部都有一个字符数组以及长度等额外信息。
  • 集合类型,比如HashMap、LinkedList等,因为集合类型内部通常会使用一些内部类来封装集合元素,比如Map.Entry。

因此Spark官方建议,在Spark编码实现中,特别是对于算子函数中的代码,尽量不要使用上述三种数据结构,尽量使用字符串替代对象,使用原始类型(比如Int、Long)替代字符串,使用数组替代集合类型,这样尽可能地减少内存占用,从而降低GC频率,提升性能。

但是在笔者的编码实践中发现,要做到该原则其实并不容易。因为我们同时要考虑到代码的可维护性,如果一个代码中,完全没有任何对象抽象,全部是字符串拼接的方式,那么对于后续的代码维护和修改,无疑是一场巨大的灾难。同理,如果所有操作都基于数组实现,而不使用HashMap、LinkedList等集合类型,那么对于我们的编码难度以及代码可维护性,也是一个极大的挑战。因此笔者建议,在可能以及合适的情况下,使用占用内存较少的数据结构,但是前提是要保证代码的可维护性。

原则十:Data Locality本地化级别

PROCESS_LOCAL:进程本地化,代码和数据在同一个进程中,也就是在同一个executor中;计算数据的task由executor执行,数据在executor的BlockManager中;性能最好

NODE_LOCAL:节点本地化,代码和数据在同一个节点中;比如说,数据作为一个HDFS block块,就在节点上,而task在节点上某个executor中运行;或者是,数据和task在一个节点上的不同executor中;数据需要在进程间进行传输
NO_PREF:对于task来说,数据从哪里获取都一样,没有好坏之分
RACK_LOCAL:机架本地化,数据和task在一个机架的两个节点上;数据需要通过网络在节点之间进行传输
ANY:数据和task可能在集群中的任何地方,而且不在一个机架中,性能最差

spark.locality.wait,默认是3s

Spark在Driver上,对Application的每一个stage的task,进行分配之前,都会计算出每个task要计算的是哪个分片数据,RDD的某个partition;Spark的task分配算法,优先,会希望每个task正好分配到它要计算的数据所在的节点,这样的话,就不用在网络间传输数据;

但是可能task没有机会分配到它的数据所在的节点,因为可能那个节点的计算资源和计算能力都满了;所以呢,这种时候,通常来说,Spark会等待一段时间,默认情况下是3s钟(不是绝对的,还有很多种情况,对不同的本地化级别,都会去等待),到最后,实在是等待不了了,就会选择一个比较差的本地化级别,比如说,将task分配到靠它要计算的数据所在节点,比较近的一个节点,然后进行计算。

但是对于第二种情况,通常来说,肯定是要发生数据传输,task会通过其所在节点的BlockManager来获取数据,BlockManager发现自己本地没有数据,会通过一个getRemote()方法,通过TransferService(网络数据传输组件)从数据所在节点的BlockManager中,获取数据,通过网络传输回task所在节点。

对于我们来说,当然不希望是类似于第二种情况的了。最好的,当然是task和数据在一个节点上,直接从本地executor的BlockManager中获取数据,纯内存,或者带一点磁盘IO;如果要通过网络传输数据的话,那么实在是,性能肯定会下降的,大量网络传输,以及磁盘IO,都是性能的杀手。

什么时候要调节这个参数?

观察日志,spark作业的运行日志,推荐大家在测试的时候,先用client模式,在本地就直接可以看到比较全的日志。
日志里面会显示,starting task。。。,PROCESS LOCAL、NODE LOCAL,观察大部分task的数据本地化级别。

如果大多都是PROCESS_LOCAL,那就不用调节了
如果是发现,好多的级别都是NODE_LOCAL、ANY,那么最好就去调节一下数据本地化的等待时长
调节完,应该是要反复调节,每次调节完以后,再来运行,观察日志
看看大部分的task的本地化级别有没有提升;看看,整个spark作业的运行时间有没有缩短

但是注意别本末倒置,本地化级别倒是提升了,但是因为大量的等待时长,spark作业的运行时间反而增加了,那就还是不要调节了。

spark.locality.wait,默认是3s;可以改成6s,10s

默认情况下,下面3个的等待时长,都是跟上面那个是一样的,都是3s

spark.locality.wait.process//建议60s
spark.locality.wait.node//建议30s
spark.locality.wait.rack//建议20s

おすすめ

転載: www.cnblogs.com/cjunn/p/12234198.html