摘要:本文整理自阿里巴巴高階技術專家祝海峰,在 Flink Forward Asia 2022 流批一體專場的分享。本篇內容主要分為四個部分:
- 背景及平臺歷程
- 平臺架構及實踐
- 平臺最佳化
- 未來規劃
一、背景及平臺歷程
搜尋、推薦、廣告是電商領域非常重要的導購場景,而這些引擎的原始資料,比如商品、賣家的事務資料、演算法資料,使用者點選日誌等都分佈在不同的儲存系統裡,這就需要有一個離線系統將這些資料聚合到一張大寬表上,再提供給引擎使用。
這樣的離線系統,它有兩個典型的特徵:
- 所有的歷史資料都需要經過批處理匯入到引擎裡。
- 實時資料需要更快的速度流入引擎裡。
我們以前開發這樣的離線系統,會面臨以下若干痛點。總的來說,流批兩次開發和資料口徑問題較難解決。需要了解計算引擎和儲存系統,運維複雜、效能調優的門檻較高,需要有專業的大資料團隊進行業務開發。
為了降低離線開發和運維的門檻,減少業務接入的成本和提高業務迭代的效率,我們從 2016 年開始研發和建設搜尋、廣告、推薦離線平臺,簡稱 SARO 平臺。
這個平臺是從開發到運維的一站式平臺,使用者可以透過拖拉拽 UI 方式開發,沒有大資料背景的人也能使用。平臺遮蔽背後的大資料技術,進一步降低使用者運維的門檻。在開發上,從資料來源到引擎,一個 ETL 流程,一次開發流批一體,平臺管理背後的作業依賴和儲存的對接。
上圖列舉了平臺上支援的部分業務。到目前為止,平臺擁有千級應用規模,日管理萬級作業量,PB 級日處理資料量,百萬級增量 TPS,秒級增量延時,連續六年成功支援雙十一。
這裡是平臺的一個發展歷程,從 2016 年開始到現在一共經歷了五個發展階段。
簡單來說,從最初的 MR、自研的 iStream 流式框架多套引擎,之後逐步透過 Blink 替換裡面的部分作業,到現在已經全面統一在一套 Flink 引擎上。在流批開發的 API 上,也緊跟 Blink 的發展步伐,從最初批流作業都跑在 DataStream 上,到後來的 Table API,再到現在全部統一到了 SQL 上。統一的計算引擎和開發 API 可以降低平臺自身的開發和運維成本。
而儲存的發展相對簡單,最初是使用 HBase,現在我們使用的是阿里雲的 Hologres 實時數倉。
二、平臺架構及實踐
上圖中間框裡的是平臺的技術架構,之上是平臺承載的業務,下面是平臺的技術底座。平臺透過 ETL 開發 Console 前端,提供使用者資料來源開發、釋出管理、部署管理、運維管理等開發運維管理能力。而這些能力則透過 Console 下面的 ETL Manager Service 來提供。
ETL Manager Service 主要負責 ETL 的生命週期管理。在它的下面一共有兩個部分:
- ETL Executor,它主要負責 ETL 的具體執行。在我們的實踐中,我們透過 Airflow 排程和 Flink 來共同完成整個 ETL 的過程。
- Catalog,它主要負責維表儲存、其他儲存的建立、Meta 管理。
此外我們還有兩個模組,分別是使用者自定義外掛程式碼的編譯管理模組和資料血緣模組。
下面以最簡單的一個使用者開發案例,來給大家講一講平臺在流批一體上的具體實踐。上圖是使用者開發的 ETL 處理流程圖,它由兩部分組成,分別是商品維度的資料和賣家維度的資料。
商品維度的資料,主要有商品表、商品圖片表、演算法模型資料,這三張表透過欄位改名等業務處理,最終 Merge 到商品維表上。需要額外說明的是,一個商品有多張圖片,所以我們需要有個 Udaf 把它聚合到商品維度上。
賣家維度的資料,主要有賣家表和對應的演算法模型資料表,同樣會 Merge 到賣家維表上。之後商品維表和賣家維表之間會進行 DimJoin,再經過 Udtf 外掛處理,最終輸出給引擎。
我們把使用者畫的這個圖叫做 Business Graph,圖上的所有輸入和輸出都是儲存表,中間是業務處理運算元。我們把業務處理運算元分成兩部分,一部分是資料聚合的處理運算元,這裡重點列舉了 Merge 維表聚合、DimJoin 維表 Join,一對多的 Udaf。另外一部分是業務邏輯處理運算元,這裡重點列舉了 Udtf 外掛。
前面講過所有的輸入輸出都是儲存表,我們進一步對儲存表抽象成一個動態表。動態表由一個全量的批表和一個增量的流表組成,它們共用一份 Schema 語義。前面使用者畫的圖就可以既表達批處理過程,也可以表達流處理過程,一次開發流批一體。
使用者畫的那個圖,在平臺背後是如何變成資料處理過程的呢?
首先有一組批流任務,將商品維度的表同步到商品維表上。同樣有一組批流任務,把賣家維度的表同步到賣家維表上。在兩張維表都同步完成後,我們會有一組批流任務,將這兩個維表進行 DimJoin,併產出給引擎。
此外,我們還有個流任務,它會去查詢賣家到商品的索引表,然後將賣家的變更觸發到商品上,這樣賣家的變化就能實時反映到引擎裡。
總的來說可以分為三個階段,分別是同步、Join、索引表查詢觸發,前面兩個階段裡都有批流任務,究竟是先跑批任務再起流任務,還是批流一直起,這取決於維表儲存的設計和選型。
前面提到了資料處理過程,其實維表儲存的設計也至關重要。我們以商品維表為例,商品表和商品圖片表有資料庫的 Binlog 可以保持實時同步,但演算法模型資料是 T+1 的,每天都需要變更。所以我們根據這個特性就把這兩類表分成了兩張表,此外我們還需要一張索引表,將賣家的變更轉換到商品上。如此一來,一個維表上其實有三類表。
對於維表的讀寫特性,批次寫入的時候我們希望不要對實時鏈路產生影響,因而希望它具備 Bulkload 的能力。批次讀的時候,我們提到裡面有多張表,那麼就需要在 scan 多張表的同時有更高效的 Join 能力。
對於流的寫入,增量在維表上大部分情況是部分 Schema 欄位寫入,因而需要 Upsert 的能力。對於維表的 Join,我們需要更高效的 KV 點查能力以及索引查詢能力。
前面提過增量總是更新維表上面的部分欄位,其他欄位的補全目前我們是在計算階段去完成。假如維表儲存有 CDC 的能力,我們可以直接消費維表的 Binlog 而不用做額外補全計算。此外我們還可以基於 CDC 做更多的最佳化,後面會有介紹我們基於 CDC 上做的最佳化。
一般的資料處理過程,首先會起一個批任務,在批任務完成之後,再起流任務去回追。如果批任務的時間較長,會對下游增量回追造成較大的壓力。假如維表儲存有 MVCC 特性,批流任務可以同時啟動,沒有任何回追,對下游引擎來說也更友好。
此外,我們會經常加減欄位,所以維表儲存也需要有 Alter Schema 的能力。
上圖是使用者圖變成具體的資料處理過程的步驟,最左邊是使用者畫的圖,經過校驗、解析、最佳化等步驟得到最佳化後的圖。其中一步最佳化是分析維表究竟有幾張表,有沒有索引表,索引欄位是誰。
最佳化後的圖經過 Flow Generate,這個階段主要根據前面說的資料處理模型的幾個階段裡的批流任務的依賴關係,繪製出的一個依賴關係圖 Flow Graph。Flow Graph 再經過 Flow Code Translate 翻譯成具體排程的程式碼。在我們實踐中,我們使用的是 Airflow 排程,所以我們會翻譯成 Airflow Python Code,之後由 Flow Runner 把排程給啟動起來。在 Airflow 的節點裡可能是一個作業節點,也可能是一些其他的預處理節點。
上圖是 Airflow 上一個作業節點 Python 程式碼的案例,我們主要看黃色高亮的部分。
首先這是一個增量同步的 Executor,這個 Executor 主要負責拉起增量同步作業。下面 snapshot 是指使用者畫的圖的具體協議,再下面是這個增量同步作業有關的子圖。這個 Executor 拿到使用者畫的圖的協議之後,再根據子圖就可以啟動對應的作業。
上圖是 Executor 拉起一個作業的具體步驟。最左邊的仍然是使用者化的圖,和前面一樣,它經過一系列的最佳化,得到了最佳化後的圖。
然後再經過 Job Generate,這個過程主要是根據子圖的資訊,還原出子圖,最後轉換出 Job Graph。這個 Job Graph 經過 Job Code Translate 會翻譯成具體作業的程式碼。在我們的實踐中,我們主要翻譯成 Flink SQL。之後我們會將這個 SQL 提交給 Job Runner,讓它執行起來。值得一提的是,Job Code Translate 和 Job Runner 都是外掛化定製的,這麼做是為了方便我們這麼多年來計算引擎的持續升級和我們核心模型的解耦。
上圖是一組批流 SQL 的案例,我們只看黃色高亮的部分。左邊是 MySQL,右邊是 drc,它是阿里資料庫 Binlog 的中介軟體。中間批流處理的過程基本是一致的,欄位也是一致的,到了最後寫出的時候,他們會寫到同一張維表上,有所不同的是流式任務還會寫到訊息佇列上。
三、平臺最佳化
還是以商品維表為例,這裡的 V1、V2 代表的是全量版本,以 V1 為今天的全量,V2 為明天的全量為例。前面提到商品有多張表,一張是保持資料庫實時同步的表,還有一張是 T+1 表。那麼我們基於這個特性把作業也分成兩部分,第一部分是左上角的商品表、商品影像表的批流任務,第二部分是上面中間框裡的演算法模型資料的批任務,左下角還有一組批流任務,完成這兩張表的聚合。
當開始明天 V2 版本全量的時候,因為商品表和商品影像表透過 Binlog 一直保持同步,所以這部分的全量同步我們可以最佳化掉。只需要對明天的演算法模型資料,再進行一個 V2 版本的批任務同步,寫到明天 V2 版本的 T+1 表裡。同樣,我們在明天起一組批流任務去消費 V1 版本的資料庫實時同步表和 V2 版本的 T+1 表,提供給引擎使用。
這樣的最佳化對使用者是無感知的,我們會去比較 V1 和 V2 兩個版本使用者圖的變化。如果資料庫部分沒有任何變化,我們會自動進行最佳化執行。此外,V2 版本的資料在引擎服務上線以後,我們才會將 V1 版本的 T+1 資料和 V1 版的部分增量作業進行下線。對使用者來說,版本的切換他是感知不到的,他感覺到的就是 24 小時不間斷的增量。
上圖主要是針對大型業務的最佳化。在這個例子中,業務的全量流程到了 T2 時刻完成,引擎才開始索引構建和服務上線。但留給引擎的時間只有 T2 到 24 點之間,資料量非常大的時候,引擎也有可能完成不了索引構建。而且在晚高峰時,線上任務可能會對離線任務進行壓制,更容易出現長尾。一旦出現失敗的情況,當天可能就無法完成線上引擎的新索引上線。
我們對資料進行了分析之後,發現少量的資料在搜尋引擎裡佔據著 80%以上的曝光,大量的資料只佔據 20%不到的曝光,符合一個二八原則的規律。所以我們據此將所有的資料分成冷熱兩部分。我們先起一個批任務,將熱資料進行處理。在這個例子中,到 T1 時刻熱資料處理完成,那麼引擎就可以從原來的 T2 提前到 T1 時刻將熱資料和昨天的冷資料合併在一起,開始做索引的構建和後續流程。在 T1 時刻之後,我們還會起一個有限流任務,繼續處理剩餘的冷資料。同時我們還會起一個增量任務,將所有的熱資料和冷資料的實時變更同步到引擎裡。
這樣一個最佳化上線以後,我們有大型業務的全量時間從七小時縮短到一小時,大大提前了索引切換上線的時間。以前會出現的 24 小時內無法完成索引構建的情況不再出現,而且在失敗之後,離線和引擎都有足夠的時間去再次重跑。
一條增量訊息來的時候,並不是所有欄位都有變化,也就意味著部分計算是可以被裁剪的。我們基於 CDC 做了一個計算剪枝的最佳化,下面我以一個最簡單的作業為例,來給大家講講這個最佳化。
首先右邊框裡的這個作業有一張 a、b 兩個欄位的源表,經過一個 UDTF1 處理,新增了一個 d 欄位,再經過一個 DimJoin 讀進一個 c 欄位,再經過一個 UDTF2,新增一個 e 欄位,這樣就有 a、b、c、d、e 五個欄位寫出到結果表裡。
使用者 UDTF 的開發是基於平臺提供的外掛框架來進行,使用者可以在外掛框架之上開發多欄位進和多欄位出的邏輯處理單元,我們叫做 Processer。一個 UDTF 裡有多個 Processer,它們之間透過欄位依賴形成欄位和邏輯處理的血緣關係圖。
為了簡化案例,在我這個例子中,UDTF1 和 UDTF2 只有一個 Processer。那麼每一個 Processer 如何判斷自己是否可以被裁剪呢?這就需要有一個執行時的 Plan。左邊就是這個 Plan 生成的過程,最左邊是一個 JobGraph,就是我剛剛說的這個作業。
這個 JobGraph 首先經過 Transform,這個階段主要是去解析使用者外掛程式碼,提取出欄位和邏輯處理的血緣圖,之後再經過一個 Weld,它主要是將這些小圖焊接成大圖之後再經過一個編譯,得到一個裸的 Plan。這個 Plan 經過編碼表的組裝和編碼過程,最終得到一份編碼後的 Plan。編碼的 Plan 裡主要以 bitset 的方式組織,這麼做是為了在執行時有更好的查詢效能。每一個 Processor 都去載入自己所需要的編碼後的 Plan,當 CDC 訊息來的時候,這個 Processor 就可以查詢這個 Plan,來決定自己是否要被裁剪掉。
來看一個例子,假如有一條 CDC 訊息變更欄位是 a,那麼 P1 和 P2 會被執行,J1 會被裁剪掉。假如這個時候是 b 欄位的變更,P1、P2 和 J1 都會被執行,沒有任何人被裁剪。如果是 c 的變更,P2 則會被裁剪。假如還有一條訊息 f 欄位變更,它並不是我們選擇的欄位。當我們收到 CDC 訊息時,我們在源頭上就會去查 Plan,發現 f 並不是我們 Plan 裡的欄位,那麼這條訊息就會被丟棄掉,從而裁剪此類的訊息。
這個最佳化上線以後,我們雙十一大促淘寶部分表 DimJoin 有 60%的裁剪率,日常裁剪率在 30%,節省資源 40%。
前面主要跟大家介紹的是系統層面的最佳化,下面我選擇一部分作業層面的最佳化給大家介紹一下。
第一個是預測執行。在離線混布環境中,因為不同機型的差異以及線上對離線任務的壓制,離線任務經常會出現長尾或者跑不完的情況。所以我們在去年 7 月份的時候,在 Blink 上開發了預測執行的功能,在 75%的 Task 完成之後,我們會去找出長尾的 Task,再開啟一個 Task 進行雙跑,最後選擇先跑完的資料作為最終的資料。
這個最佳化上線以後,我們有業務全量作業時長縮短了 50%,以前不太能跑的出來的作業,現在也能穩定產出。並且在去年 12 月份,我們團隊的同學也共同參與了把預測執行功能回饋社群的開發。
第二個是維表內多張表的 Join。前面提到一個維表是由多張表組成的,這些表的資料其實是根據 Key 經過排序的。我們基於這個特性,在 Scan Connector 裡做了一個最佳化,邊讀取這兩張表的資料並進行 Local Join。
第三個是非同步化。我們在維表讀取和寫入的 Connector 內部做了一些非同步讀寫的最佳化。
第四個是引擎訊息去重。我們對發往引擎的訊息做了一個 Udaf 處理,比較之前的訊息,如果和之前的訊息沒有任何變化,我們就會不發往引擎,如果有變化,我們就計算出變化的欄位,以提升引擎索引的效率。這個最佳化上線以後,我們有的業務訊息去重率達到 50%以上。
在資源型別上,我們的 JobManager 和 Stream 作業都跑在線上資源上,批作業跑在離線資源上。
在離線混部環境裡面,不同機型有較大的效能差異,且整體叢集規模較大,排程壓力也較大,此外還有一些單機的問題,因而大型任務 Pod 比較多的情況下,它的啟動時間會很久,所以我們在上面做了一些最佳化,調整到合適的 numberOfSlot、使用 Chain 等,使用大規格的 Pod,可以減少 Pod 的申請量,來實現啟動提速。此外,我們對重要業務進行的資源預留,以保證它更高效的啟動速度。
在 FO 代價上,我們對資源釋放保留一段時間,這樣可以在 FO 時使用原有的資源,實現更高效的 FO。對於不同優先順序的作業,我們會分不同的 fo-cost,這樣在叢集的碎片整理和作業驅逐的時候,就可以更有針對性。
在機器熱點上,解決長尾問題的預測執行前面已經講過,這裡就不再重複了。
四、未來規劃
目前我們平臺上面有大量增量極低的長尾業務,我們希望以多租戶的方式支援這部分業務,所以後面會在多租戶方面做一些探索,Flink Session Mode 可能是技術候選之一。另一方面,平臺管理著萬級作業,為了進一步提升整體的資源使用效率,我們還會在彈性上做一些探索。此外在惡劣的在離線混部環境穩定的執行我們的 Flink 作業也是將來的方向之一。
更多內容
活動推薦
阿里雲基於 Apache Flink 構建的企業級產品-實時計算Flink版現開啟活動:
99 元試用 實時計算Flink版(包年包月、10CU)即有機會獲得 Flink 獨家定製衛衣;另包 3 個月及以上還有 85 折優惠!
瞭解活動詳情:https://www.aliyun.com/product/bigdata/sc