B 站微服務 API 管理

引言

API 管理是應用開發中不可或缺的一部分。在早期服務數量不多的情況下,團隊可以自行負責 API 管理。但隨着公司規模逐漸擴張,業務接口數量爆炸式增長,此時 API 管理的任務應由統一的接口管理平臺來承擔,結束各自爲政的局面。統一管理能夠最大程度地發揮 API 的價值,減少跨部門溝通與協作的成本。本期文章將帶領大家一窺 B 站在 API 管理方面所作的設計與思考,重點介紹我們是如何收集 API 元信息並對其進行井井有條的管理,又是如何配置這些龐大 API 資源來減輕業務管理負擔,粘合跨部門間合作。

管理現狀

目前,我們的元信息統一管理平臺線上接口數達到 12w+,應用(含測試應用)總數近 2w。如此龐大規模的接口由平臺統一收集並管理,可節約大量人力維護與溝通成本,契合當前 “降本增效” 的主基調。

01 服務上線流程

一個服務的上線過程通常分爲這樣幾個階段:

  1. 需求評審與分析

PM 輸出需求,PMO 組織需求評審會,業務線開發評估需求的開發方案與所需工時,從而確定迭代週期。

  1. 撰寫與維護 API 文檔

在項目開發中,Web 項目的前後端分離開發,需要由前後端開發共同定義接口,編寫接口文檔,之後大家都根據這個接口文檔進行開發,一直維護到項目結束。API 文檔在後端技術方案確定後即可編寫。儘可能早地提供給對接方,有助於對接方提前思考實現方式和規避隱患。

  1. 前後端聯調與測試

前端根據 API 文檔初步實現功能,後端在開發完成後發佈至測試環境提供給前端聯調。

  1. 發佈上線

當一切就緒後,服務被髮布至線上,API 開始對外提供服務,需求上線。

上述整個過程中都離不開對接口文檔的管理,一個優秀的接口文檔能夠讓前端與後端開發人員更好地配合,提高工作效率,方便新加入的成員查看和維護接口、測試人員進行接口測試。

02 API 管理

2.1 爲什麼需要對 API 進行統一管理

在過去沒有統一管理的模式下,雖說每個團隊每個項目都有編寫 API 文檔的意識,但免不了出現各種管理模式的差異,例如 A 團隊習慣將文檔編寫在知識庫中,B 團隊習慣將文檔用 swagger 生成並託管至版本管理系統等等。這種管理模式上的差異會直接導致對接溝通上的低效,無法及時得發現 API 的異常,難以管理接口的版本迭代。因此我們始終推薦對 API 進行統一管理,降低對接時的溝通成本,並在接口出現變更時及時同步調用方,減少信息 gap,通過標準化的中心收集模式敏銳地捕捉到每一次接口調整。

2.2 API 元信息的收集與更新

在整個 API 管理過程中,首先需要保證接口元信息完備性和準確性,管理平臺需要充分收集接口信息。B 站的接口元信息的收集之路:手動維護 -> 自動生成。

2.2.1 手動維護

過去,我們在內網私有化部署過一套 YAPI,通過部門、業務域、應用的三級劃分的粒度管理着各個服務的接口。研發通過在 YAPI 的可視化界面上手動錄入應用的詳細接口信息。接口發佈後,前後端的研發根據文檔着手進行編碼,測試同學則根據文檔上的接口逐個進行測試,負責人對該文檔進行審批。總之,項目干係方始終都會圍繞着這份文檔來推進項目。

手動維護的缺點:

市面上有很多的五花八門的 API 信息管理平臺,如知名的 Eolink、YAPI、Apifox、Postman,但無論部署哪個平臺都無法解決一個非常核心的問題:數據的來源始終是 “人”,需要人工去操作與更新。 尤其在項目的開發階段,接口文檔的改動頻次實際上是很高的,需要開發同學多次到平臺上調整接口文檔,保證接口數據始終正確。

手工模式下,開發同學每次完成接口的增改都需要及時到平臺上同步最新的改動並通知具體的訂閱方。若某次變更未被及時同步,造成調用方與被調方之間信息不對齊,很容易造成故障。

