入門大資料---大資料調優彙總

一線大資料發表於2020-07-15

前言

不進行優化的程式碼就是耍流氓。

總體來說大資料優化主要分為三點,一是充分利用CPU,二是節省記憶體,三是減少網路傳輸。

一、Hive/MapReduce調優

1.1 本地模式

Hive預設採用叢集模式進行計算,如果對於小資料量,可以設定為單臺機器進行計算,這樣可以大大縮減查詢觸發任務時間。

使用者可以通過設定hive.exec.mode.local.auto 的值為true,來讓Hive在適當的時候自動啟動這個優化。

set hive.exec.mode.local.auto=true; //開啟本地 mr
//設定 local mr 的最大輸入資料量,當輸入資料量小於這個值時採用 local mr 的方式,
預設為 134217728,即 128M
set hive.exec.mode.local.auto.inputbytes.max=50000000;
//設定 local mr 的最大輸入檔案個數,當輸入檔案個數小於這個值時採用 local mr 的方
式,預設為 4
set hive.exec.mode.local.auto.input.files.max=10;

1.2 null值過濾OR隨機分配null值

  • null值過濾

    對於key值傾斜,有的時候是無效的null導致的,這個時候可以考慮過濾掉。

    hive (default)> insert overwrite table jointable 
    select n.* from (select * from nullidtable where id is not null ) n left join ori o on n.id = 
    o.id;
    
  • null值隨機分配

    如果null不是異常資料,那麼可以採用隨機分配將null值分到不同分割槽,解決資料傾斜。

    insert overwrite table jointable
    select n.* from nullidtable n full join ori o on 
    case when n.id is null then concat('hive', rand()) else n.id end = o.id;
    

1.3 Count(distinct)去重統計優化

對於大資料量去重,可以採用分組的方式進行優化。

hive (default)> select count(id) from (select id from bigtable group by id) a;

1.4 行列過濾

對關聯表進行過濾時,可以考慮在關聯時就進行過濾,提高查詢時間。

hive (default)> select b.id from bigtable b
join (select id from ori where id <= 10 ) o on b.id = o.id;

1.5 資料傾斜

小檔案合併

set hive.input.format= org.apache.hadoop.hive.ql.io.CombineHiveInputFormat;

複雜檔案增加Map數

增加 map 的方法為:根據computeSliteSize(Math.max(minSize,Math.min(maxSize,blocksize)))=blocksize=128M 公式,調整 maxSize 最大值。讓 maxSize 最大值低於 blocksize 就可以增加 map 的個數。

設定最大切片值為100個位元組

hive (default)> set mapreduce.input.fileinputformat.split.maxsize=100;
hive (default)> select count(*) from emp;

合理設定Reduce數

  • 調整 reduce 個數方法一

    (1)每個 Reduce 處理的資料量預設是 256MB

    hive.exec.reducers.bytes.per.reducer=256000000

    (2)每個任務最大的 reduce 數,預設為 1009

    hive.exec.reducers.max=1009

    (3)計算 reducer 數的公式

  • 調整 reduce 個數方法二

    在 hadoop 的 mapred-default.xml 檔案中修改,設定每個 job 的 Reduce 個數

    set mapreduce.job.reduces = 15;

1.6 並行執行

在共享叢集中設定併發執行可以提高執行速度。

set hive.exec.parallel=true; //開啟任務並行執行
set hive.exec.parallel.thread.number=16; //同一個 sql 允許最大並行度,預設為 8。

當然,得是在系統資源比較空閒的時候才有優勢,否則,沒資源,並行也起不來。

1.7 嚴格模式

Hive 提供了一個嚴格模式,可以防止使用者執行那些可能意向不到的不好的影響的查詢。通過設定屬性 hive.mapred.mode 值為預設是非嚴格模式 nonstrict 。開啟嚴格模式需要修改 hive.mapred.mode 值為 strict,開啟嚴格模式可以禁止 3 種型別的查詢

1).對於分割槽表,除非 where 語句中含有分割槽欄位過濾條件來限制範圍,否則不允許執行。

2).對於使用了 order by 語句的查詢,要求必須使用 limit 語句。

