Hive SQL遷移Spark SQL在滴滴的實踐

桔妹導讀:在滴滴SQL任務從Hive遷移到Spark後,Spark SQL任務佔比提升至85%,任務運行時間節省40%,運行任務需要的計算資源節省21%,內存資源節省49%。在遷移過程我們沉澱出一套遷移流程, 並且發現並解決了兩個引擎在語法,UDF,性能和功能方面的差異。



1. 
遷移背景     
Spark自從2010年面世,到2020年已經經過十年的發展,現在已經發展爲大數據批計算的首選引擎,在滴滴Spark是在2015年便開始落地使用,不過主要使用的場景是更多在數據挖掘和機器學習方向,對於數倉SQL方向,主要仍以Hive SQL爲主。

下圖是當前滴滴內部SQL任務的架構圖,滴滴各個業務線的離線任務是通過一站式數據開發平臺DataStudio調度的,DataStudio把SQL任務提交到HiveServer2或者Spark兩種計算引擎上。兩個計算引擎均依賴資源管理器YARN和文件系統HDFS。



在遷移之前我們面臨的主要問題有:


  • SQL任務運行慢:遷移前SQL任務運行的平均時間是20分鐘,主要原因是佔比高達83%的Hive SQL任務運行時間長,Hive任務執行過程中會啓動多個MR Job,Job間的中間結果存儲在HDFS,所以同一個SQL, Hive比Spark執行的時間更長;

  • Hive SQL穩定性差:一個HS2會同時執行多個用戶的Hive SQL任務,當一個異常任務導致HS2進程響應慢甚至異常退出時,運行在同一個實例的SQL任務也會運行緩慢甚至失敗。而異常任務場景各異。我們曾經遇到的異常任務有多個大SQL加載過多的分區元數據導致HS2 FullGC,加載UDF時導致HS2進程core dump,UDF訪問HDFS沒有關閉流導致HS2機器端口被打滿,這些沒有通用解法, 問題很難收斂;

  • 人力分散:兩個引擎需要投入雙倍的人力,在人員有限的情況下,對引擎的掌控力會減弱;


所以爲了SQL任務運行更快,更穩,團隊人力聚焦,對引擎有更強的掌控力,我們決定把Hive SQL遷移到Spark SQL。



2. 
遷移方案概要設計

Hive SQL遷移到Spark SQL後需滿足以下條件:


  • 保證數據一致性, 也就是相同的SQL使用Spark和Hive執行的結果應該是一樣的;
  • 保證用戶有收益, 也就是使用Spark執行SQL後應該節省資源,包括時間,cpu和memroy;
  • 遷移過程對用戶透明;


爲了滿足以上三個條件, 一個很直觀的思路就是使用兩個引擎執行用戶SQL,然後對比每個引擎的執行結果和資源消耗。 


爲了不影響用戶線上數據,使用兩個引擎執行用戶SQL有兩個可選方案:


  • 複用現有的SQL任務調度系統,再部署一套SQL任務調度系統用來遷移,這個系統與生產環境物理隔離;

  • 開發一個SQL雙跑工具,可以支持使用兩個引擎執行同一個SQL任務;


下面詳細介紹這兩個方案:


1. 方案一:複用現有的SQL任務調度系統


再部署一套SQL任務執行系統用來使用Spark執行所有的SQL,包括HDFS,HiveServer2&MetaStore和Spark,DataStudio。新部署的系統需要週期性從生產環境同步任務信息,元數據信息和HDFS數據,在這個新部署的系統中把Hive SQL任務改成Spark SQL類型任務,這樣一個用戶的SQL在原有系統中使用Hive SQL執行,在新部署的系統中使用Spark執行。如下圖所示,藍色的表示需要新部署的子系統。



2. 方案二:開發一個SQL雙跑工具