2.2.2 自動生成

接口管理平臺的作用是自動採集應用 API 並生成一份詳細且準確的接口文檔,使開發將精力全部集中在 API 本身的設計上,無需額外關注接口文檔的撰寫與維護,從而解放研發同學的雙手,提高開發效率。

爲此,我們設計瞭如下架構,研發同學遵循統一的 API 標準定義接口,走完正常應用打包上線流程,接口採集自動完成。

代碼中定義接口

對於習慣使用 Golang 進行開發的同學:

// Demo service responds to incoming requests.
service DemoService {
  option (google.api.default_host) = "api.example.com";
   // DemoBody method receives a simple message and returns it.
  rpc DemoBody(SimpleMessage) returns (SimpleMessage) {
    option (google.api.http) = {
      post: "/poc/probe/demo_body"
      body: "*"
    };
  }
}
// 請求、回覆消息
message SimpleMessage {
  int32 id = 1 [(google.api.field_behavior) = REQUIRED];
  Embedded embedded = 2;
}
message Embedded {
  int64 int64_val = 1 [(gogoproto.moretags) = 'default:"1"'];
  string string_val = 2;
  // 一個字符串列表
  repeated string repeated_string_val = 3;
  // 一個字符串Map
  map<string, string> map_string_val = 4;
}

上述 Proto 片段中定義了一個名稱爲 DemoService 的 RPC 服務,該服務包含一個簡單的 RPC 方法 DemoBody,並且引入 Google 官方提供的 annotations.proto 對該 gRPC API 增加 HTTP Post 方法的拓展定義。這種使用 Protobuf IDL 定義對應的 REST API 和 gRPC API 的方式是 Google API 指南中所推薦的最佳實踐,也是 B 站在 Kratos V2 框架中定義 API 的方式。對於框架是如何註冊 HTTP 與 gRPC 服務感興趣的同學歡迎體驗 Kratos 框架,這裏先不詳細展開。DemoService 中除了對 HTTP 方法的定義,還包括服務概要,默認域名等標記。在 Message 中除了字段類型的定義,某些字段還帶有屬性行爲的標記。我們支持用戶使用 gogoproto 以及 google.api.field_behavior 中定義的消息對字段進行一些特殊標記,如定義字段默認值,是否必填,示例用法等參數相關的屬性。

info:
    title: DemoService API
    description: Demo service responds to incoming requests.
paths:
    /bilibili.api.probe.v1.DemoService/DemoBody:
    ...(同/poc/probe/demo_body)
    /poc/probe/demo_body:
        post:
            tags:
                - DemoService
            description: DemoBody method receives a simple message and returns it.
            operationId: DemoService_DemoBody
            requestBody:
                content:
                    application/json:
                        schema:
                            $ref: '#/components/schemas/bilibili.api.probe.v1.SimpleMessage'
                required: true
            responses:
                "200":
                    description: OK
                    content:
                        application/json:
                            schema:
                                $ref: '#/components/schemas/bilibili.api.probe.v1.SimpleMessage'
components:
    schemas:
        bilibili.api.probe.v1.Embedded:
            type: object
            properties:
                stringVal:
                    type: string
                    default: hello
                repeatedStringVal:
                    type: array
                    items:
                        type: string
                    description: 一個字符串列表
                mapStringVal:
                    type: object
                    additionalProperties:
                        type: string
                    description: 一個字符串Map
        bilibili.api.probe.v1.SimpleMessage:
            required:
                - id
            type: object
            properties:
                id:
                    type: integer
                    format: int32
                embedded:
                    $ref: '#/components/schemas/bilibili.api.probe.v1.Embedded'
            description: 請求、回覆消息
tags:
    - name: DemoService

最終通過工具生成上述對應 OpenAPI 文檔,爲 Proto 中對 HTTP 方法的定義提供標準 OpenAPI 格式的接口信息,將 gRPC Method 視爲 POST 方法,生成一條類似的接口信息。