3).限制笛卡爾積的查詢。對關係型資料庫非常瞭解的使用者可能期望在執行 JOIN 查詢的時候不使用 ON 語句而是使用 where 語句,這樣關聯式資料庫的執行優化器就可以高效地將WHERE 語句轉化成那個 ON 語句。

1.8 JVM重用

在小檔案場景或者task特別多的情況下,執行時間都很短。JVM重用可以使用同一個JVM在同一個Job裡面重複使用N次。N值在mapred-site.xml檔案中進行配置。

<property>
 <name>mapreduce.job.jvm.numtasks</name>
 <value>10</value>
 <description>How many tasks to run per jvm. If set to -1, there is
 no limit. 
 </description>
</property>

缺點:JVM重用會一直佔用使用到的task插槽,以便進行重用,如果遇到了某個job裡面的reduce task分配不均勻,導致出現某幾個task佔用task時間很長,其它task空閒也沒法被其它job使用,只有所有的task都執行完後才會釋放。

1.9 推測執行

對於某些耗時的任務,可以啟動推測執行,這樣就會把“拖後腿”的任務找出來,然後啟動個備份任務執行相同的資料。最後選出執行最快的為最終結果。

設定開啟推測執行引數:Hadoop 的 mapred-site.xml 檔案中進行配置:

<property>
 <name>mapreduce.map.speculative</name>
 <value>true</value>
 <description>If true, then multiple instances of some map tasks 
 may be executed in parallel.</description>
</property>
<property>
 <name>mapreduce.reduce.speculative</name>
 <value>true</value>
 <description>If true, then multiple instances of some reduce tasks 
 may be executed in parallel.</description>
</property>

不過 hive 本身也提供了配置項來控制 reduce-side 的推測執行:

<property>
 <name>hive.mapred.reduce.tasks.speculative.execution</name>
 <value>true</value>
 <description>Whether speculative execution for reducers should be turned on. 
</description>
 </property>

PS:對於時差要求很苛刻的建議關閉掉推測執行。對於執行很長的任務也不建議開啟,因為會浪費很大資源。

1.10 HDFS小檔案解決方案

1)Hadoop Archive:

是一個高效地將小檔案放入 HDFS 塊中的檔案存檔工具,它能夠將多個小檔案打包成

一個 HAR 檔案,這樣就減少了 namenode 的記憶體使用。

2)Sequence file:

sequence file 由一系列的二進位制 key/value 組成,如果 key 為檔名,value 為檔案內容,

則可以將大批小檔案合併成一個大檔案。

3)CombineFileInputFormat:

CombineFileInputFormat 是一種新的 inputformat,用於將多個檔案合併成一個單獨的

split,另外,它會考慮資料的儲存位置。

二、Spark調優

2.1 效能監控方式

Spark Web UI

通過 http://master:4040我們可以獲得執行中的程式資訊。

(1)stages和tasks排程情況;

(2)RDD大小和記憶體使用情況;

(3)系統環境資訊;

(4)正在執行的executor資訊;

設定歷史伺服器記錄歷史資訊:

(1)在$SPARK_HOME/conf/spark-env.sh中設定:

export SPARK_HISTORY_OPTS="-Dspark.history.retainedApplications=50 Dspark.history.fs.logDirectory=hdfs://master01:9000/directory"

說明:spark.history.retainedApplica-tions僅顯示最近50個應用。

spark.history.fs.logDirectory:Spark History Server頁面只顯示該路徑下的資訊。

(2)$SPARK_HOME/conf/spark-defaults.conf

spark.eventLog.enabled true

spark.eventLog.dir hdfs://hadoop000:8020/directory #應用在執行過程中所有的資訊均記錄在該屬性指定的路徑下

spark.eventLog.compress true

(3)HistoryServer 啟動

$SPARK_HOMR/bin/start-histrory-server.sh

(4)HistoryServer 停止

$SPARK_HOMR/bin/stop-histrory-server.sh

--同樣executor的logs也是檢視的一個出處:

Standalone 模式:$SPARK_HOME/logs

YARN 模式:在 yarn-site.xml 檔案中配置了 YARN 日誌的存放位置:yarn.nodemanager.log-dirs,或使用命令獲取 yarn logs -applicationId。

其它監控工具