SQL雙跑工具,可以線下使用兩個引擎執行用戶的SQL,具體流程如下:

  • SQL收集:用戶的SQL是在HS2上執行的,所以理論上通過HS2可以收集到所有的SQL;

  • SQL改寫:執行用戶原始SQL會覆蓋線上數據,所以在執行前需要改寫SQL,把SQL的輸出的庫表名替換爲用來遷移測試的的庫表名;

  • SQL雙跑:分別使用Hive和Spark執行改寫後的SQL;


3. 方案對比


  • 方案一

    • 隔離性好,單獨的SQL執行系統不會影響生產任務,也不會影響業務數據;

    • 劣勢

      • 需要的資源多:運行多個子系統需要較多物理資

      • 部署複雜:部署多個子系統,需要多個不同的團隊相互配合;

      • 容易出錯:子系統間需要週期性同步,任何一個子系統同步出問題,都可能導致執行SQL失敗;

  • 方案二

    • 非常輕量,不需要部署很多系統,而且對物理資源需要不高;

    • 劣勢

      • 與生產公共一套環境,回放時有影響用戶數據對風險;

      • 需要開發SQL收集,SQL改寫和SQL雙跑系統;


經過權衡, 我們決定採用方案二, 因爲:


  • 通過HiveServer收集所有SQL,SQL改寫和SQL雙跑邏輯清晰,開發成本可控;

  • 創建超讀帳號,對所有庫表有讀權限,但只對用戶遷移的測試庫有寫權限,可以避免影響用戶數據的風險;



3. 
遷移方案詳細設計
1. Hive SQL提取

Hive SQL提取包括以下步驟:


  • 改造HiveHistoryImpl,每個session內執行的所有SQL和command保存到HiveServer2的一個本地文件中,這些文件按天組織,每天一個目錄

  • 定時將前一天的History目錄上傳到hdfs

  • 開發HiveHistoryParser


HiveHistoryParser的主要功能是:


  • 每天從HDFS下載所有HiveServer2的History文件;

  • SQL去重:DataStudio上的一個SQL任務可能一天執行多次(比如小時任務),任務執行一次會生成一個新的執行Id,只保留一天中最大的執行Id的SQL;

  • 合併SQL:一個shell任務可能建立多個session執行SQL,爲了後面遷移shell任務,需要把多個session的SQL合併到一起;

  • 輸出Parse結果:包括多個SQL文件和meta文件:

    • 每個任務執行的SQL保存到一個文件中,文件名是任務名稱加執行Id,我們稱作原始SQL文件

    • meta文件包含SQL文件路徑,任務名稱,項目名稱,用戶名;



2. SQL改寫&雙跑

SQL改寫會對上一步生成的每個原始SQL文件執行以下步驟:


  • 使用Spark的SessionState對SQL文件逐行分析,識別是否包含以下兩類子句:

    • insert overwrite into

    • create table as select

  • 如果包含上面的兩類子句,則提取寫入的目標庫表名稱;

  • 在測試庫中創建與目標庫表schema完全一致的兩個測試表;

  • 分別使用上一步創建的測試庫表替換原始SQL文件中的庫表名生成用於回放的SQL文件,一個原始SQL文件改寫後會生成兩個SQL文件,用於後面兩個引擎分別執行;


SQL雙跑步驟如下:


  • 併發的使用Spark和Hive執行上一步生成的兩個SQL文件;

  • 記錄使用兩種引擎執行SQL時啓動的Application和運行時間;

  • 輸出回放結果到文件中,執行每個SQL文件對會生成一條結果記錄, 包括Hive 和Spark 執行SQL的時間,啓動的Application列表,和輸出的目標庫表名稱等, 如下圖所示:



3. 結果對比


結果對比時會遍歷每個回放記錄,統計以下指標:



具體流程如下:


  • 查詢Spark SQL和Hive SQL輸出的庫表的記錄數;

  • 查詢兩種引擎輸出的HDFS文件個數和大小;

  • 對比兩種引擎的輸出數據;


分別對Spark和Hive的產出表執行以下SQL,獲取表的概要信息



