Hive優化實踐

不管是對於流行的分佈式數據計算框架(如離線的 MapReduce、流計算 Storm、 迭代內 存計算 Spark),還是分佈式計算框架新貴(如 Flink、 Beam),抑或是商業性的大數據解決 方案(如 Teradata 數據庫、 EMC Greeplum、 HP Vertica、 Oracle Exadata),“數據量大”從 來都不是問題,因爲理論上來說,都可以通過增加併發的節點數來解決。 但是如果數據傾斜或者分佈不均了,那麼就會是問題。 此時不能簡單地通過增加併發 節點數來解決問題,而必須採用針對性的措施和優化方案來解決。 這也正是本章將要討論的主要內容。 實際上, Hive SQL 的各種優化方法基本都和數據 傾斜密切相關,因此本章首先介紹“數據傾斜”的基本概念,然後在此基礎上仔細介紹各 種場景下的 Hive 優化方案。 Hive 的優化分爲 join 相關的優化和 join 無關的優化,從項目實際來說, join 相關的優 化佔了 Hive 優化的大部分內容,而join 相關的優化又分爲 mapjoin 可以解決的 join 優化和 mapjoin 無法解決的 join 優化。

離線數據處理的主要挑戰:數據傾斜

在進入具體的 Hive 各個場景優化之前,首先介紹 “數據傾斜”的概念。

實際上,並沒有專門針對數據傾斜給出的一個理論定義。 “傾斜”應該來自於統計學裏的偏態分佈。 所謂偏態分佈,即統計數據峯值與平均值不相等的頻率分佈,根據峯值小於 或大於平均值可分爲正偏函數和負偏函數,其偏離的程度可用偏態係數刻畫。 數據處理中 的傾斜和此相關,但是含義有着很多不同。 下面着重介紹數據處理中的數據傾斜。

對於分佈式數據處理來說,我們希望數據平均分佈到每個處理節點。 如果以每個處理 節點爲 X軸,每個節點處理的數據爲 Y軸,我們希望的柱狀圖是圖 所示的樣式。
在這裏插入圖片描述

但是實際上由於業務數據本身的問題或者分佈算法的問題,每個節點分配到的數據量 很可能是圖 所示的樣式。
在這裏插入圖片描述

更極端情況下還可能是圖所示的樣式。
在這裏插入圖片描述

也就是說,只有待分到最多數據的節點處理完數據,整個數據處理任務才能完成, 此 時分佈式的意義就大打折扣。 實際上,即使每個節點分配到的數據量大致相同,數據仍然可能傾斜,比如考慮統計詞頻的極端問題,如果某個節點分配到的詞都是一個詞,那麼顯 然此節點需要的耗時將很長,即使其數據量和其他節點的數據量相同。 Hive 的優化正是採用各種措施和方法對上述場景的傾斜問題進行優化和處理。

Hive 優化

在實際 Hive SQL 開發的過程中, Hive SQL 性能的問題上實際只有一小部分和數據傾 斜相關。 很多時候, Hive SQL 運行得慢是由開發人員對於使用的數據瞭解不夠以及一些不 良的使用習慣引起的。 開發人員需要確定以下幾點。
1.需要計算的指標真的需要從數據倉庫的公共明細層來自行彙總麼?是不是數據公共 層團隊開發的公共彙總層已經可以滿足自己的需求?對於大衆的、 KPI 相關的指標 等通常設計良好的數據倉庫公共層肯定已經包含了,直接使用即可。
2.真的需要掃描這麼多分區麼?比如對於銷售明細事務表來說,掃描一年的分區和掃 描一週的分區所帶來的計算、 IO 開銷完全是兩個量級,所耗費的時間肯定也是不同 的。 筆者並不是說不能掃描一年的分區,而是希望開發人員需要仔細考慮業務需求, 儘量不浪費計算和存儲資源,畢竟大數據也不是毫無代價的。
3.儘量不要使用 select * from your_table 這樣的方式,用到哪些列就指定哪些列,如 select coll, col2 from your_table。 另外, where 條件中也儘量添加過濾條件,以去 掉無關的數據行,從而減少整個 MapReduce 任務中需要處理、 分發的數據量。
4.輸入文件不要是大量的小文件。 Hive 的默認 Input Split 是 128MB (可配置),小文件 可先合併成大文件。