Nmon

Jmeter

Jprofiler

2.2 調優要點

記憶體調優要點

1.物件佔記憶體,優化資料結構

(1)使用物件陣列以及原始型別(primitive type)陣列以替代 Java 或 者 Scala 集合類(collection class)。fastutil 庫為原始資料型別提供了非常方便的集合類,且相容 Java 標準類庫。

(2)儘可能地避免採用含有指標的巢狀資料結構來儲存小物件。

(3)考慮採用數字 ID 或者列舉型別以便替代 String 型別的主鍵。

(4)如果記憶體少於 32GB,設定 JVM 引數-XX:+UseCom-pressedOops以便將 8 位元組指標修改成 4 位元組。與此同時,在 Java 7 或者更高版本,設定 JVM 引數-XX:+UseC-----ompressedStrings 以便採用 8 位元來編碼每一個 ASCII 字元。

2.頻繁 GC 或者 OOM

針對這種情況,首先要確定現象是發生在 Driver 端還是在 Executor 端,然後在分別處理。

Driver 端:通常由於計算過大的結果集被回收到 Driver 端導致,需要調大 Driver 端的記憶體解決,或者進一步減少結果集的數量。

Executor 端:

(1)以外部資料作為輸入的 Stage:可以增加 partition 的數量(即 task 的數量)來減少每個 task 要處理的資料,來減少 GC 的可能性。

(2)以 shuffle 作為輸入的 Stage:解決資料傾斜問題。

開啟推測機制

在 spark-default.conf 中新增:spark.speculation true

推測機制與以下幾個引數有關:

  1. spark.speculation.interval 100:檢測週期,單位毫秒;
  2. spark.speculation.quantile 0.75:完成 task 的百分比時啟動推測;
  3. spark.speculation.multiplier 1.5:比其他的慢多少倍時啟動推測。

資料傾斜優化

  • 查詢資料傾斜程式碼

    根據shuffler確定資料傾斜程式碼,然後通過隨機取樣找到傾斜資料。

    val sampledPairs = pairs.sample(false, 0.1)
    val sampledWordCounts = sampledPairs.countByKey()
    sampledWordCounts.foreach(println(_))
    

緩解/消除資料傾斜

避免資料來源傾斜

比如資料來源是Kafka,通常一個分割槽對應一個Task,所以如果分割槽資料不均衡,則導致spark處理不均衡。

比如資料來源是Hive,如果Hive資料不均衡,也會導致Spark資料傾斜。

解決方案是預處理或者其它。

調整並行度

比如reduceByKey(1000)。如果是group by,join需要設定引數即spark.sql.shuffle.partitions,該引數代表了shuffle read task的並行度,該值預設是200,對於很多場景來說有點過小。設定完後不同的key就能分到不同的task去處理。

將join中的shuffler避免掉

針對一個大表一個小表的join操作,使用廣播變數將較小的資料進行廣播,這樣就可以把join改為map操作。

兩階段聚合

針對RDD執行ReduceByKey等聚合shuffler運算元,以及Spark Sql執行GroupByKey等聚合運算元,針對資料傾斜,可以先在key前面打上隨機字首,進行聚合,然後再把字首去掉進行聚合,有效解決值分配不均勻問題。

示例如下:

// 第一步,給 RDD 中的每個 key 都打上一個隨機字首。
JavaPairRDD<String, Long> randomPrefixRdd = rdd.mapToPair(
 new PairFunction<Tuple2<Long,Long>, String, Long>() {
 private static final long serialVersionUID = 1L;
 @Override
 public Tuple2<String, Long> call(Tuple2<Long, Long> tuple)
 throws Exception {
 Random random = new Random();
 int prefix = random.nextInt(10);
 return new Tuple2<String, Long>(prefix + "_" + tuple._1, tuple._2);
 }
 });
// 第二步,對打上隨機字首的 key 進行區域性聚合。
JavaPairRDD<String, Long> localAggrRdd = randomPrefixRdd.reduceByKey(
 new Function2<Long, Long, Long>() {
 private static final long serialVersionUID = 1L;
 @Override
 public Long call(Long v1, Long v2) throws Exception {
 return v1 + v2;
 }
 });
