2014年4月18日 星期五

筆記:RESTful API

最近有機會看RESTful API的設計,雖然自己沒有參與,但是也把握機會看了一遍。首先先從Http協定說起,Http協定不只用在傳輸Web的內容上,也有很多Client應用程式與Server溝通都使用Http協定,例如手機App,遊戲,甚至一些雲端的服務都是透過Http協定來提供服務。但是Http只是協定,至於要以什麼順序傳,傳什麼東西,其實都是自由的,所以也造成沒章法隨便亂寫的狀況,而REST風格是一個業界公認,我們自己也可以理解接受的一種解決方案。遵照REST的風格,做出來的架構因為Server不需要記錄執行狀態,所以天生就有較好的擴展性,簡單說,只要後端資料庫夠強,就可以一直水平擴充Web Server,透括負載分配進來Web Server的請求,就可讓支援使用者數量上到百萬。這就是無狀態設計屌的地方。

其實我家老大找到的這篇,就已經完整說明設計RESTful API各種要注意的地方:
http://www.vinaysahni.com/best-practices-for-a-pragmatic-restful-api

以下是個人簡單的筆記:

透過Http協定來提供服務有以下好處:
  • Http協定已經是一個成熟穩定的傳輸協定,累積了業界的經驗,而且有許多的Http Server支援,不用自己再打造協定,只須選擇一種Http Server來使用,並把應用程式佈署進去。
  • Client端也有許多現成的Open Source實作,不需自行打造穩定可靠的Client端
  • 協定都是以文字為主,理解與除錯較簡單
  • 至於傳輸內容部份,應用程式相關資料,目前業界都是使用肉眼可讀的JSON,怕字串容量過大,各家http server都有現成的工具可壓縮。
  • 安全部份有https可以使用,加密也不需自行處理,可以使用Open Source的Solution
  • Http本身也支援Cache機制,善加利用對於處理大量讀取需求也會很有幫助。
REST:Representational State Transfer,含狀態傳輸,來自Roy Fielding博士的論文
是一種軟體架構風格,在目前的Web Service實現方案中,與SOAP,XML-RPC簡單許多,這兩種我光是看就頭暈,所以REST已經變成實現Web Service的主流。科技走向始終來自於人性,一看就覺得違背工程師懶惰天性的規格,根本不需要花太多時間去看。

RESTful Web API,是使用HTTP並遵循REST原則的Web API,比較簡單的定義可以看 http://zh.wikipedia.org/wiki/REST ,

簡單易懂的RESTful CookBook:http://restcookbook.com/
可以看看GitHub怎麼訂的:https://developer.github.com/v3/

在把HTTP Method對應到Resource存取方式時:
GET-->讀取Resource
POST-->新增Resource,呼叫多次會新增多個
PATCH-->更新Resource,可作部份更新
PUT-->替換Resource,用新的Resource換掉一個舊的Resource
DELETE-->刪除Resource

另外有一個表格一定要了解:from http://restcookbook.com/
HTTP MethodIdempotentSafe
OPTIONSyesyes
GETyesyes
HEADyesyes
PUTyesno
POSTnono
DELETEyesno
PATCHnono
Idempotent:代表Http Request會改變Server資料內容,但多次Request結果都不會改變。
Safe:代表Http Request不會改變Server資料內容

在設計RESTful API時,處理什麼資源要對應什麼Method,要小心考慮是否需要Idempotent與Safe。例如想更新某筆已經存在的使用者資料,卻用POST就瞎了。雖然還是可以作,但最好不要宣稱是RESTful。

以下是一些設計上的小筆記:

版本直接寫在URI上
只要有API就會有版本問題,早期版本資訊是放在http的request body裡面,但是這樣可讀性並不好,而且這個Request是哪個版本的,要解析Request body才知道。現在比較有彈性的設計是放在URL上,例如:
/v1/team/:id/members
/v1/team/:id/members/:user
/v3/team/:id/members
/v3/team/:id/members/:user
當Server升到v3的時候,如果實作相同,可以把v1的Request導到v3,達成Backward Compatible,如果實作不同,就保留v1的實作。

Enable與Disable的設計
要Enable/Disable某項資源時,可以做成HTTP PUT與HTTP DELETE,以下是Github在Repository加Star的例子:
  • PUT /user/starred/:owner/:repo:對某個repository加Star
  • DELETE /user/starred/:owner/:repo:對某個repository去掉Star
API就是User Story
在定義API時,用User Story來表示API比起用資料存取來表示API更好,因為這樣User Story就浮現,也不會多開放不需要的東西出去。以下是Github對於管理Team的API列表:
    1. List teams
    2. Get team
    3. Create team
    4. Edit team
    5. Delete team
    6. List team members
    7. Get team member
    8. Add team member
    9. Remove team member
    10. List team repos
    11. Check if a team manages a repository
    12. Add team repository
    13. Remove team repository
    14. List user teams
可以看到紅色的項目是特殊條件查詢,這樣把一個一個User Story列出來,開發的方式就會從資料導向變成使用者導向,我們只開放使用者需要的功能,寫出User Story就代表我們充分研究過使用者需求,寫出User Story還可進一步作BDD與ATDD,這可是一個軟體工程層次的進化!

