如何在中後臺領域玩轉 BFF 架構

前言

2021 年 08 月 14 日,平臺前端在前端早早聊大會分享了哈囉出行在 BFF 領域的實踐,以下是由平臺前端趙存分享的主題

分享大綱

本次分享主要從業務背景、BFF 核心架構、基於 Serverless 的 BFF 改造、總結四個部分組成。

業務背景

我們的供應鏈場景有很多供應商,每個供應商都有物流、資產、倉儲等多個域,而這些域我們的後端都基於 DDD 領域模型做了微服務化,此時前端在開發面向這些供應商使用的中後臺應用時,遇到了以下問題:

頁面顯示需要請求多個域:比如一個商家的詳情頁,可能既需要請求倉儲數據,還需要請求資產數據,才能將一個頁面顯示出來。

接口格式不滿足前端需求:後端微服務化後,是面向多項目,通用性的,其接口格式不一定能滿足前端需求,前端需要自己做轉換,比如單位轉換,字段裁減。

需求變化快:業務在快速迭代,需要接口的大量支持,而我們的後端域是面向多項目的,更改成本較大,需要投入更多的測試,此時如果在前端和後端中間存在一箇中間層,來做這些事情,那麼效率會有比較好的提高。

部門協作成本大:有些需求需要其它部門的後端同學支持,而其它部門的同學因爲自己部門的需求緊張,排期較滿,導致我們的需求遲遲無法排期,此時如果存在一箇中間層,在中間層去請求其它部門提供的領域服務來組合數據提供給前端,此時就可以在其它部門同學不參與的情況下,前端自己完成需求開發,部門之間的協作成本會大大降低。

基於以上背景,前端這邊引入了 BFF 架構,BFF 架構能做哪些事情:

業務編排:從後端域多接口獲取數據合併輸出給頁面。比如一個商家詳情頁即需要倉儲數據,也需要資產數據,此時我們在 BFF 層將倉儲和資產數據請求回來組裝吐給前端。

字段轉換:字段過濾、數據格式化等工作。比如資產域的商戶名字段叫 businessName,而倉儲域的商戶名字段叫 shopName,此時可以在 BFF 層統一掉,這樣前端就不需要做判斷了。

個性化數據:爲前端提供個性化服務,如數據壓縮,單位轉換等。

BFF 核心架構

核心架構

以上是 BFF 的核心架構圖,前端即中後臺應用,後端域即後端服務,右側的工具支撐是公司的一些基礎公共服務,中間的就是 BFF 核心實現,我們從上往下看:

調用鏈路

核心架構講完後,在看下整個 BFF 架構的調用鏈路:

調用鏈路從上往下,我們的中後臺應用通過 HTTP 請求到 Nginx 服務器上,Nginx 轉發到 BFF 層,BFF 層通過 RPC 調用後端域的微服務,完成整個調用過程。

這裏有兩個概念需要說明下:

ZooKeeper:可簡單理解爲服務註冊中心,後端的各個微服務都統一註冊到這個註冊中心,然後 BFF 層充當 ZooKeeper Client 去連接這個註冊中心,連接後,就可以枚舉到註冊中心每個服務的 Host 和 Port,拿到 Host 和 Port 就可以發起 RPC 調用了。

RPC:遠程過程調用,也就是說兩臺服務器 A、B,一個部署在 A 服務器上的應用需要訪問 B 服務器上的一個應用的某個方法,由於不在一個內存空間,因此需要通過網絡來表達調用的語義和傳達的數據,可以簡單理解爲 A 服務器上部署了我們的 BFF 應用,B 服務器上部署了我們的微服務。RPC 通信協議可基於 HTTP 或者 TCP 協議,我們採用的是 gRPC,即使用 HTTP/2 的一種 RPC 調用方式。

以上介紹了 BFF 的核心架構和整個調用鏈路,下面來看看 node-soa 的具體實現細節。

服務初始化

