桔妹導讀:在滴滴SQL任務從Hive遷移到Spark後,Spark SQL任務佔比提升至85%,任務運行時間節省40%,運行任務需要的計算資源節省21%,內存資源節省49%。在遷移過程中我們沉澱出一套遷移流程, 並且發現並解決了兩個引擎在語法,UDF,性能和功能方面的差異。
在遷移之前我們面臨的主要問題有:
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。
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是在HS2上執行的,所以理論上通過HS2可以收集到所有的SQL;
SQL改寫:執行用戶原始SQL會覆蓋線上數據,所以在執行前需要改寫SQL,把SQL的輸出的庫表名替換爲用來遷移測試的的庫表名;
SQL雙跑:分別使用Hive和Spark執行改寫後的SQL;
▍3. 方案對比
方案一
優勢
隔離性好,單獨的SQL執行系統不會影響生產任務,也不會影響業務數據;
劣勢
需要的資源多:運行多個子系統需要較多物理資源;
部署複雜:部署多個子系統,需要多個不同的團隊相互配合;
容易出錯:子系統間需要週期性同步,任何一個子系統同步出問題,都可能導致執行SQL失敗;
方案二
優勢
非常輕量,不需要部署很多系統,而且對物理資源需要不高;
劣勢
與生產公共一套環境,回放時有影響用戶數據對風險;
需要開發SQL收集,SQL改寫和SQL雙跑系統;
通過HiveServer收集所有SQL,SQL改寫和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文件 每個任務執行的SQL保存到一個文件中,文件名是任務名稱加執行Id,我們稱作;
meta文件包含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列表,和輸出的目標庫表名稱等, 如下圖所示:
結果對比時會遍歷每個回放記錄,統計以下指標:
具體流程如下:
查詢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滿足遷移條件,整體過程如下圖所示:
在遷移的過程中我們發現了很多兩種引擎不同的地方,主要包括語法差異,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執行過程中是多個任務併發執行的,因此記錄被讀取的順序是無法保證的.
假設數據表如下:
執行如下SQL:
執行結果:
差異說明:
collect_list執行結果的順序取決於記錄被掃描的順序,Spark SQL執行過程中是多個任務併發執行的,因此記錄被讀取的順序是無法保證的。
假設數據表如下:
執行如下SQL:
執行結果:
差異說明:
數據表建表語句:
假設數據表如下:
執行如下SQL:
執行結果:
差異說明:
Map類型是無序的,同一份數據,在query時顯示的各個key的順序會有變化。
2.1.5 sum(double/float)
假設數據表如下:
執行如下SQL:
執行結果:
差異說明:
這是由float/double類型的表示方式決定的,浮點數不能表示所有的實數,在執行運算過程中會有精度丟失,對於幾個浮點數,執行加法時的順序不同,結果有時就會不同。
對於24點Spark認爲是非法的返回NULL,而Hive任務是正常的,下表時執行unix_timestamp(concat('2020-06-01', ' 24:00:00'))時的差異。
當月或者日是00時Hive仍然會返回一個日期,但是Spark會返回NULL。
這些差異是是因爲對異常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時需要考慮線程安全問題。
另一種是取消靜態成員,如下圖所示:
▍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組合場景
經過6個多月的團隊的努力,我們遷移了1萬多個Hive SQL任務到Spark SQL,在遷移過程中,隨着spark SQL任務的增加,SQL任務的執行時間在逐漸減少,從最初的1000+秒下降到600+秒如下圖所示:
遷移之後Spark已經成爲SQL任務的主流引擎,但是還有大量的shell類型任務使用Hive執行SQL,所以後續我們會遷移shell類型任務,把shell中的Hive SQL遷移到Spark SQL。
在生產環境中,有些shuffle 比較中的任務經常會因爲shuffle fetch重試甚至失敗,我們想優化Spark External Shuffle Service。
社區推出Spark 3.x也半年多了,在功能和性能上有很大提升,所以我們也想和社區保持同步,升級Spark到3.x版本。
▬
團隊招聘
▬
掃碼瞭解更多崗位
內容編輯 | Mango
聯繫我們 | [email protected]
本文分享自微信公衆號 - 滴滴技術(didi_tech)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。