// 第三步,去除 RDD 中每個 key 的隨機字首。
JavaPairRDD<Long, Long> removedRandomPrefixRdd = localAggrRdd.mapToPair(
 new PairFunction<Tuple2<String,Long>, Long, Long>() {
 private static final long serialVersionUID = 1L;
 @Override
 public Tuple2<Long, Long> call(Tuple2<String, Long> tuple)
 throws Exception {
 long originalKey = Long.valueOf(tuple._1.split("_")[1]);
 return new Tuple2<Long, Long>(originalKey, tuple._2);
 }
 });
// 第四步,對去除了隨機字首的 RDD 進行全域性聚合。
JavaPairRDD<Long, Long> globalAggrRdd = removedRandomPrefixRdd.reduceByKey(
 new Function2<Long, Long, Long>() {
 private static final long serialVersionUID = 1L;
 @Override
 public Long call(Long v1, Long v2) throws Exception {
 return v1 + v2;
 }
 });

兩階段聚合案例

  1. 通過如下 SQL,將 id 為 9 億到 9.08 億共 800 萬條資料的 id 轉為9500048 或者 9500096,其它資料的 id 除以 100 取整。從而該資料集中,id 為 9500048 和 9500096 的資料各 400 萬,其它 id 對應的資料記錄數均為 100 條。這些資料存於名為 test 的表中。
  2. 對於另外一張小表 test_new,取出 50 萬條資料,並將 id(遞增且唯一)除以 100 取整,使得所有 id 都對應 100 條資料。
  3. 通過如下操作,實現傾斜 Key 的分散處理:
  4. 將 leftRDD 中傾斜的 key(即 9500048 與 9500096)對應的資料單獨過濾出來,且加上 1 到 24 的隨機字首,並將字首與原資料用逗號分隔(以方便之後去掉字首)形成單獨的 leftSkewRDD。
  5. 將 rightRDD 中傾斜 key 對應的資料抽取出來,並通過 flatMap 操作將該資料集中每條資料均轉換為 24 條資料(每條分別加上 1 到 24 的隨機字首),形成單獨的 rightSkewRDD。
  6. 將 leftSkewRDD 與 rightSkewRDD 進行 Join,並將並行度設定為 48,且 在 Join 過 程 中 將 隨 機 前 綴 去 掉 , 得 到 傾 斜 數 據集的 Join 結 果skewedJoinRDD。
  7. 將 leftRDD 中不包含傾斜 Key 的 數 據 抽 取 出 來 作 為 單 獨 的leftUnSkewRDD。
  8. 對 leftUnSkewRDD 與原始的 rightRDD 進行 Join,並行度也設定為 48,得到 Join 結果 unskewedJoinRDD。
  9. 通過 union 運算元將 skewedJoinRDD 與 unskewedJoinRDD 進行合併,從而得到完整的 Join 結果集。

具體實現程式碼如下:

public class SparkDataSkew{
 public static void main(String[] args) {
 int parallelism = 48;
 SparkConf sparkConf = new SparkConf();
 sparkConf.setAppName("SolveDataSkewWithRandomPrefix");
 sparkConf.set("spark.default.parallelism", parallelism + "");
 JavaSparkContext javaSparkContext = new JavaSparkContext(sparkConf);
 JavaPairRDD<String, String> leftRDD = 
javaSparkContext.textFile("hdfs://hadoop1:8020/apps/hive/warehouse/default/test/")
 .mapToPair((String row) -> {
 String[] str = row.split(",");
 return new Tuple2<String, String>(str[0], str[1]);
 });
 JavaPairRDD<String, String> rightRDD = 
javaSparkContext.textFile("hdfs://hadoop1:8020/apps/hive/warehouse/default/test_new/")
 .mapToPair((String row) -> {
 String[] str = row.split(",");
 return new Tuple2<String, String>(str[0], str[1]);
 });
 String[] skewedKeyArray = new String[]{"9500048", "9500096"};
 Set<String> skewedKeySet = new HashSet<String>();
 List<String> addList = new ArrayList<String>();
 for(int i = 1; i <=24; i++) {
 addList.add(i + "");
 }
 for(String key : skewedKeyArray) {
 skewedKeySet.add(key);
 }
 Broadcast<Set<String>> skewedKeys = javaSparkContext.broadcast(skewedKeySet);
 Broadcast<List<String>> addListKeys = javaSparkContext.broadcast(addList);
 JavaPairRDD<String, String> leftSkewRDD = leftRDD
 .filter((Tuple2<String, String> tuple) -> skewedKeys.value().contains(tuple._1()))
 .mapToPair((Tuple2<String, String> tuple) -> new Tuple2<String, String>((new 
Random().nextInt(24) + 1) + "," + tuple._1(), tuple._2()));
 JavaPairRDD<String, String> rightSkewRDD = rightRDD.filter((Tuple2<String, String> 
tuple) -> skewedKeys.value().contains(tuple._1()))
 .flatMapToPair((Tuple2<String, String> tuple) -> addListKeys.value().stream()
 .map((String i) -> new Tuple2<String, String>( i + "," + tuple._1(), tuple._2()))
 .collect(Collectors.toList())
 .iterator()
 );
 JavaPairRDD<String, String> skewedJoinRDD = leftSkewRDD
 .join(rightSkewRDD, parallelism)
 .mapToPair((Tuple2<String, Tuple2<String, String>> tuple) -> new Tuple2<String, 
String>(tuple._1().split(",")[1], tuple._2()._2()));
 JavaPairRDD<String, String> leftUnSkewRDD = leftRDD.filter((Tuple2<String, String> 
tuple) -> !skewedKeys.value().contains(tuple._1()));
 JavaPairRDD<String, String> unskewedJoinRDD = leftUnSkewRDD.join(rightRDD, 
parallelism).mapToPair((Tuple2<String, Tuple2<String, String>> tuple) -> new Tuple2<String, 
String>(tuple._1(), tuple._2()._2()));
 skewedJoinRDD.union(unskewedJoinRDD).foreachPartition((Iterator<Tuple2<String, 
String>> iterator) -> {
 AtomicInteger atomicInteger = new AtomicInteger();
 iterator.forEachRemaining((Tuple2<String, String> tuple) -> 
atomicInteger.incrementAndGet());
 });
 javaSparkContext.stop();
 javaSparkContext.close();
 } 
}

大表隨機新增 N 種隨機字首,小表擴大 N 倍

過濾少數導致傾斜的 key

2.3 Shuffle調優

調優概述

程式碼開發,資源分配和資料傾斜是重中之重,除此之外,Shuffler作為一個補充,也需要學習下。

shuffler相關引數調優

  • spark.shuffle.file.buffer

    預設值:32K

    引數說明:緩衝大小,超過緩衝大小才會寫入磁碟。

    調優建議:如果作業可用的記憶體資源較為充足的話,可以適當增加這個引數的大小(),從而減少 shuffle write 過程中溢寫磁碟檔案的次數,也就可以減少磁碟 IO 次數,進而提升效能。在實踐中發現,合理調節該引數,效能會有 1%~5%的提升。

  • spark.reducer.maxSizeInFlight

    預設值:48m

    引數說明:這個 buffer 緩衝決定了每次能夠拉取多少資料。

    調優建議:如果作業可用的記憶體資源較為充足的話,可以適當增加這個引數的大小(比如 96m),從而減少拉取資料的次數,也就可以減少網路傳輸的次數,進而提升效能。在實踐中發現,合理調節該引數,效能會有1%~5%的提升。

  • spark.shuffle.io.maxRetries

    預設值:3

    引數說明:拉去失敗重試次數。

    調優建議:對於那些包含了特別耗時的 shuffle 操作的作業,建議增加重試最大次數(比如 60 次),以避免由於 JVM 的 full gc 或者網路不穩定等因素導致的資料拉取失敗。在實踐中發現,對於針對超大資料量(數十億~上百億)的 shuffle 過程,調節該引數可以大幅度提升穩定性。

  • spark.shuffle.io.retryWait

    預設值:5s

    引數說明:重試拉取資料的等待時間,預設是5s。

    調優建議:建議加大間隔時長(比如 60s),以增加 shuffle 操作的穩定性。

  • spark.shuffle.memoryFraction

    預設值:0.2

    引數說明:分配給聚合操作的記憶體比例,預設是20%。

  • spark.shuffle.manager