比較兩張表的概要信息:


  • 如果所有對應列的值相同則認爲結果一致;

  • 如果存在不一致的列,如果該列是數值類型,則對該列計算最大精度差異, SQL如下:



  • 統計兩種引擎啓動的Application消耗的vcore和memory資源;

  • 輸出對比結果, 包括運行時間, 消耗的vcore和memory,是否一致,如果不一致輸出不一致的列名以及最大差異;

  • 彙總數據結果,並對回放的SQL分爲以下幾類:



    • 可遷移:數據完全一致, 並且使用Spark SQL執行使用更少資源,包括運行時間,vcore和memory以及文件數;

    • 經驗可遷移:在排查不一致時發現有些是邏輯正確的 (比如collect_set結果順序不一致),如果有些任務符合這些經驗,則認爲是經驗可遷移;

    • 數據不一致:兩種引擎產出的結果存在不一致的列,而且沒有命中經驗;

    • Time_High:兩種引擎產出的結果完全一致,但是Spark執行SQL的運行時間大於Hive執行SQL的時間;

    • Cpu_High:兩種引擎產出的結果完全一致,但是Spark執行SQL消耗的cpu資源大於Hive執行SQL消耗的cpu資源;

    • Memory_High:兩種引擎產出的結果完全一致,但是Spark執行SQL消耗的memory資源大於Hive執行SQL消耗的memory資源;

    • Files_High:兩種引擎產出的結果完全一致,但是Spark執行SQL產生的文件數大於Hive執行SQL產生的文件數;

    • 語法不兼容:在SQL改寫階段解析SQL時報語法錯誤;

    • 運行時異常:在雙跑階段,Hive SQL或者Spark SQL在運行過程中失敗;


4. 遷移


遷移比較簡單, 步驟如下:


  • 整理遷移任務列表以及對應的配置參數;

  • 調用DataStudio接口把任務類型修改爲SparkSQL類型;

  • 重跑任務;


5. 問題排查&修復


如果SQL是“可遷移”或者“經驗可遷移”,可以執行遷移,其它的任務需要排查,這部分是最耗時耗力的,遷移過程中大部分時間都是在調查和修復這些問題。修復之後再執行從頭開始,提取最新任務的SQL,然後SQL改寫和雙跑,結果對比,滿足遷移條件則說明修復了問題,可以遷移,否則繼續排查,因此遷移過程是一個循環往復的過程,直到SQL滿足遷移條件,整體過程如下圖所示:




4. 
引擎差異

在遷移的過程中我們發現了很多兩種引擎不同的地方,主要包括語法差異,UDF差異,功能差異和性能差異。


1. 語法差異


有些Hive SQL使用Spark SQL執行在語法分析階段就會出錯,有些語法差異我們在內部版本已經修復,目前正在反饋社區,正在和社區討論,還有一些目前沒有修復。


1.1 用例設計


  • UDTF新版initialize接口支持,對齊Hive SQL  [SPARK-33704]

  • Window Function 不支持沒有order by子句的場景

  • Join 子查詢支持rand 隨機分佈條件,增強語法兼容

  • Orc/Orcfile 存儲類型創建語句屏蔽ROW FORMAT DELIMITED限制 [SPARK-33755]

  • `DB.TB` 識別支持,對齊Hive SQL [SPARK-33686]

  • 支持CREATE TEMPORARY TABLE

  • 各類Hive UDF的支持調用,主要包括get_json_object,datediff,unix_timestamp,to_date,collect_set,date_sub  [SPARK-33721]

  • DROP不存在的表和分區,Spark SQL報錯,Hive SQL 正常  [SPARK-33637]

  • 刪除分區時支持設置過濾條件 [SPARK-33691]


1.2 未修復


  • Map類型字段不支持GROUP BY操作

  • Operation not allowed:ALTER TABLE CONCATENATE


2. UDF差異


在排查數據不一致的SQL過程中,我們發現有些是因爲輸入數據的順序不同造成的, 這些差異邏輯上是正確的,而有些是UDF對異常值的處理方式不一致造成的,還有需要注意的是UDF執行環境不同造成的結果差異。


