Hive優化

前言

Hive系列的第三篇,南國在這裏主要是總結歸納一下日常中經常會碰到的Hive的知識點。因爲Hive是基於Hadoop的數據倉庫,HiveQL內部的執行過程也是基於MapReduce實現的。

首先,我們來看看Hadoop的計算框架特性,在此特性下會衍生哪些問題?

  • 數據量大不是問題,數據傾斜是個問題。
  • jobs數比較多的作業運行效率相對比較低,比如即使有幾百行的表,如果多次關聯多次彙總,產生十幾個jobs,耗時很長。原因是map reduce作業初始化的時間是比較長的。
  • sum,count,max,min等UDAF,不怕數據傾斜問題,hadoop在map端的彙總合併優化,使數據傾斜不成問題。
  • count(distinct ),在數據量大的情況下,效率較低,如果是多count(distinct )效率更低,因爲count(distinct)是按group by 字段分組,按distinct字段排序,一般這種分佈方式是很傾斜的。舉個例子:比如男uv,女uv,像淘寶一天30億的pv,如果按性別分組,分配2個reduce,每個reduce處理15億數據。

面對這些問題,我們能有哪些有效的優化手段呢?下面列出一些在工作有效可行的優化手段:

  • 解決數據傾斜問題。
  • 減少job數。
  • 設置合理的map reduce的task數,能有效提升性能。(比如,10w+級別的計算,用160個reduce,那是相當的浪費,1個足夠)。
  • 瞭解數據分佈,自己動手解決數據傾斜問題是個不錯的選擇。set hive.groupby.skewindata=true;這是通用的算法優化,但算法優化有時不能適應特定業務背景,開發人員瞭解業務,瞭解數據,可以通過業務邏輯精確有效的解決數據傾斜問題。
  • 數據量較大的情況下,慎用count(distinct),count(distinct)容易產生傾斜問題。
  • 對小文件進行合併,是行至有效的提高調度效率的方法,假如所有的作業設置合理的文件數,對雲梯的整體調度效率也會產生積極的正向影響。
  • 優化時把握整體,單個作業最優不如整體最優。

接着,我們總結影響性能的根源是什麼?

  • hive性能優化時,把HiveQL當做M/R程序來讀,即從M/R的運行角度來考慮優化性能,從更底層思考如何優化運算性能,而不僅僅侷限於邏輯代碼的替換層面。
  • RAC(Real Application Cluster)真正應用集羣就像一輛機動靈活的小貨車,響應快;Hadoop就像吞吐量巨大的輪船,啓動開銷大,如果每次只做小數量的輸入輸出,利用率將會很低。所以用好Hadoop的首要任務是增大每次任務所搭載的數據量。
  • Hadoop的核心能力是parition和sort,因而這也是優化的根本。
    觀察Hadoop處理數據的過程,有幾個顯著的特徵:
    1.數據的大規模並不是負載重點,造成運行壓力過大是因爲運行數據的傾斜。
    2.jobs數比較多的作業運行效率相對比較低,比如即使有幾百行的表,如果多次關聯對此彙總,產生幾十個jobs,將會需要30分鐘以上的時間且大部分時間被用於作業分配,初始化和數據輸出。M/R作業初始化的時間是比較耗時間資源的一個部分。
    在使用SUM,COUNT,MAX,MIN等UDAF函數時,不怕數據傾斜問題,Hadoop在Map端的彙總合併優化過,使數據傾斜不成問題。
    3.COUNT(DISTINCT)在數據量大的情況下,效率較低,如果多COUNT(DISTINCT)效率更低,因爲COUNT(DISTINCT)是按GROUP BY字段分組,按DISTINCT字段排序,一般這種分佈式方式是很傾斜的;比如:男UV,女UV,淘寶一天30億的PV,如果按性別分組,分配2個reduce,每個reduce處理15億數據。
    4.數據傾斜是導致效率大幅降低的主要原因,可以採用多一次 Map/Reduce 的方法, 避免傾斜。
  • 最後得出的結論是:避實就虛,用 job 數的增加,輸入量的增加,佔用更多存儲空間,充分利用空閒 CPU 等各種方法,分解數據傾斜造成的負擔。

Hive優化

