Apache Spark 在愛奇藝的應用實踐

01

   Apache Spark 在愛奇藝的現狀


Apache Spark 是愛奇藝大數據平臺主要使用的離線計算框架,並支持部分流計算任務,用於數據處理、數據同步、數據查詢分析等場景:
  • 數據處理:在數據開發平臺中支持開發者提交 Spark Jar 包任務或Spark SQL 任務對數據進行ETL處理。
  • 數據同步 :愛奇藝自研的BabelX數據同步工具基於Spark 計算框架開發,支持 Hive、MySQL、MongoDB 等 15 種數據源之間的數據交換,支持多集羣、多雲間的數據同步,支持配置化的全託管數據同步任務。
  • 數據分析:數據分析師、運營同學在魔鏡即席查詢平臺上提交SQL或配置數據指標查詢,通過 Pilot 統一SQL網關調用 Spark SQL 服務進行查詢分析。

目前,愛奇藝Spark服務日均運行超過20萬Spark任務,整體佔用超過一半的大數據計算資源。
在愛奇藝大數據平臺架構升級優化的過程中,對Spark 服務進行了版本迭代、服務優化、任務SQL 化和資源成本治理等改造,大幅提升了離線任務的計算效率和資源節省。


02

   Spark計算框架應用優化


  • 優秀特性的落地

隨着內部 Spark 版本的迭代升級,我們對 Spark 新版本的一些優秀特性進行了調研和落地:動態資源分配、自適應查詢優化、動態分區裁剪等

  • 動態資源分配(DRA) :用戶申請資源存在盲目性,並且 Spark 任務各個階段的資源需求也不相同,不合理的資源配置導致任務資源浪費或執行過慢。我們在 Spark 2.4.3 開始上線了 External Shuffle Service,並開啓了動態資源分配(DRA)。開啓後,Spark會根據當前運行階段的資源需求,動態地啓動或釋放Executor。DRA上線後,Spark任務的資源消耗降低了20%。
  • 自適應查詢優化(AQE) :自適應查詢優化(AQE)是在Spark 3.0中引入的優秀特性,根據前置階段運行時的統計指標,動態優化後續階段的執行計劃,自動選擇合適的Join策略、優化傾斜的Join、合併小分區、拆分大分區等。我們在升級Spark 3.1.1後,默認開啓了AQE,很好地解決了小文件、數據傾斜等問題,並且極大地提升了Spark 的計算性能,整體性能提升了大約10%。
  • 動態分區裁剪(DPP) :SQL計算引擎中通常通過謂詞下推的方式來減少從數據源讀取的數據量,進而提升計算效率。在Spark3中引入了一種新的下推方式:動態分區裁剪和Runtime Filter,通過首先計算Join的小表,根據計算結果對Join的大表進行過濾,從而減少大表讀取的數據量。我們對這兩個特性進行調研測試,並默認開啓DPP,在部分業務場景下性能提升了33倍。不過,我們發現在Spark 3.1.1中,開啓DPP會導致含較多子查詢的SQL解析特別慢。因此,我們實現了一個優化規則:計算子查詢的數量,當超過5 個時,關閉DPP 優化。

  • 計算框架增強

在使用 Spark 的過程中,我們也遇到了一些問題,通過跟進社區最新進展,發現並打入一些 Patch 進行解決。另外,我們也自行對Spark做了一些改進,以適用於各種應用場景,並增強計算框架的穩定性。

  • 支持併發寫入
由於Spark 3.1.1默認將Hive Parquet格式的錶轉換成Spark內置的Parquet Writer,使用InsertIntoHadoopFsRelationCommand算子寫入數據(spark.sql.hive.convertMetastoreParquet=true)。靜態分區寫入時,會將臨時目錄直接建在表路徑下。當多個靜態分區寫入的任務同時寫同一個表的不同分區時,存在任務寫入失敗或者數據丟失的風險(一個任務commit時會清理整個臨時目錄,導致其他任務的數據丟失)。
我們爲InsertIntoHadoopFsRelationCommand算子加上一個forceUseStagingDir參數,使用任務專屬的Staging目錄作爲臨時目錄。這樣一來,不同的任務就使用了不同的臨時目錄,進而解決了併發寫入的問題。我們已向社區提交了相關Issue【SPARK-37210】。
  • 支持查詢子目錄
Hive升級到3.x後,默認使用Tez引擎,當執行Union語句時會產生HIVE_UNION_SUBDIR子目錄。由於Spark會忽略子目錄的數據,因此讀取不到數據。
這個問題可以通過將Parquet/Orc Reader回退到Hive Reader 解決,添加以下參數:

不過使用Spark內置Parquet Reader會有更好的性能,所以我們放棄了回退到Hive Reader的方案,而是對Spark進行改造。由於Spark已支持通過recursiveFileLookup參數讀取非分區表的子目錄,我們對此進行了擴展,支持了讀取分區表的子目錄,詳細可見:【SPARK-40600】
  • JDBC 數據源增強
在數據同步應用中有大量的JDBC數據源的任務,爲了提高運行效率並適用各種應用場景,我們對Spark內置的JDBC數據源進行了如下的一些改造:
分片條件下推 Spark對JDBC數據源分片後通過子查詢方式插入分片條件,我們發現在MySQL 5.x中對於子查詢條件無法進行下推,所以我們在數據同步節點中通過添加一個佔位符表示條件位置,並且在Spark中插入分片條件時下推到子查詢內部,進而實現分片條件下推的能力。
多種寫入模式 我們在Spark中對於JDBC數據源實現了多種的寫入模式。
  • Normal:普通模式,使用默認的INSERT INTO寫入
  • Upsert:主鍵存在時更新,以INSERT INTO...ON DUPLICATE KEY UPDATE 方式寫入
  • Ignore:主鍵存在時忽略,以INSERT IGNORE INTO方式寫入
靜默模式: JDBC寫入發生異常時,僅打印異常日誌不終止任務。
支持Map類型 :我們使用JDBC數據源讀寫ClickHouse數據,ClickHouse中的Map類型在JDBC數據源中不支持,因此我們添加了對Map類型的支持。
  • 本地磁盤寫入大小限制
Spark中的Shuffle、Cache、Spill等操作會產生一些本地文件,當寫入的本地文件過多時可能將計算節點的磁盤寫滿,進而影響集羣的穩定性。
對此,我們在Spark中添加了磁盤寫入量的指標,當磁盤寫入量達到閾值時拋出異常,並且在TaskScheduler中判斷Task失敗的異常,當捕獲到磁盤寫入限制的異常時調用DagScheduler 的cancelJob方法停止磁盤使用過大的任務。
同時,我們還在ExecutorMetric中添加Executor Disk Usage的指標暴露Spark Executor當前的磁盤使用量,便於觀察趨勢和數據分析。

  • 計算資源治理

Spark服務佔用了大量的計算資源。我們開發了異常治理平臺,針對Spark批處理任務和流計算任務,分別進行了計算資源審計和治理。
  • 資源審計
在日常運維中,我們發現大量Spark任務存在內存浪費、CPU利用率低等問題。爲了找到存在這些問題的任務,我們將Spark任務運行時的資源指標投遞到Prometheus分析任務資源利用率,通過解析Spark EventLog獲取資源配置和計算詳情數據。
  • 資源治理
通過優化任務的資源參數、開啓動態資源分配等措施有效地提升了Spark任務的計算資源利用率,Spark版本升級也帶來了大量資源節省。
資源參數的優化分爲內存和CPU優化,異常治理平臺根據任務在過去七天內的資源使用率高峯值,爲其推薦合理的資源參數設置,以此提高Spark任務的資源利用率。
以內存優化爲例,用戶常常通過增加內存來解決內存溢出(OOM)的問題,而忽視深層次排查OOM的原因。這造成大量Spark任務內存參數設置過高,隊列資源內存和CPU比例不均衡。我們通過獲取Spark Executor內存指標併發送異常工單通知用戶,引導他們合理配置內存參數和分區數量。
  • 治理收益
經過近一年的資源審計治理,異常治理平臺累計發出1600多個工單,共節省了約27% 計算資源。


03

   Spark SQL 服務的落地與優化


  • Spark SQL 服務

在愛奇藝Spark SQL服務經歷了多個階段,從Spark原生的Thrift Server服務到 Kyuubi 0.7再到Apache Kyuubi 1.4 版本,爲服務架構及穩定性帶來了很大的改善。

目前,Spark SQL服務已經代替Hive成爲了愛奇藝主要的離線數據處理引擎,日均運行15萬左右的SQL任務。

  • Spark SQL 服務優化

  • 優化存儲和計算效率
我們在Spark SQL服務的探索過程中也遇到了一些問題,主要包括了產生大量的小文件、存儲變大以及計算變慢等問題,對此我們也進行了一系列存儲和計算效率的優化。
啓用ZStandard壓縮,提高壓縮率
Zstd是Meta開源的壓縮算法,相對於其他的壓縮格式有較大的壓縮率和解壓縮效率提升。我們實測效果顯示,Zstd壓縮率與Gzip相當,解壓縮速度優於Snappy。因此,我們在Spark升級的過程中使用Zstd壓縮格式作爲默認的數據壓縮格式,並且將Shuffle數據也設置成Zstd壓縮,爲集羣存儲帶來了很大的節省,在廣告數據場景下應用,壓縮率提升3.3倍,節省76%存儲成本。
添加Rebalance階段,避免產生小文件
小文件問題是Spark SQL比較重要的一個問題:小文件過多會對Hadoop NameNode造成較大壓力,影響集羣穩定性。原生的Spark計算框架並沒有一個很好的自動化方案來解決小文件問題。對此,我們也調研了一些業界的解決方案,最終使用Kyuubi服務自帶的小文件優化方案。

Kyuubi提供的insertRepartitionBeforeWrite優化器能在Insert算子前插入Rebalance算子,結合AQE自動合併小分區、拆分大分區的邏輯,實現了輸出文件大小的控制,很好地解決了小文件問題。 啓用後,Spark SQL平均輸出文件大小從10 MB優化到262 MB,避免了大量小文件的產生。
啓用Repartition 排序推斷,進一步提高壓縮率
在開啓小文件優化後,我們發現一些任務的數據存儲變大很多。這是由於小文件優化中插入的Rebalance操作,使用的分區字段或者隨機分區進行分區,數據被隨機打散,導致Parquet格式對文件的編碼效率降低,進而導致文件壓縮率降低。

Kyuubi小文件優化的規則中,可通過spark.sql.optimizer.inferRebalanceAndSortOrders.enabled參數開啓自動推斷分區和排序字段,對於非動態分區寫入,根據前置執行計劃中的 Join、Aggregate、Sort 等算子的Keys推斷出分區和排序字段,使用推斷的分區字段進行Rebalance,或者在Rebalance前使用推斷的排序字段進行Local Sort,使得最終插入的Rebalance算子的數據分佈儘量與前置計劃保持一致,避免寫入數據被隨機打散,從而有效提高壓縮率。
啓用Zorder 優化,提高壓縮率和查詢效率
Zorder 排序是一種多維的排序算法。對於Parquet等列式存儲格式,有效的排序算法可以使得數據更加緊湊,進而提升數據壓縮率。另外,由於相似的數據被聚集在相同的存儲單元中,例如:min/max等統計範圍更小,可以加大查詢過程中Data Skipping的數據量,有效地提升查詢效率。
Kyuubi中實現了Zorder聚類的排序優化,可以爲表配置Zorder字段,在寫入時會自動加入Zorder排序。對於存量的任務也支持Optimize命令進行存量數據的Zorder優化。我們內部對一些重點業務添加了Zorder優化,數據存儲空間減少了13%,數據查詢性能提升了15%。
引入最終階段獨立AQE配置,加大計算並行度
在一些Hive任務遷移Spark 的過程中,我們發現一些任務執行的速度竟然變慢了,分析發現由於通過在寫入前插入Rebalance算子結合Spark AQE來控制小文件,我們將AQE的spark.sql.adaptive.advisoryPartitionSizeInBytes配置設爲1024M,導致了中間Shuffle階段的並行度變小了,進而使得任務執行變得比較慢。

Kyuubi中提供了最終階段配置的優化,允許爲最終階段單獨添加一些配置,這樣我們就可以爲最終控制小文件的階段添加更大的advisoryPartitionSizeInBytes,對於前面階段使用較小的advisoryPartitionSizeInBytes來加大計算的並行度,並且減少了Shuffle階段溢寫磁盤,有效的提升計算效率,添加此配置後Spark SQL任務整體執行時間縮短25%,資源約節省9%。
推斷動態寫入單分區任務,避免過大Shuffle分區
對於動態分區寫入,Kyuubi小文件優化會使用動態分區字段進行Rebalance。對於使用動態分區方式寫入單個分區的任務,Shuffle數據會全都寫入到同一個Shuffle分區中。愛奇藝內部使用Apache Uniffle作爲Remote Shuffle Service,大分區會造成對Shuffle Server的單點壓力,甚至觸發限流導致寫入降速。爲此我們開發了一個優化規則,捕獲寫入的分區過濾條件,推斷是否以動態分區方式寫入單個分區的數據;對於此類任務,我們不再以動態分區字段進行Rebalance,而是使用隨機Rebalance,這樣就避免了生成一個較大的Shuffle分區,詳細可見:【KYUUBI-5079】。

  • 異常SQL檢測攔截
數據質量存在問題或者用戶對於數據分佈不熟悉時容易提交一些異常的SQL,可能導致嚴重的資源浪費和計算效率低的情況。我們對Spark SQL服務添加了一些監控指標,並且對於一些異常的計算場景進行檢測和攔截。
限制大查詢
在愛奇藝,數據分析師通過魔鏡即席查詢平臺提交SQL進行Ad-hoc查詢分析,該平臺爲用戶提供秒級查詢能力。我們使用Kyuubi的共享引擎作爲後端處理引擎,避免每次查詢都啓動新的引擎浪費啓動時間和計算資源,共享引擎常駐後臺可以爲用戶帶來更快的交互體驗。
對於共享引擎,多個請求會相互搶佔資源,即使我們開啓了動態資源分配,也存在資源被某些大查詢佔滿的情況,導致其它查詢被阻塞。對此,我們在Kyuubi的Spark插件中,實現了大查詢攔截的功能,通過解析SQL執行計劃中的Table Scan等操作,統計查詢的分區數和掃描的數據量,如果超過了指定閾值,則判定爲大查詢並攔截執行。
魔鏡平臺根據判定結果,將大查詢改用獨立引擎執行。另外,魔鏡中定義了分鐘級的超時時間,對於使用共享引擎執行超時的任務將取消執行並且自動轉換成獨立引擎執行。整個過程對於用戶無感,既有效地避免了普通查詢被阻塞,又允許大查詢使用獨立的資源繼續運行。
監測數據膨脹
Spark SQL中的一些Explode、Join、Count Distinct等操作會導致數據膨脹,如果數據膨脹得很大,可能會導致溢寫磁盤、Full GC甚至OOM,並且也使得計算效率變差。我們可以在Spark UI的SQL Tab頁的SQL執行計劃圖中,根據前後節點的number of output rows指標很容易的看出來是否發生了數據膨脹。

Spark SQL執行計劃圖中的指標是通過Task執行事件以及Executor Heartbeat事件上報給Driver,並在Driver中進行聚合。 爲了更加及時的採集到運行時的指標,我們對Kyuubi中SQLOperationListener進行擴展,監聽SparkListenerSQLExecutionStart事件維護sparkPlanInfo,同時監聽SparkListenerExecutorMetricsUpdate事件,捕獲運行節點的SQL統計指標變化,並對比當前運行節點的number of output rows指標和前置子節點的number of output rows指標,計算數據膨脹率判斷是否發生嚴重的數據膨脹,當發生數據膨脹時採集異常事件或攔截異常任務。
定位Join傾斜Key
數據傾斜問題是Spark SQL中比較常見並且影響性能問題,儘管Spark AQE中已經有了一些自動優化數據傾斜的規則,但是它們並不總是生效的,另外,數據傾斜問題很有可能是用戶對於數據理解不夠深入而編寫了錯誤的分析邏輯,或者是數據本身就存在數據質量的問題,所以我們很有必要分析出數據傾斜的任務並定位傾斜的Key值。

我們可以容易的通過 Spark UI中Stage Tasks統計信息確定任務是否發生了數據傾斜,如上圖中Task的Duration和Shuffle Read的Max值相對於75th percentile值超過很大,所以很明顯的發生了數據傾斜。
然而計算傾斜任務中導致傾斜的Key值,通常是對SQL進行手動拆分,然後以Count Group By Keys 的方式計算各個階段Keys的分佈情況,來確定傾斜的Key值,這通常是一個比較耗時的過程。

對此,我們在SortMergeJoinExec中實現了TopN的Keys統計。 SortMergeJoin的實現是先對Key進行排序後再做Join操作,這樣我們就可以很容易的通過累加的方式統計Key的TopN值。 我們實現了一個TopNAccumulator累加器,內部維護一個Map[String, Long]類型的對象,使用Join的Key值作爲Map的Key並維護該Key的Count值在Map的Value中,在SortMergeJoinExec中對於每行數據進行累加計算,由於數據是有序的我們只需要對插入的Key進行累加,並在插入新的Key時判斷是否達到N值,淘汰掉最少的Key。 另外,Spark只支持展示Long類型的統計指標,我們還對SQL統計指標的展示邏輯進行修改適配了Map類型的值。 上圖中展示了兩個表進行Join的Top 5的Join Key值,其中key爲id字段,並且id=1有3行。

  • Hive SQL 遷移 Spark兼容性改造

我們經過一系列的調研測試,發現Spark SQL相較於Hive,在性能和資源使用方面均有顯著提升。然而,在將Hive SQL遷移到Spark的過程中,我們也遇到了許多問題。通過對Spark SQL服務進行一些兼容性改造和適配,我們成功地將大部分的Hive SQL任務遷移到Spark。
  • UDF兼容性
Spark SQL對Hive UDF的支持在實際使用中存在一些問題。例如,業務常使用reflect函數調用Java靜態方法處理數據,當反射調用發生異常時,Hive會返回NULL值,而Spark SQL則會拋出異常導致任務失敗。爲此,我們對Spark的reflect函數進行改造,捕獲反射調用異常返回NULL值,與Hive保持一致。
另一個問題是Spark SQL不支持Hive UDAF的私有化構造函數,這會導致部分業務的UDAF無法初始化。我們改造了Spark的函數註冊邏輯,支持了Hive UDAF私有構造函數。
  • 內置函數兼容性
Spark SQL與Hive 1.2版本的內置GROUPING_ID函數計算邏輯存在差異,導致雙跑階段出現數據不一致。在Hive 3.1版本中該函數計算邏輯變更後與Spark的邏輯一致,所以我們推進用戶對SQL邏輯進行更新,適配Spark中該函數的邏輯,以保證計算邏輯的正確性。
此外,Spark SQL的哈希函數使用了Murmur3 Hash算法,與Hive的實現邏輯不同,我們建議用戶通過手動註冊Hive內置的哈希函數保證遷移前後數據的一致性。
  • 類型轉換兼容性
Spark SQL從3.0版本開始引入了ANSI SQL規範,相比於Hive SQL,其對類型一致性要求更加嚴格,比如:禁止String與數字類型的自動轉換。爲了避免業務中由於數據類型定義不規範導致的自動轉換異常,我們建議用戶自行在SQL中增加CAST進行顯式轉換,對於大量改造可暫時添加配置 spark.sql.storeAssignmentPolicy=LEGACY降低Spark SQL的類型檢查等級,避免遷移發生異常。
Hive 中 str_to_map 函數對於重複的 Key 會自動保留最後一個的值,而Spark中則拋出異常導致任務失敗。對此,我們建議用戶對上游數據質量進行審計,或者添加spark.sql.mapKeyDedupPolicy=LAST_WIN配置,保留最後一個重複的值,與Hive保持一致。
  • 其他語法兼容性
Spark SQL與Hive SQL的Hint語法有不兼容之處,遷移時需用戶手動刪除相關配置。常見的Hive Hint如廣播小表,由於Spark AQE功能對於小表的廣播和任務傾斜的優化更加智能,通常情況下無需用戶進行額外配置。
Spark SQL和Hive的DDL語句也存在一些兼容性的問題,我們通常建議用戶使用平臺進行Hive表的DDL操作。對於一些分區操作的命令,如:刪除不存在分區【KYUUBI-1583】、不等值的Alter Partition語句等兼容性問題,我們也通過擴展Spark插件進行了兼容。

04

   總結與展望


目前,我們已將公司內絕大部分Hive任務遷移到Spark,因此Spark已經成爲愛奇藝最主要的離線處理引擎。我們對Spark引擎完成了初步的資源審計和性能優化工作,爲公司帶來了可觀的支出節省。後續,我們將持續優化Spark服務和計算框架的性能與穩定性。對剩餘極少數的Hive任務,我們也將進一步推進遷移。

隨着公司數據湖的落地,越來越多的業務正在遷移到Iceberg數據湖。由於Iceberg持續推進了Spark DataSourceV2的功能完善,Spark 3.1已經不能滿足一些新的數據湖分析需求,因此我們即將推進Spark 3.4的升級。同時,也對一些新的特性,如Runtime Filter、Storage Partitioned Join等進行了調研,希望能夠結合業務需求,進一步提升Spark計算框架的性能。

另外,爲了推進大數據計算雲原生化的進程,我們引入了Apache Uniffle這一遠程Shuffle服務(RSS)。在使用過程中,我們發現其與Spark AQE結合存在性能問題,例如BroadcastHashJoin傾斜優化【SPARK-44065】、前面提到的大分區問題以及如何更好地進行AQE分區規劃等,後續我們也會繼續對此進行更加深入的研究和優化。



也許你還想看
愛奇藝數據湖實戰
愛奇藝數據湖實戰 - 廣告數據湖應用
愛奇藝數據湖實戰 - 基於數據湖的日誌平臺架構演進

本文分享自微信公衆號 - 愛奇藝技術產品團隊(iQIYI-TP)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章