微服務架構,這個在幾年前還算比較前衛的技術在現在遍地開花。得益於開源社區的支持,咱們能夠輕鬆地利用 Spring Cloud 以及 Docker 容器化快速搭建一個微服務架構的原型。不論是成熟的互聯網公司、創業公司仍是我的開發者,對於微服務架構的接納程度都至關高,微服務架構的普遍應用也天然促進了技術自己更好的發展以及更多的實踐。本文將結合項目實踐,剖析在微服務的背景下,如何經過先後端分離的方式開發移動應用。html
對於微服務自己,咱們能夠參考 Martin Fowler 對 Microservice 的闡述。簡單說來,微服務是一種架構風格。經過對特定業務領域的分析與建模,將複雜的應用分解成小而專注、耦合度低而且高度自治的一組服務。微服務中的每一個服務都是很小的應用,這些應用服務相互獨立而且可部署。微服務經過對複雜應用的拆分,達到簡化應用的目的,而這些耦合度較低的服務則經過 API 形式進行通訊,因此服務之間對外暴露的都是 API,不論是對資源的獲取仍是修改。前端
微服務架構的這種理念,和先後端分離的理念不謀而合,前端應用控制本身全部的 UI 層面的邏輯,而數據層面則經過對微服務系統的 API 調用完成。以 JSP (Java Server Pages) 爲表明的先後端交互方式也逐漸退出歷史舞臺。先後端分離的迅速發展也得益於前端 Web 框架 (Angular, React 等) 的不斷涌現,單頁面應用(Single Page Application)迅速成爲了一種前端開發標準範式。加之移動互聯網的發展,不論是 Mobile Native 開發方式,仍是 React Native / PhoneGap 之流表明的 Hybrid 應用開發方式,先後端分離讓 Web 和移動應用成爲了客戶端。客戶端只須要經過 API 進行資源的查詢以及修改便可。web
BFF 概況及演進數據庫
Backend for Frontends(如下簡稱BFF) 顧名思義,是爲前端而存在的後端(服務)中間層。即傳統的先後端分離應用中,前端應用直接調用後端服務,後端服務再根據相關的業務邏輯進行數據的增刪查改等。那麼引用了 BFF 以後,前端應用將直接和 BFF 通訊,BFF 再和後端進行 API 通訊,因此本質上來講,BFF 更像是一種「中間層」服務。下圖看到沒有BFF以及加入BFF的先後端項目上的主要區別。express
1. 沒有BFF 的先後端架構後端
在傳統的先後端設計中,一般是 App 或者 Web 端直接訪問後端服務,後臺微服務之間相互調用,而後返回最終的結果給前端消費。對於客戶端(特別是移動端)來講,過多的 HTTP 請求是很昂貴的,因此開發過程當中,爲了儘可能減小請求的次數,前端通常會傾向於把有關聯的數據經過一個 API 獲取。在微服務模式下,意味着有時爲了迎合客戶端的需求,服務器常會作一些與UI有關的邏輯處理。api
2. 加入了BFF 的先後端架構服務器
加入了BFF的先後端架構中,最大的區別就是前端(Mobile, Web) 再也不直接訪問後端微服務,而是經過 BFF 層進行訪問。而且每種客戶端都會有一個BFF服務。從微服務的角度來看,有了 BFF 以後,微服務之間的相互調用更少了。這是由於一些UI的邏輯在 BFF 層進行了處理。網絡
BFF 和 API Gateway數據結構
從上文對 BFF 的瞭解來看,BFF 既然是先後端訪問的中間層服務,那麼 BFF 和 API Gateway 有什麼區別呢?咱們首先來看下 API Gateway 常見的實現方式。(API Gateway 的設計方式可能不少,這裏只列舉以下三種)
1. API Gateway 的第一種實現:一個 API Gateway 對全部客戶端提供同一種 API
單個 API Gateway 實例,爲多種客戶端提供同一種API服務,這種狀況下,API Gateway 不對客戶端類型作區分。即全部 /api/users的處理都是一致的,API Gateway 不作任何的區分。以下圖所示:
2. API Gateway 的第二種實現:一個 API Gateway 對每種客戶端提供分別的 API
單個 API Gateway 實例,爲多種客戶端提供各自不一樣的API。好比對於 users 列表資源的訪問,web 端和 App 端分別經過 /services/mobile/api/users, /services/web/api/users服務。API Gateway 根據不一樣的 API 斷定來自於哪一個客戶端,而後分別進行處理,返回不一樣客戶端所需的資源。
3. API Gateway 的第三種實現:多個 API Gateway 分別對每種客戶端提供分別的 API
在這種實現下,針對每種類型的客戶端,都會有一個單獨的 API Gateway 響應其 API 請求。因此說 BFF 實際上是 API Gateway 的其中一種實現模式。
GraphQL 與 REST
GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data. GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools.
GraphQL 做爲一種 API 查詢語句,於2015年被 Facebook 推出,主要是爲了替代傳統的 REST 模式,那麼對於 GraphQL 和 REST 究竟有哪些異同點呢?咱們能夠經過下面的例子進行理解。
按照 REST 的設計標準來看,全部的訪問都是基於對資源的訪問(增刪查改)。若是對系統中 users資源的訪問,REST 可能經過下面的方式訪問:
Request:
Response:
對於一樣的請求若是用 GraphQL 來訪問,過程以下:
Request:
Body:
Response:
關於 GraphQL 更詳細的用法,咱們能夠經過查看文檔以及其餘文章更加詳細的去了解。相比於 REST 風格,GraphQL 具備以下特性:
1. 定義數據模型:按需獲取
GraphQL 在服務器實現端,須要定義不一樣的數據模型。前端的全部訪問,最終都是經過 GraphQL 後端定義的數據模型來進行映射和解析。而且這種基於模型的定義,可以作到按需索取。好比對上文 /users資源的獲取,若是客戶端只關心 user.id, user.name信息。那麼在客戶端調用的時候,query中只須要傳入 users {id\n name}便可。後臺定義模型,客戶端只須要獲取本身關心的數據便可。
2. 數據分層
查詢一組users數據,可能須要獲取 user.friends, user.friends.addr等信息,因此針對 users 的本次查詢,實際上分別涉及到對 user, frind, addr三類數據。GraphQL 對分層數據的查詢,大大減小了客戶端請求次數。由於在 REST 模式下,可能意味着每次獲取 user數據以後,須要再次發送 API 去請求 friends 接口。而 GraphQL 經過數據分層,可以讓客戶端經過一個 API獲取全部須要的數據。這也就是 GraphQL(圖查詢語句 Graph Query Language)名稱的由來。
3. 強類型
GraphQL 的類型系統定義了包括 Int, Float, String, Boolean, ID, Object, List, Non-Null 等數據類型。因此在開發過程當中,利用強大的強類型檢查,可以大大節省開發的時間,同時也很方便先後端進行調試。
4. 協議而非存儲
GraphQL 自己並不直接提供後端存儲的能力,它不綁定任何的數據庫或者存儲引擎。它利用已有的代碼和技術進行數據源的管理。好比做爲在 BFF 層使用 GraphQL, 這一層的 BFF 並不須要任何的數據庫或者存儲媒介。GraphQL 只是解析客戶端請求,知道客戶端的「意圖」以後,再經過對微服務API的訪問獲取到數據,對數據進行一系列的組裝或者過濾。
5. 無須版本化
GraphQL 服務端可以經過添加 deprecationReason,自動將某個字段標註爲棄用狀態。而且基於 GraphQL 高度的可擴展性,若是不須要某個數據,那麼只須要使用新的字段或者結構便可,老的棄用字段給老的客戶端提供服務,全部新的客戶端使用新的字段獲取相關信息。而且考慮到全部的 graphql 請求,都是按照 POST /graphql發送請求,因此在 GraphQL 中是無須進行版本化的。
GraphQL 和 REST
對於 GraphQL 和 REST 之間的對比,主要有以下不一樣:
1. 數據獲取:REST 缺少可擴展性, GraphQL 可以按需獲取。GraphQL API 調用時,payload 是能夠擴展的;
2. API 調用:REST 針對每種資源的操做都是一個 endpoint, GraphQL 只須要一個 endpoint( /graphql), 只是 post body 不同;
3. 複雜數據請求:REST 對於嵌套的複雜數據須要屢次調用,GraphQL 一次調用, 減小網絡開銷;
4. 錯誤碼處理:REST 可以精確返回HTTP錯誤碼,GraphQL 統一返回200,對錯誤信息進行包裝;
5. 版本號:REST經過 v1/v2 實現,GraphQL 經過 Schema 擴展實現;
微服務 + GraphQL + BFF 實踐
在微服務下基於 GraphQL 構建 BFF,咱們在項目中已經開始了相關的實踐。在咱們項目對應的業務場景下,微服務後臺有近 10 個微服務,客戶端包括針對不一樣角色的4個 App 以及一個 Web 端。對於每種類型的 App,都有一個 BFF 與之對應。每種 BFF 只服務於這個 App。BFF 解析到客戶端請求以後,會經過 BFF 端的服務發現,去對應的微服務後臺經過 CQRS 的方式進行數據查詢或修改。
1. BFF 端技術棧
咱們使用 GraphQL-express 框架構建項目的 BFF 端,而後經過 Docker 進行部署。BFF 和微服務後臺之間,仍是經過 registrator 和 Consul 進行服務註冊和發現。
在 BFF 的路由設置中,對於客戶端的處理,主要有 /graphql和 /api/${serviceName}兩部分。/graphql處理的是全部 GraphQL 查詢請求,同時咱們在 BFF 端增長了/api/${serviceName}進行 API 透傳,對於一些沒有必要進行 GraphQL 封裝的請求,能夠直接經過透傳訪問到相關的微服務中。
2. 總體技術架構
總體來看,咱們的先後端架構圖以下,三個 App 客戶端分別使用 GraphQL 的形式請求對應的 BFF。BFF 層再經過 Consul 服務發現和後端通訊。
關於系統中的鑑權問題
用戶登陸後,App 直接訪問 KeyCloak 服務獲取到 id_token,而後經過 id_token透傳訪問 auth-api服務獲取到 access_token, access_token 以 JWT (Json Web Token) 的形式放置到後續 http 請求的頭信息中。
在咱們這個系統中 BFF 層並不作鑑權服務,全部的鑑權過程所有由各自的微服務模塊負責。BFF 只提供中轉的功能。BFF 是否須要集成鑑權認證,主要看各系統本身的設計,並非一個標準的實踐。
3. GraphQL + BFF 實踐
經過以下幾個方面,能夠思考基於 GraphQL 的 BFF 的一些更好的特質:
GraphQL 和 BFF 對業務點的關注
從業務上來看,PM App(使用者:物業經理)關注的是property,物業經理管理着一批房屋,因此須要知道全部房屋概況,對於每一個房屋須要知道有沒有對應的維修申請。因此 PM App BFF 在定義數據結構是,maintemamceRequests是property的子屬性。
一樣相似的數據,Supplier App(使用者:房屋維修供應商)關注的是 maintenanceRequest(維修工單),因此在 Supplier App 獲取的數據裏,咱們的主體是maintenanceRequest。維修供應商關注的是 workOrder.maintenanceRequest。
因此不一樣的客戶端,由於存在着不一樣的使用場景,因此對於一樣的數據卻有着不一樣的關注點。BFF is pary of Application。從這個角度來看,BFF 中定義的數據結構,就是客戶端所真正關心的。BFF 就是爲客戶端而生,是客戶端的一部分。須要說明的是,對於「業務的關注」並非說,BFF會處理全部的業務邏輯,業務邏輯仍是應該由微服務關心,BFF 關注的是客戶端須要什麼。
GraphQL 對版本化的支持
假設 BFF 端已經發布到生產環境,提供了 inspection相關的 tenants和 landlords的查詢。如今須要將圖一的結構變動爲圖二的結構,可是爲了避免影響老用戶的 API 訪問,這時候咱們的 BFF API 必須進行兼容。若是在 REST 中,可能會增長api/v2/inspections進行 API 升級。可是在 BFF 中,爲了向前兼容,咱們可使用圖三的結構。這時候老的 APP 使用黃色區域的數據結構,而新的 APP 則使用藍色區域定義的結構。
GraphQL Mutation 與 CQRS
若是你詳細閱讀了 GraphQL 的文檔,能夠發現 GraphQL 對 query和 mutation進行了分離。全部的查詢應該使用 query { ...},相應的 mutaition 須要使用mutation { ... }。雖然看起來像是一個convention,可是 GraphQL 的這種設計和後端 API 的 讀寫職責分離(Command Query Responsibility Segregation)不謀而合。而實際上咱們使用的時候也聽從這個規範。因此的 mutation 都會調用後臺的 API,然後端的 API 對於資源的修改也是經過 SpringBoot EventListener 實現的 CQRS 模式。
如何作好測試
在引入了 BFF 的項目,咱們的測試仍然使用金字塔原理,只是在客戶端和後臺之間,須要添加對 BFF 的測試。
Client 的 integration-test 關心的是 App 訪問 BFF 的連通性,App 中全部訪問 BFF 的請求都須要進行測試;
BFF 的 integration-test 測試的是 BFF 到微服務 API 的連通性,BFF 中依賴的全部 API 都應該有集成測試的保障;
API 的 integration-test 關注的是這個服務對外暴露的全部 API,一般測試全部的 Controller 中的 API;
結語
微服務下基於 GraphQL 構建 BFF 並非銀彈,也並不必定適合全部的項目,好比當你使用 GraphQL 以後,你可能得面臨屢次查詢性能問題等,但這不妨礙它成爲一個不錯的嘗試。你也的確看到 Facebook 早已經使用 GraphQL,並且 Github 也開放了 GraphQL 的API。而 BFF, 其實不少團隊也都已經在實踐了,在微服務下等特殊場景下,GraphQL + BFF 也許能夠給你的項目帶來驚喜。
參考資料
【注】部分圖片來自網絡
https://martinfowler.com/articles/microservices.html
https://www.thoughtworks.com/insights/blog/bff-soundcloud
http://philcalcado.com/2015/09/18/thebackendforfrontendpattern_bff.html
http://samnewman.io/patterns/architectural/bff
https://medium.com/netflix-techblog/embracing-the-differences-inside-the-netflix-api-redesign-15fd8b3dc49d
文/ThoughtWorks龔銘
原文連接:https://insights.thoughtworks.cn/use-graphql-build-bff-in-microservices/