天天看點

去哪兒酒店數倉流量鍊路優化實踐

作者:閃念基因

一、背景

随着旅遊市場的回暖、出行需求的激增,去哪兒網酒店的單日預訂量也重新整理了曆年的前高還在不斷突破産生新高。

與此同時,酒店數倉每天處理的資料也在不斷上漲,為了保障日常 SA 級的報表正常産出,需要我們持續優化資料處理的鍊路,消除存在的瓶頸與卡點。

酒店流量鍊路産出的核心寬表為:搜尋( search S頁 )、清單( list L頁 )、詳情( Detail D 頁)、預訂( booking B 頁)和送出訂單( order O 頁)流量表,對應了酒店主流程各個頁面的使用者流量資料。

我們以一個具體的案例 “ L 頁流量表” 優化作為切入點,來展開對流量鍊路的優化實踐,承諾 SLA、體量夠大、關聯夠多、邏輯夠複雜、使用夠廣一直是 L 頁流量表的内在标簽。

二、問題

酒店清單頁流量表(簡稱L頁流量表)為主流程最常用表,是進入酒店主流程業務的第一入口。

同時又承接了使用者窄口徑轉化、搜尋排序、分銷提流等衆多 SA 級别報表,需要確定 L 頁流量表産出的及時性來保障我們 SA 級報表的及時産出。

在各頁流量表中L頁是任務鍊路最長、産出時間最長的表,随着流量的不斷上漲産出時間一再告破 SLA 基線。

能夠為S級報表( S 級報表的 SLA 基線為 9 點)預留的執行 buffer 時間越來越少,尤其進入 4 月後對應 S 報表的産出時間出現了明顯的延遲。

三、現狀分析

去哪兒酒店數倉流量鍊路優化實踐

(酒店清單頁優化前執行流程圖)

1、圖解說明

圖中紫色背景為産出L頁流量表的兩大任務:L 頁中間表和 L 頁流量表,其中L 頁流量表背景包含了依賴上遊的事實表和相關次元表。

資料産出的先後時間通過顔色由淺及深進行了表示,顔色最淺任務産出資料在 1 點前,顔色最深的産出資料時間在 4 點後。

”相對穩定 【 5 分鐘】【 1 點 30 】“分别代表了上遊依賴中:資料産出時間的穩定性;第一個中擴号為任務的執行的時間;第二個中擴号為執行完成時間。

2、問題分析

序号⑩:L 頁流量表的産出任務,随流量上漲和上遊依賴的不穩定産出時間也一直後延,從年初 6 點左右到四月中已經需要 8 9 點才能産出;關聯的都是大表,任務本身執行時間長,其中個别 Stage 并行度非常低;

序号⑥:L 頁中間表前置任務,從原始日志中進行了清洗、轉化并擴充了外圍的基礎資訊; 該任務邏輯非常重本身執行時間就長,需要 2~3 個小時才能完成,因為依賴上遊不穩定的 ③④ 事實,導緻該任務遲遲不能執行;

序号①②:L 頁的主資料,資料處理的路徑通過紫色箭頭辨別一直到序号⑦,這條路徑上遊依賴的産出時間為 1 點半左右,可以單獨拆分;

序号③:公共埋點資料,為依賴的外部資料産出時間的部位的,一方面需要積極的推動外部進行優化解決;另一方面在後置該資料的使用時間;

序号④:主流程接口請求資料,本身執行時間不穩定的定位原因進行解決,依賴上遊③公共埋點資料也是其産出晚的重要原因;

序号⑤:使用者通路路徑(窄口徑)資料,任務本身的執行時長穩定,不穩定的原因在于上遊;

序号⑧⑨:基礎維表的一系列資料,隻使用各表中的幾個個别字段對L頁流量表的基礎資訊進行了擴充,這類資料産出時間非常早,可提前進行‘列’、‘行’的剪裁;

綜上通過分析各個環節存在的問題,後去定位深挖其根本的原因,我們可以通過資料鍊路、資料傾斜、資料存儲三個方面來入手進行優化解決。

四、優化方案

