Hive必知必會的優化細節和原理釋義

一、 常用參數優化
參數名 參數釋義和用法
列裁剪和分區裁剪 列裁剪就是在查詢時只讀取需要的列,分區裁剪就是隻讀取需要的分區。解析階段對應的則是ColumnPruner邏輯優化器
hive.optimize.cp True(默認)
hive.optimize.pruner True(默認)
謂詞下推 關係型數據庫MySQL中,也有謂詞下推(Predicate Pushdown,PPD)的概念。它就是將SQL語句中的where謂詞邏輯都儘可能提前執行,減少下游處理的數據量。對應邏輯優化器是PredicatePushDown
hive.optimize.ppd true
group by配置調整
hive.map.aggr(map端聚合) 默認 true,並不是所有的聚合操作都需要在reduce部分進行,很多聚合操作都可以先在Map端進行部分聚合,然後reduce端得出最終結果。
對應的優化器爲GroupByOptimizer
hive.groupby.mapaggr.checkinterval 設置map端預聚合的行數閾值,超過該值就會分拆job,默認值100000
hive.groupby.skewindata false, 解釋:在group by時啓動兩個MR job。第一個job會將map端數據隨機輸入reducer,每個reducer做部分聚合,相同的key就會分佈在不同的reducer中。第二個job再將前面預處理過的數據按key聚合並輸出結果,這樣就起到了均衡的效果。
啓用壓縮 壓縮job的中間結果數據和輸出數據,可以用少量CPU時間節省很多空間。壓縮方式一般選擇Snappy,效率最高。
hive.exec.compress.intermediate default is false
hive.intermediate.compression.codec org.apache.hadoop.io.compress.SnappyCodec
hive.intermediate.compression.type 可以選擇對塊(BLOCK)還是記錄(RECORD)壓縮,BLOCK的壓縮率比較高。
hive.exec.compress.output=true 輸出壓縮配置項
Join基礎優化
build table(小表)前置 小表叫build table,大表叫probe table
Hive在解析帶join的SQL語句時,會默認將最後一個表作爲probe table,將前面的表作爲build table並試圖將它們讀進內存。如果表順序寫反,probe table在前面,引發OOM的風險就高了
多表join時key相同 這種情況會將多個join合併爲一個MR job來處理,負責這個的是相關性優化器CorrelationOptimizer,參考wiki
利用map join特性 map join特別適合大小表join的情況。Hive會將build table和probe table在map端直接完成join過程,消滅了reduce,效率很高。
hive.auto.convert.join 默認值true,對應邏輯優化器是MapJoinProcessor。
hive.mapjoin.smalltable.filesize 默認值25000000(25MB)
hive.mapjoin.cache.numrows 表示緩存build table的多少行數據到內存,默認值25000。
傾斜均衡配置項
hive.optimize.skewjoin False
hive.skewjoin.key 如果開啓了,在join過程中Hive會將計數超過閾值hive.skewjoin.key(默認100000)的傾斜key對應的行臨時寫進文件中,然後再啓動另一個job做map join生成結果。通過hive.skewjoin.mapjoin.map.tasks參數還可以控制第二個job的mapper數量,默認10000。
使用向量化查詢 向量化查詢執行通過一次性批量執行1024行而不是每次單行執行,從而提供掃描、聚合、篩選器和連接等操作的性能。在Hive 0.13中引入,此功能顯着提高了查詢執行時間,並可通過兩個參數設置輕鬆啓用:
hive.vectorized.execution.enabled true
hive.vectorizedexecution.reduce.enabled true
CBO(cost based query optimization)
自動優化HQL中多個JOIN的順序,並選擇合適的JOIN算法 hive.cbo.enable = true;
hive.compute.query.using.stats = true;
hive.stats.fetch.column.stats = true;
hive.stats.fetch.partition.stats = true;
推測執行
Hadoop推測執行可以觸發執行一些重複的任務,儘管因對重複的數據進行計算而導致消耗更多的計算資源,不過這個功能的目標是通過加快獲取單個task的結果以偵測執行慢的TaskTracker加入到沒名單的方式來提高整體的任務執行效率。Hadoop的推測執行功能由2個配置控制着,通過mapred-site.xml中配置 mapred.map.tasks.speculative.execution=true
mapred.reduce.tasks.speculative.execution=true




