在工作中使用hive比較多,也寫了很多HiveQL。這里從三個方面對 Hive 常用的一些性能優化進行了總結。
表設計層面優化
利用分區表優化
分區表 是在某一個或者幾個維度上對數據進行分類存儲,一個分區對應一個目錄。如果篩選條件里有分區字段,那么 Hive 只需要遍歷對應分區目錄下的文件即可,不需要遍歷全局數據,使得處理的數據量大大減少,從而提高查詢效率。
當一個 Hive 表的查詢大多數情況下,會根據某一個字段進行篩選時,那么非常適合創建為分區表。
利用桶表優化
指定桶的個數后,存儲數據時,根據某一個字段進行哈希后,確定存儲在哪個桶里,這樣做的目的和分區表類似,也是使得篩選時不用全局遍歷所有的數據,只需要遍歷所在桶就可以了。
選擇合適的文件存儲格式
Apache Hive 支持 Apache Hadoop 中使用的幾種熟悉的文件格式。
TextFile
默認格式,如果建表時不指定默認為此格式。
存儲方式:行存儲。
每一行都是一條記錄,每行都以換行符\n
結尾。數據不做壓縮時,磁盤會開銷比較大,數據解析開銷也比較大。
可結合 Gzip、Bzip2 等壓縮方式一起使用(系統會自動檢查,查詢時會自動解壓),但對于某些壓縮算法 hive 不會對數據進行切分,從而無法對數據進行并行操作。
SequenceFile
一種Hadoop API 提供的二進制文件,使用方便、可分割、個壓縮的特點。
支持三種壓縮選擇:NONE、RECORD、BLOCK。RECORD壓縮率低,一般建議使用BLOCK壓縮。
RCFile
存儲方式:數據按行分塊,每塊按照列存儲 。
- 首先,將數據按行分塊,保證同一個record在一個塊上,避免讀一個記錄需要讀取多個block。
- 其次,塊數據列式存儲,有利于數據壓縮和快速的列存取。
ORC
存儲方式:數據按行分塊,每塊按照列存儲
Hive 提供的新格式,屬于 RCFile 的升級版,性能有大幅度提升,而且數據可以壓縮存儲,壓縮快,快速列存取。
Parquet
存儲方式:列式存儲
Parquet 對于大型查詢的類型是高效的。對于掃描特定表格中的特定列查詢,Parquet特別有用。Parquet一般使用 Snappy、Gzip 壓縮。默認 Snappy。
Parquet 支持 Impala 查詢引擎。
表的文件存儲格式盡量采用 Parquet 或 ORC,不僅降低存儲量,還優化了查詢,壓縮,表關聯等性能;
選擇合適的壓縮方式
Hive 語句最終是轉化為 MapReduce 程序來執行的,而 MapReduce 的性能瓶頸在與 網絡IO 和 磁盤IO,要解決性能瓶頸,最主要的是 減少數據量,對數據進行壓縮是個好方式。壓縮雖然是減少了數據量,但是壓縮過程要消耗CPU,但是在Hadoop中,往往性能瓶頸不在于CPU,CPU壓力并不大,所以壓縮充分利用了比較空閑的CPU。
常用壓縮算法對比
如何選擇壓縮方式
- 壓縮比率
- 壓縮解壓速度
- 是否支持split
支持分割的文件可以并行的有多個 mapper 程序處理大數據文件,大多數文件不支持可分割是因為這些文件只能從頭開始讀。
語法和參數層面優化
列裁剪
Hive 在讀數據的時候,可以只讀取查詢中所需要用到的列,而忽略其他的列。這樣做可以節省讀取開銷,中間表存儲開銷和數據整合開銷。
set hive.optimize.cp = true; -- 列裁剪,取數只取查詢中需要用到的列,默認為真
分區裁剪
在查詢的過程中只選擇需要的分區,可以減少讀入的分區數目,減少讀入的數據量。
set hive.optimize.pruner=true; // 默認為true
合并小文件
Map 輸入合并
在執行 MapReduce 程序的時候,一般情況是一個文件需要一個 mapper 來處理。但是如果數據源是大量的小文件,這樣豈不是會啟動大量的 mapper 任務,這樣會浪費大量資源。可以將輸入的小文件進行合并,從而減少mapper任務數量。詳細分析
set hive.input.format=org.apache.hadoop.hive.ql.io.CombineHiveInputFormat; -- Map端輸入、合并文件之后按照block的大小分割(默認)
set hive.input.format=org.apache.hadoop.hive.ql.io.HiveInputFormat; -- Map端輸入,不合并
Map/Reduce輸出合并
大量的小文件會給 HDFS 帶來壓力,影響處理效率。可以通過合并 Map 和 Reduce 的結果文件來消除影響。
set hive.merge.mapfiles=true; -- 是否合并Map輸出文件, 默認值為真
set hive.merge.mapredfiles=true; -- 是否合并Reduce 端輸出文件,默認值為假
set hive.merge.size.per.task=25610001000; -- 合并文件的大小,默認值為 256000000
合理控制 map/reduce 任務數量
合理控制 mapper 數量
減少 mapper 數可以通過合并小文件來實現
增加 mapper 數可以通過控制上一個 reduce
默認的 mapper 個數計算方式
輸入文件總大小:total_size
hdfs 設置的數據塊大小:dfs_block_size
default_mapper_num = total_size/dfs_block_size
MapReduce 中提供了如下參數來控制 map 任務個數:
set mapred.map.tasks=10;
從字面上看,貌似是可以直接設置 mapper 個數的樣子,但是很遺憾不行,這個參數設置只有在大于default_mapper_num
的時候,才會生效。
那如果我們需要減少 mapper 數量,但是文件大小是固定的,那該怎么辦呢?
可以通過mapred.min.split.size
設置每個任務處理的文件的大小,這個大小只有在大于dfs_block_size
的時候才會生效
split_size=max(mapred.min.split.size, dfs_block_size)
split_num=total_size/split_size
compute_map_num = min(split_num, max(default_mapper_num, mapred.map.tasks))
這樣就可以減少mapper數量了。
總結一下控制 mapper 個數的方法:
- 如果想增加 mapper 個數,可以設置
mapred.map.tasks
為一個較大的值 - 如果想減少 mapper 個數,可以設置
maperd.min.split.size
為一個較大的值 - 如果輸入是大量小文件,想減少 mapper 個數,可以通過設置
hive.input.format
合并小文件
如果想要調整 mapper 個數,在調整之前,需要確定處理的文件大概大小以及文件的存在形式(是大量小文件,還是單個大文件),然后再設置合適的參數。
合理控制reducer數量
如果 reducer 數量過多,一個 reducer 會產生一個結數量果文件,這樣就會生成很多小文件,那么如果這些結果文件會作為下一個 job 的輸入,則會出現小文件需要進行合并的問題,而且啟動和初始化 reducer 需要耗費和資源。
如果 reducer 數量過少,這樣一個 reducer 就需要處理大量的數據,并且還有可能會出現數據傾斜的問題,使得整個查詢耗時長。
默認情況下,hive 分配的 reducer 個數由下列參數決定:
- 參數1:
hive.exec.reducers.bytes.per.reducer
(默認1G) - 參數2:
hive.exec.reducers.max
(默認為999)
reducer的計算公式為:
N = min(參數2, 總輸入數據量/參數1)
可以通過改變上述兩個參數的值來控制reducer的數量。
也可以通過
set mapred.map.tasks=10;
直接控制reducer個數,如果設置了該參數,上面兩個參數就會忽略。
Join優化
優先過濾數據
盡量減少每個階段的數據量,對于分區表能用上分區字段的盡量使用,同時只選擇后面需要使用到的列,最大限度的減少參與 join 的數據量。
小表 join 大表原則
小表 join 大表的時應遵守小表 join 大表原則,原因是 join 操作的 reduce 階段,位于 join 左邊的表內容會被加載進內存,將條目少的表放在左邊,可以有效減少發生內存溢出的幾率。join 中執行順序是從左到右生成 Job,應該保證連續查詢中的表的大小從左到右是依次增加的。
使用相同的連接鍵
在 hive 中,當對 3 個或更多張表進行 join 時,如果 on 條件使用相同字段,那么它們會合并為一個 MapReduce Job,利用這種特性,可以將相同的 join on 的放入一個 job 來節省執行時間。
啟用 mapjoin
mapjoin 是將 join 雙方比較小的表直接分發到各個 map 進程的內存中,在 map 進程中進行 join 操作,這樣就不用進行 reduce 步驟,從而提高了速度。只有 join 操作才能啟用 mapjoin。
set hive.auto.convert.join = true; -- 是否根據輸入小表的大小,自動將reduce端的common join 轉化為map join,將小表刷入內存中。
set hive.mapjoin.smalltable.filesize = 2500000; -- 刷入內存表的大小(字節)
set hive.mapjoin.maxsize=1000000; -- Map Join所處理的最大的行數。超過此行數,Map Join進程會異常退出
盡量原子操作
盡量避免一個SQL包含復雜的邏輯,可以使用中間表來完成復雜的邏輯。
桶表 mapjoin
當兩個分桶表 join 時,如果 join on的是分桶字段,小表的分桶數是大表的倍數時,可以啟用 mapjoin 來提高效率。
set hive.optimize.bucketmapjoin = true; -- 啟用桶表 map join
Group By 優化
默認情況下,Map階段同一個Key的數據會分發到一個Reduce上,當一個Key的數據過大時會產生 數據傾斜。進行group by
操作時可以從以下兩個方面進行優化:
1. Map端部分聚合
事實上并不是所有的聚合操作都需要在 Reduce 部分進行,很多聚合操作都可以先在 Map 端進行部分聚合,然后在 Reduce 端的得出最終結果。
set hive.map.aggr=true; -- 開啟Map端聚合參數設置
set hive.grouby.mapaggr.checkinterval=100000; -- 在Map端進行聚合操作的條目數目
2. 有數據傾斜時進行負載均衡
set hive.groupby.skewindata = true; -- 有數據傾斜的時候進行負載均衡(默認是false)
當選項設定為 true 時,生成的查詢計劃有兩個 MapReduce 任務。在第一個 MapReduce 任務中,map 的輸出結果會隨機分布到 reduce 中,每個 reduce 做部分聚合操作,并輸出結果,這樣處理的結果是相同的group by key
有可能分發到不同的 reduce 中,從而達到負載均衡的目的;第二個 MapReduce 任務再根據預處理的數據結果按照group by key
分布到各個 reduce 中,最后完成最終的聚合操作。
Order By 優化
order by
只能是在一個reduce進程中進行,所以如果對一個大數據集進行order by
,會導致一個reduce進程中處理的數據相當大,造成查詢執行緩慢。
- 在最終結果上進行
order by
,不要在中間的大數據集上進行排序。如果最終結果較少,可以在一個reduce上進行排序時,那么就在最后的結果集上進行order by
。 - 如果是去排序后的前N條數據,可以使用
distribute by
和sort by
在各個reduce上進行排序后前N條,然后再對各個reduce的結果集合合并后在一個reduce中全局排序,再取前N條,因為參與全局排序的order by
的數據量最多是reduce個數 * N
,所以執行效率很高。
COUNT DISTINCT優化
-- 優化前(只有一個reduce,先去重再count負擔比較大):
select count(distinct id) from tablename;
-- 優化后(啟動兩個job,一個job負責子查詢(可以有多個reduce),另一個job負責count(1)):
select count(1) from (select distinct id from tablename) tmp;
一次讀取多次插入
有些場景是從一張表讀取數據后,要多次利用,這時可以使用multi insert
語法:
from sale_detail
insert overwrite table sale_detail_multi partition (sale_date='2010', region='china' )
select shop_name, customer_id, total_price where .....
insert overwrite table sale_detail_multi partition (sale_date='2011', region='china' )
select shop_name, customer_id, total_price where .....;
說明:
- 一般情況下,單個SQL中最多可以寫128路輸出,超過128路,則報語法錯誤。
- 在一個multi insert中:
- 對于分區表,同一個目標分區不允許出現多次。
- 對于未分區表,該表不能出現多次。
- 對于同一張分區表的不同分區,不能同時有
insert overwrite
和insert into
操作,否則報錯返回。
啟用壓縮
map 輸出壓縮
set mapreduce.map.output.compress=true;
set mapreduce.map.output.compress.codec=org.apache.hadoop.io.compress.SnappyCodec;
中間數據壓縮
中間數據壓縮就是對 hive 查詢的多個 job 之間的數據進行壓縮。最好是選擇一個節省CPU耗時的壓縮方式。可以采用snappy
壓縮算法,該算法的壓縮和解壓效率都非常高。
set hive.exec.compress.intermediate=true;
set hive.intermediate.compression.codec=org.apache.hadoop.io.compress.SnappyCodec;
set hive.intermediate.compression.type=BLOCK;
結果數據壓縮
最終的結果數據(Reducer輸出數據)也是可以進行壓縮的,可以選擇一個壓縮效果比較好的,可以減少數據的大小和數據的磁盤讀寫時間;
注:常用的gzip,snappy壓縮算法是不支持并行處理的,如果數據源是gzip/snappy壓縮文件大文件,這樣只會有有個mapper來處理這個文件,會嚴重影響查詢效率。
所以如果結果數據需要作為其他查詢任務的數據源,可以選擇支持splitable的LZO
算法,這樣既能對結果文件進行壓縮,還可以并行的處理,這樣就可以大大的提高job執行的速度了。關于如何給Hadoop集群安裝LZO壓縮庫可以查看這篇文章。
set hive.exec.compress.output=true;
set mapreduce.output.fileoutputformat.compress=true;
set mapreduce.output.fileoutputformat.compress.codec=org.apache.hadoop.io.compress.GzipCodec;
set mapreduce.output.fileoutputformat.compress.type=BLOCK;
Hadoop集群支持一下算法:
- org.apache.hadoop.io.compress.DefaultCodec
- org.apache.hadoop.io.compress.GzipCodec
- org.apache.hadoop.io.compress.BZip2Codec
- org.apache.hadoop.io.compress.DeflateCodec
- org.apache.hadoop.io.compress.SnappyCodec
- org.apache.hadoop.io.compress.Lz4Codec
- com.hadoop.compression.lzo.LzoCodec
- com.hadoop.compression.lzo.LzopCodec
Hive架構層面優化
啟用直接抓取
Hive 從 HDFS 中讀取數據,有兩種方式:啟用 MapReduce 讀取、直接抓取。
直接抓取數據比 MapReduce 方式讀取數據要快的多,但是只有少數操作可以使用直接抓取方式。
可以通過hive.fetch.task.conversion
參數來配置在什么情況下采用直接抓取方式:
-
minimal:只有
select *
、在分區字段上where
過濾、有limit
這三種場景下才啟用直接抓取方式。 -
more:在
select
、where
篩選、limit
時,都啟用直接抓取方式。
set hive.fetch.task.conversion=more; -- 啟用fetch more模式
本地化執行
Hive 在集群上查詢時,默認是在集群上多臺機器上運行,需要多個機器進行協調運行,這種方式很好的解決了大數據量的查詢問題。但是在Hive查詢處理的數據量比較小的時候,其實沒有必要啟動分布式模式去執行,因為以分布式方式執行設計到跨網絡傳輸、多節點協調等,并且消耗資源。對于小數據集,可以通過本地模式,在單臺機器上處理所有任務,執行時間明顯被縮短。
set hive.exec.mode.local.auto=true; -- 打開hive自動判斷是否啟動本地模式的開關
set hive.exec.mode.local.auto.input.files.max=4; -- map任務數最大值
set hive.exec.mode.local.auto.inputbytes.max=134217728; -- map輸入文件最大大小
JVM重用
Hive 語句最終會轉換為一系列的 MapReduce 任務,每一個MapReduce 任務是由一系列的Map Task 和 Reduce Task 組成的,默認情況下,MapReduce 中一個 Map Task 或者 Reduce Task 就會啟動一個 JVM 進程,一個 Task 執行完畢后,JVM進程就會退出。這樣如果任務花費時間很短,又要多次啟動 JVM 的情況下,JVM的啟動時間會變成一個比較大的消耗,這時,可以通過重用 JVM 來解決。
set mapred.job.reuse.jvm.num.tasks=5;
JVM也是有缺點的,開啟JVM重用會一直占用使用到的 task 的插槽,以便進行重用,直到任務完成后才會釋放。如果某個
不平衡的job
中有幾個 reduce task 執行的時間要比其他的 reduce task 消耗的時間要多得多的話,那么保留的插槽就會一直空閑卻無法被其他的 job 使用,直到所有的 task 都結束了才會釋放。
并行執行
有的查詢語句,hive會將其轉化為一個或多個階段,包括:MapReduce 階段、抽樣階段、合并階段、limit 階段等。默認情況下,一次只執行一個階段。但是,如果某些階段不是互相依賴,是可以并行執行的。多階段并行是比較耗系統資源的。
set hive.exec.parallel=true; -- 可以開啟并發執行。
set hive.exec.parallel.thread.number=16; -- 同一個sql允許最大并行度,默認為8。
推測執行
在分布式集群環境下,因為程序Bug(包括Hadoop本身的bug),負載不均衡或者資源分布不均等原因,會造成同一個作業的多個任務之間運行速度不一致,有些任務的運行速度可能明顯慢于其他任務(比如一個作業的某個任務進度只有50%,而其他所有任務已經運行完畢),則這些任務會拖慢作業的整體執行進度。為了避免這種情況發生,Hadoop采用了推測執行(Speculative Execution)機制,它根據一定的法則推測出“拖后腿”的任務,并為這樣的任務啟動一個備份任務,讓該任務與原始任務同時處理同一份數據,并最終選用最先成功運行完成任務的計算結果作為最終結果。
set mapreduce.map.speculative=true;
set mapreduce.reduce.speculative=true;
建議:
如果用戶對于運行時的偏差非常敏感的話,那么可以將這些功能關閉掉。如果用戶因為輸入數據量很大而需要執行長時間的map或者Reduce task的話,那么啟動推測執行造成的浪費是非常巨大大。
擴展閱讀