在保證了上述幾點之後,有的時候發現 Hive SQL 還是要運行很長時間,甚至運行不出 來, 這時就需要真正的 Hive 優化技術了。 下面逐一詳細介紹各種場景下的 Hive 優化方法,但是開發人員需要了解自己的 SQL, 並根據執行過程中慢的環節來定位是何種問題,進而採用下述針對性解決方案。

join 無關的優化

Hive SQL 性能問題基本上大部分都和 join 相關,對於和 join 無關的問題主要有 group by 相關的傾斜和 count distinct 相關的優化。

group by 引起的傾斜優化

group by 引起的傾斜主要是輸入數據行按照 group by 列分佈不均勻引起的,比如, 假設按照供應商對銷售明細事實表來統計訂單數,那麼部分大供應商的訂單量顯然非常多,而多數供應商的訂單量就一般,由於 group by 的時候是按照供應商的 ID 分發到每個 Reduce Task,那麼此時分配到大供應商的 Reduce Task 就分配了更多的訂單,從而導致數 據傾斜。

對於 group by 引起的傾斜,優化措施非常簡單,只需設置下面參數即可:

set hive.map.aggr = true
set hive.groupby.skewindata=true 

此時 Hive 在數據傾斜的時候會進行負載均衡,生成的查詢計劃會有兩個 MapReduce Job。 第 一個 MapReduce Job 中, Map 的輸出結果集合會隨機分佈到 Reduce 中, 每個 Reduce 做部分聚合操作並輸出結果,這樣處理的結果是相同的 GroupBy Key 有可能被分佈 到不同的 Reduce 中,從而達到負載均衡的目的;第二個 MapReduce Job 再根據預處理的數 據結果按照 GroupBy Key 分佈到 Reduce 中(這個過程可以保證相同的 GroupBy Key 被分 布到同一個 Reduce 中),最後完成最終的聚合操作。

count distinct 優化

在 Hive 開發過程中,應該小心使用 count distinct,因爲很容易引起性能問題,比如下 面的 SQL:

select count(distinct user) from some_table; 

由於必須去重,因此 Hive 將會把 Map 階段的輸出全部分佈到一個 Reduce Task 上, 此 時很容易引起性能問題。 對於這種情況,可以通過先 group by 再 count 的方式來優化, 優 化後的 SQL 如下:

select count(*)
from 
(	select user 
	from some table 
	group by user
) tmp; 

其原理爲:利用 group by 去重,再統計 group by 的行數目 。

大表 join 小表優化

和 join 相關的優化主要分爲 mapjoin 可以解決的優化 C liP大表 join 小表) 和 mapjoin 無 法解決的優化(即大表 join 大表)。 大表join 小表相對容易解決,大表 join 大表相對複雜和 難以解決,但也不是不可解決的,只是相對比較麻煩而已。 首先介紹大表join 小表優化。 仍以銷售明細事實表爲例來說明大表join 小表的場景。 假如供應商會進行評級,比如(五星、四星、 三星、 兩星、 一星),此時業務人員希望能夠分析各供應商星級的每天銷售情況及其佔比。

開發人員一般會寫出如下 SQL:

select 
	Seller_star ,
	count(order_id) as order_cnt from 
(
	Select order_id,seller_id 
	from dwd_sls_act_detail_table 
	where partition_value='20170101'
)a
left outer join
(
	Select seller_id, seller_star 
	from dim seller 
	where partition value='20170101'
) b 
on a.seller id=b.seller id group by b.seller_star; 

但正如上述所言,現實世界的二八準則將導致訂單集中在部分供應商上,而好的供應 商的評級通常會更高,此時更加劇了數據傾斜的程度,如果不加以優化,上述 SQL 將會耗 費很長時間,甚至運行不出結果。

通常來說,供應商是有限的,比如上千家、上萬家,數據量不會很大,而銷售明細事實表 比較大,這就是典型的大表 join 小表問題,可以通過 mapjoin 的方式來優化,只需添加 mapjoin hint 即可 , 優化後的 SQL 如下:

 select /*+mapjoin(b)*/
	Seller star
	,count(order_id) as order cnt