1、"拆"、"提"、"并"、"延"對資料的絕對把握

去哪兒酒店數倉流量鍊路優化實踐

1.1 “拆” 大段長邏輯拆分

可以梳理依賴的上遊和邏輯的相關性對大段的 SQL 進行拆分,如序号 ⑥ 中産出的 mid 表任務中依賴了多張資料表,使用了大段的串聯邏輯,隻有在上遊依賴全部完成才能執行。

拆分如下:

  • USER:新老客(9)
  • DIM:基礎資訊(⑧)
  • LIST:酒店清單主資料(①②)
  • EVENT:埋點曝光(③)
  • SEARCH:搜尋資訊(④)

通過以上五線路的拆分,非依賴的資料可以在上遊依賴任務完成後及時的進入處理狀态中。

這裡需要注意錯峰執行,一個關鍵資料的産出往往對應非常多的下遊依賴任務,我們應該将非重要、非核心任務改為定時執行,選擇執行任務個數較少的時間段來執行。

減少瞬時任務堆積造成計算資源的争搶,還可以降低 HiveServer 伺服器的負載,避免 HiveServer 因負載過高超過門檻值自動重新開機,導緻跑任務失敗重新執行,更加延長了資料的産出時間。

1.2 “提” 上遊完成立即執行

“提”這裡分為兩方面:

一是,提前執行,還是在一個任務中對大段邏輯按照上遊依賴拆分出的多個子邏輯,放入不同的執行方法,分别檢測各自的上遊任務的完成情況,上遊完成後可以立即執行。

如對 MID 表 ⑥ 産出任務的拆分,可以拆邏輯而不拆分任務,確定任務的可讀性。

二是,提前過濾,對資料過濾這裡分為兩部分:把過濾條件進行提前,裁剪下遊不使用的字段。

如合并任務 ⑦ 中的“剔除非主流程資料” 可以直接提前至序号 ① ODS 的直接下遊進行過濾,從源頭來減少資料量;

各維表 ⑧ 中的資料不是都會使用到,提前把不需要的字段裁剪掉以減少需要處理的資料量。

1.3 “并” 非依賴子查詢并行

并行執行提高資源的利用效率,在更短的時間内完成大量資料處理任務,适用于核心鍊路的資料産出,需要平衡執行隊列確定有足夠的資源。

而在産出 MID 表 ⑥ 這個大任務中拆分出的四條執行線,各自上遊依賴完成的時間不同,可以在各自的時間線上并行執行,避免了對計算資源的争搶;

對取維表 ⑧ 和新老客 ⑨ 的一系列擴充資訊,做提前過濾和列剪裁也都可以并行來完成。

1.4 “延” 為不穩定的依賴留足 buffer

當我們資料鍊路長的時候,一些産出時間晚、産出時間不穩定的資料,放到執行鍊路的末尾來最大程度的降低其帶來的影響,也可以為其預留更多的 Buffer 時間來執行。

埋點曝光鍊路 ③ 和主流程搜尋請求 ④ 鍊路受外部依賴不穩定和自身資料傾斜的影響,産出時間晚一直拖累着 MID 表;

将這兩條鍊路的産出資料後置到 DWD 結果表 ⑩ 中進行使用,加 DWD 結果表的 MR 并行度進行提速,關聯任務所在的 Stage 可以在 5~10 分鐘完成,要比在 mid 表中等待花費的時間小好多倍。

2、消除長尾帶來的時間增長

去哪兒酒店數倉流量鍊路優化實踐

長尾任務是指在一些列任務中,少數的任務處理的資料量遠遠大于大多數任務的資料量,使得這少數任務的執行時間顯著長于其他任務,成為整個任務的瓶頸。

2.1 小心動态分區排序的陷阱

表中為主流程接口請求資料 ④,随着資料量的增加處理時間出現了非常不合理的翻倍的上漲。

資料量(G) 處理時間(小時)
25.4 01:15
30.9 01:33
34 01:43
38.4 02:19
43.3 03:35
47.2 03:50

