Triton Ho 的技術淺談-RESTful(6/29 台北場)- 心得分享

Network 心得

在 6/29 聽了 Triton 大大的 RESTful 技術淺談,有些心得想跟大家分享,此場淺談主要是來說明 RESTful 設計的注意事項()與 Protocol 的介紹,以下的說明除了當天的心得外,還會加入站長的一些看法,就讓我娓娓道來 ~

HTTP 基礎介紹

首先一開始說明的是 HTTP 是通訊協定,這部分其實滿多新手會搞混的,以前好像還聽過有人說是語言之類的,要記得他是通訊協定。

什麼是通訊協定呢?就是平常我們在溝通的語言,就像我們可能會使用中文溝通或是英文溝通,這些溝通方式就是我們平常在說的通訊協定。

HTTP 的設計方法其實就是資源加上方法,例如現在想要做一隻取得使用者資訊的 API 這時候就會設計成:

  • URL (resource): example.com/v1/users
  • Method: GET

這邊需要注意的地方是 resource 一定是名詞,在設計上有些動作要做成 API 就會使用動名詞來命名,以投影片的範例來說銀行轉帳就會使用 “MoneyTransfers” 來設計。

HTTP Method

說到 HTTP method,通常會使用大家約定俗成的設定方法來設計,也請大家設計的時候遵守此規範,新手一開始可能會有一個疑問,今天我把它設計成 Method: Delete 是一個取得資料的 API 不行嗎?當然可以,如果系統只是自己在使用當然隨意怎麼設計都好,但是當系統是大家一起開發或是需要與其他人串接的時候使用約定俗成的設計方法設計可以減少雙方溝通的時間,因此才希望設計得時候遵守規定。

GET

會使用 GET 通常就是要取得一些資料,而且資料是僅限讀取得,就會使用這個 method,以簡報的範例來說:

  • URL: example.com/v1/users/89072
  • Method: GET

此範例主要是說明一個取得使用者 ID 是 89072 的資料,通常會是一個物件資料,如果想要設計成要取得一個集合就會設計成:

  • URL: example.com/v1/users
  • Method: GET

一般來說集合通常會有非常多的資料這時候就會需要篩選,而建立篩選條件就會使用 QueryString 的方法來設計:

  • URL: example.com/v1/users?AgeMax=20
  • Method: GET

上面的方法是要取得年齡是 20 歲以下的資料,如果要設計成想要 10 歲以上 20 歲以下的資料就會設計成:

  • URL: example.com/v1/users?AgeMax=20&AgeMin=10
  • Method: GET

這邊有一個需要注意的點是 Query String 只有 GET 才應該使用,其他的 Method 理論上不太會使用 Query String。

DELETE

需要移除某些資源會使用 DELETE,如下範例是一個想要移除使用者 ID: 89072 資源的一個範例:

  • URL: example.com/v1/users/89072
  • Method: DELETE

POST

POST 是一個只要有用過 HTTP 的人一定知道的 Method,也是非常常使用的 Method,站長還聽說過有人用 GET 加上 POST Method 寫完整個系統 XD
不過這是一個不太好的示範,通常設計還是應該使用不同的 Method 來完成設計,才是一個良好的設計。

如果想要建立新的使用者資源,就會使用如簡報的範例:

  • URL: example.com/v1/users
  • Method: POST

通常來說會把資源的 ID 回傳給使用者,在這邊 Triton 提到了一個延伸思考的問題

在不穩定網路下,用戶建立一份資源時, 卻發出了二次的 POST ,怎麼辦?

大家應該都知道在網路上什麼樣的事情都會發生,舉凡 Wifi 掉包、有線網路卡一下、瀏覽器卡一下之類的情況都有機會造成 TCP 重傳,在這些情況下伺服器端就有可能收到兩次 POST。

說到這個問題就要說到 idempotent (idempotent 在後面的小節有簡單的介紹),在 Spec 上面可以看到 POST 是 non-idempotent ,那遇到上面說的問題怎麼辦呢?那就自行解決吧 ~ 畢竟如簡報所說 Spec 並沒有說不行自行解決這個問題,Triton 提供了一個解決方法是可以在 POST 裡面放入 timestamp,當使用者發出 POST 的時候就去檢查這個 timestamp,就可以解決 non-idempotent 發生的問題了 ~

PATCH

