Go 工程化 -四- API 設計上: 項目結構 - 設計

API 項目結構與管理

API 定義方式

b 站內部主要使用 grpc 作爲內部通信的方式,因爲他使用 protobuf 文件定義可以支持對語言代碼生成,同時還避免了手寫文檔導致的文檔錯誤過時等情況,具體的原因其實在第一課的筆記當中就有提到,如果感興趣可以查看 微服務 (二) 服務發現 & 多租戶 #gRPC

我們目前使用類似 http restful 的方式進行對外對內提供服務,但是我們之前的 API 管理其實是比較混亂的,分爲以下幾種情況:

  1. 暴露給 web 的 api:有使用 swagger 的,有在文檔平臺上寫文檔的,還有沒有寫文檔的

  2. 暴露給其他服務調用的 api: 有註冊到內部的接口網關的,但是內部的接口網關上有的有參數,有的沒有,沒有返回值定義

所以就存在很多問題:

  1. 想要接口不知道從哪兒找,只能到處問人

  2. 有時候從內部網關平臺上找到接口但是不知道怎麼調用,沒有寫任何參數,有的寫了還有可能是錯的

  3. 有的壓根沒有接口文檔,對接的同學也沒有時間寫,然後讓你直接看代碼

  4. 有的對接同學扔給你一個接口文檔,然後試了半天發現,有問題,溝通排查之後發現文檔很久沒有更新了 o(╥﹏╥)o

所以課程上毛老師提到的利用 protobuf 來定義接口的方式非常令人心動,因爲 protobuf 當中包含了接口的函數簽名,入參和返回值同時還支持註釋,就是一份天然的文檔,同時也不用擔心出現代碼更新了但是文檔沒有更新的情況,因爲它既是文檔也是代碼,服務端也需要使用,所以代碼更新之後文檔也一定會更新。自然而然的就少了很多溝通的成本。如上圖所示於此同時我們還可以利用 protobuf 文件生成對應語言的客戶端代碼,就不用每個項目都去維護一套 sdk 了,同時我們使用接口生成代碼,在 go 當中可以使用 gomock 非常方便的對代碼進行 mock。

API Project

使用 protobuf 定義接口可以解決我們找到 api 文檔之後,文檔不準確,缺失的問題,但是我們應該如何找到我們的 api 呢?我們生成出的 api 文件調用方應該如何引用呢?難道我們給每個調用方都去開一個項目的權限麼?那明顯是不太行的,接下來我們就看看我們 api 該如何管理和組織。

毛老師他們仿照 googleapis/googleapis,istio/api 等知名項目在 b 站內部搞了一個 bapis 的倉庫用於同一存放 api 定義文檔,然後通過 ci/cd 生成對應的客戶端代碼放到各個語言的子倉庫當中工作流程如上圖所示

API Project Layout

我們的 api 項目是如何定義的呢?看下圖

API 設計

API 兼容性設計

隨着應用的不斷開發,業務的不斷髮展我們的 api 肯定會不斷的進行修改,在修改 api 的時候考慮 api 的兼容性就會很重要了,如果我們做了一些破壞性的變更就有可能會導致依賴我們的服務或者是客戶端報錯,這樣就會帶來事故。

向下兼容的變更

一般而言新增都是相對安全的,但是我們要注意的是新增字段不能改變我們原本的邏輯,如果改變了 api 的邏輯,那就不一定安全了

向下不兼容的變更(破壞性變更)

API 命名規範

包名

| 產品名 | product | | --- | --- | | 應用名 | app | | 版本號 | v1 | | 包名 | product.app.v1 | | 目錄結構 | api/product/app/v1/xx.proto |

API 定義

| 「標準方法」 | 「HTTP 映射」 | | --- | --- | | List | GET | | Get | GET | | Update | PUT 或者 PATCH | | Create | POST | | Delete | DELETE |

除了標準的也有一些非標準的,例如同步數據可能會用 Sync  等,不過大部分的 api 應該都是標準的

示例

// api/product/app/v1/blog.proto

syntax = "proto3";

package product.app.v1;

import "google/api/annotations.proto";

// blog service is a blog demo
service BlogService {

	rpc GetArticles(GetArticlesReq) returns (GetArticlesResp) {
		option (google.api.http) = {
			get: "/v1/articles"
			additional_bindings {
				get: "/v1/author/{author_id}/articles"
			}
		};
	}
}

注意,一般而言我們應該爲每個接口都創建一個自定義的 message,爲了後面擴展,如果我們用 Empty 的話後續就沒有辦法新增字段了

API Error

錯誤定義

先說我們當前的問題,我們一直用的 http 然後我們返回是使用的下面這種格式,然後 http code 統一返回 200

{
  "code": 1,
  "msg""xxx",
  "data"{}
}

這種做法就存在一個比較大的問題,做監控的時候不太好做,很多現成的東西沒有辦法直接使用,因爲我們都返回的成功。參照 google 的錯誤定義,將 http code 和 grpc 錯誤碼進行映射,返回對應的錯誤信息但是這樣還是不行,因爲這樣很多業務錯誤信息無法區分,毛老師他們的 kratos v2 的做法是做了兩層,使用下面的方式進行定義

message Status {
  // 錯誤碼,跟 grpc-status 一致,並且在HTTP中可映射成 http-status
  int32 code = 1;
  // 錯誤原因,定義爲業務判定錯誤碼
  string reason = 2;
  // 錯誤信息,爲用戶可讀的信息,可作爲用戶提示內容
  string message = 3;
  // 錯誤詳細信息,可以附加自定義的信息列表
  repeated google.protobuf.Any details = 4;
}

和我們當前的方式差不太多,但是我們是在原來的基礎上返回了 http code,剩下的字段還是和原來保持一致

錯誤傳播

這一點我們之前做的還行,錯誤傳播這一部分很容易出的問題就是,當前服務直接把上游服務的錯誤給返回了,這樣會導致一些問題:

正確的做法應該是把上游錯誤信息吞掉,返回當前服務自己定義的錯誤信息就可以了。

總結

毛老師課上講的 api 設計思路用起來還是挺爽的,我們已經在一個項目當中進行了試點,cicd 的流程也跑了起來,最爽的一點就是終於不用找接口文檔了,然後還節省了一些代碼量,我們之前的接口調用方式都是十分原始的,每個項目都自己去封裝相關的 sdk 然後我們對單元測試還有要求,http 接口的 mock 是挺麻煩的事情,通過 protobuf 定義接口之後我寫了一個結合內部網關的 sdk 代碼生成器,直接生成相關接口代碼,go interface 的 mock 實現也在 ci 流程中生產好了,調用方只需要調用不同的實現就行了。下一篇我們就通過寫一個 從 proto 生成 gin 代碼的生成器來看看這個代碼生成器改如何實現。

參考文獻

  1. Go 進階訓練營 - 極客時間

  2. GitHub - istio/api: API definitions for the Istio project

  3. GitHub - envoyproxy/data-plane-api: [READ ONLY MIRROR] Envoy REST/proto API definitions and documentation.

  4. GitHub - googleapis/googleapis: Public interface definitions of Google APIs.

  5. API 設計指南  |  Google Cloud

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