我們結合 Hive 的執行計劃來逐漸分析來定位問題點,首先觀察各階段 Stage 的執行情況,發現最後一個 Stage-6 出現了長尾情況,并且 MR 中有 reduce 任務。

Stage-6 為 MR 資料寫入

去哪兒酒店數倉流量鍊路優化實踐

在 Hive 中最後一個 Stage 的 MR 任務是用于寫表的任務(output task),在輸出任務的 MR 中通常情況下 Reduce 階段不一定是必需的。

如果輸出資料可以直接寫入 HDFS 或其他存儲系統,而無需進行聚合或排序操作,則可以省略 Reduce 階段,這樣可以提高任務的性能。

這種情況的 MR 任務是 map-only 任務,但是如果需要進行聚合或排序操作,則需要執行 Reduce 階段。

search 任務部厘清洗邏輯

去哪兒酒店數倉流量鍊路優化實踐

上面是任務中的部分代碼段,主要為對 ods 日志的一個清洗轉化過程,最後并沒有聚合和排序操作;

分析這裡就比較奇怪了,是什麼導緻了寫表任務出現了排序,我們可以通過執行計劃來一探究竟。

檢視執行計劃

去哪兒酒店數倉流量鍊路優化實踐

執行計劃 Stage-6部分

去哪兒酒店數倉流量鍊路優化實踐

整體的執行計劃非常長我們隻取 Stage-6 這個階段,最為關鍵的是 Reduce Output Operator 這一步

MapReduce 計算引擎,在 Map 階段和 Reduce 階段輸出的都是鍵-值對的形式

  • key expressions:表示為 Map 階段輸出的鍵(key)所用的資料列
  • sort order:表示輸出是否進行排序,+ 表示正序,- 表示倒序
  • Map-reduce partition columns:表示 Map 階段輸出到 Reduce 階段的分區列
  • value expressions:表示為 Map 階段輸出的值(value)所用的資料列
去哪兒酒店數倉流量鍊路優化實踐

Map 階段輸出的 key 正是我們分區的字段_ col22:platform _col23:hour,對key進行了正序排列

insert overwrite table dw_hotel_search_di partition(dt=${DATE},platform,hour)

到這裡就定位到了我們在寫入表的時候,dt 是直接指定的靜态分區;platform和 hour 是動态分區,現在動态分區出現了排序的情況并且導緻了長尾任務。

Hive 中動态分區的分區個數非常大的時候,會出現 OOM。在每個 task 進行資料寫出時為每個分區目錄開啟一個檔案寫入器(file writer),資料會先進入緩存區後批量寫入。

如我們在刷一個大表的曆史資料時,當記憶體中的檔案句柄越來越多的時候資料記憶體會被逐漸填滿,導緻 OOM 的發生。

而動态分區排序正是為了解決這個問題,開啟動态排序後會對分區 key 進行全局排序,排序後每個 task 内對應一個分區的資料這樣有效的解決了打開檔案句柄多 OOM 的發生。

但是,同樣也引入了一個問題,全局排序後某 Reduce 對應分區中的資料量非常多的時候出現傾斜,執行緩慢。

公司的 hive 元件是預設開啟動态分區排序的 "hive.optimize.sort.dynamic.partition=true"。

我們現在 dw_hotel_search_di 表中 platform 通常為 adr 和 ios,hour 是 24 小時。分區個數極少開啟的檔案寫入器不會造成記憶體被吃滿 OOM 的發生,将動态分區排序進行關閉。

2.2 JOIN KEY的空值加“鹽”

窄口徑是指通過使用者的請求 Trace 把通路過的頁面逐級串聯起來,去統計使用者的轉化率。

下圖是 L 頁流量表通過 Trace 去串聯各頁面的流程圖,TraceID 是使用者通路目前頁面産生的,TraceLog 為透傳的上一個頁面的 TraceID,這樣就可以逐級串聯起來。

如圖我們有十個使用者通路了 L 頁,其中兩個不太滿意也就不會進入下一個頁面 D 頁,也就是我們所謂的流失使用者,同樣用這兩個使用者的 L 頁 Traceid 是在 D 頁的 TraceLog 中找不到。