PATCH 是用來改動資源內容的,以簡報範例來說今天想要改動使用者 ID: 80972 的年齡從 30 改成 31 會這樣設計:

  • URL: example.com/v1/users/89072
  • Method: PATCH
  • Body: { “Age”: 31 }

就 Spec 定義來說 PATCH 是 non-idempotent 的,但是不是 idempotent 主要還是要看伺服器端的設計方法,如下設計方法是 idempotent:

  • SET Age = 31

如下設計方法是 non-idempotent:

  • SET Age = Age + 1

PUT

Spec 上面定義的是上傳內容會覆蓋本來的資源,但是通常會在 PUT 中支援部分改動,不再另開 PATCH method(開發者通常都很懶 XD)。

  • URL: example.com/v1/users/89072
  • Method: PUT
  • Body: { “Age”: 31 }

OPTIONS

定義上是用來查詢某一資源能使用的 methods,瀏覽器在 Cross-Origin Resource Sharing (CORS) 之前會先發出 OPTIONS 的查詢,用於保護使用者,也保護要求是來自於不同網域 (Domain) 的請求。

範例為:

  • URL: example.com/v1/users
  • Method: OPTIONS

CORS 的詳細流程之後有機會可能會再發文說明,這邊就不提到細節。

HTTP idempotent

大家有興趣可以看看原文介紹 9.1 Safe and Idempotent Methods,所謂的 idempotent 簡單來說就是不管執行幾次都會得到同樣的結果就是 idempotent,在 spec 上面說到的 idempotent 有 GET、HEAD、PUT 與 DELETE,所以 POST 與 PATCH 就是 non-idempotent。

常使用的 HTTP 狀態碼

200 OK

最常用到的狀態碼,代表成功,會回傳內容。

202 Accepted

表示伺服器收到請求,內容檢查也沒有問題,但是工作可能還沒做完,通常會主動推送結果給用戶端。

主動推送的方法很多種可以做 Email 推送、Slack 通知、簡訊通知各種方法都可以,純粹看需求設計。

204 No Content

與 200 差不多,表示已經成功沒有內容回傳,通常是 DELETE 或是 PUT 使用。

302 Found

重新導向,會利用 HTTP Header 的 Location 告訴使用者下一步需要去哪裡,只有 GET 或 HEAD 兩種可以直接導向,範例如:

  • Location: https://example.com/

304 Not Modified

客戶端有一份副本但是需要確認要不要更新,因此會在 HTTP Header 加入 If-Modified-Since 發出要求,如果客戶端是最新的就會回應 304,如果不是會回應 200,並且會傳內容,發出要求的 header 範例如:

  • If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT

400 Bad Request

請求的內容有錯誤,伺服器拒絕,範例如:

  • URL: example.com/v1/users?AgeMax=20ABC
  • Method: GET

AgeMax 應該要是數字結果請求的內容不是數字。

401 Unauthorized

使用者沒有權限,需要先登入系統,通常會檢查 HTTP Header 的 Authorization。

403 Forbidden

此狀態代表伺服器有看懂使用者的請求,但是請求內容與邏輯矛盾,伺服器拒絕此請求。

404 Not Found

請求的資源不存在,比較常用到的是檔案不存在,如簡報範例來說:

  • URL: example.com/v1/users/89073
  • Method: GET

系統沒有 89072 這個使用者就會回傳 404。

需要注意的如果今天的請求如:

  • URL: example.com/v1/users?AgeMin=30&AgeMax=20
  • Method: GET

今天要搜尋的條件是正常可以搜尋的,但是並沒有符合的資料應該要返回 200,而不是 404。

409 Conflict

通常與 optimistic lock 有關係,使用者目前改動的內容已經先被改動了,例如有一個網頁的頁面上面原本的內容是 A B C,使用者 A 改動了把網頁頁面改成 A C B 並且說明改動時間是 2019/07/03 12:00,使用者 B 在 2019/07/03 11:00 開啟了網頁把網頁內容改成 C B A,網頁放很久後使用者 B 在 2019/07/03 13:00 的時候發出了請求,瀏覽器帶出的請求卻是 2019/07/03 11:00,此時候伺服器比對 2019/07/03 12:00 有一個變更,因此發生了 optimistic lock 的情況,就會回應 409。

500 Internal Server Error

通常是程式有 Bug,可能是 try catch 的錯誤,或是其他非預期錯誤,也可能是外連伺服器異常,像是資料庫異常等 ……。

502 Bad Gateway

伺服器的分流層出現問題,可能是負載平衡 (Load Balancing),或其他分流服務。