我們知道了性能低下的根源,我們可以從Hive的配置解讀去優化。Hive系統內部已針對不同的查詢預設定了優化方法,用戶可以通過調整配置進行控制, 以下舉例介紹部分優化的策略以及優化控制選項。

1. 列裁剪

Hive 在讀數據的時候,可以只讀取查詢中所需要用到的列,而忽略其它列。 例如,若有以下查詢:

SELECT a,b FROM q WHERE e<10;

在實施此項查詢中,Q 表有 5 列(a,b,c,d,e),Hive 只讀取查詢邏輯中真實需要 的 3 列 a、b、e,而忽略列 c,d;這樣做節省了讀取開銷,中間表存儲開銷和數據整合開銷。

裁剪所對應的參數項爲:hive.optimize.cp=true(默認值爲真)

2. 分區裁剪

可以在查詢的過程中減少不必要的分區。 例如,若有以下查詢:

SELECT * FROM (SELECTT a1,COUNT(1) FROM T GROUP BY a1) subq WHERE subq.prtn=100; 
-- 多餘分區
SELECT * FROM T1 JOIN (SELECT * FROM T2) subq ON (T1.a1=subq.a2) WHERE subq.prtn=100;

查詢語句若將“subq.prtn=100”條件放入子查詢中更爲高效,可以減少讀入的分區 數目。 Hive 自動執行這種裁剪優化。

分區參數爲:hive.optimize.pruner=true(默認值爲真)

3. JOIN操作

在編寫帶有 join 操作的代碼語句時,應該將條目少的表/子查詢放在 Join 操作符的左邊。 因爲在 Reduce 階段,位於 Join 操作符左邊的表的內容會被加載進內存,載入條目較少的表 可以有效減少 OOM(out of memory)即內存溢出。所以對於同一個 key 來說,對應的 value 值小的放前,大的放後,這便是“小表放前”原則。 若一條語句中有多個 Join,依據 Join 的條件相同與否,有不同的處理方法。

3.1 JOIN原則

在使用寫有 Join 操作的查詢語句時有一條原則:應該將條目少的表/子查詢放在 Join 操作符的左邊。原因是在 Join 操作的 Reduce 階段,位於 Join 操作符左邊的表的內容會被加載進內存,將條目少的表放在左邊,可以有效減少發生 OOM 錯誤的機率。
對於一條語句中有多個 Join 的情況,如果 Join 的條件相同,比如查詢:

INSERT OVERWRITE TABLE pv_users
SELECT pv.pageid, u.age FROM page_view p
JOIN user u ON (pv.userid = u.userid)
JOIN newuser x ON (u.userid = x.userid);

如果 Join 的 key 相同,不管有多少個表,都會則會合併爲一個 Map-Reduce,不是 ‘n’ 個。在做 OUTER JOIN 的時候也是一樣

如果 Join 的條件不相同,比如:

INSERT OVERWRITE TABLE pv_users
SELECT pv.pageid, u.age FROM page_view p
JOIN user u ON (pv.userid = u.userid)
JOIN newuser x on (u.age = x.age);

Map-Reduce 的任務數目和 Join 操作的數目是對應的,上述查詢和以下查詢是等價的:

INSERT OVERWRITE TABLE tmptable
SELECT * FROM page_view p JOIN user u
ON (pv.userid = u.userid);
INSERT OVERWRITE TABLE pv_users
SELECT x.pageid, x.age FROM tmptable x
JOIN newuser y ON (x.age = y.age);

3.2 MAP JOIN操作

Join 操作在 Map 階段完成,不再需要Reduce,前提條件是需要的數據在 Map 的過程中可以訪問到。比如查詢:

INSERT OVERWRITE TABLE pv_users
SELECT /*+ MAPJOIN(pv) */ pv.pageid, u.age
FROM page_view pv
JOIN user u ON (pv.userid = u.userid);

可以在 Map 階段完成 Join.
相關的參數爲:

hive.join.emit.interval = 1000
hive.mapjoin.size.key = 10000
hive.mapjoin.cache.numrows = 10000