去哪兒酒店數倉流量鍊路優化實踐

這樣就會出現一個問題,十個使用者:D 頁 TraceID 擷取的為空 2 個,B 頁TraceID 擷取的為空 6 個,O 頁 TraceID 擷取的為空 7 個;

在進行 JOIN 操作時,如果 JOIN KEY 中包含為空的列,會導緻資料傾斜。因為 Hive 會将所有空值都映射到同一個 Reduce 任務中,如果某個鍵對應的資料量很大,會使這個 Reduce 任務成為瓶頸。

這些空的 TraceID 作為 JOIN KEY 在關聯下一級頁面的時候會被配置設定到一個 reduce 中造成資料傾斜,嚴重拖累任務整體的完成進度。

解決方法是在空值列上追加一個随機“鹽”,使相同的空值也能映射到不同的 Reduce 任務,進而緩解資料傾斜的問題。

去哪兒酒店數倉流量鍊路優化實踐

3、選擇壓縮時機産生的不同收益

Hive 支援在多個層面上進行資料壓縮選擇不同的壓縮時機,可獲得不同的收益如:

  • 在資料存儲上壓縮可以減少存儲空間和讀取資料量
  • 在中間結果序列化上壓縮可以減少 Shuffle 資料量
  • 在輸出階段壓縮可以減少最終結果資料量

需要根據具體場景選擇最佳的壓縮政策,我們這次的案例是針對中間表和最終産出表來進行的優化壓縮實踐

3.1 臨時中間表的壓縮場景

臨時表的壓縮有兩個不同的場景小的維表和大的中間結果表

1、先說小的次元表:

在鍊路優化中将 DIM :基礎資訊 (⑧) 的關聯邏輯從進行了拆分,從多段的 join 子查詢中提了出來。

提前錯峰執行緩解瞬時任務堆積造成計算資源壓力是一方面原因。

更重要的是提前對資料做了行和列的剪裁,然後對産出臨時表做資料的壓縮,進一步為了降低資料量進而降低到滿足 mapjoin 的門檻值;

使其放入到 hashTable 裡共享至分布式緩存中,在 map 端完成 JOIN 操作,由 CommonJoin 轉化為 MapJoin,極大的提高 join 效率。

當然也需要我們來觀察剪裁後的表是否滿足 hive.mapjoin.smalltable.filesize 門檻值,在差異不大的情況下可以适當對該值調大,預設是非常保守的 25M。

2、然後再說中間結果表:

背景:hive 中的表一般都是 ORC 預設用 Zlib 即使我們不指定壓縮格式也會進行自動壓縮,通常會有十幾倍的壓縮效率。

如果我産出的一個中間結果表使用了 ORC 格式,占用存儲大小就會大大降低,然後在用這個表去 JOIN 其他表的時候,按資料大小切片後産生的 Task 個數(并行度)就會降低很多倍。

問題分析:L 頁流量表的産出任務中就存在了這樣的情況,下面是任務的各 Stage 執行情況:

去哪兒酒店數倉流量鍊路優化實踐

共有 15 個 Stage ,開頭 Stage-22 是小表 (319kb) 處理,結尾 Stage-12 是寫表 Task 這兩個任務較小

Stage:17、18、19 是處理詳情、預訂、送出三個階段窄口徑的,用的都是UserPath 使用者窄口徑是以并行度一緻

Stage1:是整個關聯查詢 SQL 主表(也就是最左表的第一個關聯查詢)的執行階段

最左表為 L 頁中間表使用了 ORC 格式,map 任務在資料切片時候才拆分了 187 個任務,需要 40 多分鐘才能執行完成

其他 Stage 的并行度都很勻稱,平均 map 在 8K 左右、reduce 在 2.5K 左右,執行時間也無卡點

通常 map 任務個數=表資料存儲大小 /mapred.max.split.size ,L 頁中間表每天平均 ORC 存儲大小為幾十G資料,分片大小為 128Mb