通過調用 node-soa 提供的 init 方法來完成服務的初始化,其中 dep 即爲各後端域的微服務。

我們在看看 init 的具體實現:


首先創建了一個 ZooKeeper Client 去連接 ZooKeeper 集羣,連接上以後通過 listChildren 方法枚舉 ZooKeeper 集羣的所有子節點,拿到子節點的 Host 和 Port 後創建了一個 grpcClient,之後就可以通過這個 grpcClient 發起 RPC 調用了。

服務調用

服務初始化後就可以發起 RPC 調用了,node-soa 提供了 request 方法,通過這個方法即可發起 RPC 調用,其中 service 就是後端域,method 即爲 java 側提供的方法。

我們在看看 request 裏面的具體實現:

通過服務初始時創建的 grpcClient 發起 RPC 調用,拿到數據後 resolve 回去,即完成一次 RPC 調用,在整個 RPC 調用過程中利用 Jaeger + OpenTracing 做了調用鏈路的追蹤,利用 node-log 做了請求日誌的落盤。

以上即爲我們第一代 BFF 架構的核心內容,這套架構在當時的業務背景下是一個比較好的解決方案,但隨着業務的快速發展,這個架構也遇到了一些問題:

鑑於這些痛點,我們引入了 SFF(Serverless For Frontend)架構,通過將 BFF 構建於 Serverless 之上,用雲函數的方式取代傳統基於 NodeJS 的 BFF 層。

基於 Serverless 的 BFF 改造

SFF 架構

上圖是改造後的 BFF 架構,相比於一代的 BFF 架構,這裏主要多了兩塊內容,一塊是 FaaS 層,另外一塊是開發者平臺。

實現方案選型

目前主流的方案主要是基於容器和基於進程兩種方式。

容器方案:基本實現是利用 K8s + Docker, 每個雲函數執行的時候都啓動一個容器去執行,執行完後容器銷燬,整個容器的管理、併發處理、擴縮容都是用 K8s 來管理。

進程方案:每個雲函數的執行都啓動一個新的進程去執行,執行完後進程銷燬。

對於實現方案的選型,我們需要考慮以下幾個方面:

我們的業務並不複雜,中後臺應用幾乎沒有高併發,目前公司對於容器的使用還沒有大推,團隊人手也不是很夠,加上缺少容器這方面的實戰經驗,最終採用了基於進程的方式來實現。

實現

基本的實現如下:

用戶發起 HTTP 請求,Node 主進程去數據庫讀取該請求的函數實現,拿到函數實現後會 Fork 一個子進程執行函數,函數執行完後子進程銷燬。這裏需要注意的一點是控制子進程的執行時間,防止因爲函數執行異常,導致子進程無法銷燬。

我們在看下執行函數的具體實現:

通過 VM2 模塊來執行我們的雲函數,從而保證子進程和主進程之間的 Context 隔離,如果不進行隔離,有可能出現的一種情況是,子進程裏面如果調用了 process.exit(),此時我們的 Node 主進程就會被退出去。

做了進程的 Context 隔離還不夠,我們可以利用進程池來優化每次 Fork 子進程的時間,利用 CGroup 來限制子進程的 CPU 使用率、內存佔用、磁盤 IO 等。CGroup 是 Linux 內核中的一個核心能力,提供了將不同進程按分組進行管理的能力,並且能對不同的分組限制其所使用的計算資源(CPU、內存、磁盤 IO 等),我們可以通過限制用來執行函數的子進程所能消耗的最大內存、磁盤及網絡帶寬,同時控制進程所能使用的最大 CPU 佔用率等方式來保證系統的穩定性。

最終的實現如下:

以上就是基於 Serverless 的 BFF 改造的核心內容,相比於一代的 BFF 架構,基於 Serverless 的 BFF 改造有以下幾點優勢:

總結

以上就是平臺前端本次分享的主要內容,我們做下總結:

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/gE_n1diROi1qhxB_AwittQ