預設值:sort

2.4 程式開發調優

原則一:避免建立重複的 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(...)

原則四:儘量避免使用 shuffle 類運算元

開發過程中,能避免則儘可能避免使用 reduceByKey、join、distinct、repartition 等會進行 shuffle 的運算元,儘量使用 map 類的非shuffle 運算元。這樣的話,沒有 shuffle 操作或者僅有較少 shuffle 操作的Spark 作業,可以大大減少效能開銷。

Broadcast 與 map 進行 join 程式碼示例:

// 傳統的 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 的全量資料。

原則五:使用 map-side 預聚合的 shuffle 操作

如果因為業務需要,一定要使用 shuffle 操作,無法用 map 類的運算元來替代,那麼儘量使用可以 map-side 預聚合的運算元。

使用reduceByKey,aggregateByKey代替groupByKey,因為reduceByKey和aggregateByKey會進行預聚合,groupByKey不會。

原則六:使用高效能的運算元

使用 reduceByKey/aggregateByKey 替代 groupByKey,詳情見“原則五:使用 map-side 預聚合的 shuffle 操作”。

使用 mapPartitions 替代普通 map。

使用 filter 之後進行 coalesce 操作。

使用 repartitionAndSortWithinPartitions 替代 repartition 與 sort 類操作。

原則七:廣播大變數

有時在開發過程中,會遇到需要在運算元函式中使用外部變數的場景,那麼此時就應該使用 Spark的廣播(Broadcast)功能來提升效能。因為如果不使用廣播變數,那麼每個任務會拉取資料並建立一個副本,這樣會大大增加網路開銷,並佔用系統記憶體。如果使用廣播變數的話,資料就會保留一份。

廣播大變數程式碼示例:

// 以下程式碼在運算元函式中,使用了外部的變數。
// 此時沒有做任何特殊操作,每個 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 優化序列化效能

程式碼示例:

// 建立 SparkConf 物件。
val conf = new SparkConf().setMaster(...).setAppName(...)
// 設定序列化器為 KryoSerializer。
conf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
// 註冊要序列化的自定義型別。
conf.registerKryoClasses(Array(classOf[MyClass1], classOf[MyClass2]))

原則九:優化資料結構

Java 中,有三種型別比較耗費記憶體:

1.物件。 2.集合型別,比如HashMap,LinedList等。3.字串,每個字串內部都有一個字元陣列以及長度等額外資訊。

2.5 執行資源調優