二、SQL語法相關的優化
  1. sort by代替order by
    HiveSQL中的order by與其他SQL方言中的功能一樣,就是將結果按某字段全局排序,這會導致所有map端數據都進入一個reducer中,在數據量大時可能會長時間計算不完。

    如果使用sort by,那麼還是會視情況啓動多個reducer進行排序,並且保證每個reducer內局部有序。爲了控制map端數據分配到reducer的key,往往還要配合distribute by一同使用。如果不加distribute by的話,map端數據就會隨機分配到reducer。
    舉個例子,假如要以UID爲key,以上傳時間倒序、記錄類型倒序輸出記錄數據。

    select uid,upload_time,event_type,record_data
    from calendar_record_log
    where pt_date >= 20190201 and pt_date <= 20190224
    distribute by uid
    sort by upload_time desc,event_type desc;
    
  2. group by代替distinct

    當要統計某一列的去重數時,如果數據量很大,count(distinct)就會非常慢,原因與order by類似,count(distinct)邏輯只會有很少的reducer來處理。這時可以用group by來改寫:

    select count(1) from (
    	select uid from calendar_record_log
    	where pt_date >= 20190101
    	group by uid
    ) t;
    

    這樣寫會啓動兩個MR job(單純distinct只會啓動一個),所以要確保數據量大到啓動job的overhead遠小於計算耗時,才考慮這種方法。

  3. 優化SQL處理join數據傾斜

    空值或無意義值

    這種情況很常見,比如當事實表是日誌類數據時,往往會有一些項沒有記錄到,我們視情況會將它置爲null,或者空字符串、-1等。如果缺失的項很多,在做join時這些空值就會非常集中,拖累進度。

    因此,若不需要空值數據,就提前寫where語句過濾掉。需要保留的話,將空值key用隨機方式打散,例如將用戶ID爲null的記錄隨機改爲負值:

    select a.uid,a.event_type,b.nickname,b.age
    from (
    select
      (case when uid is null then cast(rand()*-10240 as int) else uid end) as uid,
      event_type from calendar_record_log
    where pt_date >= 20190201
    ) a left outer join (
    select uid,nickname,age from user_info where status = 4
    ) b on a.uid = b.uid;
    
    單獨處理傾斜key

    這其實是上面處理空值方法的拓展,不過傾斜的key變成了有意義的。一般來講傾斜的key都很少,我們可以將它們抽樣出來,對應的行單獨存入臨時表中,然後打上一個較小的隨機數前綴(比如0~9),最後再進行聚合。

    不同數據類型

    這種情況不太常見,主要出現在相同業務含義的列發生過邏輯上的變化時。 舉個例子,假如我們有一舊一新兩張日曆記錄表,舊錶的記錄類型字段是(event_type int),新表的是(event_type string)。爲了兼容舊版記錄,新表的event_type也會以字符串形式存儲舊版的值,比如’17’。當這兩張表join時,經常要耗費很長時間。其原因就是如果不轉換類型,計算key的hash值時默認是以int型做的,這就導致所有“真正的”string型key都分配到一個reducer上。所以要注意類型轉換:

    select a.uid,a.event_type,b.record_data
    from calendar_record_log a
    left outer join (
    select uid,event_type from calendar_record_log_2
    where pt_date = 20190228
    ) b on a.uid = b.uid and b.event_type = cast(a.event_type as string)
    where a.pt_date = 20190228;
    
    build table過大

    有時,build table會大到無法直接使用map join的地步,比如全量用戶維度表,而使用普通join又有數據分佈不均的問題。這時就要充分利用probe table的限制條件,削減build table的數據量,再使用map join解決。代價就是需要進行兩次join。舉個例子:

    select /*+mapjoin(b)*/ a.uid,a.event_type,b.status,b.extra_info
    from calendar_record_log a
    left outer join (
    select /*+mapjoin(s)*/ t.uid,t.status,t.extra_info
    from (select distinct uid from calendar_record_log where pt_date = 20190228) s
    inner join user_info t on s.uid = t.uid
    ) b on a.uid = b.uid
    where a.pt_date = 20190228;
    
  4. MapReduce優化

    調整mapper數

    mapper數量與輸入文件的split數息息相關,在Hadoop源碼org.apache.hadoop.mapreduce.lib.input.FileInputFormat類中可以看到split劃分的具體邏輯。這裏不貼代碼,直接敘述mapper數是如何確定的。

    • 可以直接通過參數mapred.map.tasks(默認值2)來設定mapper數的期望值,但它不一定會生效,下面會提到。
    • 設輸入文件的總大小爲total_input_size。HDFS中,一個塊的大小由參數dfs.block.size指定,默認值64MB或128MB。在默認情況下,mapper數就是: default_mapper_num = total_input_size / dfs.block.size
    • 參數mapred.min.split.size(默認值1B)和mapred.max.split.size(默認值64MB)分別用來指定split的最小和最大大小。split大小和split數計算規則是: split_size = MAX(mapred.min.split.size, MIN(mapred.max.split.size, dfs.block.size))split_num = total_input_size / split_size
    • 得出mapper數: mapper_num = MIN(split_num, MAX(default_num, mapred.map.tasks))

    可見,如果想減少mapper數,就適當調高mapred.min.split.size,split數就減少了。如果想增大mapper數,除了降低mapred.min.split.size之外,也可以調高mapred.map.tasks

    一般來講,如果輸入文件是少量大文件,就減少mapper數;如果輸入文件是大量非小文件,就增大mapper數;至於大量小文件的情況,則需要合併小文件後再處理。

    調整reducer數

    reducer數量的確定方法比mapper簡單得多。使用參數mapred.reduce.tasks可以直接設定reducer數量,不像mapper一樣是期望值。但如果不設這個參數的話,Hive就會自行推測,邏輯如下:

    • 參數hive.exec.reducers.bytes.per.reducer用來設定每個reducer能夠處理的最大數據量,默認值1G(1.2版本之前)或256M(1.2版本之後)。
    • 參數hive.exec.reducers.max用來設定每個job的最大reducer數量,默認值999(1.2版本之前)或1009(1.2版本之後)。
    • 得出reducer數: reducer_num = MIN(total_input_size / reducers.bytes.per.reducer, reducers.max)

    reducer數量與輸出文件的數量相關。如果reducer數太多,會產生大量小文件,對HDFS造成壓力。如果reducer數太少,每個reducer要處理很多數據,容易拖慢運行時間或者造成OOM。

    合併小文件
    • 輸入階段合併 需要更改Hive的輸入文件格式,即參數hive.input.format,默認值是org.apache.hadoop.hive.ql.io.HiveInputFormat,我們改成org.apache.hadoop.hive.ql.io.CombineHiveInputFormat。 這樣比起上面調整mapper數時,又會多出兩個參數,分別是mapred.min.split.size.per.nodemapred.min.split.size.per.rack,含義是單節點和單機架上的最小split大小。如果發現有split大小小於這兩個值(默認都是100MB),則會進行合併。具體邏輯可以參看Hive源碼中的對應類。
    • 輸出階段合併 直接將hive.merge.mapfileshive.merge.mapredfiles都設爲true即可,前者表示將map-only任務的輸出合併,後者表示將map-reduce任務的輸出合併。 另外,hive.merge.size.per.task可以指定每個task輸出後合併文件大小的期望值,hive.merge.size.smallfiles.avgsize可以指定所有輸出文件大小的均值閾值,默認值都是1GB。如果平均大小不足的話,就會另外啓動一個任務來進行合併。
    啓用壓縮

    壓縮job的中間結果數據和輸出數據,可以用少量CPU時間節省很多空間。壓縮方式一般選擇Snappy,效率最高。

    Job輸出文件按照block以Gzip的方式進行壓縮:

    set mapreduce.output.fileoutputformat.compress=true 
    -- 默認值是 false
    set mapreduce.output.fileoutputformat.compress.type=BLOCK 
    -- 默認值是 Record
    --可以選擇對塊(BLOCK)還是記錄(RECORD)壓縮,BLOCK的壓縮率比較高
    set mapreduce.output.fileoutputformat.compress.codec
    =org.apache.hadoop.io.compress.GzipCodec 
    --默認值是 org.apache.hadoop.io.compress.DefaultCodec
    

    Map輸出結果也以Gzip進行壓縮:

    set mapred.map.output.compress=true
    set mapreduce.map.output.compress.codec=org.apache.hadoop.io.compress.GzipCodec -- 默認值是 org.apache.hadoop.io.compress.DefaultCodec 
    

    對Hive輸出結果和中間都進行壓縮:

    set hive.exec.compress.output=true 
    --默認值是 false,不壓縮
    set hive.exec.compress.intermediate=true 
    --默認值是 false,爲 true 時 MR 設置的壓縮才啓用
    
    JVM重用

    在MR job中,默認是每執行一個task就啓動一個JVM。如果task非常小而碎,那麼JVM啓動和關閉的耗時就會很長。可以通過調節參數mapred.job.reuse.jvm.num.tasks來重用。例如將這個參數設成5,那麼就代表同一個MR job中順序執行的5個task可以重複使用一個JVM,減少啓動和關閉的開銷。但它對不同MR job中的task無效。

    並行執行與本地模式
    • 並行執行 Hive中互相沒有依賴關係的job間是可以並行執行的,最典型的就是多個子查詢union all。在集羣資源相對充足的情況下,可以開啓並行執行,即將參數hive.exec.parallel設爲true。另外hive.exec.parallel.thread.number可以設定並行執行的線程數,默認爲8,一般都夠用。
    • 本地模式 Hive也可以不將任務提交到集羣進行運算,而是直接在一臺節點上處理。因爲消除了提交到集羣的overhead,所以比較適合數據量很小,且邏輯不復雜的任務。 設置hive.exec.mode.local.auto爲true可以開啓本地模式。但任務的輸入數據總量必須小於hive.exec.mode.local.auto.inputbytes.max(默認值128MB),且mapper數必須小於hive.exec.mode.local.auto.tasks.max(默認值4),reducer數必須爲0或1,纔會真正用本地模式執行。
    嚴格模式

    所謂嚴格模式,就是強制不允許用戶執行3種有風險的HiveSQL語句,一旦執行會直接失敗。這3種語句是:

    • 查詢分區表時不限定分區列的語句;
    • 兩表join產生了笛卡爾積的語句;
    • 用order by來排序但沒有指定limit的語句。

    要開啓嚴格模式,需要將參數==hive.mapred.mode==設爲strict。

    採用合適的存儲格式

    在HiveSQL的create table語句中,可以使用stored as ...指定表的存儲格式。Hive表支持的存儲格式有TextFile、SequenceFile、RCFile、Avro、ORC、Parquet等。 存儲格式一般需要根據業務進行選擇,在我們的實操中,絕大多數表都採用TextFile與Parquet兩種存儲格式之一。 TextFile是最簡單的存儲格式,它是純文本記錄,也是Hive的默認格式。雖然它的磁盤開銷比較大,查詢效率也低,但它更多地是作爲跳板來使用。RCFile、ORC、Parquet等格式的表都不能由文件直接導入數據,必須由TextFile來做中轉。 Parquet和ORC都是Apache旗下的開源列式存儲格式。列式存儲比起傳統的行式存儲更適合批量OLAP查詢,並且也支持更好的壓縮和編碼。我們選擇Parquet的原因主要是它支持Impala查詢引擎,並且我們對update、delete和事務性操作需求很低。 這裏就不展開講它們的細節,可以參考各自的官網: https://parquet.apache.org/ https://orc.apache.org/

  5. 單個大文件處理辦法

    當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 strored as orc as 
          select * from a distribute by rand(123);
    

    distribute by :用來控制map輸出結果的分發,即map端如何拆分數據給reduce端。 會根據distribute by 後邊定義的列,根據reduce的個數進行數據分發,默認是採用hash算法。當 distribute by 後邊跟的列是:rand()函數時,即表示保證每個分區的數據量基本一致。

    • cluster by: 對同一字段分桶並排序,不能和sort by連用;
    • distribute by + sort by: 分桶,保證同一字段值只存在一個結果文件當中,結合sort by 保證每個reduceTask結果有序;
    • sort by: 單機排序,單個reduce結果有序
    • order by:全局排序,缺陷是隻能使用一個reduce

    擴展參考:https://blog.csdn.net/qq_40795214/article/details/82190827

  6. 優化in/exists語句

    雖然經過測驗,hive1.2.1也支持in/exists操作,但還是推薦使用hive的一個高效替代方案:left semi join

    比如說:

    select a.id, a.name from a where a.id in (select b.id from b);
    select a.id, a.name from a where exists (select id from b where a.id = b.id);
    

    應該轉換成:

    select a.id, a.name from a left semi join b on a.id = b.id;      
    
  7. 補充