2.1 順序差異


這些因爲輸入數據的順序不同造成的結果差異邏輯上是一致的,對業務無影響,因此在遷移過程中可以忽略這些差異,這類差異的SQL任務屬於經驗可遷移。


2.1.1 collect_set


  • 假設數據表如下:



  • 執行如下SQL:


  • 執行結果:



  • 差異說明:


collect_set執行結果的順序取決於記錄被掃描的順序,Spark SQL執行過程中是多個任務併發執行的,因此記錄被讀取的順序是無法保證的.


2 .1.2  collect_list

  • 假設數據表如下:



  • 執行如下SQL:



  • 執行結果:



  • 差異說明:


collect_list執行結果的順序取決於記錄被掃描的順序,Spark SQL執行過程中是多個任務併發執行的,因此記錄被讀取的順序是無法保證的。


2 .1.3  row_number

  • 假設數據表如下:



  • 執行如下SQL:



  • 執行結果:



  • 差異說明:


執行row_number時,在一個分區內部,可以保證order by字段是有序的,對於非分區非order by字段的順序是沒有保證的。

2 .1.4  map類型字段讀寫


  • 數據表建表語句:



  • 假設數據表如下:



  • 執行如下SQL:



  • 執行結果:



  • 差異說明:


Map類型是無序的,同一份數據,在query時顯示的各個key的順序會有變化。


2.1.5 sum(double/float)


  • 假設數據表如下:


  • 執行如下SQL:



  • 執行結果:



  • 差異說明:


這是由float/double類型的表示方式決定的,浮點數不能表示所有的實數,在執行運算過程中會有精度丟失,對於幾個浮點數,執行加法時的順序不同,結果有時就會不同。



2 .1.6  順序差異解決方案

由以上UDF造成的差異可以忽略,相關任務如果在資源方面也有節省,那麼最終的狀態是經驗可遷移狀態,符合遷移條件。

2.2  非順序差異

下面幾個日期/時間相關函數,當有異常輸入是Spark SQL會返回NULL,而Hive SQL會返回一個非NULL值。

2.2.1  datediff

對於異常日期,比如0000-00-00執行datediff兩者會存在差異。


2.2.2  unix_timestamp


對於24點Spark認爲是非法的返回NULL,而Hive任務是正常的,下表時執行unix_timestamp(concat('2020-06-01', ' 24:00:00'))時的差異。



2.2.3  to_date


當月或者日是00時Hive仍然會返回一個日期,但是Spark會返回NULL。



2.2.4 date_sub

當月或者日是00時Hive仍然會返回一個日期,但是Spark會返回NULL。


2.2.5  date_add

當月或者日是00時Hive仍然會返回一個日期,但是Spark會返回NULL。


2.2.6  非順序差異解決方案


這些差異是是因爲對異常UDF參數的處理邏輯不同造成的,雖然Spark SQL返回NULL更合理,但是現有的Hive SQL任務用戶適應了這種處理邏輯,所以爲了不影響現有SQL任務,我們對這類UDF做了兼容處理,用戶可以通過配置來決定使用Hive內置函數還是Spark的內置UDF。


2.3 UDF執行環境差異


2.3.1 差異說明


基於MapReduce的Hive SQL一個Task會啓動一個進程,進程中的主線程負責數據處理, 因此在Hive SQL中UDF只會在單程中執行。


而Spark 一個Executor可能會啓動多個Task,如下圖所示。因此在Spark SQL中自定義UDF時需要考慮線程安全問題。



2.3.2 差異解決方案

下面是一個非線程安全的示例,UDF內部共享靜態變量,在執行UDF時會讀寫這個靜態變量。


解決方案也比較簡單,一種是加鎖,如下圖所示:


另一種是取消靜態成員,如下圖所示:



3. 性能&功能差異


3.1 小文件合併


Hive SQL可以通過設置以下配置合併小文件,MR Job結束後,判斷生成文件的平均大小,如果小於閥值,就再啓動一個Job來合併文件。



