1. 程式人生 > >微服務下使用GraphQL構建BFF

微服務下使用GraphQL構建BFF

微服務架構,這個在幾年前還算比較前衛的技術在如今遍地開花。得益於開源社群的支援,我們可以輕鬆地利用 Spring Cloud 以及 Docker 容器化快速搭建一個微服務架構的原型。不管是成熟的網際網路公司、創業公司還是個人開發者,對於微服務架構的接納程度都相當高,微服務架構的廣泛應用也自然促進了技術本身更好的發展以及更多的實踐。本文將結合專案實踐,剖析在微服務的背景下,如何通過前後端分離的方式開發移動應用。

對於微服務本身,我們可以參考 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 進行資源的查詢以及修改即可。

BFF 概況及演進

Backend for Frontends(以下簡稱BFF) 顧名思義,是為前端而存在的後端(服務)中間層。即傳統的前後端分離應用中,前端應用直接呼叫後端服務,後端服務再根據相關的業務邏輯進行資料的增刪查改等。那麼引用了 BFF 之後,前端應用將直接和 BFF 通訊,BFF 再和後端進行 API 通訊,所以本質上來說,BFF 更像是一種“中間層”服務。下圖看到沒有BFF以及加入BFF的前後端專案上的主要區別。

1. 沒有BFF 的前後端架構


在傳統的前後端設計中,通常是 App 或者 Web 端直接訪問後端服務,後臺微服務之間相互呼叫,然後返回最終的結果給前端消費。對於客戶端(特別是移動端)來說,過多的 HTTP 請求是很昂貴的,所以開發過程中,為了儘量減少請求的次數,前端一般會傾向於把有關聯的資料通過一個 API 獲取。在微服務模式下,意味著有時為了迎合客戶端的需求,伺服器常會做一些與UI有關的邏輯處理

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:

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"
      },
      ...
    ]
  }
}

關於 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)名稱的由來。

{
  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 的型別系統定義了包括 Int, Float, String, Boolean, ID, Object, List, Non-Null 等資料型別。所以在開發過程中,利用強大的強型別檢查,能夠大大節省開發的時間,同時也很方便前後端進行除錯。

4. 協議而非儲存

GraphQL 本身並不直接提供後端儲存的能力,它不繫結任何的資料庫或者儲存引擎。它利用已有的程式碼和技術進行資料來源的管理。比如作為在 BFF 層使用 GraphQL, 這一層的 BFF 並不需要任何的資料庫或者儲存媒介。GraphQL 只是解析客戶端請求,知道客戶端的“意圖”之後,再通過對微服務API的訪問獲取到資料,對資料進行一系列的組裝或者過濾。

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 服務端能夠通過新增 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 進行服務註冊和發現。

addRoutes () {
    this.express.use('/graphql', this.resolveFromRequestScopeAndHandle('GraphqlHandler'))
    this.serviceNames.forEach(serviceName => {
      this.express.use(`/api/${serviceName}`, this.routers.apiProxy.createRouter(serviceName))
    })
  }

在 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 在定義資料結構是,maintemamceRequestsproperty 的子屬性。

同樣類似的資料,Supplier App(使用者:房屋維修供應商)關注的是 maintenanceRequest(維修工單),所以在 Supplier App 獲取的資料裡,我們的主體是maintenanceRequest。維修供應商關注的是workOrder.maintenanceRequest

所以不同的客戶端,因為存在著不同的使用場景,所以對於同樣的資料卻有著不同的關注點。BFF is pary of Application。從這個角度來看,BFF 中定義的資料結構,就是客戶端所真正關心的。BFF 就是為客戶端而生,是客戶端的一部分。需要說明的是,對於“業務的關注”並不是說,BFF會處理所有的業務邏輯,業務邏輯還是應該由微服務關心,BFF 關注的是客戶端需要什麼。

GraphQL 對版本化的支援


假設 BFF 端已經發布到生產環境,提供了 inspection 相關的 tenantslandlords 的查詢。現在需要將圖一的結構變更為圖二的結構,但是為了不影響老使用者的 API 訪問,這時候我們的 BFF API 必須進行相容。如果在 REST 中,可能會增加api/v2/inspections進行 API 升級。但是在 BFF 中,為了向前相容,我們可以使用圖三的結構。這時候老的 APP 使用黃色區域的資料結構,而新的 APP 則使用藍色區域定義的結構。

GraphQL Mutation 與 CQRS

mutation {
  area {
    create (input: {
      areaId:"111", 
      name:"test", 
    })
  }
}

如果你詳細閱讀了 GraphQL 的文件,可以發現 GraphQL 對 querymutation 進行了分離。所有的查詢應該使用 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 也許可以給你的專案帶來驚喜。

參考資料

【注】部分圖片來自網路

文/ThoughtWorks 龔銘