from
(
	Select order_id,seller_id
	from dwd_sls_fact_detail_table
	where partition_value='20170101'
)a
left outer join
(
	Select seller_id, seller_star 
	from dim_seller 
	where partition value='20170101'
)b
on a.seller_id = b.seller_id
group by b.seller_star;

/+mapjoin(b)/即 mapjoin hint ,如果需要 mapjoin 多個表,則格式爲 /+mapjoin(b,c,d)/ 。 Hive 對於 mapjoin 是默認開啓的,設置參數爲:

Set hive.auto.convert.join=ture;

mapjoin 優化是在 Map 階段進行 join ,而不是像通常那樣在 Reduce 階段按照 join 列進
行分發後在每個 Reduce 任務節點上進行 join ,不需要分發也就沒有傾斜的問題,相反 Hive
會將小表全量複製到每個 Map 任務節點(對於本例是 dim_seller 表,當然僅全量複製 b 表
sql 指定的列),然後每個 Map 任務節點執行 lookup 小表即可 。

從上述分析可以看出,小表不能太大,否則全 量復 制分發得不償失, 實際上Hive 根據參數 hive.mapjoin.smalltable.filesize ( 0 . 11.0 版 本後是 hive.auto.convert.join.
noconditionaltask.size ) 來確定小表的大小是否滿足條件(默認 25MB ),實際中 此參數值所
允許的最大值可以修改,但是一般最大不能超過1GB (太大的話 Map 任務所在的節點內存
會撐爆, Hive 會報錯。 另外需要注意的是, HDFS 顯示的文件大小是壓縮後的大小, 當實
際加載到內存的時候,容量會增大很多,很多場景下可能會膨脹 1 0 倍) 。

大表 join 大表優化

如果上述 mapjoin 中小表 dim_seller很大呢?比如超過了 1GB 的大小?這種就是大表
join 大表的問題 。 此類問題相對比較複雜,因此本節首先引人一個具體 的問題場景 ,然後
基於此介紹各種優化方案 。

問題場景

問題場景如下 。
A 表爲一個彙總表,彙總的是賣家買家最近 N天交易彙總信息,即對於每個賣家最近
N 天,其每個買家共成交了多少單、總金額是多少,爲了專注於本節要解決的問題, N 只取
90 天,彙總值僅取成交單數 。 A 表的字段有 : buyer_id 、 seller_id 和 pay_cnt_90d 。

B 表爲賣家基本信息表,其中包含賣家的一個分層評級信息,比如把賣家分爲 6 個級
別: S0 、 S1 、S2、 S3 、 S4 和 S5 。

要獲得的結果是每個買家在各個級別賣家的成交比例信息,比如:
某買家 : S0:10%; S1:20%; S2:20%; S3:10%; S4:20%; S4:10%; S5:10%; 。
B 表的字段有: seller_ id 和 s_level 。

正如 mapjoin 中的例子一樣,第一反應是直接 join 兩表並統計 :

select
    m.buyer_id
    ,sum(pay_cnt_90d) as pay_cnt_90d
    ,sum(case when m.s_level=0 then pay_cnt_90d end) as pay_cnt_90d_s0
    ,sum(case when m.s_level=l then pay_cnt_90d end) as pay_cnt_90d_sl
    ,sum(case when m.s_level=2 then pay_cnt_90d end) as pay_cnt_90d_s2
    ,sum(case when m.s level=3 then pay cnt 90d end) as pay cnt 90d s3
    ,sum(case when m.s_level=4 then pay_cnt_90d end) as pay_cnt_90d_s4
    ,sum(case when m.s_level=S then pay_cnt_90d end) as pay_cnt_90d_s5
from
(
	select
	a.buyer_id,a.seller_id,b.s_level,a.pay_cnt_90d
	from
	(
		select buyer_id ,seller_id,pay_cnt_90d
		from table A
	) a
join
	(
		select seller_id,s_level
		from table B
	) b
on a.seller_id=b.seller_id
) m
group by m.buyer_id