對於習慣使用 JAVA 進行開發的同學而言,同樣地:

@GetMapping("/demo")
@Operation(summary = "用戶接口 - debug",description = "示例")
@Parameter(name = "count", required = false)
public String debug(@RequestParam(defaultValue = "128", required = false) int count) {
    String randomString = RandomStringUtils.randomAlphabetic(count);
    LOGGER.debug(randomString);
    return randomString;
}

上述代碼引入 io.swagger.v3 包,定義了一個 path 爲 '/demo' 的接口,使用 swagger 註解對 controller 類中的 map 方法進行修飾。暴露生成接口文檔方式十分簡單,在 B 站自研的 JAVA web 框架 Pleidaes/ Kraten 下開發,應用啓動後直接調用 /api-docs 的接口就可以輕易拿到。這種利用 swagger 註解提供的來聲明和操作輸出,爲 JAVA 應用實時生成接口文檔是 JAVA 同學熟知的一種方式,對平臺來說,要做的只是調用接口拿到 JAVA 應用的文檔即可。

文檔收集

我們的終極目標是做到全公司研發同學使用統一的框架,使用統一的 API 的定義和生成方式,管理平臺就可以採集的標準且權威的 API 元信息,即實現公司 API 標準化。但這個目標不是一蹴而就的,在研發開發流程尚不 "標準" 時,我們通過多種渠道盡可能得獲取到應用的接口信息,保證平臺接口數據的完備性。

a. CI 時生成(最佳實踐)

對於採用 gRPC 協議的接口來說,B 站採用的是單倉庫管理模型管理協議文件:將協議原始文件 Proto 集中放到一個倉庫中,根據內部服務治理的標準將文件劃分至獨立的命名空間進行細粒度管理,對外提供根據 Proto 生成的目標語言倉庫,例如 go 語言 proto-gen-go 倉庫, JAVA 語言 proto-gen-java 倉庫,依賴某服務時直接 import 該倉庫即可。

接口管理平臺作爲 Proto 以及 stub 的管理員,肩負着管理內部統一 Proto 倉庫的責任。但不論是協議文件還是存根代碼,只是接口的定義,包括版本、命令定義、資源定義和錯誤碼定義等等,不適宜直接作爲接口文檔展示給調用方。我們在推動內部 API 標準化的過程中,對定義 Proto 文件中接口的參數、默認行爲、屬性、必要註釋等行爲給出統一的規範,在通過在 CI Pipeline 中安裝 protoc-gen-bilibili-openapi 插件,該插件負責解析代碼中的 Proto 文件,並對應生成一份標準 OpenAPI 文檔。當用戶請求合併代碼到主分支時,自動觸發流水線中接口平臺的埋入的任務:掃描代碼中的 Proto,提取出接口信息,生成對應文檔後導入接口管理平臺,完成對該應用接口文檔的自動刷新。用戶只是完成了一次基本的 Proto 的書寫,就不再需要考慮後續其他的協作方面的事宜,接口文檔,測試,樁代碼,一切交給管理平臺進行打理。

b. 在線服務採集

對於 Java 語言應用而言,應用部署後通過註冊中心暴露服務地址,接口管理平臺到指定環境中調用該地址下 /api-docs 接口獲取到應用的接口文檔,並與歷史接口版本進行比對與更新,實現對 Java 應用接口文檔的自動更新,整個過程對於開發者來說沒有額外維護文檔的負擔,也不再需要關心自己的接口數據如何去暴露和分享給調用方。

在 API 標準化尚未推廣之前,公司內的 go 服務可能使用的是 Kratos 早期定義接口的方式,這部分應用通過 /metadata 接口對外暴露 path 信息。平臺通過該接口採集到所有的接口路徑後,由用戶對接口的文檔進行手動補全,等應用實現 API 標準化後再逐步由半自動進入全自動收集的模式。

03 版本管理