三、原理釋義
  1. Map join 工作原理釋義
    在這裏插入圖片描述
    ​ 上圖是Hive MapJoin的原理圖,從圖中可以看出MapJoin分爲兩個階段:
    (1)通過MapReduce Local Task,將小表讀入內存,生成內存HashTableFiles上傳至Distributed Cache中,這裏會對HashTableFiles進行壓縮。
    (2)MapReduce Job在Map階段,每個Mapper從Distributed Cache讀取HashTableFiles到內存中,順序掃描大表,在Map階段直接進行Join,將數據傳遞給下一個MapReduce任務。也就是在map端進行join避免了shuffle。
    ​ Join操作在Map階段完成,不再需要Reduce,有多少個Map Task,就有多少個結果文件。

  2. Hive SQL Join的實現原理

    select u.name, o.orderid from order o join user u on o.uid = u.uid;
    

    在map的輸出value中爲不同表的數據打上tag標記,在reduce階段根據tag判斷數據來源。

    MapReduce CommonJoin的實現

  1. Hive Group by 實現原理

    select rank, isonline, count(*) from city group by rank, isonline;
    

    將GroupBy的字段組合爲map的輸出key值,利用MapReduce的排序,在reduce階段保存LastKey區分不同的key。MapReduce的過程如下(當然這裏只是說明Reduce端的非Hash聚合過程)

    MapReduce Group By的實現
  2. Distinct的實現原理

    select dealid, count(distinct uid) num from order group by dealid;
    
    • 當只有一個distinct字段時,如果不考慮Map階段的Hash GroupBy,只需要將GroupBy字段和Distinct字段組合爲map輸出key,利用mapreduce的排序,同時將GroupBy字段作爲reduce的key,在reduce階段保存LastKey即可完成去重.
      MapReduce Distinct的實現

    • 如果有多個distinct字段呢,如下面的SQL

    select dealid, count(distinct uid), count(distinct date) 
    from order group by dealid;
    

    實現方式有兩種:
    (1)如果仍然按照上面一個distinct字段的方法,即下圖這種實現方式,無法跟據uid和date分別排序,也就無法通過LastKey去重,仍然需要在reduce階段在內存中通過Hash去重。

    MapReduce Multi Distinct的實現

    (2)第二種實現方式,可以對所有的distinct字段編號,每行數據生成n行數據,那麼相同字段就會分別排序,這時只需要在reduce階段記錄LastKey即可去重。
    這種實現方式很好的利用了MapReduce的排序,節省了reduce階段去重的內存消耗,但是缺點是增加了shuffle的數據量。

    需要注意的是,在生成reduce value時,除第一個distinct字段所在行需要保留value值,其餘distinct數據行value字段均可爲空。

    MapReduce Multi Distinct的實現

    簡單解釋下,如上圖,對多字段的去重是從打編號開始的,如 uid=0,date=1,然後每一行按照groupby 字段拆成兩行如下:

    <1001,0,1>
    <1001,1,1101>
    <1001,0,2>
    <1001,1,1101>
    <1001,0,2>
    <1001,1,1102>
    
  3. Hive SQL轉化爲MapReduce的過程

    整個編譯過程分爲六個階段:

    1. Antlr定義SQL的語法規則,完成SQL詞法,語法解析,將SQL轉化爲抽象語法樹AST Tree
    2. 遍歷AST Tree,抽象出查詢的基本組成單元QueryBlock
    3. 遍歷QueryBlock,翻譯爲執行操作樹OperatorTree
    4. 邏輯層優化器進行OperatorTree變換,合併不必要的ReduceSinkOperator,減少shuffle數據量
    5. 遍歷OperatorTree,翻譯爲MapReduce任務
    6. 物理層優化器進行MapReduce任務的變換,生成最終的執行計劃
  4. MapReduce過程詳解及其性能優化