但是此 SQL 會引起數據傾斜,原因在於賣家 的 二八準則,某些賣家 90 天內會有幾
百萬甚至上千萬的買家,但是大部分賣家 90 天內的買家數目並不多, join table_A 和 table_
B 的時候 ODPS 會按照 Seller id 進行分發, table A 的大賣家引起了數據傾斜 。

但是本數據傾斜問題無法用 mapjoin table_B 解決,因爲賣家有超過千萬條、文件大小
有幾個 GB ,超過了 mapjoin 表最大1GB 的限制 。

方案 1 :轉化爲 ma時oin

一個很正常的想法是,儘管 B 表無法直接 mapjoin ,但是否可以間接地 mapjoin 它呢?
實際上此思路有兩種途徑:限制行和限制列 。

限制行的思路是不需要 join B 全表, 而只需要 join 其在 A 表中存在的 。 對於本問題場
景,就是過濾掉 90 天內沒有成交的賣家 。

限制列的思路是隻取需要的字段 。

加上如上行列限制後 ,檢查過濾後的 B 表是否滿足了 Hive mapjoin 的條件,如果能夠
滿足 ,那麼添加過濾條件生成一個臨時 B 表,然後 mapjoin 該表即可 。 採用此思路的僞代
碼如下所示:

select
    m.buyer_id
    ,sum(pay_cnt_90d) as pay_cnt_90d
    ,sum(case when m.s_level=O then pay_cnt_90d end) as pay_cnt_90d_s0
    ,sum(case when m.s_level=l then pay_cnt_90d end) as pay_cnt_90d_sl
    ,sum(case when m.s_level=2 then pay_cnt_90d end) as pay_cnt_90d_s2
    ,sum(case when m.s level=3 then pay cnt 90d end) as pay cnt 90d s3
    ,sum(case when m.s_level=4 then pay_cnt_90d end) as pay_cnt_90d_s4
    ,sum(case when m.s_level=S then pay_cnt_90d end) as pay_cnt_90d_s5
from
(
	select /*+mapjoin(b)*/
		a.buyer_id,a.seller_id,b.s_level,a.pay_cnt_90d
	from
	(
		select buyer_id,seller_id,pay_cnt_90d
		from table_A
	) a
join
(
	select b0.seller_id,b0.s_level
	from table_B b0
join
    (select seller_id from table_A group by seller_ id) a0
    on b0.seller_id=a0.seller_id
) b
	on a.seller id=b.seller id
)m
group by m.buyer_id

此方案在一些情況下可以起作用 , 但是很多時候還是無法解決上述問題,因爲大部分
賣家儘管 90 天內買家不多 ,但還是有一些的 , 過濾後的 B 表仍然很大。

方案 2: join 時用 case when 語句

此種解決方案應用場景爲:傾斜的值是明確的而且數量很少, 比如 null 值引起的傾斜。
其核心是將這些引起傾斜的值隨機分發到 Reduce , 其主要核心邏輯在於 join 時對這些特殊
值 concat 隨機數 ,從而達到隨機分發的目的 。 此方案的核心邏輯如下:

Select a . user_id,a.order_id,b.user_id
From table_a a
Join table_b b
On (case when a.user_id is null then concat ('hive' ,rand()) else a.user_id
end)=b.user_id

Hive 已 對 此進 行了 優化 , 只 需要 設 置參 數 skewinfo 和 skewjoin 參數,不需要修改
SQL 代碼 , 例如,由 於 table_B 的值 “0”和 “ 1 ”引起了傾斜,只需作如下設置 :

set hive.optimize.skewinfo=table B:(seller_id)[("0")("1")]
set hive.optimize.skewjoin=true;

但是方案 2 也無法解決本問題場景的傾斜問題,因爲傾斜的賣家大量存在而且動態變化。

方案 3 :倍數 B 表,再取模 join

1. 通用方案

