# Ch 09. 異步 API **同步 API** 有限制,當資源被更新了,無法由服務端主動更新客戶端,需要客戶端主動向服務端發請求取得最新的資料 **異步 API** 是將更新資料的主動權交由服務端,客戶端只負責處裡事件的更新 ## 輪詢的問題 客戶端常見的更新資料的方式,就是每隔一段時間定期跟服務端要資料,這種稱為 **API 輪詢**,因此有以下缺點: 1. 要額外實作比對/更新頁面的邏輯 2. 會對 Server 有額外的請求,需要思考一下發起請求的頻率,不然可能會變成 DDOS 自己的服務 API 輪詢因為是透過 HTTP 一來一往來刷新資料,因此以上的問題難以避免,因此有人提出用 **異步 API** 來解決以上的問題 ## 帶來新局面的異步 API 異步 API 也是一間公司的數位能力的一環,相較於傳統的 REST API,可以帶來以下特性 1. 即時性 資料可以即時被更新 2. 價值性 因為跟傳統同步 API 比起來多了新的特性,因此可以替自己與客戶帶來新的可能性,可能可以替客戶帶來新的附加價值 3. 效率性 減少客戶端的輪詢請求,降低服務端的壓力,進而減少 Server 的成本開銷 ## 檢視收發訊息的本質 **訊息** 是指攜帶資料的資訊,一定有 *傳送方* 及 *接收方*,訊息可分為 3 大類: * 命令訊息 * 用於 **"要求"** 執行某些事務,寫成英文通常是祈使動詞,ex. CreateOrder、RegiesterPayment 等等 * 通常是指 API 的 Request * 回應訊息 * 用於提供 **命令訊息** 的結果,結尾通常會加上 Result、Reply 或 Response,ex. CreateOrderResult、 RegisterPaymentReply 等等 * 通常是指 API 的 Response * 事件訊息 * 用於告知接收方 **一件事件** 的發生,通常會使用過去式,表示已發生,ex. OrderCreated、PaymentSubmitted * **"事件"** 可以是某個商業邏輯發生了或是某種結果已經發生了 :::info 訊息是不可變的(Immutable),如果狀態需要被更新,應該需要發送一筆新的請求,而不是直接修改已發出的訊息 ::: ![](https://i.imgur.com/x3VcZmn.jpg) ### 訊息的風格與區域性 #### 風格 * 同步訊息 發佈方送出訊息,並等待接收方處理並回覆 * 異步訊息 發佈方與接收方彼此不等待對方的回覆,即射後不理 (Fire and Forget) #### 區域性 * 本地通訊 在同一個程式內進行通訊 (可能是 ClassA 的 MethodA call ClassB 的 MehtodB 這樣?) * 跨程序通訊 * 同一個 Instance 內的不同程式進行通訊 * 某台 Windows Os 裡的 A.exe 更改了某個檔案或是某塊記憶體資料,此時同一個 Instance 內的 B.exe 會因為資料受到異動,而觸發一些不同的事件 * 或者也蠻像 exe 使用 Local DB 的概念 (? * 分佈式通訊 橫跨不同的 Instance 之間的通訊,需要依賴特定的通訊協議之類的,ex. MQTT、AMQP、SOAP、REST 等等 ### 訊息的組成元素 * Body 訊息的本體,通常會用 Json 或 XML 表示,也有可能用二進位,通常也會一起包著 Metadata 來額外表達一些其他不屬於資源本身,但是額外提供的資訊 * Header 其他的周邊資訊,通常會放一些通訊協議或是一些與該 API 完全無關的資訊,比方說給瀏覽器辨認的 CSP Info、TTL(Time To Live,存活時間)、優先度等等 ### 訊息的中介代理 (Message Queue) > 這段感覺大部分都是市面上的 Message Queue 通常會存在的特性 訊息的發佈方及接收方之間透過中介代理去處理訊息要如何接受及交給誰接受,發佈方只負責發出訊息,不管到底是誰要接受,接收方也是,不需要知道訊息是誰發出的,依此來降低系統間的耦合度,ex. RabbitMQ、ActiveMQ、Kafka 等等 中介代理通常有以下特性 * 管控交易 管理訊息的交易狀態,可以將訊息標記為 **"已發佈"**,等到交易完成在標記為 **已送達** 或 **已完成** * 持續保存 訊息確認送達前會保存起來,直到有某個接收方將其接受 * 感知客端 Broker 可以決定訊息是否傳送成功,可以是以下 2 種機制擇一,視需求決定: * 送出後即直接認定成功 * 等到 Consumer 處理完再認定為成功 * 失敗轉送 再有 **感知客端** 的情況下,如果 Consumer 接收失敗,則將該訊息轉送給其他的 Consumer 處理,而不會造成訊息直接遺失 * 死信隊列 為了實現 **失敗轉送** 的機制,因此出現死信隊列 (DLQ,Dead Letter Queue) 來將失敗的訊息排入後,再轉由人工排查或自動重試 * 優先順序 & 存活時間 (TTL,Time To Live) Message Queue 可以透過優先順序及 TTL 來識別訊息的優先度,並且將 **逾期且未成功發送** 的訊息移除 * 標準協議 通常是使用 AMQP 協議 :::success 之前只覺得 Message Queue 是一個用來當作當交易量太大時才會用的技術,因為可以先全部存在 Queue 裡,再交由 Consumer 去慢慢消耗,避免服務瞬間被打爆,而如果 API 的交易量不大,就完全沒必要用 但其實真正的意義(內心的感想)是為了解開服務及服務間的耦合,因為統一只往一個地方送,但背後要怎麼處理,發送方完全不需要管,今天如果是 Consumer 想更換成新寫的服務,對於發送方來說完全不需要知道,我只要知道目的依然有達成就好了 因為如果公司內的服務是用很多不同的技術去實現的話(可能不同部門用不同的技術去各自實現 Endpoint),這樣可以讓大家照著 Broker 所使用的通訊協定去實作訊息的發送及接收 而因為是透過 Message Queue,所以會享受到 Message Queue 的優點及缺點 ::: ### 點對點訊息發佈 (Queue) 一種訊息只會有一種接收方,通常是很單純的一對一,會是一個服務然後有多個 Consumer 這樣,失敗了會轉交給另一個 Instance ![](https://i.imgur.com/xE02ohw.png) ### 扇出式訊息發佈 (Topic) Broker 裡面會有多個主題,而每個主題又有多個訂閱者,因此會是多個服務同時接收一個訊息,訂閱者只知道自己有收到訊息,不會知道其他訂閱者是否有收到訊息 此模式下不關心單一訂閱者的狀態,只關心是否送出成功 ![](https://i.imgur.com/OUMEjei.png) ### 訊息串流基礎 串流與 Topic 類似,他將訊息發送給全部的訂閱者,但訂閱者自己管理自己的串流狀態,當系統需要的時候隨時回到指定的片段重新再來一遍,因此錯誤處理的責任也回到訂閱者身上 :::danger 待補,不確定他是什麼 ::: ## 異步 API 風格 ### Webhook ![](https://i.imgur.com/3ABOgYN.png) ### SSE 古老的技術,會固定占用瀏覽器對同一網域的 TCP 連線數,因此只要連線 6 條後,會造成該網頁完全無法載入任何來自該網域的任何東西,包含 API 或 Static Files **優點** * 能將後端狀態變更給前端知道 * 利用 HTTP 的特性來推送事件,因此不需要特別實作什麼特殊的傳輸機制或是架設 Message Queue 服務等等 * 如果查詢的資料量太大,可以透過 SSE 來將資料分批傳回,避免記憶體被塞爆 **缺點** * 如果服務端有 API Gateway,且 API Gateway 不支援持久連接,則客戶端會一直重連 * 會占用瀏覽器對單一網域的 TCP 連線數,而且只要沒有斷線就會持續連著,容易影響其他的服務 * 部分瀏覽器不支援,如 IE * 需要雙向通訊的場合,比較適合 WebSocket,SSE 不支援從客戶端發出請求 ![](https://i.imgur.com/dfAVi6Z.png) Ref. https://blog.darkthread.net/blog/server-sent-events-aspx/ ### WebSocket 大多數瀏覽器都支援 WebSocket,且 WebSocket 具有 HTTP 的特性,因此可以通過 API Gateway 等等的 HTTP 代理服務 WebSocket 只負責建立通道,因此後續的資料傳輸要使用哪個格式是交由客戶端及服務端各自處理 WebSocket 支援雙向通訊,因此如果需要客戶端向服務端發出請求的場景的話,可以選用 WebSocket ### gRPC TCP 在設計上是適合持久連接且雙向通訊的,因此基於 TCP 為基礎設計的 HTTP/1.1 如果想併發,則需要開啟多條 TCP 連線 因此衍生出了 HTTP/2,HTTP/2 是基於 Google 的 SPDY 協議,然後裡面有一個重點是多路復用 (Multiplexing),這使得客戶端可以用一條連線來併發及重複使用,降低服務端對外的 InBound Port 消耗 而且 HTTP/2 也支援直接推送資料到客戶端,不需要客戶端先發動請求,可以在客戶端發出請求之前,先將這些已知會被請求的資訊推送給客戶端 gRPC 運用 HTTP/2 的雙向通信特性,因此可以同時支援同步及異步的 API 設計;且 gRPC 不需要考慮子協議的問題,都是使用 ProtoBuf 來進行通訊,但問題是因為瀏覽器原生不支援,因此需要其他的前端套件來支援,但第三方套件有其使用上的限制,因此通常使用於 Server to Server 的場景 ![](https://i.imgur.com/vrY3EU4.png) Ref. https://hieven.medium.com/http-2-%E5%BE%9E%E9%9B%B6%E5%88%B0%E4%B8%80-be221087cd35 Ref. https://www.ithome.com.tw/voice/119260 ### 如何挑選異步 API 風格 * Webhook * 唯一只能從服務端打到服務端的異步 API,因為瀏覽器無法處理來自服務端的 HTTP 請求 * 不能被防火牆擋住,不然資料會送不過去 * SSE * 幾種方法中最簡單的,瀏覽器原生支援 * 但無法雙向通信,僅能從服務端到客戶端 * WebSocket * 因為子協議需要客戶端及服務端各自定義,因此比較複雜一點 * 具有雙向通訊的特性,且主流瀏覽器目前也都有原生支援 * gRPC * 因為是基於 HTTP/2 定義的,所以所有使用到他的地方都需要支援 HTTP/2 * 跟 WebSocket 一樣都支援全雙工,但瀏覽器目前尚未原生支援,可以透過套件擴充支援,但最適合的場景還是以服務端對服務端為主 ## 設計異步 API 設計異步 API 時應該考慮以下元素 * 命令訊息 * 用於通知對方執行某項工作,因此訊息內容應該要是命令的細節,重點是 **"提供足夠的細節,而不是交由對方摸索"** * 如果客戶端希望在服務端更新資料成功後收到訊息的話,可以在 Request 加上 Callback 的欄位讓服務端主動通知,如果還有其他的客戶端也希望收到通知的話,可以讓客戶端通通都訂閱同一個事件,當服務更新完畢後就會去通知客戶了 * 事件通知 * 用於通知訂閱者某件事件的發生,僅提供必要資訊給訂閱者,訂閱者收到訊息後會自行決定要如何運作 * 訂閱者收到訊息後,可能會需要跟服務端取得一些資訊,可以透過直接提供 hypermedia 的資料給訂閱者,讓他可以省去一些功 * 不帶細節的事件通知通常發生在常常變動的資源上,有強迫訂閱者要主動更新資料的性質 * 帶有狀態變更的事件 * 表示通知裡面包含了資源變動的完整資訊,訂閱者不需要主動發出請求來取得資料 * 通常訊息較大,可能需要較多網路時間來傳輸 * 如過異步 API 發送的對象是另一個中介服務 (Message Queue),此時就不用讓 Message Queue 來重新取得資料也能順利轉送給下一個服務 * 批次事件 * 多數異步 API 是設計成一有訊息就發送,但這樣有可能在訊息量太多的時候,使得服務壓力變大,因此可以考慮設計成一次發送多筆事件 * 事件的排序 * 理想的事件應該是先進先出,但有時候可能因為外在因素而使得訊息無法依序發出,ex. Consumer 斷線導致重新連線後,同時接收到之前累積的事件及新產生的事件、或者是因為分散式服務而使得 Broker 無法保證同一個訊息鍊裡面的訊息是有序發送的 * 分散式系統的話可以考慮使用 Lamport timestamps 來處理 (但沒有很理解) Ref. https://juejin.cn/post/6844904147053969421 ## 撰寫異步 API 文件 可以參考 AsyncAPI 規格書來規範異步 API 的文件規格 ###### tags: `Web API 設計原則:API與微服務傳遞價值之道`