微服務(wù)下如何使用GraphQL構(gòu)建BFF
微服務(wù)架構(gòu),這個在幾年前還算比較前衛(wèi)的技術(shù)在如今遍地開花。得益于開源社區(qū)的支持,我們可以輕松地利用 Spring Cloud 以及 Docker 容器化快速搭建一個微服務(wù)架構(gòu)的原型。不管是成熟的互聯(lián)網(wǎng)公司、創(chuàng)業(yè)公司還是個人開發(fā)者,對于微服務(wù)架構(gòu)的接納程度都相當高,微服務(wù)架構(gòu)的廣泛應(yīng)用也自然促進了技術(shù)本身更好的發(fā)展以及更多的實踐。本文將結(jié)合項目實踐,剖析在微服務(wù)的背景下,如何通過前后端分離的方式開發(fā)移動應(yīng)用。
對于微服務(wù)本身,我們可以參考 Martin Fowler 對 Microservice 的闡述。簡單說來,微服務(wù)是一種架構(gòu)風格。通過對特定業(yè)務(wù)領(lǐng)域的分析與建模,將復(fù)雜的應(yīng)用分解成小而專一、耦合度低并且高度自治的一組服務(wù)。微服務(wù)中的每個服務(wù)都是很小的應(yīng)用,這些應(yīng)用服務(wù)相互獨立并且可部署。微服務(wù)通過對復(fù)雜應(yīng)用的拆分,達到簡化應(yīng)用的目的,而這些耦合度較低的服務(wù)則通過 API 形式進行通信,所以服務(wù)之間對外暴露的都是 API,不管是對資源的獲取還是修改。
微服務(wù)架構(gòu)的這種理念,和前后端分離的理念不謀而合,前端應(yīng)用控制自己所有的 UI 層面的邏輯,而數(shù)據(jù)層面則通過對微服務(wù)系統(tǒng)的 API 調(diào)用完成。以 JSP (Java Server Pages) 為代表的前后端交互方式也逐漸退出歷史舞臺。前后端分離的迅速發(fā)展也得益于前端 Web 框架 (Angular, React 等) 的不斷涌現(xiàn),單頁面應(yīng)用(Single Page Application)迅速成為了一種前端開發(fā)標準范式。加之移動互聯(lián)網(wǎng)的發(fā)展,不管是 Mobile Native 開發(fā)方式,還是 React Native / PhoneGap 之流代表的 Hybrid 應(yīng)用開發(fā)方式,前后端分離讓 Web 和移動應(yīng)用成為了客戶端??蛻舳酥恍枰ㄟ^ API 進行資源的查詢以及修改即可。
一、BFF 概況及演進
Backend for Frontends(以下簡稱BFF) 顧名思義,是為前端而存在的后端(服務(wù))中間層。即傳統(tǒng)的前后端分離應(yīng)用中,前端應(yīng)用直接調(diào)用后端服務(wù),后端服務(wù)再根據(jù)相關(guān)的業(yè)務(wù)邏輯進行數(shù)據(jù)的增刪查改等。那么引用了 BFF 之后,前端應(yīng)用將直接和 BFF 通信,BFF 再和后端進行 API 通信,所以本質(zhì)上來說,BFF 更像是一種“中間層”服務(wù)。下圖看到?jīng)]有BFF以及加入BFF的前后端項目上的主要區(qū)別。
1. 沒有BFF 的前后端架構(gòu)
在傳統(tǒng)的前后端設(shè)計中,通常是 App 或者 Web 端直接訪問后端服務(wù),后臺微服務(wù)之間相互調(diào)用,然后返回最終的結(jié)果給前端消費。對于客戶端(特別是移動端)來說,過多的 HTTP 請求是很昂貴的,所以開發(fā)過程中,為了盡量減少請求的次數(shù),前端一般會傾向于把有關(guān)聯(lián)的數(shù)據(jù)通過一個 API 獲取。在微服務(wù)模式下,意味著有時為了迎合客戶端的需求,服務(wù)器常會做一些與UI有關(guān)的邏輯處理。
2. 加入了BFF 的前后端架構(gòu)
加入了BFF的前后端架構(gòu)中,最大的區(qū)別就是前端(Mobile, Web) 不再直接訪問后端微服務(wù),而是通過 BFF 層進行訪問。并且每種客戶端都會有一個BFF服務(wù)。從微服務(wù)的角度來看,有了 BFF 之后,微服務(wù)之間的相互調(diào)用更少了。這是因為一些UI的邏輯在 BFF 層進行了處理。
二、BFF 和 API Gateway
從上文對 BFF 的了解來看,BFF 既然是前后端訪問的中間層服務(wù),那么 BFF 和 API Gateway 有什么區(qū)別呢?我們首先來看下 API Gateway 常見的實現(xiàn)方式。(API Gateway 的設(shè)計方式可能很多,這里只列舉如下三種)
1. API Gateway 的第一種實現(xiàn):一個 API Gateway 對所有客戶端提供同一種 API
單個 API Gateway 實例,為多種客戶端提供同一種API服務(wù),這種情況下,API Gateway 不對客戶端類型做區(qū)分。即所有 /api/users的處理都是一致的,API Gateway 不做任何的區(qū)分。如下圖所示:
2. API Gateway 的第二種實現(xiàn):一個 API Gateway 對每種客戶端提供分別的 API
單個 API Gateway 實例,為多種客戶端提供各自不同的API。比如對于 users 列表資源的訪問,web 端和 App 端分別通過 /services/mobile/api/users, /services/web/api/users服務(wù)。API Gateway 根據(jù)不同的 API 判定來自于哪個客戶端,然后分別進行處理,返回不同客戶端所需的資源。
3. API Gateway 的第三種實現(xiàn):多個 API Gateway 分別對每種客戶端提供分別的 API
在這種實現(xiàn)下,針對每種類型的客戶端,都會有一個單獨的 API Gateway 響應(yīng)其 API 請求。所以說 BFF 其實是 API Gateway 的其中一種實現(xiàn)模式。
三、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 推出,主要是為了替代傳統(tǒng)的 REST 模式,那么對于 GraphQL 和 REST 究竟有哪些異同點呢?我們可以通過下面的例子進行理解。
按照 REST 的設(shè)計標準來看,所有的訪問都是基于對資源的訪問(增刪查改)。如果對系統(tǒng)中 users 資源的訪問,REST 可能通過下面的方式訪問:
Request:
- GET http://localhost/api/users
Response:
- [
- {
- "id": 1,
- "name": "abc",
- "avatar": "http://cdn.image.com/image_avatar1"
- },
- ...
- ]
對于同樣的請求如果用 GraphQL 來訪問,過程如下:
Request:
- POST http://localhost/graphql
Body:
- query {users { id, name, avatar } }
Response:
- {
- "data": {
- "users": [
- {
- "id": 1,
- "name": "abc",
- "avatar": "http://cdn.image.com/image_avatar1"
- },
- ...
- ]
- }
- }
關(guān)于 GraphQL 更詳細的用法,我們可以通過查看文檔以及其他文章更加詳細的去了解。相比于 REST 風格,GraphQL 具有如下特性:
1. 定義數(shù)據(jù)模型:按需獲取
GraphQL 在服務(wù)器實現(xiàn)端,需要定義不同的數(shù)據(jù)模型。前端的所有訪問,最終都是通過 GraphQL 后端定義的數(shù)據(jù)模型來進行映射和解析。并且這種基于模型的定義,能夠做到按需索取。比如對上文 /users 資源的獲取,如果客戶端只關(guān)心 user.id, user.name 信息。那么在客戶端調(diào)用的時候,query 中只需要傳入 users {id \n name}即可。后臺定義模型,客戶端只需要獲取自己關(guān)心的數(shù)據(jù)即可。
2. 數(shù)據(jù)分層
查詢一組users數(shù)據(jù),可能需要獲取 user.friends, user.friends.addr等信息,所以針對 users 的本次查詢,實際上分別涉及到對 user, frind, addr 三類數(shù)據(jù)。GraphQL 對分層數(shù)據(jù)的查詢,大大減少了客戶端請求次數(shù)。因為在 REST 模式下,可能意味著每次獲取 `user` 數(shù)據(jù)之后,需要再次發(fā)送 API 去請求 friends 接口。而 GraphQL 通過數(shù)據(jù)分層,能夠讓客戶端通過一個 API獲取所有需要的數(shù)據(jù)。這也就是 GraphQL(圖查詢語句 Graph Query Language)名稱的由來。
- {
- user(id:1001) { // 第一層
- name,
- friends { // 第二層
- name,
- addr { // 第三層
- country,
- city
- }
- }
- }
- }
3. 強類型
- const Meeting = new GraphQLObjectType({
- name: 'Meeting',
- fields: () => ({
- meetingId: {type: new GraphQLNonNull(GraphQLString)},
- meetingStatus: {type: new GraphQLNonNull(GraphQLString), defaultValue: ''}
- })
- })
GraphQL 的類型系統(tǒng)定義了包括 Int, Float, String, Boolean, ID, Object, List, Non-Null 等數(shù)據(jù)類型。所以在開發(fā)過程中,利用強大的強類型檢查,能夠大大節(jié)省開發(fā)的時間,同時也很方便前后端進行調(diào)試。
4. 協(xié)議而非存儲
GraphQL 本身并不直接提供后端存儲的能力,它不綁定任何的數(shù)據(jù)庫或者存儲引擎。它利用已有的代碼和技術(shù)進行數(shù)據(jù)源的管理。比如作為在 BFF 層使用 GraphQL, 這一層的 BFF 并不需要任何的數(shù)據(jù)庫或者存儲媒介。GraphQL 只是解析客戶端請求,知道客戶端的“意圖”之后,再通過對微服務(wù)API的訪問獲取到數(shù)據(jù),對數(shù)據(jù)進行一系列的組裝或者過濾。
5. 無須版本化
- const PhotoType = new GraphQLObjectType({
- name: 'Photo',
- fields: () => ({
- photoId: {type: new GraphQLNonNull(GraphQLID)},
- file: {
- type: new GraphQLNonNull(FileType),
- deprecationReason: 'FileModel should be removed after offline app code merged.',
- resolve: (parent) => {
- return parent.file
- }
- },
- fileId: {type: new GraphQLNonNull(GraphQLID)}
- })
- })
GraphQL 服務(wù)端能夠通過添加 deprecationReason,自動將某個字段標注為棄用狀態(tài)。并且基于 GraphQL 高度的可擴展性,如果不需要某個數(shù)據(jù),那么只需要使用新的字段或者結(jié)構(gòu)即可,老的棄用字段給老的客戶端提供服務(wù),所有新的客戶端使用新的字段獲取相關(guān)信息。并且考慮到所有的 graphql 請求,都是按照 POST /graphql 發(fā)送請求,所以在 GraphQL 中是無須進行版本化的。
四、GraphQL 與 REST
對于 GraphQL 和 REST 之間的對比,主要有如下不同:
- 數(shù)據(jù)獲?。篟EST 缺乏可擴展性, GraphQL 能夠按需獲取。GraphQL API 調(diào)用時,payload 是可以擴展的;
- API 調(diào)用:REST 針對每種資源的操作都是一個 endpoint, GraphQL 只需要一個 endpoint( /graphql), 只是 post body 不一樣;
- 復(fù)雜數(shù)據(jù)請求:REST 對于嵌套的復(fù)雜數(shù)據(jù)需要多次調(diào)用,GraphQL 一次調(diào)用, 減少網(wǎng)絡(luò)開銷;
- 錯誤碼處理:REST 能夠精確返回HTTP錯誤碼,GraphQL 統(tǒng)一返回200,對錯誤信息進行包裝;
- 版本號:REST通過 v1/v2 實現(xiàn),GraphQL 通過 Schema 擴展實現(xiàn);
五、微服務(wù) + GraphQL + BFF 實踐
在微服務(wù)下基于 GraphQL 構(gòu)建 BFF,我們在項目中已經(jīng)開始了相關(guān)的實踐。在我們項目對應(yīng)的業(yè)務(wù)場景下,微服務(wù)后臺有近 10 個微服務(wù),客戶端包括針對不同角色的4個 App 以及一個 Web 端。對于每種類型的 App,都有一個 BFF 與之對應(yīng)。每種 BFF 只服務(wù)于這個 App。BFF 解析到客戶端請求之后,會通過 BFF 端的服務(wù)發(fā)現(xiàn),去對應(yīng)的微服務(wù)后臺通過 CQRS 的方式進行數(shù)據(jù)查詢或修改。
1. BFF 端技術(shù)棧
我們使用 GraphQL-express 框架構(gòu)建項目的 BFF 端,然后通過 Docker 進行部署。BFF 和微服務(wù)后臺之間,還是通過 registrator 和 Consul 進行服務(wù)注冊和發(fā)現(xiàn)。
- addRoutes () {
- this.express.use('/graphql', this.resolveFromRequestScopeAndHandle('GraphqlHandler'))
- this.serviceNames.forEach(serviceName => {
- this.express.use(`/api/${serviceName}`, this.routers.apiProxy.createRouter(serviceName))
- })
- }
在 BFF 的路由設(shè)置中,對于客戶端的處理,主要有 /graphql 和 /api/${serviceName}兩部分。/graphql 處理的是所有 GraphQL 查詢請求,同時我們在 BFF 端增加了 /api/${serviceName} 進行 API 透傳,對于一些沒有必要進行 GraphQL 封裝的請求,可以直接通過透傳訪問到相關(guān)的微服務(wù)中。
2. 整體技術(shù)架構(gòu)
整體來看,我們的前后端架構(gòu)圖如下,三個 App 客戶端分別使用 GraphQL 的形式請求對應(yīng)的 BFF。BFF 層再通過 Consul 服務(wù)發(fā)現(xiàn)和后端通信。
關(guān)于系統(tǒng)中的鑒權(quán)問題:
用戶登錄后,App 直接訪問 KeyCloak 服務(wù)獲取到 id_token,然后通過 id_token 透傳訪問 auth-api 服務(wù)獲取到 access_token, access_token 以 JWT (Json Web Token) 的形式放置到后續(xù) http 請求的頭信息中。
在我們這個系統(tǒng)中 BFF 層并不做鑒權(quán)服務(wù),所有的鑒權(quán)過程全部由各自的微服務(wù)模塊負責。BFF 只提供中轉(zhuǎn)的功能。BFF 是否需要集成鑒權(quán)認證,主要看各系統(tǒng)自己的設(shè)計,并不是一個標準的實踐。
3. GraphQL + BFF 實踐
通過如下幾個方面,可以思考基于 GraphQL 的 BFF 的一些更好的特質(zhì):
(1) GraphQL 和 BFF 對業(yè)務(wù)點的關(guān)注
從業(yè)務(wù)上來看,PM App(使用者:物業(yè)經(jīng)理)關(guān)注的是property,物業(yè)經(jīng)理管理著一批房屋,所以需要知道所有房屋概況,對于每個房屋需要知道有沒有對應(yīng)的維修申請。所以 PM App BFF 在定義數(shù)據(jù)結(jié)構(gòu)是,maintemamceRequests 是 property 的子屬性。
同樣類似的數(shù)據(jù),Supplier App(使用者:房屋維修供應(yīng)商)關(guān)注的是 maintenanceRequest(維修工單),所以在 Supplier App 獲取的數(shù)據(jù)里,我們的主體是maintenanceRequest。維修供應(yīng)商關(guān)注的是 workOrder.maintenanceRequest。
所以不同的客戶端,因為存在著不同的使用場景,所以對于同樣的數(shù)據(jù)卻有著不同的關(guān)注點。BFF is pary of Application。從這個角度來看,BFF 中定義的數(shù)據(jù)結(jié)構(gòu),就是客戶端所真正關(guān)心的。BFF 就是為客戶端而生,是客戶端的一部分。需要說明的是,對于“業(yè)務(wù)的關(guān)注”并不是說,BFF會處理所有的業(yè)務(wù)邏輯,業(yè)務(wù)邏輯還是應(yīng)該由微服務(wù)關(guān)心,BFF 關(guān)注的是客戶端需要什么。
(2) GraphQL 對版本化的支持
假設(shè) BFF 端已經(jīng)發(fā)布到生產(chǎn)環(huán)境,提供了 inspection 相關(guān)的 tenants 和 landlords 的查詢?,F(xiàn)在需要將圖一的結(jié)構(gòu)變更為圖二的結(jié)構(gòu),但是為了不影響老用戶的 API 訪問,這時候我們的 BFF API 必須進行兼容。如果在 REST 中,可能會增加 api/v2/inspections進行 API 升級。但是在 BFF 中,為了向前兼容,我們可以使用圖三的結(jié)構(gòu)。這時候老的 APP 使用黃色區(qū)域的數(shù)據(jù)結(jié)構(gòu),而新的 APP 則使用藍色區(qū)域定義的結(jié)構(gòu)。
(3) GraphQL Mutation 與 CQRS
- mutation {
- area {
- create (input: {
- areaId:"111",
- name:"test",
- })
- }
- }
如果你詳細閱讀了 GraphQL 的文檔,可以發(fā)現(xiàn) GraphQL 對 query 和 mutation 進行了分離。所有的查詢應(yīng)該使用 query { ...},相應(yīng)的 mutaition 需要使用 mutation { ... }。雖然看起來像是一個convention,但是 GraphQL 的這種設(shè)計和后端 API 的 讀寫職責分離(Command Query Responsibility Segregation)不謀而合。而實際上我們使用的時候也遵從這個規(guī)范。所以的 mutation 都會調(diào)用后臺的 API,而后端的 API 對于資源的修改也是通過 SpringBoot EventListener 實現(xiàn)的 CQRS 模式。
六、如何做好測試
在引入了 BFF 的項目,我們的測試仍然使用金字塔原理,只是在客戶端和后臺之間,需要添加對 BFF 的測試。
- Client 的 integration-test 關(guān)心的是 App 訪問 BFF 的連通性,App 中所有訪問 BFF 的請求都需要進行測試;
- BFF 的 integration-test 測試的是 BFF 到微服務(wù) API 的連通性,BFF 中依賴的所有 API 都應(yīng)該有集成測試的保障;
- API 的 integration-test 關(guān)注的是這個服務(wù)對外暴露的所有 API,通常測試所有的 Controller 中的 API。
七、結(jié)語
微服務(wù)下基于 GraphQL 構(gòu)建 BFF 并不是銀彈,也并不一定適合所有的項目,比如當你使用 GraphQL 之后,你可能得面臨多次查詢性能問題等,但這不妨礙它成為一個不錯的嘗試。你也的確看到 Facebook 早已經(jīng)使用 GraphQL,而且 Github 也開放了 GraphQL 的API。而 BFF, 其實很多團隊也都已經(jīng)在實踐了,在微服務(wù)下等特殊場景下,GraphQL + BFF 也許可以給你的項目帶來驚喜。
【本文是51CTO專欄作者“ThoughtWorks”的原創(chuàng)稿件,微信公眾號:思特沃克,轉(zhuǎn)載請聯(lián)系原作者】