此種方案的思路是建立一個 numbers 表,其值只有一列 int 行,比如從 1 到 10 (具體值可根據傾斜程度確定),然後放大 B 表 10 倍,再取模 join 。 這樣說比較抽象,請參考如下代碼(關鍵代碼已經用黑體加粗標記出來):

  select
      m.buyer_id
      ,sum(pay_cnt_90d) as pay_cnt_90d
      ,sum(case when m.s_level=O then pay_cnt_90d end) as pay cnt 90d so
      ,sum(case when m.s_level=l then pay cnt 90d end) as pay cnt 90d_sl
      ,sum(case when m.s_level=2 then pay_cnt_90d end) as pay_cnt_90d s2
      ,sum(case when m.s_level=3 then pay_cnt_90d end) as pay_cnt_90d_s3
      ,sum(case when m.s_level=4 then pay_cnt_90d end) as pay cnt 90d s4
      ,sum(case when m.s level=S then pay cnt 90d end) as pay cnt 90d s5
  from
  (
  	select
  	a.buyer_id,a.seller_id,b.s_level,a.pay_cnt_90d
  	from
  	(
  		select buyer_id,seller_id,pay_cnt_90d
 		 from table_A
  	) a
  JOin
    (
      select /*+mapjoin(members)*/
      seller_id,s_level,member
      from table_B
  	  join
  	  members
    ) b
  on a.seller_id=b.seller_id
  and mod(a.pay_cnt_90d,10)+1=b.number
  ) m
  group by m.buyer_id

此思路的核心在於:既然按照 seller id 分發會傾斜,那麼再人工增加一列進行分發,
這樣之前傾斜的值的傾斜程度會減爲原來的 1/10 。 可以通過配置 nubmers 表修改放大倍數
來降低傾斜程度,但這樣做的一個弊端是 B 表也會膨脹 N倍 。

2. 專用方案

通用方案的思路把 B 表的每條數據都放大了相同的倍數,實際上這是不需要的,只需
要把大賣家放大倍數即可:

需要首先知道大賣家的名單,即先建立一個臨時表動態存放每日最新的大賣家(比如
dim_big_seller),同時此表的大賣家要膨脹預先設定的倍數(比如 1000 倍) 。

在 A 表和 B 表中分別新建一個 join 列,其邏輯爲:如果是大賣家,那麼 concat 一個隨
機分配正整數( 0 到預定義的倍數之間,本例爲 0 ~ 1000 );如果不是,保持不變 。

具體僞代碼如下(關鍵代碼已經用黑體加粗標記出來):

select
    m.buyer_id
    ,sum(pay_cnt_90d) as pay_cnt_90d
    ,sum(case when m.s_level=O then pay_cnt_90d end) as pay_cnt_90d_s0
    ,sum(case when m.s_level=l then pay_cnt_90d end) as pay_cnt_90d_sl
    ,sum(case when m.s_level=2 then pay_cnt_90d end) as pay_cnt_90d_s2
    ,sum(case when m.s_level=3 then pay_cnt_90d end) as pay_cnt_90d_s3
    ,sum(case when m.s_level=4 then pay_cnt_90d end) as pay_cnt_90d_s4
    ,sum(case when m.s_level=5 then pay_cnt_90d end) as pay_cnt_90d_s5
