良好的 API 設計在微服務架構中很重要,因為服務之間的所有資料交換都是透過訊息或 API 呼叫進行。 API 必須有效率,以避免建立 閒聊的 I/O。 因為服務是由小組獨立設計,所以 API 必須有妥善定義的語意和版本設定配置,讓更新不會中斷其他服務。
請務必區分兩種類型的 API:
- 用戶端應用程式所呼叫的公用 API。
- 用於服務間通訊的後端 API。
這兩個使用案例有一些不同的需求。 公用 API 必須與用戶端應用程式相容,通常是瀏覽器應用程式或原生行動應用程式。 大部分時候,這表示公用 API 會透過 HTTP 使用 REST。 不過,針對後端 API,您必須將網路效能納入考慮。 根據您的服務粒度,服務間通訊可能會導致大量的網路流量。 服務可以快速變成 I/O 系結。 因此,串行化速度和承載大小等考慮會變得更加重要。 使用 REST over HTTP 的一些熱門替代方案包括 gRPC、Apache Avro 和 Apache Thrift。 這些通訊協議支援二進位串行化,而且通常比 HTTP 更有效率。
考量
以下是選擇如何實作 API 時要考慮的一些事項。
REST 與 RPC。 請考慮使用 REST 樣式介面與 RPC 樣式介面之間的取捨。
REST 模型資源,這可以是表達領域模型的自然方式。 它會根據 HTTP 動詞來定義統一介面,以鼓勵進化。 它在等冪性、副作用和響應碼方面具有妥善定義的語意。 而且它會強制執行無狀態通訊,以改善延展性。
RPC 更面向於作業或命令。 因為 RPC 介面看起來像本機方法呼叫,所以可能會導致您設計過於閒聊的 API。 不過,這並不表示 RPC 必須閒聊。 這表示您在設計介面時需要使用護理。
針對 RESTful 介面,最常見的選擇是使用 JSON 透過 HTTP REST。 針對 RPC 樣式介面,有數個熱門架構,包括 gRPC、Apache Avro 和 Apache Thrift。
效率。 請考慮速度、記憶體和承載大小方面的效率。 gRPC 型介面通常比透過 HTTP 的 REST 更快。
介面定義語言 (IDL)。 IDL 可用來定義 API 的方法、參數和傳回值。 IDL 可用來產生用戶端程式代碼、串行化程式代碼和 API 檔。 API 測試工具也可以取用IDL。 gRPC、Avro 和 Thrift 等架構會定義自己的 IDL 規格。 REST over HTTP 沒有標準 IDL 格式,但常見的選擇是 OpenAPI(先前稱為 Swagger)。 您也可以建立 HTTP REST API,而不使用正式定義語言,但您就會失去程式代碼產生和測試的優點。
序列化。 物件如何透過網路串行化? 選項包括文字型格式(主要是 JSON)和二進位格式,例如通訊協定緩衝區。 二進位格式通常比文字格式快。 不過,JSON 在互操作性方面具有優勢,因為大部分的語言和架構都支援 JSON 串行化。 某些串行化格式需要固定的架構,有些則需要編譯架構定義檔。 在此情況下,您必須將此步驟併入您的建置程式。
架構和語言支援。 幾乎每個架構和語言都支援 HTTP。 gRPC、Avro 和 Thrift 都有適用於 C++、C#、Java 和 Python 的連結庫。 節儉和 gRPC 也支援 Go。
相容性和互操作性。 如果您選擇 gRPC 之類的通訊協定,您可能需要公用 API 與後端之間的通訊協定轉譯層。 閘道可以執行該函式。 如果您使用服務網格,請考慮哪些通訊協定與服務網格相容。 例如,Linkerd 有 HTTP、Thrift 和 gRPC 的內建支援。
我們的基準建議是選擇 REST over HTTP,除非您需要二進位通訊協定的效能優點。 REST over HTTP 不需要任何特殊連結庫。 它會建立最少的結合,因為呼叫端不需要用戶端存根來與服務通訊。 有豐富的工具生態系統,可支援 RESTful HTTP 端點的架構定義、測試和監視。 最後,HTTP 與瀏覽器用戶端相容,因此您不需要用戶端與後端之間的通訊協定轉譯層。
不過,如果您選擇 REST over HTTP,您應該在開發程式早期執行效能和負載測試,以驗證它是否對您的案例執行得足夠好。
RESTful API 設計
有許多資源可用於設計 RESTful API。 以下是一些您可能會發現有説明的:
以下是請記住的一些特定考慮。
請留意洩漏內部實作詳細數據的 API,或只是鏡像內部資料庫架構。 API 應該建立網域的模型。 這是服務之間的合約,在理想情況下,只有在新增新功能時才會變更,而不只是因為您重構了某些程式代碼或標準化資料庫數據表。
不同類型的用戶端,例如行動應用程式和桌面網頁瀏覽器,可能需要不同的承載大小或互動模式。 請考慮使用 前端後端模式 來為每個用戶端建立個別的後端,這會公開該用戶端的最佳介面。
針對副作用的作業,請考慮將它們設為等冪,並將其實作為PUT方法。 這會啟用安全的重試,並可以改善復原能力。 Interservice 通訊一文會更詳細地討論此問題。
HTTP 方法可以有異步語意,其中方法會立即傳回回應,但服務會以異步方式執行作業。 在此情況下,方法應該會傳回 HTTP 202 回應碼,指出已接受要求進行處理,但尚未完成處理。 如需詳細資訊,請參閱 異步要求-回復模式。
將 REST 對應至 DDD 模式
實體、匯總和值物件等模式的設計目的是將特定條件約束放在定義域模型中的物件上。 在 DDD 的許多討論中,模式是使用面向物件 (OO) 語言概念來建立模型,例如建構函式或屬性 getter 和 setter。 例如, 值對象 應該是不可變的。 在 OO 程式設計語言中,您會藉由在建構函式中指派值,並讓屬性成為唯讀,來強制執行這項設定:
export class Location {
readonly latitude: number;
readonly longitude: number;
constructor(latitude: number, longitude: number) {
if (latitude < -90 || latitude > 90) {
throw new RangeError('latitude must be between -90 and 90');
}
if (longitude < -180 || longitude > 180) {
throw new RangeError('longitude must be between -180 and 180');
}
this.latitude = latitude;
this.longitude = longitude;
}
}
建置傳統整合型應用程式時,這些程式代碼撰寫做法特別重要。 使用大型程式代碼基底時,許多子系統可能會使用 Location
物件,因此對對象強制執行正確行為很重要。
另一個範例是存放庫模式,可確保應用程式的其他部分不會直接讀取或寫入資料存放區:
不過,在微服務架構中,服務不會共用相同的程式代碼基底,也不會共用數據存放區。 相反地,它們會透過 API 進行通訊。 請考慮排程器服務向無人機服務要求無人機相關信息的情況。 無人機服務具有其無人機的內部模型,透過程式代碼表示。 但是排程器沒有看到這一點。 相反地,它會傳回 無人機實體的表示 ,可能是 HTTP 回應中的 JSON 物件。
此範例適用於飛機和航空航天產業。
排程器服務無法修改無人機服務的內部模型,或寫入無人機服務的數據存放區。 這表示實作無人機服務的程式代碼與傳統整合型中的程序代碼相比,其表面區域較小。 如果無人機服務定義 Location 類別,該類別的範圍會受到限制, 沒有其他服務會直接取用 類別。
基於這些原因,本指南並不著重於程式代碼撰寫實務,因為它們與戰術 DDD 模式有關。 但事實證明,您也可以透過 REST API 建立許多 DDD 模式的模型。
例如:
匯總自然地對應至 REST 中的資源 。 例如,傳遞匯總會由傳遞 API 公開為資源。
匯總是一致性界限。 匯總的作業不應該讓匯總處於不一致的狀態。 因此,您應該避免建立可讓用戶端操作匯總內部狀態的 API。 相反地,偏好公開匯總為資源的粗略 API。
實體具有唯一的身分識別。 在 REST 中,資源具有 URL 格式的唯一識別碼。 建立對應至實體網域身分識別的資源URL。 從 URL 到網域身分識別的對應可能不透明到用戶端。
您可以從根實體巡覽來觸達匯總的子實體。 如果您遵循 HATEOAS 原則,則可以透過父實體表示法中的連結來連線子實體。
因為值物件是不可變的,所以會藉由取代整個值對象來執行更新。 在 REST 中,透過 PUT 或 PATCH 要求實作更新。
存放庫可讓客戶端查詢、新增或移除集合中的物件,以擷取基礎數據存放區的詳細數據。 在 REST 中,集合可以是不同的資源,其中包含查詢集合或將新實體新增至集合的方法。
當您設計 API 時,請思考其如何表達領域模型,而不只是模型內的數據,以及商務作業和數據的限制。
DDD 概念 | REST 對等專案 | 範例 |
---|---|---|
彙總 | 資源 | { "1":1234, "status":"pending"... } |
身分識別 | URL | https://delivery-service/deliveries/1 |
子實體 | 連結 | { "href": "/deliveries/1/confirmation" } |
更新值物件 | PUT 或 PATCH | PUT https://delivery-service/deliveries/1/dropoff |
存放庫 | 集合 | https://delivery-service/deliveries?status=pending |
API 版本設定
API 是服務與該服務的用戶端或取用者之間的合約。 如果 API 變更,則不論這些用戶端是外部用戶端或其他微服務,都有可能中斷相依於 API 的用戶端。 因此,最好將您所做的 API 變更數目降到最低。 基礎實作中的變更通常不需要對 API 進行任何變更。 不過,在某個時候,您會想要新增需要變更現有 API 的新功能或新功能。
盡可能進行 API 變更回溯相容。 例如,請避免從模型移除欄位,因為這樣可能會中斷預期欄位存在用戶端。 新增欄位不會中斷相容性,因為客戶端應該忽略回應中無法瞭解的任何字段。 不過,服務必須處理舊版用戶端在要求中省略新字段的情況。
支援 API 合約中的版本控制。 如果您引進重大 API 變更,請引進新的 API 版本。 繼續支援舊版,並讓客戶端選取要呼叫的版本。 有幾種方式可以執行這項操作。 其中一個只是為了在相同的服務中公開這兩個版本。 另一個選項是並行執行兩個版本的服務,並根據 HTTP 路由規則,將要求路由傳送至一或另一個版本。
圖表有兩個部分。 「服務支援兩個版本」顯示 v1 用戶端和 v2 用戶端都指向一個服務。 「並存部署」會顯示指向 v1 服務的 v1 用戶端,以及指向 v2 服務的 v2 用戶端。
在開發人員時間、測試和作業額外負荷方面,支援多個版本的成本。 因此,最好儘快淘汰舊版本。 針對內部 API,擁有 API 的小組可以與其他小組合作,協助他們移轉至新版本。 這是當跨小組治理程式很有用時。 針對外部(公用)API,可能會更難取代 API 版本,特別是當第三方或原生用戶端應用程式取用 API 時。
當服務實作變更時,使用版本標記變更會很有用。 版本會在疑難解答錯誤時提供重要資訊。 對於根本原因分析而言,瞭解確切呼叫哪一個服務版本會很有説明。 請考慮針對服務版本使用 語意版本 控制。 語意版本設定使用 MAJOR。次要。PATCH 格式。 不過,客戶端應該只依主要版本號碼選取 API,或者如果次要版本之間有重大(但非中斷性)變更,則可能是次要版本。 換句話說,用戶端在 API 第 1 版和第 2 版之間選取是合理的,但不能選取 2.1.3 版。 如果您允許該層級的數據粒度,您就有可能支援版本激增。
如需 API 版本設定的進一步討論,請參閱 建立 RESTful Web API 的版本設定。
等冪作業
如果可以多次呼叫作業,而不在第一次呼叫之後產生額外的副作用,則作業是 等冪 的。 等冪性可以是實用的復原策略,因為它可讓上游服務安全地叫用作業多次。 如需此點的討論,請參閱 分散式交易。
HTTP 規格指出 GET、PUT 和 DELETE 方法必須是等冪。 POST 方法不保證為等冪。 如果 POST 方法建立新的資源,通常不保證此作業具有等冪性。 此規格會以這種方式定義等冪:
如果與該方法相同之多個要求之伺服器的預期效果與單一這類要求的效果相同,則要求方法會被視為「等冪」。 (RFC 7231)
請務必瞭解建立新實體時 PUT 和 POST 語意之間的差異。 在這兩種情況下,用戶端會在要求主體中傳送實體的表示法。 但 URI 的意義不同。
如果是POST方法,URI代表新實體的父資源,例如集合。 例如,若要建立新的傳遞,URI 可能是
/api/deliveries
。 伺服器會建立實體並指派新的 URI,例如/api/deliveries/39660
。 此 URI 會在回應的 Location 標頭中傳回。 每次用戶端傳送要求時,伺服器都會使用新的 URI 建立新的實體。針對 PUT 方法,URI 會識別實體。 如果已經有具有該 URI 的實體,伺服器就會以要求中的版本取代現有的實體。 如果該 URI 中沒有任何實體存在,伺服器就會建立一個實體。 例如,假設用戶端將 PUT 要求傳送至
api/deliveries/39660
。 假設該 URI 沒有傳遞,伺服器就會建立新的 URI。 現在,如果用戶端再次傳送相同的要求,伺服器將會取代現有的實體。
以下是傳遞服務的PUT方法實作。
[HttpPut("{id}")]
[ProducesResponseType(typeof(Delivery), 201)]
[ProducesResponseType(typeof(void), 204)]
public async Task<IActionResult> Put([FromBody]Delivery delivery, string id)
{
logger.LogInformation("In Put action with delivery {Id}: {@DeliveryInfo}", id, delivery.ToLogInfo());
try
{
var internalDelivery = delivery.ToInternal();
// Create the new delivery entity.
await deliveryRepository.CreateAsync(internalDelivery);
// Create a delivery status event.
var deliveryStatusEvent = new DeliveryStatusEvent { DeliveryId = delivery.Id, Stage = DeliveryEventType.Created };
await deliveryStatusEventRepository.AddAsync(deliveryStatusEvent);
// Return HTTP 201 (Created)
return CreatedAtRoute("GetDelivery", new { id= delivery.Id }, delivery);
}
catch (DuplicateResourceException)
{
// This method is mainly used to create deliveries. If the delivery already exists then update it.
logger.LogInformation("Updating resource with delivery id: {DeliveryId}", id);
var internalDelivery = delivery.ToInternal();
await deliveryRepository.UpdateAsync(id, internalDelivery);
// Return HTTP 204 (No Content)
return NoContent();
}
}
預期大部分的要求都會建立新的實體,因此方法會以開放式方式呼叫 CreateAsync
存放庫物件,然後改為更新資源來處理任何重複的資源例外狀況。
下一步
瞭解如何在用戶端應用程式與微服務之間的界限使用 API 閘道。