以Aggregate為單位操作
Aggregate(你也可以叫做Entity,我在這裡是用DDD的講法)概念的使用,早期Http PATCH不風行,每次要更新資料,都要把完整一包資料準備好,一併Http PUT上去,較耗時間與計算資源,所以一種變通的作法,就是細分出很多URL,來對一個Aggregate作paritial update,例如有一個team Object,JSON如下:


{
  "url": "https://api.github.com/teams/1",
  "name": "Owners",
  "id": 1,
  "permission": "admin",
  "members_count": 3,
  "repos_count": 10,
  "organization": {
    "login": "github",
    "id": 1,
    "url": "https://api.github.com/orgs/github",
    "avatar_url": "https://github.com/images/error/octocat_happy.gif"
  }
}
假設orgnization又臭又長,有1000 char,每次我要更新team的members_count我必需
PUT /teams/:id
這樣每次要傳那個orgnization,消費太大又沒意義,所以可以用
PUT /teams/:id/members_count
這類的URL,只需要傳members_count。
以上作法沒什麼錯,但是這樣URI會增加很多,而且team是一個邏輯上完整的Aggregate,而members_count離開了team,就什麼也不是。以架構的角度來看,API每次給出來的,都應該是完整的資料。唯一需要部份資料更新的情境,應該就是Web UI想要減少傳輸量所以要做partial update,至於partial get,只會讓API提供者與API使用者搞死自己。

現在有Http PATCH有支援partial update,所以更新資料可以用以下形式:
{
  "url": "https://api.github.com/teams/1",
  "name": "Owners",
  "members_count": 3,
}
只要用PATCH /teams/:id就可以了
既然現在PATCH已經成為主流,那更應該保持API給出的資料是個完整的Aggregate,這樣可以讓簡化資料處理邏輯,也可減少URI的數量。

URI不一定要完整表示資料的階層
假設一個資料階層是:School包含Department包含Class包含Student
如果要拿某一個student,可能會設計成
/school/:schoolid/department/:departmentid/class/:classid/student/:studentid
這樣又臭又長的URI,這個作法把資料階層的複雜度直接呈現給使用者,如果用以下URI
/student/:studentid
是不是相對簡單,但是我們怎麼只根據studentid找到學生?這部份就是後台設計的問題,或許可以讓使用者指定schoolid與departmentid與classid在Request Body裡面,或許可以將studentid定義成整個應用程式都不會衝突的UUID,然後直接搜尋這個id,不管如何,設計URI的考量應該以想要怎麼實現User Story,而不是以內部資料階層來侷限User Story。

反過來說,如果User Story需要呈現Class與Student的關係,那也是可以定義成:
/class/:classid/student/:studentid

動詞放在URI的最後面
RESTful的標準用法都是以資源存取為主,但是有許多User Story不只使用資源,而是有一些動作要系統執行,那要用API來呈現呢?
  1. 把要執行的動作當作一種資源,然後去Patch,壞處是把動作當資源這種觀念上的轉換不太直覺,Ex: PATCH /comuters/:computerid/activated
  2. 把要執行的動作的目標資源,後面加上sub resource,然後作PUT/DELETE,等同上述的Enable/Disable,這其實也不太直覺,Ex: PUT /computers/:comuterid/active,DELETE /computers/:computerid/active
  3. 把要執行的動作放在URL的最後面,這個方式打破RESTful架構在URI只放名詞的規範,但是如果把動詞放在URI的最後面,且API從頭到尾保持一致,個人也覺得是最有說明性的方式。https://developers.google.com/prediction/docs/reference/v1.6/,在GET  /project/trainedmodels/id/analyze 就是拿專案某id的訓練模型來做....analyze。
Parameter的使用方式
parameter是拿來放filter條件或是Sort條件,state=是filter條件,sort=是sort條件
GET /tickets?state=closed&sort=-updated_at
更複雜的運用就是放Search Criteria

總是使用SSL
使用SSL連線可以免去每個Request都要作認證授權的麻煩,最重要的是安全性!我們無法預期用戶會在什麼地方呼叫我們的API,最好最省時間的方式就是全部都用SSL連線!Https早就有這個基礎設施。雖然最近有個HeartBleed的案件,但是還是應該使用,真的中鏢再積極補救。

總是使用gzip
可以省掉60%的頻寬,而Twitter自己作的研究極端的Case可以省80%的頻寬,另外就是用gzip就不用擔心回傳格式排版,可以排成肉眼方便讀取的格式,也不會佔空間。

Error Code的定義
Http協定本身就已經定義了Error code以及其所代表的意義,規劃RESTful API時一般都是會按照Http協定的定義,來將應用程式的錯誤映射到Http Error Code:
4xx:代表Client傳來的資料有問題,可能是格式錯誤,欄位錯誤等。
5xx:代表Server內部處理時有問題,可能是連不到資料庫,程式處理錯誤等等,Client可以嘗試用同樣的Request再發一次。

把應用程式自己的錯誤碼映射到Http Error Code的理由是符合Client的預期,Client就算不看任何文件,光是看Http Error Code就可以知道是自己錯還是Server錯

至於Error code 500則是有不同意見,一種意見是如果Server有錯,不知道要發什麼就發500。一是完全不要發500,以免有人惡意攻擊。












沒有留言:

張貼留言

DevOps Lessons Learned at Microsoft Engineering 筆記

原文: https://www.infoq.com/articles/devops-lessons-microsoft 筆記 組織 講Microsoft裡面的DevOps 故事描述的是Cloud & Enterprise and the Bing ...