img

參考:https://www.cnblogs.com/felixzh/p/8604188.html

爲這個圖的作者點個贊,製作精良,通俗易懂。簡述如下,詳情參考鏈接。

【map階段】
1、讀取數據HDFS [map數:輸入文件數目,輸入文件的大小,配置參數]
2、處理數據 (分區-> 環形緩衝區)
分區:默認是通過計算key的hash值後對Reduce task的數量取模獲得
環形緩衝區:Map的輸出結果是由collector處理的,每個Map任務不斷地將鍵值對輸出到在內存中構造的一個環形數據結構中。使用環形數據結構是爲了更有效地使用內存空間,在內存中放置儘可能多的數據
3、寫數據 (Combiner -> spill -> 壓縮 )

【reduce階段】
1、拷貝 (Reduce任務通過HTTP向各個Map任務下載它所需要的數據(網絡傳輸)
2、合併
1)內存到內存(memToMemMerger)
2)內存中Merge(inMemoryMerger)
3)磁盤上的Merge(onDiskMerger)具體包括兩個:(一)Copy過程中磁盤合併(二)磁盤到磁盤

參考:
https://cloud.tencent.com/developer/article/1453464
https://tech.meituan.com/2014/02/12/hive-sql-to-mapreduce.html
https://www.cnblogs.com/swordfall/p/11037539.html

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