解決方案:要想提升任務的并行度,一個是降低切片的大小、另一個是增加表資料存儲的大小

  • 降低切片大小:如果我們将分片大小從 128mb 降到 96mb,Stage1 本身收益并不明顯,同時其他的 Stage 資料體量大并行度會激增對我們計算資源造成壓力
  • 增加表存儲大小:我們隻增大 L 頁中間表的存儲大小也就是 Stage1 處理的資料量來提個其并行度,其他的 Stage 完全不受影響

我們最終選擇增加表存儲的大小來提高并行度,具體操作為對L頁中間表轉為 textfile 來存儲并且不使用壓縮算法,放大其資料量,進而提高資料切片個數增大 map 任務的并行度。

優化效果如下:在其他 Stage 依然保持均衡,極大的提高了 Stage1 的并行度,優化後 Stage1 從原來的 40 分鐘縮短至十多分鐘。

去哪兒酒店數倉流量鍊路優化實踐

3.2 産出表如何獲得更好的壓縮率

L 頁流量表 (dwd_flow_app_searchlist_di) 一直是使用 ORC 資料格式進行存儲預設壓縮工具是 Zlib。

通常 ORC+Zlib 的壓縮率有十幾倍,而 L 頁流量表的壓縮比(壓縮前/壓縮後)隻有 3.5 : 1 完全沒有達到正常的壓縮比,而且查詢性能低。

去哪兒酒店數倉流量鍊路優化實踐

1、我們來了解一下 ORC 的存儲結構,這樣才能更好的發揮其壓縮效果

ORC 是列式存儲的自解析檔案,檔案中有自己的中繼資料,包括了多個Stripe,Stripe 中也包括了索引資訊、主資料和中繼資料資訊。整體的層級分為三級,每級都有對應的索引資訊

  • 檔案級 (file):這一級的索引資訊記錄檔案中所有 stripe 的位置資訊,以及檔案中所存儲的每列資料的統計資訊;
  • 條帶級 (stripe):該級别索引記錄每個 stripe 所存儲資料的統計資訊;
  • 行組級 (row):在 stripe 中,每 10000 行構成一個行組,該級别的索引資訊就是記錄這個行組中存儲的資料的統計資訊。

我們聚焦在行組這個級别,1W 行構成的行組中每一個字段以單獨的 Stream 來存儲,每個 Stream 中的資料類型為一緻,這樣在行組中資料同質化越高壓縮效果越明顯

架構如下:

去哪兒酒店數倉流量鍊路優化實踐

2、線上實際表中的 ORC 是如何存儲的呢,我們可以一探究竟

先從線上表的目錄中将一個 ORC 的資料檔案 down 下來,可以通過 orc-tools 來進行分析其中繼資料内容。

頭部的基礎資訊包括了檔案的大小,版本,條數,壓縮格式和資料列名稱類型 schema 資訊。

重點關注 Stripe Statistics,對其中每個 Stripe 的統計項:

  • count: 目前 stripe 中該值的個數
  • hasNull: 是否包括 null
  • min: 最小值
  • max: 最大值
  • sum: 累計值
去哪兒酒店數倉流量鍊路優化實踐

在列印出來的 ORC 中繼資料中我們可以清晰的看到資料在檔案中的分布,當一個 stripe 中存儲的資料段同質化的程度越高的時候壓縮的比率就會越大

3、L頁流量表的優化實作,同質化資料的集中

如何使表中的資料在任務執行後寫入檔案時,将同質化的資料寫入到一起呢?

我們可以采用重新排序(distribute by xxx),當然排序字段需要保證相對較高的基數避免資料傾斜;計算引擎會以排序字段為分區鍵 hash 進行分發到不同的計算節點,同一計算節點的 hash key 是一緻的這樣就保證了資料的同質化程度,但毫無疑問的是直接引入額外的排序後會影響我任務的産出效率。

如何能夠使資料同質化和産出性能二者兼顧呢?

我們可以具體分析 L 頁流量表中是酒店清單頁的展示資料,同一次請求中( trace 粒度)包括的基礎資訊重複度就會越高,但是 traceid 的基數非常高以它來排序的話,資料相對太分散。

我們退而求其次選用使用者粒度,兼顧了資訊重複度和資料基數,那有如何避免引入額外的排序呢?