在spark-submit調節資源引數來提高資源利用率。

  • num-executors

    引數說明:設定spark作業總共用多少個executor來執行。

    引數調優建議:每個spark作業一般設定50~100個左右的Executor程式比較合適。太小不能充分利用資源,太大佇列無法提供足夠的資源。

  • executor-memory

    引數說明:設定每個Executor程式的記憶體。

    引數調優建議:每個 Executor 程式的記憶體設定 4G~8G 較為合適。但是這只是一個參考值,具體的設定還是得根據不同部門的資源佇列來定。可以看看自己團隊的資源佇列的最大記憶體限制是多少,num-executors 乘 以 executor-memory,是不能超過佇列的最大記憶體量的。此外,如果你是跟團隊裡其他人共享這個資源佇列,那麼申請的記憶體量最好不要超過資源佇列最大總記憶體的 1/3~1/2,避免你自己的 Spark 作業佔用了佇列所有的資源,導致別的同學的作業無法執行。

  • executor-cores

    引數說明:設定每個Executor程式CUP core數量。因為每個cpu core一個時間只能執行一個task,所以cpu core數量越多,執行速度越快。

    引數調優建議:Executor 的 CPU core 數量設定為 2~4 個較為合適。同樣得根據不同部門的資源佇列來定,可以看看自己的資源佇列的最大CPU core 限制是多少,再依據設定的 Executor 數量,來決定每個 Executor程式可以分配到幾個 CPU core。同樣建議,如果是跟他人共享這個佇列,那 麼 num-executors * executor-cores 不 要 超 過 隊 列 總 CPU core 的1/3~1/2 左右比較合適,也是避免影響其他同學的作業執行。

  • driver-memory

    引數說明:設定Driver程式的記憶體。

    引數調優建議:Driver 的記憶體通常來說不設定,或者設定 1G 左右應該就夠了。

  • spark.default.parallelism

    引數說明:該引數用於設定每個 stage 的預設 task 數量。這個引數極為重要,如果不設定可能會直接影響你的 Spark 作業效能。

    引數調優建議:Spark 作業的預設 task 數量為 500~1000 個較為合適。很 多 同 學 常 犯 的 一 個 錯 誤 就 是 不 去 設 置 這 個 參 數 , 那 麼 此 時 就 會 導 致Spark 自己根據底層 HDFS 的 block 數量來設定 task 的數量,預設是一個HDFS block 對應一個 task。通常來說,Spark 預設設定的數量是偏少的(比如就幾十個 task),如果 task 數量偏少的話,就會導致你前面設定好的Executor 的引數都前功盡棄。試想一下,無論你的 Executor 程式有多少個,記憶體和 CPU 有多大,但是 task 只有 1 個或者 10 個,那麼 90%的 Executor程式可能根本就沒有 task 執行,也就是白白浪費了資源!因此 Spark 官網建議的設定原則是,設定該引數為 num-executors * executor-cores 的 2~3倍較為合適,比如 Executor 的總 CPU core 數量為 300 個,那麼設定 1000個 task 是可以的,此時可以充分地利用 Spark 叢集的資源。

  • spark.storage.memoryFraction

    引數說明:設定持久化資料在Executor佔比,預設是0.6。

    根據你選擇的不同的持久化策略,如果記憶體不夠時,可能資料就不會持久化,或者資料會寫入磁碟。

    引數調優建議:根據實際,可以適當提高,讓資料寫入記憶體。

  • spark.shuffle.memoryFraction

    引數說明:該引數用於設定 shuffle 過程中一個 task 拉取到上個 stage的 task 的輸出後,進行聚合操作時能夠使用的 Executor 記憶體的比例,預設是 0.2。

    引數調優建議:如果 Spark 作業中的 RDD 持久化操作較少,shuffle 操作較多時,建議降低持久化操作的記憶體佔比,提高 shuffle 操作的記憶體佔比比例,避免 shuffle 過程中資料過多時記憶體不夠用,必須溢寫到磁碟上,降低了效能。此外,如果發現作業由於頻繁的 gc 導致執行緩慢,意味著 task執行使用者程式碼的記憶體不夠用,那麼同樣建議調低這個引數的值。

    資源引數的調優,沒有一個固定的值,需要同學們根據自己的實際情況(包括 Spark 作業中的 shuffle 運算元量、RDD 持久化運算元量以及 spark web ui 中顯示的作業 gc 情況),同時參考給出的原理以及調優建議,合理地設定上述引數。

    資源引數參考示例:

    以下是一份 spark-submit 命令的示例,大家可以參考一下,並根據自己的實際情況進行調節。

    ./bin/spark-submit \
    --master yarn-cluster \
    --num-executors 100 \
    --executor-memory 6G \
    --executor-cores 4 \
    --driver-memory 1G \
    --conf spark.default.parallelism=1000 \
    --conf spark.storage.memoryFraction=0.5 \
    --conf spark.shuffle.memoryFraction=0.3 \
    

三、Flink調優

3.1 Backpressure調優

  • web.backpressure.cleanup-interval

    說明:當啟動反壓資料採集後,獲取反壓前等待時間,預設是60s。

  • web.backpressure.delay-between-samples:Stack Trace

    說明:抽樣到確認反壓狀態之間的時延,預設為50ms。

  • web.backpressure.num-samples

    說明:設定Stack Trace抽樣數以確定反壓狀態,預設為100。

3.2 Checkpointing優化

通過調整Checkpointing之間的時間間隔進行優化。

val env=StreamExecutionEnvironment.getExecutionEnvironment
env.getCheckpointConfig.setMinPauseBetweenCheckpoints(milliseconds)

3.3 狀態資料壓縮

目前可用的壓縮演算法是Snappy,設定如下:

val env=StreamExecutionEnvironment.getExecutionEnvironment
val config = env.getConfig
config.setUseSnapshotCompression(true)

3.4 Flink記憶體優化

Flink,Spark等大資料引擎都實現了自己的記憶體管理,有效解決JVM記憶體溢位問題。

  • JobManager配置

    jobmanager.heap.size:設定JobManager堆記憶體大小,預設為1024MB。
    
  • TaskManager配置

    TaskManager作為Flink叢集中的工作節點,所有任務的計算邏輯均執行在TaskManager之上,因此對TaskManager記憶體配置顯得尤為重要,可以通過以下引數配置對TaskManager進行優化和調整。

    taskmanager.heap.size

    說明:設定TaskManager堆記憶體大小,預設值為1024M,如果在Yarn的叢集中,TaskManager取決於Yarn分配給TaskManager Container的記憶體大小,且Yarn環境下一般會減掉一部分記憶體用於Container的容錯。

    taskmanager.jvm-exit-on-oom

    說明:設定TaskManager是否會因為JVM發生記憶體溢位而停止,預設為false,當TaskManager發生記憶體溢位時,也不會導致TaskManager停止。

    taskmanager.memory.size

    說明:設定TaskManager記憶體大小,預設為0,如果不設定該值將會使用taskmanager.memory.fraction作為記憶體分配依據。

    taskmanager.memory.fraction

    說明:設定TaskManager堆中去除Network Buffers記憶體後的記憶體分配比例。該記憶體主要用於TaskManager任務排序、快取中間結果等操作。例如,如果設定為0.8,則代表TaskManager保留80%記憶體用於中間結果資料的快取,剩下20%的記憶體用於建立使用者定義函式中的資料物件儲存。注意,該引數只有在taskmanager.memory.size不設定的情況下才生效。

    taskmanager.memory.off-heap

    說明:設定是否開啟堆外記憶體供Managed Memory或者Network Buffers使用。

    taskmanager.memory.preallocate

    說明:設定是否在啟動TaskManager過程中直接分配TaskManager管理記憶體。

    taskmanager.numberOfTaskSlots

    說明:每個TaskManager分配的slot數量。

3.5 設定Network記憶體比例

taskmanager.network.memory.fraction

說明:JVM中用於Network Buffers的記憶體比例。

taskmanager.network.memory.min

說明:最小的Network Buffers記憶體大小,預設為64MB。

taskmanager.network.memory.max

說明:最大的Network Buffers記憶體大小,預設1GB。

taskmanager.memory.segment-size

說明:記憶體管理器和Network棧使用的Buffer大小,預設為32KB。

3.6 堆記憶體調優

預設Flink使用的Parallel Scavenge的垃圾回收器,可以改用G1垃圾回收器。

啟動引數:

env.java.opts= -server -XX:+UseG1GC -XX:MaxGCPauseMillis=300 -XX:+PrintGCDetails

  • -XX:MaxGCPauseMillis:設定允許的最大GC停頓時間,預設是200ms。

  • -XX:G1HeapRegionSize:每個分割槽的大小,預設值會根據整個堆區的大小計算出來,範圍是1M~32M,取值是2的冪,計算的傾向是儘量有2048個分割槽數。

  • -XX:MaxTenuringThreshold=n:晉升到老年代的“年齡”閾值,預設值為15。

  • -XX:InitiatingHeapOccupancyPercent:一般會簡寫IHOP,預設是45%,這個佔比跟併發週期的啟動相關,當空間佔比達到這個值時,會啟動併發週期。如果經常出現FullGC,可以調低該值,今早的回收可以減少FullGC的觸發,但如果過低,則併發階段會更加頻繁,降低應用的吞吐。

  • -XX:G1NewSizePercent:年輕代最小的堆空間佔比,預設是5%。

  • -XX:G1MaxNewSizePercent:年輕代最大的堆空間佔比,預設是60%。

  • -XX:ConcGCThreads:併發執行的執行緒數,預設值接近整個應用程式數的1/4。

  • -XX:-XX:G1HeapWastePercent:允許的浪費空間的佔比,預設是5%。如果併發標記可回收的空間小於5%,則不會丟擲MixedGC。

  • -XX:G1MixedGCCountTarget:一次全域性併發標記之後,後續最多執行的MixedGC次數。預設值是8。

    系列傳送門

相關文章