好未來資料中臺 Node.js BFF實踐系列文章列表:
- 基礎篇
- 實戰篇(TODO)
- 進階篇(TODO)
好未來資料中臺的Node.js中間層從7月份開始討論可行性,截止到9月已經支援了4個平臺,其中3個平臺生產環境穩定,另1個在測試階段近期上線。
我4月份剛加入資料中臺,原本的想法是半年內不做大刀闊斧的改變,優先完善團隊現有的基建設施,比如元件庫、charts庫、工具、規範等。Node.js中間層的立項完全是一個意外。
某次中臺周例會上討論到前後端協作效率問題,我一時嘴賤提到Node.js中間層的想法,將應用平臺的一些與DB無關的邏輯放到Node層讓前端夥伴們負責,讓後端夥伴集中精力做底層服務建設。
本來就隨口一提,結果老闆當真了,當場拍板:搞!我當時的表情大概是...
沒辦法,自己畫的餅,哭著也要吃完。
咦?怎麼有點往日重現的感覺?曾經在騰訊雲,剛給客戶吹完牛逼就想抽自己大嘴巴~
Node.js 的定位
資料中臺 Node.js 中間層的定位類似一層 API Gateway,承載介面代理、聚合以及與DB 無關的部分業務邏輯。
在現階段資料中臺的服務體系中有兩類服務:常規 Java 後端和 T-Service 。後者是資料中臺將 OneService 方法論落地的統一資料服務,即服務於各個前臺事業部,也為資料中臺內部的各個應用平臺提供資料服務。
使用 T-Service 的協作流程簡單描述就是數倉夥伴建表後將資料來源接入 T-Service,然後 Java 後端夥伴配置取數 SQL,最後前端從統一的 query 介面查數展示。
T-Service 不直接對接前端,舊架構體系下需要在前端與 T-Service 之間搭建一層 Java 服務,說白了就是一堆 Controller,從 T-Service 取數後做一些很簡單的二次加工給到前端。Node.js 中間層的目標之一就是將這些複用性很差的 Controller 拿過來,好處有兩點:
-
舊架構體系下完成一個資料查詢功能需要牽涉數倉、後端和前端三方,新架構下只需要前端和數倉;
前提是前端夥伴需要掌握 SQL,前期由後端同事輔助,過渡後需要前端夥伴自己寫 SQL。
-
基於第一條, Java 後端夥伴的生產力被解放,集中精力做底層建設或通用性更強的介面。
除了以上兩條以外還有另一個隱藏優勢,前端的能力邊界擴寬後技術視野也會更寬闊,能夠從縱向角度上全面思考業務。
具體到職責分配上,Node.js 作為直接與客戶端互動的服務層,登入認證是最基本的功能之一,Java 後端服務只需要關注 Node.js 傳遞的使用者 ID 即可。
資料中臺有一個統一的使用者管理中心提供登入/登出服務,客戶端登入後會接收管理中心下發的 JWT,後續業務介面的請求會驗證 JWT 的有效性。接入 Node.js 中間層以後,JWT 的驗證邏輯就上浮到了 Node 層。
由於 Node 層只需要驗證(解密) JWT,不需要 JWT 加密運算元,所以非對稱加密是相對較優的方案,比如 RS256。使用者中心提供一個獲取公鑰的 API,Node.js 拿到公鑰後進行解密即可。
技術選型
技術選型方面並沒有糾結太久,其實用什麼框架都行,雖然每個框架都有各自的優缺點,但由於 Node.js 中間層的邏輯不會很重,複雜度不高,框架層面的問題幾乎不會成為瓶頸。所以一句話總結技術選型的核心出發點就是:用著舒服就行了。
最終用的 NestJS v7,當然也並不是完全沒有量化的因素。首先跟 express 和 koa 相比,NestJS 的模組抽象層次更高,將中介軟體進一步抽象為 guards 、 filter 、 interceptor 等等,能夠滿足大多數場景,幾乎不需要感知中介軟體這個概念。雖然有一定的理解門檻,但熟悉之後寫程式碼能夠將各模組的功能劃分更加清晰容易維護。其次,NestJS 與 Express 完全相容,生態足夠豐富。最後,完美支援 TypeScript,搭配 DI 、IoC 等機制,程式碼的結構和模組體系非常清晰。
之所以選了 v7 而沒有用最新的 v8 版本,原因之一是 NestJS 的 v8 版本依賴 RxJS v7。RxJS v7 廢棄了很多 v6 版本的操作符,用慣了 v6 一時之間切換過來很不習慣。
不過 NestJS 也並不是沒有缺點。舉個例子,Node.js 通常使用 async hooks 進行非同步資源跟蹤,比如日誌。NestJS 的依賴注入機制提供了一種 Request 作用域的 Provider,表面上看完全可以解決請求上下文的資源共享,但實際上並不好用,因為 NestJS 對 Request 作用域的 Provider 有一條額外的限制:依賴 Request 作用域 Provider 的 Provider 也必須是 Request 作用域的(很拗口吧)。由於日誌模組是通用模組,被很多模組依賴,所以在這條限制下,從 app scope 到 module scope,幾乎每個 Provider 都會被牽涉。所以最後還是用了常規方案:cls-hooked。
也有可能是我學藝不精,還沒掌握 NestJS 的精髓,歡迎指正。
服務治理
Node.js 服務部署在公司的未來雲 k8s,上層沒有接閘道器,所以 https 的支援是在由 ingress 這一層提供,目前的方案比較原始,沒有自動化工具,需要手動修改 ingress 配置。除此之外,在服務治理方面需要重點關注兩個方面:守護程式和日誌管理。
守護程式
在 k8s 普及之前,Node.js 的程式守護需要藉助一些第三方工具,比較知名的比如 forever 和 pm2。使用這些工具會佔用一些額外的機器資源(cpu、記憶體),藉助 k8s 探針完全可以取代它們。
未來雲提供了兩種存活檢查的探針:Http 和 TCP。
Http 探針本質上是向某個介面發起 Get 請求,響應成功狀態碼代表服務健康,否則判定為壞死重啟 pod。對於 Node.js 來說就相當於一次請求,所以需要 Node.js 提供一個專用的介面比如/health
,需要額外工作,並且這個介面不應該記錄日誌。
TCP 探針本質上是嘗試與容器建立 socket 連線,成功代表服務存活,否則判定為壞死重啟 pod,對 Node.js 服務本身沒有依賴和影響。
Http 探針由於發起的是一個真實請求,所以通用性更強,但是需要額外的工作。
如果 Node.js 上層有額外的一層反向代理比如Nginx,那麼一定不要使用 TCP 探針。因為 Nginx 本身就能夠建立 TCP 連線,所以如果用 TCP 探針的話檢測結果永遠是健康的。
資料中臺的 Node.js 服務每個 pod 都是單核,沒有起多程式,也就沒有使用反向代理的必要性,所以最終使用 TCP 探針做存活檢測。
日誌管理
與 Java 服務的日誌一樣,Node.js 服務的日誌也是 ELK 一把梭。需要注意兩點:
-
與 Java 後端的日誌串聯。
Node.js 與 Java 後端約定一個日誌串聯的規範,Node.js 向 Java 發起的請求頭中攜帶一個額外欄位x-trace-id
,值為 Node.js 生成的 requestId。 -
過期日誌檔案及時清理。
Node.js 的日誌檔案以天為單位分割檔案,每天都會建立幾個單獨的檔案(errors/warnsing/infos/expcetions),如果不及時清理的話會把磁碟打爆進而造成服務重啟,所以需要新增一個定時任務清理過期檔案。
總結
現階段的 Node.js 中間層剛起步,還比較輕量,以後還會遇到更多挑戰。本文簡單記錄了一些搭建過程中的經驗,有正面的可能也有反面的,歡迎指導。
下一篇會寫一下目前接入的幾個專案中 Node.js 中間層扮演的角色和具體做的事情,敬請期待。