去哪兒酒店數倉流量鍊路優化實踐

實際任務中往往我們調整一下 join 表的順序就可以了,如産出 L 頁流量表我們把關聯新老客的資料放到結尾;

這樣最後的執行任務以 user_id 為 joinkey,hash 後分發到不同的下遊處理階段完成 Join 操作,而我們也達到了想要的目的。

五、優化效果

通過以上一些列的政策進行優化後,新鍊路任務在 4 月 18 号完成上線,産出4 月 17 号 (T-1) 的資料為優化後的。

1、産出時間提前了近 4 個小時

任務平均完成時間從原來 9 點半提前至 5 點半,産出時間提前了近 4 個小時;天任務運作時間從原來 2.5~3 小時縮短至 1 個小時,近期随着預訂五一酒店的流量上漲運作時間依然穩定在 1 小時。

任務的開始時間為 4 點左右并不是依賴的上遊流量鍊路晚,而是現在已經卷到訂單鍊路。目前依賴的訂單資金産出是時間為 4 點左右,後續我們對訂單鍊路優化後流量的産出時間會更早。

去哪兒酒店數倉流量鍊路優化實踐

2、存儲空間節約了 93%

L 頁流量表原先就是 ORC 存儲預設使用 Zlib 進行壓縮,優化後存儲和壓縮格式都沒有改變完全相容了曆史 DT 的資料避免了資料回刷。

表中除了沒有删減字段還新增一個資金相關的字段,存儲資源的節約得益于對 ORC 資料格式特性的充分了解和利用,占用的存儲資源平均由原來 230G 下降至 15G,存儲空間節約了 93%。

去哪兒酒店數倉流量鍊路優化實踐

3、查詢效率提升了6倍

查詢效率對于産、運、分析同學更為關注。在下面驗證查詢效率的 SQL 中總共有 18 個去重統計項,10 個彙總統計項,驗證是在一個空閑的資源隊列完成的。

分别用優化前(16日)、優化後(17日)的資料進行統計,查詢時間由原來 2 小時 21 分縮短至 22 分鐘,查詢效率提升了 6 倍!

去哪兒酒店數倉流量鍊路優化實踐

查詢效率驗證 SQL

去哪兒酒店數倉流量鍊路優化實踐

六、總結

在政策上:通過"拆"、"提"、"并"、"延"的方法對流量鍊路進行了調整。:

在技術上:對任務執行過程進行分析解決了傾斜的卡點、提高了壓縮的效率。

在外部:通過值班歸因避免 case by case ,推動了日志收集、排程系統、公共埋點和平台服務的穩定性降低起夜率,為保障五一注入了強力穩定劑。

資料的産出時間大大縮短,存儲空間顯著減小,查詢效率也得到了極大的提升,優化效果顯著。

另外對上遊鍊路中 search 任務的優化産生了更多的附加收益。

去哪兒酒店數倉流量鍊路優化實踐

涵蓋了主流程的所有接口請求日志,通過對不同接口的拆解我們會得到搜尋( search S 頁)、詳情( Detail D 頁)、預訂( booking B 頁)和送出訂單( order O 頁)流量表。

在今年五一期間流量到達峰後(上表中資料量為十幾倍壓縮率壓縮後的值),資料的處理時間表現非常平穩,保障了下遊 S、D、B、O 各級流量表産出的及時性。

未來展望,将我們的政策、優化方法進行沉澱,形成一系列的規則,展開應用到全鍊路中。

最後送給各位讀者一句筆者總結的心得:勤于思考 善于追問 抓的住本質 拿的到結果~

作者:惠善祥

惠善祥,21年入職去哪兒網,在國内酒店資料倉庫團隊負責酒店離線、實時數倉的開發、維護與優化。專注于計算效率提升、資料治理等工作,負責S級報表SLA保障、鍊路唯一化建設等項目;長期關注大資料計算演變與性能提升。

來源:微信公衆号:Qunar技術沙龍

出處:https://mp.weixin.qq.com/s/oE4B0Dox56L4WJ_TbdQ_qA

繼續閱讀