如何在中後臺領域玩轉 BFF 架構
前言
2021 年 08 月 14 日,平臺前端在前端早早聊大會分享了哈囉出行在 BFF 領域的實踐,以下是由平臺前端趙存分享的主題
分享大綱
本次分享主要從業務背景、BFF 核心架構、基於 Serverless 的 BFF 改造、總結四個部分組成。
業務背景
我們的供應鏈場景有很多供應商,每個供應商都有物流、資產、倉儲等多個域,而這些域我們的後端都基於 DDD 領域模型做了微服務化,此時前端在開發面向這些供應商使用的中後臺應用時,遇到了以下問題:
頁面顯示需要請求多個域:比如一個商家的詳情頁,可能既需要請求倉儲數據,還需要請求資產數據,才能將一個頁面顯示出來。
接口格式不滿足前端需求:後端微服務化後,是面向多項目,通用性的,其接口格式不一定能滿足前端需求,前端需要自己做轉換,比如單位轉換,字段裁減。
需求變化快:業務在快速迭代,需要接口的大量支持,而我們的後端域是面向多項目的,更改成本較大,需要投入更多的測試,此時如果在前端和後端中間存在一箇中間層,來做這些事情,那麼效率會有比較好的提高。
部門協作成本大:有些需求需要其它部門的後端同學支持,而其它部門的同學因爲自己部門的需求緊張,排期較滿,導致我們的需求遲遲無法排期,此時如果存在一箇中間層,在中間層去請求其它部門提供的領域服務來組合數據提供給前端,此時就可以在其它部門同學不參與的情況下,前端自己完成需求開發,部門之間的協作成本會大大降低。
基於以上背景,前端這邊引入了 BFF 架構,BFF 架構能做哪些事情:
業務編排:從後端域多接口獲取數據合併輸出給頁面。比如一個商家詳情頁即需要倉儲數據,也需要資產數據,此時我們在 BFF 層將倉儲和資產數據請求回來組裝吐給前端。
字段轉換:字段過濾、數據格式化等工作。比如資產域的商戶名字段叫 businessName,而倉儲域的商戶名字段叫 shopName,此時可以在 BFF 層統一掉,這樣前端就不需要做判斷了。
個性化數據:爲前端提供個性化服務,如數據壓縮,單位轉換等。
BFF 核心架構
核心架構
以上是 BFF 的核心架構圖,前端即中後臺應用,後端域即後端服務,右側的工具支撐是公司的一些基礎公共服務,中間的就是 BFF 核心實現,我們從上往下看:
-
業務:可以在這一層做業務編排,字段轉換,個性定製等業務邏輯,同時提供了一個 node-auth 包,可以利用該包做用戶鑑權。
-
基礎框架:基礎框架這層,主要封裝了 node-soa 這個 npm 包,node-soa 裏面包含了 node-log 日誌工具、node-hook 代碼規則校驗工具、node-zk 集羣鏈接工具等。
-
Node 框架:Node 框架這塊選型的是 Koa2。
調用鏈路
核心架構講完後,在看下整個 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 架構的核心內容,這套架構在當時的業務背景下是一個比較好的解決方案,但隨着業務的快速發展,這個架構也遇到了一些問題:
-
運維成本增大:隨着 BFF 應用的增多,需要更多的機器來部署 BFF 應用。
-
發佈流程長:新增一個 BFF 的接口,需要走完編譯,構建,部署一整套流程,無法做到秒級部署。
-
域名不收斂:每個 BFF 都有各自獨立的域名,增加記憶成本。
鑑於這些痛點,我們引入了 SFF(Serverless For Frontend)架構,通過將 BFF 構建於 Serverless 之上,用雲函數的方式取代傳統基於 NodeJS 的 BFF 層。
基於 Serverless 的 BFF 改造
SFF 架構
上圖是改造後的 BFF 架構,相比於一代的 BFF 架構,這裏主要多了兩塊內容,一塊是 FaaS 層,另外一塊是開發者平臺。
-
開發者平臺是在線編寫雲函數的,主要提供了函數管理、發佈管理等功能,發佈的每個函數都會保存在數據庫中。
-
FaaS 層主要就是一個個 Function,一個 BFF 接口請求過來,首先會去數據庫獲取對應的函數,然後執行該函數。
實現方案選型
目前主流的方案主要是基於容器和基於進程兩種方式。
容器方案:基本實現是利用 K8s + Docker, 每個雲函數執行的時候都啓動一個容器去執行,執行完後容器銷燬,整個容器的管理、併發處理、擴縮容都是用 K8s 來管理。
進程方案:每個雲函數的執行都啓動一個新的進程去執行,執行完後進程銷燬。
對於實現方案的選型,我們需要考慮以下幾個方面:
-
業務場景複雜性:高併發採用容器方案更好;併發少,選用進程更輕量,也更容易實現。
-
基建 & 運維能力:容器方案對基建和運維能力有更高的要求,要考慮公司的運維能不能 Cover 住。
-
團隊人力 / 能力:基於容器方案技術上的要求會更高一些,實現難度也會更大一點,要考慮團隊的小夥伴這方面的經驗有沒有,團隊的人手夠不夠。
我們的業務並不複雜,中後臺應用幾乎沒有高併發,目前公司對於容器的使用還沒有大推,團隊人手也不是很夠,加上缺少容器這方面的實戰經驗,最終採用了基於進程的方式來實現。
實現
基本的實現如下:
用戶發起 HTTP 請求,Node 主進程去數據庫讀取該請求的函數實現,拿到函數實現後會 Fork 一個子進程執行函數,函數執行完後子進程銷燬。這裏需要注意的一點是控制子進程的執行時間,防止因爲函數執行異常,導致子進程無法銷燬。
我們在看下執行函數的具體實現:
通過 VM2 模塊來執行我們的雲函數,從而保證子進程和主進程之間的 Context 隔離,如果不進行隔離,有可能出現的一種情況是,子進程裏面如果調用了 process.exit(),此時我們的 Node 主進程就會被退出去。
做了進程的 Context 隔離還不夠,我們可以利用進程池來優化每次 Fork 子進程的時間,利用 CGroup 來限制子進程的 CPU 使用率、內存佔用、磁盤 IO 等。CGroup 是 Linux 內核中的一個核心能力,提供了將不同進程按分組進行管理的能力,並且能對不同的分組限制其所使用的計算資源(CPU、內存、磁盤 IO 等),我們可以通過限制用來執行函數的子進程所能消耗的最大內存、磁盤及網絡帶寬,同時控制進程所能使用的最大 CPU 佔用率等方式來保證系統的穩定性。
最終的實現如下:
以上就是基於 Serverless 的 BFF 改造的核心內容,相比於一代的 BFF 架構,基於 Serverless 的 BFF 改造有以下幾點優勢:
-
效率提升:獨立雲函數,動態編寫,秒級部署。
-
降本:應用收斂,有效降低運維、機器成本。
總結
以上就是平臺前端本次分享的主要內容,我們做下總結:
-
後端領域微服務化後,需要一套能提供業務編排、字段轉換、個性定製的機制來保證業務的快速迭代。
-
BFF 架構能夠有效的做到業務編排、字段轉換、個性定製,且讓前端進入全棧領域。
-
構建於 Serverless 之上的 BFF 進一步的降低了運維、機器成本,提高了人效。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/gE_n1diROi1qhxB_AwittQ