API 是用戶與應用之間的約定,包括 URI 模式,有效負載結構,字段和參數名稱,預期行爲以及其他內容。在應用迭代的過程中,不可避免需要添加新的資源、修改資源或調整接口參數,隨之會帶來的接口變更管理的問題。例如,某個應用新加的 feature 改動了接口,但此時改動沒經過測試,相當於只是草稿版本。開發希望能將草稿版本的接口分享給聯調的人員,又不想影響正式版本,這在過去其實是一件比較棘手的事情。接口管理平臺要做的就是通過版本管理區分好接口不同狀態、不同來源、不同時期的信息。

3.1 接口版本控制

接口參數的每一點變更一定都源於代碼的變動,代碼的提交纔可能會導致接口版本的升級。服務本身沒有變更,開發者代碼沒有產生過提交,是不會導致接口憑空變化的。基於此點共識,我們將應用**代碼的提交 (Commit ID) 與管理平臺上的接口版本 (API Version) **關聯。

接口管理平臺對接口的正式版本及測試版本進行區分。測試版本的來源是測試環境中的應用,與 dev 代碼分支的某次 commit 記錄相關聯;正式版本的來源是生產環境中的應用,與主分支代碼的某個 tag 相關聯。

研發每次提交代碼,不管是用於測試發佈還是正式發佈,接口管理平臺都會爲接口生成相應新版本,對比新版本與歷史版本的差異,這在多人協作開發的項目中非常受用。研發同學將某一次實驗性版本的應用部署到測試環境後,就可以在接口平臺上直接對剛剛提交的接口進行初步驗證,或者由自動化測試又或是 QA 進行系統的測試;而正式版本經過完整的流水線測試、全鏈路灰度驗證、部署在生產環境後,可直接被分享給調用方。

3.2 應用版本管理

對於接口的調用方來說,大多隻會關心接口功能及使用參數。比如說調用方需要接入某個應用時,他想知道應用當前 V2 版本使用哪些接口可以滿足他的需求,而不會關心這些接口有哪些版本。或者說,調用方接入的是歷史版本 V1,暫時還不想升級到 V2,他想知道 V1 版本使用的接口是哪些參數。這種場景下,直接將 V1 版本的接口文檔發給調用方即可。

應用是接口的集合,應用版本是接口版本的集合。有了接口的版本控制之後,再進行應用的版本管理就變得很容易了。接口管理平臺可以爲一組接口版本創建一個快照,這個快照就是應用版本。當應用每次正式上線後,我們可以通爲應用創建一個該版本的接口快照,通過這樣的管理方式,我們可以觀察每個接口在各個應用版本下的變更情況,並追蹤接口在應用中的生命週期變化。

04 接口協作、分享、調試

API 管理平臺不僅是對接口元信息進行管理,打通數據、提升研發效率以及發掘元數據本身的價值同樣是 API 管理平臺的使命。

聯動接口周邊服務

例如我們將 API 管理平臺與 API Gateway 打通,對於需要集成服務網關功能的接口,只需要在 API 管理平臺就上可以方便得跳轉到對應地方進行配置,其他與接口有關的配置同樣如此, 用戶可以將接口管理平臺作爲入口,跳轉至其他基礎平臺,提高用戶效率的同時更好得與其他平臺進行配合。

文檔導出、分享

我們對這些接口信息以不同的格式進行展示,支持導出標準的 OpenAPI 格式的 json 文件。對於那些習慣使用第三方工具查看接口數據的研發同學來說,可通過工具導入 OpenAPI 文件或直接訂閱平臺,在本地客戶端實時查看自己關注的接口。

接口調試、運行

接口調試可以簡單分爲兩種:

API 管理平臺對這種兩種接口的請求方式的進行了統一,用戶不需要關心自己的接口是哪種協議,就可以直接點擊調試。平臺管理本身管理着 Proto 倉庫,擁有全部內部協作的 Proto 元信息,即使需要客戶端 Token 的 RPC 也可輕鬆發起,幫助用戶進行初步的接口調試。並且在平臺調試時也無需像普通調試工具一樣指定域名、IP 後纔可調試。平臺側打通註冊中心,獲取服務的信息,自動爲用戶鍵入目標地址。用戶對於調試這件事僅僅需關注兩點:接口及返回結果, 其他均由接口管理平臺包辦。