503 Service Unavailable

伺服器目前忙碌中,可能是使用者太多或是任何原因造成後端服務異常,前端服務直接拒絕此請求。

HTTP 狀態碼總結

所有的 4XX 系列錯誤通常都是使用者端可以自己處理的,但是 5XX 系列是使用者端沒有辦法自行解決的錯誤。

API 設計的重要性

談完了 HTTP 的基礎後,現在要來談談 API 設計的重要性,通常設計 API 都會有 v1 這個 path 的設計,這是因為當 API 改動的時候前端與後端都需要修改,如果加入了版本號可以讓新舊 API 共存減少一點修改困難的問題,但也因為如此可以知道 API 修改其實是滿困難的。

在這邊提一個延伸討論,為什麼會把 URL 設計成 https://example.com/v1/users 而不是把它設計成 https://example.com/users/v1,這是因為可以讓分流伺服器看到 https://example.com/v1 就把 https://example.com/v1/* 指定到特別的 Server,這樣有什麼特別的嗎?

假設今天發現 v1 版本的 API 有效能問題,我們可能就會把 v1 版本的 API 導入到特別的伺服器,不要讓他拖慢整個系統。

那為什麼要放在 PATH?我放在 body 不行嗎?這時候可以思考幾件事情

  • 分流伺服器可以看懂 body 嗎?在比較老的分流伺服器是沒辦法看懂 body 的,在現在是有分流伺服器可以看懂。
  • 在 API 設計上比較常見到的 body 內容是 JSON,對於分流伺服器來說解析一個 URL 的時間跟解析 JSON 的時間比較,一定是解析 URL 的速度快於解析 JSON 的。

而 API 在設計的時候要特別注意,錯誤的 API 設計可能發生的問題:

  • 效能問題
  • 讓整個系統邏輯變得更複雜
  • 讓後端資料庫設計扭曲
  • 數據錯誤

以上是一些 API 設計錯誤可能引起的問題,也還會有其他的問題,因此在設計的時候要非常小心。

API 設計重點

統一介面 (uniform interface),但是千萬不要為這個問題吵太久,因為不太會發生太大的問題。

API 設計不要限制前端的設計,就算這個設計會違反統一介面的原則也沒關係,一切都是以使用者的感受為最大原則。

傳統網頁設計與前後端分離的網頁設計

傳統網頁的架構與流程

上圖為傳統的網頁設計架構,會有以下幾個缺點:

  • 網頁無法產生 cache (快取):因為 Data (資料) 與 Presentation (外觀) 一起,如果 Data 有改變頁面就不同,因此無法產生 cache。
  • 影響開發、測試與除錯:因為 Presentation、Business Logic (商業邏輯) 與 Data 無法測底分割。
  • 花費的 CPU 資源比較高:因為伺服器需要將 Data 與 Presentation 整合 (Data Binding)。
前後端分離的架構與流程

使用者端快取

從上面來看使用者第一次做完 (2) 就會有快取,之後的要求會直接回應 304,非常節省流量。只要圖片不要更新或使用者不要移除快取就可以節省伺服器端的流量。快取除了做在 NGINX 或 APACHE 上面還可以進階使用 CDN 來製作快取,增加效能增加使用者體驗。

有些前端的 Library 會有免費的 CDN 可以用,大家可以多多利用。

資料與外觀分離

開發前端的人可以直接使用 mock API 來輔助開發,後端伺服器在寫測試的時候也可以更簡單。

節省伺服器資源

可以讓伺服器做越少事情就做越少事情節省伺服器資源。Data Binding 給瀏覽器處理省下伺服器端的 CPU 資源 。

RESTful API 設計

基本上 RESTful 的設計就是最初 HTTP 的設計哲學。

對於 RESTful 的誤解

伺服器端需要 Stateless (無狀態)?

  • HTTP 要求 Protocol 本身要 Stateless,並沒有要求伺服器要 Stateless。
  • RESTful 要求 Application Server 要 Stateless,伺服器端的其他部分可以 Stateful (有狀態)。例如:可以先把使用者目前是在線上這件事情存入資料庫,限制某些功能只有線上的狀態可以使用。

一定要用上 HTTP 的 Method?

  • 統一介面只是建議使用 HTTP,使用其他的協定技術也可以實作 RESTful 達到需求。
  • RESTful 並沒有反對建立限定讀取的端點,所以使用 GraphQL 也是可行的。

RESTful 的要求