目前Spark SQL不支持小文件合併,在遷移過程中,我們經常發現Spark SQL生成的文件數多於Hive SQL,爲此我們參考Hive SQL的實現在Spark SQL中引入了小文件合併功能。


在InsertIntoHiveTable 中判斷如果開啓小文件合併,並且文件的平均大小低於閾值則執行合併,合併之後再執行loadTable或者loadPartition操作。


3.2 Spark SQL支持Cluster模式


Hive SQL任務是DataStudio通過beeline -f執行的,客戶端只負責發送SQL語句給HS2,已經獲取執行結果,因此是非常輕量的。而Spark SQL只支持Client模式,Driver在Client進程中,因此Client模式執行Spark SQL時,有時會佔用很多的資源,DataStudio無法感知Spark Driver的資源開銷,所以在DataStudio層面會帶來以下問題:


  • 形成資源熱點,影響任務執行;

  • 隨着遷移到Spark SQL的任務越來越多,DataStudio需要越來越多的機器調度SQL任務;

  • Client模式日誌保留在本地,排查問題時不方便看日誌;


所以我們開發了Spark SQL支持Cluster模式,該模式只支持非交互式方式執行SQL,包括spark-sql -e和spark-sql -f,不支持交互式模式。


3.3 分區剪裁優化


遷移過程中我們發現大部分任務的分區條件包括concat, concat_ws, substr等UDF, HiveServer2會調用MetaStore的getPartitionsByExpr方法返回符合分區條件的有效分區,避免無效的掃描, 但是Spark SQL的分區剪裁只支持由Attribute和Literal組成key/value結構的謂詞條件,這一方面導致無法有效分區剪裁,會查詢所有分區的數據, 造成讀取大量無效數據,另一方面查詢所有分區的元數據,導致MetaStore對MySQL查詢壓力激增,導致mysql進程把cpu打滿。我們在社區版本的基礎上迭代支持了多種場景的分區聯合剪裁,目前能夠覆蓋生產任務90%以上的場景。


  • concat/concat_ws聯合剪裁場景



  • substr 聯合剪裁場景



  • concat/concat_ws&substr組合場景



目前已經反饋社區,正在討論中,具體可參考[SPARK-33707][SQL] Support multiple types of function partition pruning on hive metastore


5. 
遷移結果

經過6個多月的團隊的努力,我們遷移了1萬多個Hive SQL任務到Spark SQL,在遷移過程中,隨着spark SQL任務的增加,SQL任務的執行時間在逐漸減少,從最初的1000+秒下降到600+秒如下圖所示:



遷移後Spark SQL任務佔比85%,SQL任務運行時間節省40%,計算資源節省21%,內存資源節省49%,遷移的收益是非常大的。




6. 
下一步計劃

遷移之後Spark已經成爲SQL任務的主流引擎,但是還有大量的shell類型任務使用Hive執行SQL,所以後續我們會遷移shell類型任務,把shell中的Hive SQL遷移到Spark SQL。


在生產環境中,有些shuffle 比較中的任務經常會因爲shuffle fetch重試甚至失敗,我們想優化Spark External Shuffle Service。


社區推出Spark 3.x也半年多了,在功能和性能上有很大提升,所以我們也想和社區保持同步,升級Spark到3.x版本。



本文作者



團隊招聘


滴滴大數據架構部主要負責滴滴大數據存儲與計算等引擎的開發與運維工作,通過持續應用和研發新一代大數據技術,構建穩定可靠、高性能、低成本 的大數據基礎設施,更多賦能業務,創造更多價值。團隊近期招聘:
Flink/ClickHouse/Elast icSearch/HDFS/Presto/融合計算等領域專家,參與滴滴大數據建設工作, 歡迎加入。可投遞簡歷至 [email protected]


掃碼瞭解更多崗位


延伸閱讀

內容編輯 | Mango

聯繫我們 | [email protected]


   
   
   

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

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