from
(
    select
    a.buyer_id,a.seller_id,b.s_level,a.pay_cnt_90d
    from
    (
    select /*mapjoin(big)*/
    buyer_id,seller_id,pay_cnt_90d,
    if(big.seller_id is not null,concat(table_A.seller_id,'rnd' , cast
    (rand() *1000 as bigint),table_A.seller_id) as seller_id_joinkey
	from table_A
left outer join
--big 表 seller 有重複,請注意一定要 group by 後再 j oin,保證 table_A 的行數保持不變--
(select seller_id from dim_big_seller group by seller_id) big
on table_A.seller_ id=big.seller_id
) a
JOin
(
    select /*mapjoin(big)*/
    seller_id,s_level,
    --big 表的 seller_id_joinkey 生成邏輯和上面的生成邏輯一樣
    coalesce(seller_id_joinkey,table_B.seller_id) as seller_id_joinkey
    from table B
    left outer join
--table_B 表 join 大賣家表後大賣家行數放大 1000 倍,其他賣家行數保持不變
(select seller_id, seller_id_joinkey from dim_big_seller) big
on table_B.seller_id=big.seller_id
) b
on a.seller_id_joinkey=b.seller_id_joinkey
)m
group by m.buyer_id

相比通用方案,專用方案的運行效率明顯好了很多,因爲只是將 B 表中大賣家的行數
放大了 1000 倍,其他賣家的行數保持不變,但同時也可以看到代碼也複雜了很多,而且必
須首先建立大賣家表。

方案 4 :動態一分爲二

實際上方案 2 和 3 都用到了一分爲二的思想,但是都不徹底,對於 mapjoin 不能解決的問題,終極解決方案就是動態一分爲二 ,即對傾斜的鍵值和不傾斜的鍵值分開處理,不傾斜的正常 join 即可,傾斜的把它們找出來然後做 mapjoin ,最後 union all 其結果即可 。

但是此種解決方案比較麻煩,代碼會變得複雜而且需要一個臨時表存放傾斜的鍵值。

採用此解決方案的僞代碼如下所示:

--由於數據傾斜,先找出近 9 0 天買家數超過 10000 的賣家
insert overwrite table trnp table B
select
    m.seller_id ,
    n.s_level,
from(
    select
    seller_id
    from(
        select
        seller_id,
        count(buyer_id) as byr_cnt
	from
		table_A
	group by
		seller_id
	) a
where a.byr_cnt>1000
) m
left outer join(
select
    user_id,
    s_level,
from
	table_B
) n
on m.seller i d=n.user_id;

--對於 90 天買家數超過 10000 的賣家直接 map join ,對於其他賣家正常 join 即可
select
    m.buyer_id
    ,sum(pay_cnt”’90d) as pay_cnt_90d
    ,sum(case when m.s_level=O then pay_cnt_90d end) as pay_cnt_90d_s0
    ,sum(case when m.s_level=l then pay_cnt_90d end) as pay_cnt_90d_sl
    ,sum(case when m.s_level=2 then pay_cnt_90d end) as pay_cnt_90d_s2
    ,sum(case when m.s_level=3 then pay_cnt_90d end) as pay_cnt_90d_s3
    ,sum(case when m.s_level=4 then pay_cnt_90d end) as pay_cnt_90d_s4
    ,sum(case when m.s_level=5 then pay_cnt_90d end) as pay_cnt_90d_s5
from
(
    select
    	a.buyer_id,a.seller_id,b.s_level,a.pay_cnt_90d
    from
(
    select buyer_id,seller id,pay_cnt_90d
    from table_A
) a
join
(
    select seller_id,a.s_level
    from table_A a
    left outer join tmp_table_B b
    on a.user_id=b.seller_id
    where b.seller id is null
) b
on a.seller id=b.seller id
union all
select /*+mapjoin(b)*/
	a.buyer_id,a.seller_id,b.s_level,a.pay_cnt_90d
from
(
    select buyer_id,seller_id,pay_cnt_90d
    from table_A
) a
join
    select seller_id,s_level
    from table B
) b
	on a.seller_id=b.seller_id
) m group by m.buyer_id
)m 
group by m.buyer_id

總結起來,方案 1 、 2 以及方案 3 中的通用方案不能保證解決大表 join 大表問題,因爲
它們都存在種種不同的限制和特定的使用場景 。

而方案 3 的專用方案和方案 4 是本節推薦的優化方案,但是它們都需要新建一個臨時
表來存放每日動態變化的大賣家 。 相對方案 4 來說,方案 3 的專用方案不需要對代碼框架
進行修改,但是 B 表會被放大,所以一定要是維度表,不然統計結果會是錯誤的 。

方案 4
的解決方案最通用,自由度最高,但是對代碼的更改也最大,甚至需要更改代碼框架,可
作爲終極方案來使用 。

小節

首先概要介紹了數據傾斜的概念,然
後對 Hive SQL 優化進行了概要性介紹,在此基礎上分別介紹了 join 無關的優化場景一-
group by 的傾斜優化和 count distinct 優化,然後重點介紹了 mapjoin 的優化以及 mapjoin 無
法解決的場景的優化。 mapjoin 無法解決的優化共有 5 種方案,實際項目中,用戶可以根據
情況選用適合自己的優化方案 。

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