05 接口 Mock

5.1 爲什麼需要對測試對象的依賴進行

Mock?

Mock 的本質是在調試期間構造出一些虛擬的返回對象。一個常見的場景:前後端分離,前端開發某個頁面,需要後端先完成 API 的開發工作,兩者進度不一致,出現前端等待後端的情況。如果使用 Mock 就可以減小這種影響,通過 Mock API 事先編寫好 API 的數據生成規則,請求接口平臺動態生成 API 的返回數據。前端開發可以通過訪問 Mock API 來獲得頁面所需要的數據,繼續開展工作。

Mock 的好處:

通過 Mock 構造各種正常和異常的返回結果,更充分地測試目標對象

依賴的真實行爲可能延時高,資源消耗大,而模擬是一種非常快的行爲,能  加快整個測試流程。

5.2 服務級 Mock 架構設計

Mock 粒度由細到粗分爲方法級、類級別、接口級、服務級。大多 API-test 工具由於無法覆蓋全部接口,只做到接口級別 Mock,但對於擁有全部接口元信息的接口管理平臺來說,做到服務級別的 Mock 是順水推舟的事情。

狹隘的理解服務端 Mock 是將服務的所有的接口無差別地全部 Mock,相當於是接口級別的極端做法。例如,某次在測試環境中進行服務聯調時,對於某些尚未開發完成的接口或者不能在測試環境中被調用的接口,可採用 Mock 進行過渡;但對於已經上線的接口來說,流量應直接透傳至真實服務,待拿到真實的響應數據後再返回給上游。這樣不管是對減輕測試用例管理的負擔還是提高測試的準確性都有很大的增益。

基於上述思想,我們設計瞭如下圖所示的架構:

對於需要進行被 Mock 的服務,接口管理平臺會在註冊中心爲該服務的註冊出一個染色實例,並與註冊中心維持心跳,在測試環境中部署的真實服務則作爲兜底用的基準版本。收到流量時,匹配指定染色成功的請求會被註冊中心轉發至 Mock 實例,該實例實際是接口管理平臺在提供服務。Mock 實例判斷流量是否命中事先配置好的規則,若命中成功,則直接返回 規則中的響應; 若未能命中規則或未配置規則,則將流量原封不動地轉發給基準版本的實例,由基準返回。我們的做法實際上是通過修改註冊中心上服務與服務地址的映射關係,將依賴服務地址改成 Mock 地址實現 Mock 注入。

06 微服務標準化

我們一直致力於實現公司內部的微服務的標準化,包括接口規範化、接口標準化、數據格式統一化,這些標準是明確、可行且統一的,以保證各個微服務之間的可互操作性。B 站內部使用 Go 語言開發同學多數是在 Kratos 框架下進行開發的,使用 JAVA 語言開發的同學使用自研的 Pleiades/ Kraten 框架,這兩種框架都爲平臺能順利採集到接口信息提供了極大的便利。我們藉助框架的力量,將 OpenAPI 格式 API 的標準集成在框架中,編譯時期或 CI 階段產生接口文檔,開發各種配套的 Swagger、OpenAPI 的工具用來充分提取文檔的接口信息。

API 元信息的管理是 API 標準化中的一環,我們希望利用標準的力量來做更多的事情,如監控、自動生成代碼... 探索更多的玩法,成爲強有力的生產力工具。

參考文獻:

[1]API 定義 | Kratos:https://go-kratos.dev/docs/component/api/

[2] 自定義方法 - API Design Guide:https://google-cloud.gitbook.io/api-design-guide/custom_methods

[3] 對 API 進行版本控制的重要性和實現方式 - EOLINEKR BLOG:http://blog.eolinker.com/?p=2644

[4] 乾貨!用大白話告訴你什麼是 Mock 測試 - 51CTO.COM:https://www.51cto.com/article/647732.html

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/mn6V73d_87eicd5b_gjG_w