總結(jié)了 Hive 的常用優(yōu)化手段。
列裁剪及分區(qū)裁剪
這是最基本的操作。所謂列裁剪就是在查詢時只讀取需要的列,分區(qū)裁剪就是只讀取需要的分區(qū)儿惫。
比如當(dāng)列很多或者數(shù)據(jù)量很大時,如果使用 select * from order_table;
或者不指定分區(qū)伸但,全列掃描和全表掃描效率都很低肾请。
這個時候我們可以指定列:
select
uid,
price
from
order_table;
或者當(dāng)這個表是分區(qū)表的時候,指定分區(qū):
select
uid,
price
from
order_table
where
pt_date = "20200901";
Hive 中與列裁剪優(yōu)化相關(guān)的配置項是hive.optimize.cp
更胖,與分區(qū)裁剪優(yōu)化相關(guān)的則是hive.optimize.pruner
铛铁,默認都是true。
謂詞下推
在關(guān)系型數(shù)據(jù)庫如 MySQL 中却妨,也有謂詞下推(Predicate Pushdown饵逐,PPD)的概念。就是將 SQL 語句中的 where 謂詞邏輯都盡可能提前執(zhí)行彪标,減少下游處理的數(shù)據(jù)量倍权。
select
a.uid,
b.user_id,
...
from
user_table a
inner join
(select
uid,
user_id,
price
from
order_table
where
pt_date = "20200901"
and
paystate = 2)b
on
a.user_id = b.user_id;
對 order_table 做過濾的 where 語句寫在子查詢內(nèi)部,而不是外部捞烟。Hive 中有謂詞下推優(yōu)化的配置項hive.optimize.ppd
薄声,默認值true,與它對應(yīng)的邏輯優(yōu)化器是 PredicatePushDown题画。該優(yōu)化器就是將 OperatorTree 中的 FilterOperator 向上提默辨。
sort by 代替 order by
HiveSQL 中的 order by 與其他 SQL 方言中的功能一樣,就是將結(jié)果按某字段全局排序婴程,這會導(dǎo)致所有 map 端數(shù)據(jù)都進入一個 reducer 中廓奕,在數(shù)據(jù)量大時可能會長時間計算不完抱婉。
如果使用 sort by档叔,那么還是會視情況啟動多個 reducer 進行排序桌粉,并且保證每個 reducer 內(nèi)局部有序。為了控制 map 端數(shù)據(jù)分配到 reducer 的 key衙四,往往還要配合 distribute by 一同使用铃肯。如果不加 distribute by 的話,map 端數(shù)據(jù)就會隨機分配到 reducer传蹈。
舉個例子押逼,假如要以 uid 為 key,以訂單時間倒序惦界、訂單金額倒序輸出記錄數(shù)據(jù):
select
uid,
user_id,
price
from
order_table
where
pt_date = "20200901"
and
paystate = 2
distribute by
uid
sort by
order_time desc,
price desc;
group by 代替 distinct
數(shù)據(jù)量較大的情況下挑格,慎用count(distinct),count(distinct)容易產(chǎn)生傾斜問題
當(dāng)要統(tǒng)計某一列的去重數(shù)時沾歪,如果數(shù)據(jù)量很大漂彤,count(distinct) 就會非常慢,原因與 order by 類似灾搏,count(distinct) 邏輯只會有很少的 reducer 來處理挫望。這時可以用 group by 來改寫:
select
count(1)
from
(
select
uid
from
order_table
where
pt_date = "20200901"
group by
uid
);
但是這樣寫會啟動兩個 MR job(單純 distinct 只會啟動一個),所以要確保數(shù)據(jù)量大到啟動 job 的 overhead 遠小于計算耗時狂窑,才考慮這種方法媳板。當(dāng)數(shù)據(jù)集很小或者 key 的傾斜比較明顯時,group by 還可能會比 distinct 慢泉哈。
group by 配置調(diào)整
map 端預(yù)聚合
group by 時蛉幸,如果先起一個 combiner 在 map 端做部分預(yù)聚合,可以有效減少 shuffle 數(shù)據(jù)量丛晦。預(yù)聚合的配置項是 hive.map.aggr
巨缘,默認值 true,對應(yīng)的優(yōu)化器為 GroupByOptimizer采呐,簡單方便若锁。
通過 hive.groupby.mapaggr.checkinterval
參數(shù)也可以設(shè)置 map 端預(yù)聚合的行數(shù)閾值,超過該值就會分拆 job斧吐,默認值 100000又固。
傾斜均衡配置項
group by 時如果某些 key 對應(yīng)的數(shù)據(jù)量過大,就會發(fā)生數(shù)據(jù)傾斜煤率。Hive 自帶了一個均衡數(shù)據(jù)傾斜的配置項hive.groupby.skewindata
仰冠,默認值false。
其實現(xiàn)方法是在 group by 時啟動兩個 MR job蝶糯。第一個 job 會將 map 端數(shù)據(jù)隨機輸入 reducer洋只,每個 reducer 做部分聚合,相同的 key 就會分布在不同的 reducer 中。第二個 job 再將前面預(yù)處理過的數(shù)據(jù)按 key 聚合并輸出結(jié)果识虚,這樣就起到了均衡的效果肢扯。
join 基礎(chǔ)優(yōu)化
build table(小表)前置
在最常見的 hash join 方法中,一般總有一張相對小的表和一張相對大的表担锤,小表叫 build table蔚晨,大表叫 probe table。
Hive 在解析帶 join 的 SQL 語句時肛循,會默認將最后一個表作為 probe table铭腕,將前面的表作為 build table 并試圖將它們讀進內(nèi)存。如果表順序?qū)懛炊嗫罚琾robe table 在前面累舷,引發(fā) OOM 的風(fēng)險就高了。
在維度建模數(shù)據(jù)倉庫中夹孔,事實表就是 probe table被盈,維度表就是 build table。
多表 join 時 key 相同
這種情況會將多個 join 合并為一個 MR job 來處理:
select
a.uid,
b.user_id,
...
from
user_table a
inner join
(select
uid,
user_id,
price
from
order_table
where
pt_date = "20200901"
and
paystate = 2)b
on
a.user_id = b.user_id
inner join
parent_table c
on
a.user_id = c.user_id;
如果上面兩個 join 的條件不相同析蝴,比如改成 a.uid = c.uid
害捕,就會拆成兩個 MR job 計算。
負責(zé)這個的是相關(guān)性優(yōu)化器 CorrelationOptimizer闷畸,它的功能除此之外還非常多尝盼,邏輯復(fù)雜,可以參考 Hive官方的文檔佑菩。
利用 map join 特性
map join 特別適合大小表 join 的情況盾沫。Hive 會將 build table 和 probe table 在 map 端直接完成 join 過程,消滅了 reduce殿漠,效率很高赴精。
select
/*+ mapjoin(a) */
a.uid,
b.user_id,
...
from
user_table a
inner join
(select
uid,
user_id,
price
from
order_table
where
pt_date = "20200901"
and
paystate = 2)b
on
a.user_id = b.user_id;
優(yōu)化 SQL 處理 join 數(shù)據(jù)傾斜
空值或無意義值
空值或無意義值很常見,比如日志類型的數(shù)據(jù)绞幌,要統(tǒng)計每天的活躍手機號蕾哟,但是總有一些日志數(shù)據(jù)沒有收集到手機號,或為空莲蜘、為 NULL 等谭确,這個時候就需要提前將這些無意義的數(shù)據(jù)過濾掉,避免消耗票渠。
build table 過大
有時逐哈,build table 會大到無法直接使用 map join 的地步,比如全量用戶維度表问顷,而使用普通 join 又有數(shù)據(jù)分布不均的問題昂秃。這時就要充分利用 probe table 的限制條件禀梳,削減 build table 的數(shù)據(jù)量,再使用 map join 解決肠骆。代價就是需要進行兩次 join算途。舉個例子:
select
/*+ mapjoin(aa) */
aa.uid,
aa.user_id,
...
from
(select
/*+ mapjoin(a) */
a.uid,
b.user_id,
...
from
user_table a
inner join
(select
uid,
user_id,
price
from
order_table
where
pt_date = "20200901"
and
paystate = 2)b
on
a.user_id = b.user_id)aa
inner join
(select
uid,
user_id,
price
from
order_table
where
pt_date = "20200902"
and
paystate = 2)bb;
MapReduce 優(yōu)化
調(diào)整 mapper 數(shù)
mapper 數(shù)量與輸入文件的 split 數(shù)息息相關(guān),在 Hadoop 源碼org.apache.hadoop.mapreduce.lib.input.FileInputFormat
類中可以看到 split 劃分的具體邏輯哗戈。
可以直接通過參數(shù) mapred.map.tasks(默認值2)
來設(shè)定 mapper 數(shù)的期望值郊艘,但它不一定會生效荷科,下面會提到唯咬。
設(shè)輸入文件的總大小為 total_input_size。HDFS 中畏浆,一個塊的大小由參數(shù) dfs.block.size 指定胆胰,默認值 64MB 或 128MB。在默認情況下刻获,mapper數(shù)就是:
default_mapper_num = total_input_size / dfs.block.size
蜀涨。
參數(shù) mapred.min.split.size(默認值1B)
和mapred.max.split.size(默認值64MB)
分別用來指定 split 的最小和最大大小。split 大小和 split 數(shù)計算規(guī)則是:
split_size = MAX(mapred.min.split.size, MIN(mapred.max.split.size, dfs.block.size))
蝎毡;
split_num = total_input_size / split_size
厚柳。
得出 mapper 數(shù):
mapper_num = MIN(split_num, MAX(default_num, mapred.map.tasks))
。
可見沐兵,如果想減少 mapper 數(shù)别垮,就適當(dāng)調(diào)高 mapred.min.split.size,split 數(shù)就減少了扎谎。如果想增大 mapper 數(shù)碳想,除了降低 mapred.min.split.size 之外,也可以調(diào)高 mapred.map.tasks毁靶。
一般來講胧奔,如果輸入文件是少量大文件,就減少 mapper 數(shù)预吆;如果輸入文件是大量非小文件龙填,就增大 mapper 數(shù);至于大量小文件的情況拐叉,得參考下面“合并小文件”一節(jié)的方法處理岩遗。
調(diào)整 reducer 數(shù)
reducer 數(shù)量的確定方法比 mapper 簡單得多。使用參數(shù)mapred.reduce.tasks
可以直接設(shè)定 reducer 數(shù)量巷嚣,不像 mapper 一樣是期望值喘先。但如果不設(shè)這個參數(shù)的話,Hive 就會自行推測廷粒,邏輯如下:
參數(shù)hive.exec.reducers.bytes.per.reducer
用來設(shè)定每個 reducer 能夠處理的最大數(shù)據(jù)量窘拯,默認值 1G(1.2版本之前)或 256M(1.2版本之后)红且。
參數(shù) hive.exec.reducers.max
用來設(shè)定每個 job 的最大 reducer 數(shù)量,默認值 999(1.2版本之前)或 1009(1.2版本之后)涤姊。
得出 reducer 數(shù):
reducer_num = MIN(total_input_size / reducers.bytes.per.reducer, reducers.max)
暇番。
reducer 數(shù)量與輸出文件的數(shù)量相關(guān)。如果 reducer 數(shù)太多思喊,會產(chǎn)生大量小文件壁酬,對 HDFS 造成壓力。如果 reducer 數(shù)太少恨课,每個 reducer 要處理很多數(shù)據(jù)舆乔,容易拖慢運行時間或者造成 OOM。
合并小文件
輸入階段合并
需要更改 Hive 的輸入文件格式剂公,即參數(shù)hive.input.format
希俩,默認值是org.apache.hadoop.hive.ql.io.HiveInputFormat
,我們改成org.apache.hadoop.hive.ql.io.CombineHiveInputFormat
纲辽。這樣比起上面調(diào)整 mapper 數(shù)時颜武,又會多出兩個參數(shù),分別是mapred.min.split.size.per.node
和mapred.min.split.size.per.rack
拖吼,含義是單節(jié)點和單機架上的最小 split 大小鳞上。如果發(fā)現(xiàn)有 split 大小小于這兩個值(默認都是 100MB),則會進行合并吊档。具體邏輯可以參看 Hive 源碼中的對應(yīng)類篙议。
輸出階段合并
直接將 hive.merge.mapfiles
和 hive.merge.mapredfiles
都設(shè)為 true 即可,前者表示將 map-only 任務(wù)的輸出合并籍铁,后者表示將 map-reduce 任務(wù)的輸出合并涡上。另外,hive.merge.size.per.task
可以指定每個task輸出后合并文件大小的期望值拒名,hive.merge.size.smallfiles.avgsize
可以指定所有輸出文件大小的均值閾值吩愧,默認值都是 1GB。如果平均大小不足的話增显,就會另外啟動一個任務(wù)來進行合并雁佳。
啟用壓縮
壓縮 job 的中間結(jié)果數(shù)據(jù)和輸出數(shù)據(jù),可以用少量 CPU 時間節(jié)省很多空間同云。壓縮方式一般選擇 Snappy糖权,效率最高。
要啟用中間壓縮炸站,需要設(shè)定hive.exec.compress.intermediate
為 true星澳,同時指定壓縮方式hive.intermediate.compression.codec
為org.apache.hadoop.io.compress.SnappyCodec
。另外旱易,參數(shù)hive.intermediate.compression.type
可以選擇對塊(BLOCK)還是記錄(RECORD)壓縮禁偎,BLOCK的壓縮率比較高腿堤。
輸出壓縮的配置基本相同,打開hive.exec.compress.output
即可如暖。
JVM 重用
在 MR job 中笆檀,默認是每執(zhí)行一個 task 就啟動一個 JVM。如果 task 非常小而碎盒至,那么 JVM 啟動和關(guān)閉的耗時就會很長酗洒。可以通過調(diào)節(jié)參數(shù)mapred.job.reuse.jvm.num.tasks
來重用枷遂。例如將這個參數(shù)設(shè)成 5樱衷,那么就代表同一個 MR job 中順序執(zhí)行的 5 個 task 可以重復(fù)使用一個 JVM,減少啟動和關(guān)閉的開銷登淘。但它對不同 MR job 中的 task 無效箫老。
并行執(zhí)行與本地模式
并行執(zhí)行
Hive 中互相沒有依賴關(guān)系的 job 間是可以并行執(zhí)行的封字,最典型的就是多個子查詢 union all黔州。在集群資源相對充足的情況下,可以開啟并行執(zhí)行阔籽,即將參數(shù)hive.exec.parallel設(shè)為true流妻。另外hive.exec.parallel.thread.number可以設(shè)定并行執(zhí)行的線程數(shù),默認為8笆制,一般都夠用绅这。
本地模式
Hive 也可以不將任務(wù)提交到集群進行運算,而是直接在一臺節(jié)點上處理在辆。因為消除了提交到集群的 overhead证薇,所以比較適合數(shù)據(jù)量很小,且邏輯不復(fù)雜的任務(wù)匆篓。
設(shè)置 hive.exec.mode.local.auto
為 true 可以開啟本地模式浑度。但任務(wù)的輸入數(shù)據(jù)總量必須小于 hive.exec.mode.local.auto.inputbytes.max(默認值128MB)
,且 mapper 數(shù)必須小于 hive.exec.mode.local.auto.tasks.max(默認值4)
鸦概,reducer 數(shù)必須為 0 或 1箩张,才會真正用本地模式執(zhí)行。
嚴格模式
所謂嚴格模式窗市,就是強制不允許用戶執(zhí)行 3 種有風(fēng)險的 HiveSQL 語句先慷,一旦執(zhí)行會直接失敗。這 3 種語句是:
- 查詢分區(qū)表時不限定分區(qū)列的語句咨察;
- 兩表 join 產(chǎn)生了笛卡爾積的語句论熙;
- 用 order by 來排序但沒有指定 limit 的語句。
要開啟嚴格模式摄狱,需要將參數(shù)hive.mapred.mode
設(shè)為 strict脓诡。
采用合適的存儲格式
在 HiveSQL 的 create table 語句中素跺,可以使用 stored as ... 指定表的存儲格式。Hive 表支持的存儲格式有 TextFile誉券、SequenceFile指厌、RCFile、Avro踊跟、ORC踩验、Parquet 等。
存儲格式一般需要根據(jù)業(yè)務(wù)進行選擇商玫,在我們的實操中箕憾,絕大多數(shù)表都采用 TextFile 與 Parquet 兩種存儲格式之一。
TextFile 是最簡單的存儲格式拳昌,它是純文本記錄袭异,也是 Hive 的默認格式。雖然它的磁盤開銷比較大炬藤,查詢效率也低御铃,但它更多地是作為跳板來使用。RCFile沈矿、ORC上真、Parquet 等格式的表都不能由文件直接導(dǎo)入數(shù)據(jù),必須由 TextFile 來做中轉(zhuǎn)羹膳。
Parquet 和 ORC 都是 Apache 旗下的開源列式存儲格式睡互。列式存儲比起傳統(tǒng)的行式存儲更適合批量 OLAP 查詢,并且也支持更好的壓縮和編碼陵像。我們選擇 Parquet 的原因主要是它支持 Impala 查詢引擎就珠,并且我們對 update、delete 和事務(wù)性操作需求很低醒颖。