在這裏插入圖片描述
MapJoin簡單說就是在Map階段將小表讀入內存,順序掃描大表完成Join
上圖是Hive MapJoin的原理圖,出自Facebook工程師Liyin Tang的一篇介紹Join優化的slice,從圖中可以看出MapJoin分爲兩個階段:
(1)通過MapReduce Local Task,將小表讀入內存,生成HashTableFiles上傳至Distributed Cache中,這裏會對HashTableFiles進行壓縮。
(2)MapReduce Job在Map階段,每個Mapper從Distributed Cache讀取HashTableFiles到內存中,順序掃描大表,在Map階段直接進行Join,將數據傳遞給下一個MapReduce任務。
也就是在map端進行join避免了shuffle

4. GROUP BY操作

進行GROUP BY操作時需要注意一下幾點:

4.1 Map端部分聚合

事實上並不是所有的聚合操作都需要在reduce部分進行,很多聚合操作都可以先在Map端進行部分聚合,然後reduce端得出最終結果。

這裏需要修改的參數爲:

# 用於設定是否在 map 端進行聚合,默認值爲真
hive.map.aggr=true
 
# 用於設定 map 端進行聚合操作的條目數
hive.groupby.mapaggr.checkinterval=100000

4.2 有數據傾斜時進行負載均衡

此處需要設定 hive.groupby.skewindata,當選項設定爲 true 是,生成的查詢計劃有兩 個 MapReduce 任務。在第一個 MapReduce 中,map 的輸出結果集合會隨機分佈到 reduce 中, 每個 reduce 做部分聚合操作,並輸出結果。這樣處理的結果是,相同的 Group By Key 有可 能分發到不同的 reduce 中,從而達到負載均衡的目的;第二個 MapReduce 任務再根據預處 理的數據結果按照 Group By Key 分佈到 reduce 中(這個過程可以保證相同的 Group By Key 分佈到同一個 reduce 中),最後完成最終的聚合操作。

5.Map階段優化

	mapred.min.split.size: 指的是數據的最小分割單元大小;min的默認值是1B
	mapred.max.split.size: 指的是數據的最大分割單元大小;max的默認值是256MB
	通過調整max可以起到調整map數的作用,減小max可以增加map數,增大max可以減少map數。
	需要提醒的是,直接調整mapred.map.tasks這個參數是沒有效果的。

舉例:
a) 假設input目錄下有1個文件a,大小爲780M,那麼hadoop會將該文件a分隔成7個塊(6個128m的塊和1個12m的塊),從而產生7個map數
b) 假設input目錄下有3個文件a,b,c,大小分別爲10m,20m,130m,那麼hadoop會分隔成4個塊(10m,20m,128m,2m),從而產生4個map數
即,如果文件大於塊大小(128m),那麼會拆分,如果小於塊大小,則把該文件當成一個塊。

其實這就涉及到小文件的問題:如果一個任務有很多小文件(遠遠小於塊大小128m),則每個小文件也會被當做一個塊,用一個map任務來完成,
而一個map任務啓動和初始化的時間遠遠大於邏輯處理的時間,就會造成很大的資源浪費。
而且,同時可執行的map數是受限的。那麼問題又來了。。是不是保證每個map處理接近128m的文件塊,就高枕無憂了?
答案也是不一定。比如有一個127m的文件,正常會用一個map去完成,但這個文件只有一個或者兩個小字段,卻有幾千萬的記錄,
如果map處理的邏輯比較複雜,用一個map任務去做,肯定也比較耗時。

我們該如何去解決呢???
我們需要採取兩種方式來解決:即減少map數和增加map數

5.1 減少map數量

假設一個SQL任務:

Select count(1) from popt_tbaccountcopy_mes where pt = ‘2012-07-04’;

該任務的inputdir /group/p_sdo_data/p_sdo_data_etl/pt/popt_tbaccountcopy_mes/pt=2012-07-04
共有194個文件,其中很多是遠遠小於128m的小文件,總大小9G,正常執行會用194個map任務。
Map總共消耗的計算資源: SLOTS_MILLIS_MAPS= 623,020

我通過以下方法來在map執行前合併小文件,減少map數:
set mapred.max.split.size=100000000;
set mapred.min.split.size.per.node=100000000;
set mapred.min.split.size.per.rack=100000000;
set hive.input.format=org.apache.hadoop.hive.ql.io.CombineHiveInputFormat;
再執行上面的語句,用了74個map任務,map消耗的計算資源:SLOTS_MILLIS_MAPS= 333,500

對於這個簡單SQL任務,執行時間上可能差不多,但節省了一半的計算資源。
大概解釋一下,100000000表示100M, set hive.input.format=org.apache.hadoop.hive.ql.io.CombineHiveInputFormat;這個參數表示執行前進行小文件合併,前面三個參數確定合併文件塊的大小,大於文件塊大小128m的,按照128m來分隔,小於128m,大於100m的,按照100m來分隔,把那些小於100m的(包括小文件和分隔大文件剩下的),進行合併,最終生成了74個塊。

5.2 增大map數量

如何適當的增加map數?
當input的文件都很大,任務邏輯複雜,map執行非常慢的時候,可以考慮增加Map數,
來使得每個map處理的數據量減少,從而提高任務的執行效率。
假設有這樣一個任務:

   Select data_desc,
          count(1),
          count(distinct id),
          sum(case when …),
          sum(case when ...),
          sum(…)
  from a group by data_desc

如果表a只有一個文件,大小爲120M,但包含幾千萬的記錄,如果用1個map去完成這個任務,
肯定是比較耗時的,這種情況下,我們要考慮將這一個文件合理的拆分成多個,
這樣就可以用多個map任務去完成。

     set mapred.reduce.tasks=10;
      create table a_1 as 
      select * from a 
      distribute by rand(123); 

這樣會將a表的記錄,隨機的分散到包含10個文件的a_1表中,再用a_1代替上面sql中的a表,
則會用10個map任務去完成。
每個map任務處理大於12M(幾百萬記錄)的數據,效率肯定會好很多。

看上去,貌似這兩種有些矛盾,一個是要合併小文件,一個是要把大文件拆成小文件,
這點正是重點需要關注的地方:使單個map任務處理合適的數據量

6. reduce階段優化

Reduce的個數對整個作業的運行性能有很大影響。
如果Reduce設置的過大,那麼將會產生很多小文件,對NameNode會產生一定的影響,而且整個作業的運行時間未必會減少;如果Reduce設置的過小,那麼單個Reduce處理的數據將會加大,很可能會引起OOM異常。

如果設置了mapred.reduce.tasks/mapreduce.job.reduces參數,那麼Hive會直接使用它的值作爲Reduce的個數;
如果mapred.reduce.tasks/mapreduce.job.reduces的值沒有設置(也就是-1),那麼Hive會根據輸入文件的大小估算出Reduce的個數。
根據輸入文件估算Reduce的個數可能未必很準確,因爲Reduce的輸入是Map的輸出,而Map的輸出可能會比輸入要小,
所以最準確的數根據Map的輸出估算Reduce的個數。

Hive自己如何確定reduce數

reduce個數的設定極大影響任務執行效率,不指定reduce個數的情況下,Hive會猜測確定一個reduce個數,基於以下兩個設定:
hive.exec.reducers.bytes.per.reducer(每個reduce任務處理的數據量,默認爲1000^3=1G)
hive.exec.reducers.max(每個任務最大的reduce數,默認爲999)

計算reducer數的公式很簡單N=min(參數2,總輸入數據量/參數1)
即,如果reduce的輸入(map的輸出)總大小不超過1G,那麼只會有一個reduce任務;

如:select pt,count(1) from popt_tbaccountcopy_mes where pt = '2012-07-04' group by pt; 
            /group/p_sdo_data/p_sdo_data_etl/pt/popt_tbaccountcopy_mes/pt=2012-07-04 總大小爲9G多,
            因此這句有10個reduce

調整reduce個數方法一
調整hive.exec.reducers.bytes.per.reducer參數的值;

set hive.exec.reducers.bytes.per.reducer=500000000; (500M)
select pt,count(1) from popt_tbaccountcopy_mes where pt = '2012-07-04' group by pt; 這次有20個reduce

調整reduce個數方法二

set mapred.reduce.tasks = 15;
select pt,count(1) from popt_tbaccountcopy_mes where pt = '2012-07-04' group by pt;這次有15個reduce

reduce個數並不是越多越好
同map一樣,啓動和初始化reduce也會消耗時間和資源;
另外,有多少個reduce,就會有多少個輸出文件,如果生成了很多個小文件,那麼如果這些小文件作爲下一個任務的輸入,
則也會出現小文件過多的問題;

什麼情況下只有一個reduce
很多時候你會發現任務中不管數據量多大,不管你有沒有設置調整reduce個數的參數,任務中一直都只有一個reduce任務;
其實只有一個reduce任務的情況,除了數據量小於hive.exec.reducers.bytes.per.reducer參數值的情況外,還有以下原因:
沒有group by的彙總,比如把select pt,count(1) from popt_tbaccountcopy_mes where pt = ‘2012-07-04’ group by pt;
寫成 select count(1) from popt_tbaccountcopy_mes where pt = ‘2012-07-04’;
這點非常常見,希望大家儘量改寫。

用了Order by

和map階段設置同理,在設置reduce個數的時候也需要考慮這兩個原則:使大數據量利用合適的reduce數;使單個reduce任務處理合適的數據量;

7. 合併小文件

我們知道文件數目小,容易在文件存儲端造成瓶頸,給 HDFS 帶來壓力,影響處理效率。對此,可以通過合併Map和Reduce的結果文件來消除這樣的影響。

用於設置合併屬性的參數有:

# 是否合併Map輸出文件,默認值爲真
hive.merge.mapfiles=true
 
# 是否合併Reduce 端輸出文件,默認值爲假
hive.merge.mapredfiles=false
 
# 合併文件的大小,默認值爲 256000000
hive.merge.size.per.task=256*1000*1000
Hive優化之小文件問題及其解決方案:

1.小文件是如何產生的

  • 1.動態分區插入數據,產生大量的小文件,從而導致map數量劇增。
  • 2.reduce數量越多,小文件也越多(reduce的個數和輸出文件是對應的)。
  • 3.數據源本身就包含大量的小文件。

2.小文件問題的影響

  • 1.從Hive的角度看,小文件會開很多map,一個map開一個JVM去執行,所以這些任務的初始化,啓動,執行會浪費大量的資源,嚴重影響性能。
  • 2.在HDFS中,每個小文件對象約佔150byte,如果小文件過多會佔用大量內存。這樣NameNode內存容量嚴重製約了集羣的擴展。

3.小文件問題的解決方案
從小文件產生的途經就可以從源頭上控制小文件數量,方法如下:

  • 1.使用Sequencefile作爲表存儲格式,不要用textfile,在一定程度上可以減少小文件。
  • 2.減少reduce的數量(可以使用參數進行控制)。
  • 3.少用動態分區,用時記得按distribute by分區。

對於已有的小文件,我們可以通過以下幾種方案解決

  • 1.使用hadoop archive命令把小文件進行歸檔。
  • 2.重建表,建表時減少reduce數量。
  • 3.通過參數進行調節,設置map/reduce端的相關參數,如下:
設置map輸入合併小文件的相關參數:

[java] view plain copy
//每個Map最大輸入大小(這個值決定了合併後文件的數量)  
set mapred.max.split.size=256000000;    
//一個節點上split的至少的大小(這個值決定了多個DataNode上的文件是否需要合併)  
set mapred.min.split.size.per.node=100000000;  
//一個交換機下split的至少的大小(這個值決定了多個交換機上的文件是否需要合併)    
set mapred.min.split.size.per.rack=100000000;  
//執行Map前進行小文件合併  
set hive.input.format=org.apache.hadoop.hive.ql.io.CombineHiveInputFormat;   

設置map輸出和reduce輸出進行合併的相關參數:
[java] view plain copy
//設置map端輸出進行合併,默認爲true  
set hive.merge.mapfiles = true  
//設置reduce端輸出進行合併,默認爲false  
set hive.merge.mapredfiles = true  
//設置合併文件的大小  
set hive.merge.size.per.task = 256*1000*1000  
//當輸出文件的平均大小小於該值時,啓動一個獨立的MapReduce任務進行文件merge。  
set hive.merge.smallfiles.avgsize=16000000  
  • 4.存儲格式:可以使用列裁剪,分區裁剪,orc,parquet等存儲格式。
    Hive支持ORCfile,這是一種新的表格存儲格式,通過諸如謂詞下推,壓縮等技術來提高執行速度提升。
    對於每個HIVE表使用ORCFile應該是一件容易的事情,並且對於獲得HIVE查詢的快速響應時間非常有益。
    作爲一個例子,考慮兩個大表A和B(作爲文本文件存儲,其中一些列未在此處指定,即行試存儲的缺點)以及一個簡單的查詢,如:
SELECT A.customerID, A.name, A.age, A.address join
B.role, B.department, B.salary
ON A.customerID=B.customerID;

此查詢可能需要很長時間才能執行,因爲表A和B都以TEXT形式存儲,進行全表掃描。

將這些表格轉換爲ORCFile格式通常會顯着減少查詢時間:
ORC支持壓縮存儲(使用ZLIB或如上所示使用SNAPPY),但也支持未壓縮的存儲。

CREATE TABLE A_ORC (
	customerID int, name string, age int, address string
	) STORED AS ORC tblproperties (“orc.compress" = “SNAPPY”);

	INSERT INTO TABLE A_ORC SELECT * FROM A;


	CREATE TABLE B_ORC (
	customerID int, role string, salary float, department string
	) STORED AS ORC tblproperties (“orc.compress" = “SNAPPY”);

	INSERT INTO TABLE B_ORC SELECT * FROM B;

	SELECT A_ORC.customerID, A_ORC.name,
	A_ORC.age, A_ORC.address join
	B_ORC.role, B_ORC.department, B_ORC.salary
	ON A_ORC.customerID=B_ORC.customerID;
  • 5.壓縮格式:大數據場景下存儲格式壓縮格式尤爲關鍵,可以提升計算速度,減少存儲空間,降低網絡io,磁盤io,所以要選擇合適的壓縮格式和存儲格式

8. 熟練使用SQL來優化查詢

場景:有一張 user 表,爲賣家每天收到表,user_id,ds(日期)爲 key,屬性有主營類目,指標有交易金額,交易筆數。每天要取前10天的總收入,總筆數,和最近一天的主營類目。

8.1 解決方法一

如下所示:常用方法

INSERT OVERWRITE TABLE t1
SELECT user_id,substr(MAX(CONCAT(ds,cat),9) AS main_cat) FROM users
-- 20120329爲日期列的值,實際代碼中可以用函數表示出當天日期 
WHERE ds=20120329 
GROUP BY user_id;
 
INSERT OVERWRITE TABLE t2
SELECT user_id,sum(qty) AS qty,SUM(amt) AS amt FROM users
WHERE ds BETWEEN 20120301 AND 20120329
GROUP BY user_id
SELECT t1.user_id,t1.main_cat,t2.qty,t2.amt FROM t1
JOIN t2 ON t1.user_id=t2.user_id

下面給出方法一的思路,實現步驟如下:

第一步:利用分析函數,取每個 user_id 最近一天的主營類目,存入臨時表 t1。
第二步:彙總 10 天的總交易金額,交易筆數,存入臨時表 t2。
第三步:關聯 t1,t2,得到最終的結果。

8.2 解決方法二

如下所示:優化方法

SELECT user_id,substr(MAX(CONCAT(ds,cat)),9) AS main_cat,SUM(qty),SUM(amt) FROM users
WHERE ds BETWEEN 20120301 AND 20120329
GROUP BY user_id

在工作中我們總結出:方案二的開銷等於方案一的第二步的開銷,性能提升,由原有的25分鐘完成,縮短爲10分鐘以內完成。節省了兩個臨時表的讀寫是一個關鍵原因,這種方式也適用於 Oracle 中的數據查找工作。
SQL 具有普適性,很多 SQL 通用的優化方案在 Hadoop 分佈式計算方式中也可以達到效果。

9. 無效ID在關聯時的數據傾斜問題

問題:日誌中常會出現信息丟失,比如每日約爲 20 億的全網日誌,其中的 user_id 爲主 鍵,在日誌收集過程中會丟失,出現主鍵爲 null 的情況,如果取其中的 user_id 和 bmw_users 關聯,就會碰到數據傾斜的問題。原因是 Hive 中,主鍵爲 null 值的項會被當做相同的 Key 而分配進同一個計算 Map。

9.1 解決方法一

如下所示:user_id 爲空的不參與關聯,子查詢過濾 null

SELECT * FROM log a
JOIN bmw_users b ON a.user_id IS NOT NULL AND a.user_id=b.user_id
UNION All SELECT * FROM log a WHERE a.user_id IS NULL;

9.2 解決方法二

如下所示:函數過濾 null

SELECT * FROM log a LEFT OUTER
JOIN bmw_users b ON
CASE WHEN a.user_id IS NULL THEN CONCAT(‘dp_hive’,RAND()) ELSE a.user_id END =b.user_id;

調優結果:原先由於數據傾斜導致運行時長超過 1 小時,解決方法一運行每日平均時長 25 分鐘,解決方法二運行的每日平均時長在 20 分鐘左右。優化效果很明顯。

我們在工作中總結出:解決方法二比解決方法一效果更好,不但IO少了,而且作業數也少了。解決方法一中log讀取兩次,job 數爲2。解決方法二中 job 數是1。這個優化適合無效 id(比如-99、 ‘’,null 等)產生的傾斜問題。把空值的 key 變成一個字符串加上隨機數,就能把傾斜的 數據分到不同的Reduce上,從而解決數據傾斜問題。因爲空值不參與關聯,即使分到不同 的 Reduce 上,也不會影響最終的結果。附上 Hadoop 通用關聯的實現方法是:關聯通過二次排序實現的,關聯的列爲 partion key,關聯的列和表的 tag 組成排序的 group key,根據 pariton key分配Reduce。同一Reduce內根據group key排序。

10. 不同數據類型關聯產生的傾斜問題

問題:不同數據類型 id 的關聯會產生數據傾斜問題。
一張表 s8 的日誌,每個商品一條記錄,要和商品表關聯。但關聯卻碰到傾斜的問題。 s8 的日誌中有 32 爲字符串商品 id,也有數值商品 id,日誌中類型是 string 的,但商品中的 數值 id 是 bigint 的。猜想問題的原因是把 s8 的商品 id 轉成數值 id 做 hash 來分配 Reduce, 所以字符串 id 的 s8 日誌,都到一個 Reduce 上了,解決的方法驗證了這個猜測。

解決方法:把數據類型轉換成字符串類型
SELECT * FROM s8_log a LEFT OUTERJOIN r_auction_auctions b ON a.auction_id=CASE(b.auction_id AS STRING);
調優結果顯示:數據表處理由 1 小時 30 分鐘經代碼調整後可以在 20 分鐘內完成。

11. 利用Hive對UNION ALL優化的特性

1.多表 union all 會優化成一個 job。
問題:比如推廣效果表要和商品表關聯,效果表中的 auction_id 列既有 32 爲字符串商 品 id,也有數字 id,和商品表關聯得到商品的信息。

解決方法:Hive SQL 性能會比較好

SELECT * FROM effect a
JOIN
(SELECT auction_id AS auction_id FROM auctions
UNION All
SELECT auction_string_id AS auction_id FROM auctions) b
ON a.auction_id=b.auction_id;

比分別過濾數字 id,字符串 id 然後分別和商品表關聯性能要好。
這樣寫的好處:1 個 MapReduce 作業,商品表只讀一次,推廣效果表只讀取一次。把 這個 SQL 換成 Map/Reduce 代碼的話,Map 的時候,把 a 表的記錄打上標籤 a,商品表記錄 每讀取一條,打上標籤 b,變成兩個<key,value>對,<(b,數字 id),value>,<(b,字符串 id),value>。
所以商品表的 HDFS 讀取只會是一次。

解決Hive對UNION ALL優化的短板:對 union all 優化只侷限於非嵌套查詢。

1.消滅子查詢內的 group by

SELECT * FROM
(SELECT * FROM t1 GROUP BY c1,c2,c3 UNION ALL SELECT * FROM t2 GROUP BY c1,c2,c3)t3
GROUP BY c1,c2,c3;

從業務邏輯上說,子查詢內的 GROUP BY 怎麼都看顯得多餘(功能上的多餘,除非有 COUNT(DISTINCT)),如果不是因爲 Hive Bug 或者性能上的考量(曾經出現如果不執行子查詢 GROUP BY,數據得不到正確的結果的 Hive Bug)。所以這個 Hive 按經驗轉換成如下所示:

SELECT * FROM (SELECT * FROM t1 UNION ALL SELECT * FROM t2)t3 GROUP BY c1,c2,c3;

調優結果:經過測試,並未出現 union all 的 Hive Bug,數據是一致的。MapReduce 的 作業數由 3 減少到 1。
t1 相當於一個目錄,t2 相當於一個目錄,對 Map/Reduce 程序來說,t1,t2 可以作爲 Map/Reduce 作業的 mutli inputs。這可以通過一個 Map/Reduce 來解決這個問題。Hadoop 的 計算框架,不怕數據多,就怕作業數多。
但如果換成是其他計算平臺如 Oracle,那就不一定了,因爲把大的輸入拆成兩個輸入, 分別排序彙總後 merge(假如兩個子排序是並行的話),是有可能性能更優的(比如希爾排 序比冒泡排序的性能更優)。

2.消滅子查詢內的 COUNT(DISTINCT),MAX,MIN

SELECT * FROM
(SELECT * FROM t1
UNION ALL SELECT c1,c2,c3 COUNT(DISTINCT c4) FROM t2 GROUP BY c1,c2,c3) t3
GROUP BY c1,c2,c3;

由於子查詢裏頭有 COUNT(DISTINCT)操作,直接去 GROUP BY 將達不到業務目標。這時採用 臨時表消滅 COUNT(DISTINCT)作業不但能解決傾斜問題,還能有效減少 jobs。

INSERT t4 SELECT c1,c2,c3,c4 FROM t2 GROUP BY c1,c2,c3;
SELECT c1,c2,c3,SUM(income),SUM(uv) FROM
(SELECT c1,c2,c3,income,0 AS uv FROM t1
UNION ALL
SELECT c1,c2,c3,0 AS income,1 AS uv FROM t2) t3
GROUP BY c1,c2,c3;

job 數是 2,減少一半,而且兩次 Map/Reduce 比 COUNT(DISTINCT)效率更高。
調優結果:千萬級別的類目表,member 表,與 10 億級得商品表關聯。原先 1963s 的任務經過調整,1152s 即完成。

3.消滅子查詢內的 JOIN

SELECT * FROM
(SELECT * FROM t1 UNION ALL SELECT * FROM t4 UNION ALL SELECT * FROM t2 JOIN t3 ON t2.id=t3.id) x
GROUP BY c1,c2;

上面代碼運行會有 5 個 jobs。加入先 JOIN 生存臨時表的話 t5,然後 UNION ALL,會變成 2 個 jobs。

INSERT OVERWRITE TABLE t5
SELECT * FROM t2 JOIN t3 ON t2.id=t3.id;
SELECT * FROM (t1 UNION ALL t4 UNION ALL t5);

調優結果顯示:針對千萬級別的廣告位表,由原先 5 個 Job 共 15 分鐘,分解爲 2 個 job 一個 8-10 分鐘,一個3分鐘。

12. GROUP BY替代COUNT(DISTINCT)達到優化效果

計算 uv 的時候,經常會用到 COUNT(DISTINCT),但在數據比較傾斜的時候 COUNT(DISTINCT) 會比較慢。這時可以嘗試用 GROUP BY 改寫代碼計算 uv。

12.1 原有代碼

INSERT OVERWRITE TABLE s_dw_tanx_adzone_uv PARTITION (ds=20120329) 
SELECT 20120329 AS thedate,adzoneid,COUNT(DISTINCT acookie) AS uv 
FROM s_ods_log_tanx_pv t 
WHERE t.ds=20120329 
GROUP BY adzoneid;

12.2 測試用例

/* 
 * 關於COUNT(DISTINCT)的數據傾斜問題不能一概而論,要依情況而定,下面是我測試的一組數據:
 * 測試數據:169857條
 */
 
-- 統計每日IP,耗時:24.805 seconds
CREATE TABLE ip_2014_12_29 AS SELECT COUNT(DISTINCT ip) AS IP 
FROM logdfs WHERE logdate=’2014_12_29′;
 
-- 統計每日IP(改造),耗時:46.833 seconds
CREATE TABLE ip_2014_12_29 AS SELECT COUNT(1) AS IP FROM (SELECT DISTINCT ip 
from logdfs WHERE logdate=’2014_12_29′) tmp;

測試結果表名:明顯改造後的語句比之前耗時,這是因爲改造後的語句有2個SELECT,多了一個job,這樣在數據量小的時候,數據不會存在傾斜問題。

參考資料:
1.大數據Hive系列之Hive性能優化(全面)
2.Hive總結篇及Hive的優化

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