有需要可以看看 Wiki 的介紹可能比較清楚,這邊簡略說明一下:

  • 用戶端-伺服器(Client-Server)(客戶端與伺服器的結構)
  • 無狀態(Stateless protocol)(通訊協定無狀態)
  • 快取(Cacheability)(利用快取增加效能)
  • 統一介面(Uniform Interface)
  • 分層系統(Layered System)

用戶端-伺服器(Client-Server)

  • RESTful 必須要在客戶端與伺服器端,簡單來說就是分離性。
  • RESTful 只有規範伺服器與客戶端之間的通訊協定。
    – 就是說客戶端之間的 P2P 通訊並沒有在 RESTful 的規範內。
  • 可以輕易地移植,今天的 Client 如果不是瀏覽器是手機 App 也可以輕易的使用。

無狀態(Stateless protocol)

無狀態可以分幾個重點來看:通訊協定無狀態性、atomic (原子性)、足夠完整的資料、Idempotence 以下幾點:

通訊協定無狀態性
  • RESTful 伺服器是被允許有狀態的。
  • RESTful 允許把狀態儲存到資料庫中,例如登入狀態的短期資料放到短期資料庫(Redis)
  • RESTful 基本上在開發上禁止使用 Session
  • 如果要使用 Session,資料要放在 Global Storage,不可以放在 Local Memory
    • Global Storage 像是 Redis
  • 今天以一個電商系統來說,在 RESTful 的設計下,在做結算的情況下應該直接帶入商品 A 與商品 B,而不是先丟入商品 A 再丟入商品 B 然後從 Session 拿出來去結算。
atomic (原子性)

簡單來說就是不可以呼叫兩個或以上的 API 去完成一個動作,下面來舉幾個符合原子性的例子:

  • 以轉帳系統來說用戶 A 要轉帳給 B 帳號 API 裡面要帶入 A 與 B 的帳號
  • 搶票系統來說要把使用者與票卷一起帶入 API

以上面的例子來說不符合原子性的範例如下:

  • 以轉帳系統來說用戶 A 要轉帳給 B 帳號先使用 API Call A 使用者轉出再使用 API Call B 使用者轉入
  • 搶票系統來說先 Call 使用者購買再 Call 購買票券
足夠完整的資料

API 呼叫需要考慮到伺服器需要足夠完整的資料,不應該假設伺服器知道任何的狀態,應該確保前後 API 沒有關連。

Idempotence

假設有問題使用者端會重新發送,所以伺服器需要有辦法處理重複的請求,在發送請求的時候要把客戶端的時間放入請求(request)之中可以做到最基本的保護,但是其實不過嚴謹,如果想要更嚴謹可以參考此篇此篇文章,基本上放入時間可以簡單的讓伺服器分辨是否收到重複的要求。

快取(Cacheability)

  • 系統內所有的東西都必須定義能否使用快取
  • 客戶端可以建立本地快取
  • 使用者會在下次請求的時候把本地快取的版本連同請求發出,伺服器端檢查如果沒有改動就回應 HTTP 304,告知客戶端可以直接使用本地快取。
  • 變動量比較少的物件可以設定為可快取,常遇到的是網頁介面、網頁函式庫檔案、網頁圖片等 ……。
  • 做了快取可以省下 CPU 資源與網頁流量。

統一介面(Uniform Interface)

所有的 URL 都應該基於物件,而不是行動。一個物件通常會有 4 種行動:查詢、修改、要求、刪除

  • 查詢:通常使用 HTTP GET
  • 修改:通常使用 HTTP PUT/PATCH
  • 要求:通常使用 HTTP POST
  • 刪除:通常使用 HTTP DELETE

分層系統(Layered System)

分層式系統

通常使用者不會直接碰到 Application Server,前面還會有一個或多個 Load Balancer 在前方控制流量,不應該讓後端吃不下的流量進入到 Application Server,發現後端吃不下前方的 Load Balancer 應該直接回應掉,跟前面的 HTTP 502 呼應。

流量到 Application Server 後會視情況看需不需要到後端的資料庫取得資料,再進階可能會有 Redis,Redis 還可以區分 Hot Data(熱資料) 與 Cache Data(快取資料),處理完對應的商業邏輯後回傳資料。

結語

以上的內容是整理淺談的技術分享再加上站長的一些想法以及內容,希望可以幫到剛開始寫 API 的新手,如果對以上的內容有問題或是有其他指教都可以在下面留言。

參考資料: