概述
微服務所使用的協議自然要根據服務的特點和類型來選擇
微服務類型 | 推薦協議 | 推薦理由 |
---|---|---|
Web Service | Restful via HTTP | 簡單實用, 應用廣泛 |
VoIP 及 Telephony Service | 信令用SIP, 媒體用RTP | 支持的終端和媒體網關眾多 |
多媒體流服務 Multimedia Stream Service | RTP/SRTP/RTSP | 基于傳輸延遲考慮 |
實時消息服務 Realtime Message Service | XMPP, PDU via TCP | XMPP 是開源的標準協議, 效率不高,手機應用不推薦 |
異步消息服務 Async Message Service | JMS/AMQP | ActiveMQ 用 JMS, RabbitMQ 用后者 |
這里說的協議主要是指應用層協議, 傳輸層協議一般都是TCP, 除非是媒體傳輸考慮用低延遲的 UDP
簡單來說, 一般的信令控制協議用基于 HTTP 的 REST 協議就夠了, 或者是基于 TCP /WebSocket 的用 Protobuf 來封裝應用層消息體也不錯.
媒體傳輸一般用 RTP 及 SRTP 或 RTSP 來承載音頻或視頻, 在多方會議共享及遠程控制應用中也常用如下協議
- BFCP -- Binary Floor Control Protocol 二進制層控制協議, 用來管理共享的資源
- RDP -- Remote Desktop Protocol, 遠程桌面協議, 微軟提出并使用在它的遠程桌面中
- RFB -- Remote Frame Buffer, 遠程幀緩沖協議, VNC(Virtual Network Computing )中使用的
REST
先從應用最廣的 REST 說起, REST (Representational State Transfer) 可表現的狀態遷移, 是2000年由 Roy Fielding 在他的關于REST的博士論文中提出的.
REST準確來說不算是一種協議, 而是一種設計分布式系統的架構風格, 它是指資源在網絡中以某種表現形式進行狀態轉移.
也就是說它是面向資源的, 每種資源都有相對應的URI, 每個URI 都指向一個資源, 而資源是可展現的(Representational ) 和有狀態的(state), 而HTTP 請求則是無狀態的, 即它不需要依賴其它的請求, 每個請求都是相對獨立的, 超媒體 Hypermedia 可以通過 鏈接Link 和 URI 把資源連接起來, Web成功的秘訣也就是用鏈接把世界連接起來.
這里主要指用 HTTP 和 Json 承載的面向資源的 Restful 風格的協議.
由于HTTP協議比較簡單, 系統對外的接口被分為多個資源 API, 都可以獨立地進行測試, 并且符合無狀態通信的原則, 天然具有比較好的松耦合性和可伸縮性.
在介紹完它的特性之后, 我們就會明白它為什么會在分布式系統中大受歡迎
REST 的特點
- REpresentational State Transfer 可表現的狀態遷移
- Nouns, not verbs, in endpoints 在各端點中資源是名詞而非動詞
- All state the client needs is queryable 客戶端所需的所有狀態是可查詢到的
- Server has a complete picture of system state 服務端具有完整的系統狀態
- Particularly useful for intermittently-connected clients 對間斷性連接的客戶端特別有用
REST 的好處
簡單
HTTP + Json 地球人都知道,HTTP method 表示對于資源的 CRUD 簡單明了可伸縮
短連接,無狀態,易于橫向擴展松耦合
基于 URL 和 API 的協作,保持接口簡單,一致和穩定,避免產生復雜的網狀結構和閉環,耦合自然沒那么緊
REST 的風格
客戶-服務器(Client-Server)
通信只能由客戶端單方面發起,表現為請求-響應的形式。無狀態(Stateless)
通信的會話狀態(Session State)應該全部由客戶端負責維護。緩存(Cache)
響應內容可以在通信鏈的某處被緩存,以改善網絡效率。統一接口(Uniform Interface)
通信鏈的組件之間通過統一的接口相互通信,以提高交互的可見性。分層系統(Layered System)
通過限制組件的行為(即,每個組件只能“看到”與其交互的緊鄰層),將架構分解為若干等級的層。按需代碼(Code-On-Demand,可選)
支持通過下載并執行一些代碼(例如Java Applet、Flash或JavaScript),對客戶端的功能進行擴展。
REST 的特性
- 面向資源 Resource Oriented
要考慮合適的粒度, 可緩存性, 修改頻率和可變性 - 可尋址 Addressability
- 連通性 Connectedness
- 無狀態 Statelessness
- 統一接口 Uniform Interface
POST, GET, PUT, DELETE , PATCH, HEAD, OPTIONS, TRACE, Connect - 超文本驅動 Hypertext Driven
REST 的原則
- 它基于無狀態, 客戶端-服務器, 可緩存的通訊協議
- 資源以易于理解的目錄結構的URI 來公布
- 以JSON或XML形式傳輸來表示數據對象和屬性。
- 消息明確地使用了 HTTP 方法(例如,GET,POST,PUT和DELETE)。
- 在HTTP請求與請求之間的無狀態交互不在服務器上存儲客戶端上下文。
狀態依賴性限制了可擴展性, 所以在客戶端存儲會話狀態使得橫向擴展更加容易
用 HTTP 方法來表示 CRUD
格式為 [HTTP Method] https://host/{service}/{apiclass}/v{version}
HTTP 方法 | 含義 | 冪等嗎? |
---|---|---|
POST | 創建資源 Create | N |
GET | 獲取或查詢資源 Retrieve | Y |
PUT | 全部替換資源 Update | Y |
DELETE | 刪除資源 Delete | Y |
PATCH | 部分修改資源 | N |
HEAD | 類似于 GET, 但是只傳輸狀態行和 HTTP 頭 | Y |
OPTIONS | 描述目標資源的通信選項 | Y |
TRACE | 執行沿目標資源路徑的消息環回測試。 | Y |
CONNECT | 建立到由給定URI標識的服務器的隧道 | Y |
所謂冪等性 Idempotence, 它的意思是你調用一次和調用多次的效果是一樣的
簡單列舉一下一些在 REST 中常用的 Http header
常見的 Http 頭域
Header name | Header value example | 備注 |
---|---|---|
Accept | application/json | Respond 406 not acceptable if not support the format |
Content-Type | application/json | 媒體內容類型, |
If-Modified-Since | Respond 304 not modified if the data is not changed | |
If-None-Match | Respond 304 not modified if the data is not changed | |
If-Match | 412 precondition failed if the ETag is not matched | |
ETag | The version of the resource for integrity | |
Location | 201 response contains it contains the URI of the new created resource |
還有一些擴展頭:
X-Forwarded-For
HTTP 請求到達 HTTP Server 的時候往往已經過了反向代理服務器,所以這時候看到的 TCP 源地址已經不是真正的客戶端應用的地址了,這個擴展頭就是代理服務器所添加的真正的 source IP 地址, 它由 https://tools.ietf.org/html/rfc7239 定義
比如在 Citrix 的負載均衡器 netscaler 可以這樣配置, 參見insert client ip to http header
set service Service-HTTP-1 -CIP enabled X-Forwarded-For
Origin 和 Access-Control-Allow-Origin
現代瀏覽器允許突破同源策略(Same Origin Policy), 使用稱為跨域資源共享 CORS(Cross-Origin Resource Sharing), 微軟的 IE8/9 并不支持,需要用 XDomainRequest 替換 XHTTPRequest
例如請求頭如下,表示請求源自哪里:
Origin: https://www.example.com
響應頭有
Access-Control-Allow-Headers: AppId, MetricsTicket, ConfID, SiteID, TimeStamp, APPName, Ver
Access-Control-Allow-Methods: OPTIONS, POST, PUT
Access-Control-Allow-Origin: https://www.example.com
這樣一來, XHTTPRequest 對 www.example.com 的訪問就是合法的。
X-RateLimit-Limit
現在許多 public API 都限定了客戶端的請求頻率, 比如 twitter, github 等,在響應頭中有如下擴展頭:
- X-RateLimit-Limit: 單位時間的訪問上限
- X-RateLimit-Remaining: 剩余的訪問次數
- X-RateLimit-Reset 訪問次數重置的時間
常見的 Http 狀態碼
2xx
- 200 OK with Etag head
- 201 Created with Location head
- 204 No content
- 206 Partial content
3xx
- 301 Move Permanently
- 302 Found
- 304 Not Modified
4xx
- 401 Unauthorized with WWW-Authorizate head
- 403 Forbidden
- 404 Not Found
- 405 Not Allowed with Allow head
- 406 Not Acceptable
- 409 Conflict
- 410 Gone
- 412 Precondition Failed
- 413 Request Entity Too Large
- 415 Unsupported Media Type
- 451 Unavailable For Legal Reasons
5xx
- 500 Internal Server Error
- 501 Not Implemented
- 502 Bad Gateway
- 503 Service Unavailable with Retry-After head
- 504 Gateway Timeout
URI 設計
REST 是面向資源的, 如何定位和尋找資源呢, 就象找人一樣, 資源也需要象人那樣的身份證號碼 URI
- URI - Uniform Resource Identifier 是指統一資源標識符, 包括 URL 和 URN
- URL - Uniform Resource Locator 是指統一資源定位符, 常見如下的web url , ftp url 等等
- URN - A Uniform Resource Name 是指統一資源名稱, 例如
- tel:+1-816-555-1212
在設計資源URI 的時候,
- 一是要注意它們是名詞,
- 二是要注意區分單復數
- 三是要注意 URI 有長度限制, 建議小于1k
- 四是要注意在 URI 中不要放未經加密的敏感信息, 即使使用TLS/HTTPS
我們可以用
/ 來表示層次關系, 例如
[http://api.t.sina.com.cn/groups/groupId/users/$userId-
;, 來表示并列關系, 例如
-
用 - 來提高可讀性, 最好全用小寫, 例如
-
用參數或者HTTP Range Header 來限定范圍, 例如
常用方法
緩存控制
我們可以利用一些 HTTP Header 來控制資源的緩存以及防止并發問題
- ETag 實體標簽: 一般為資源實體的哈希值
- Expires 過期的時間:
Expires: Thu, 01 Feb 2015 17:00:00 GMT
- Cache-Control 可以有如下屬性
- public 公有緩存
- private 私有緩存
- no-cache 不可緩存
- max-age 緩存的最大時間, 單位為秒, 一般來說 max-age是相對時間, 比 Expires 的絕對時間要好, 不會有客戶端和服務器時間誤差的問題, 優先使用它
- Age 緩存了多少秒
- Last-Modified 資源的最后修改時間
- If-None-Match 如果不匹配的話
- If-Modified-Since 從何時起資源有更新
- If-Match 如果匹配的話
- if-Unmodified-Since 從何時起資源無更新
當服務器發現Http請求的 Header 中有 If-None-Match, 就取出它的值, 與緩存中的資源的Etag 比較, 如果一致的話, 返回 304 Not Modified, 節省從數據庫查詢和網絡傳輸成本
當服務器發現Http請求的 Header 中 If-Modified-Since, 就取出它的值, 與緩存中的資源的Last-Modified 比較, 如果 If-Modified-Since中指示的最后修改時間大于或等于資源的Last-Modified時間的話, 也返回 304 Not Modified, 即它是從資源最后一次修改之后獲取的, 最近無更改, 無需重新查詢
當然, 如果不一致的話, 則得重新查詢數據庫并刷新緩存, 返回最新的資源信息, 狀態為 200 OK
并發控制
如果多個請求對資源進行修改, 會出現丟失修改或者無效刪除的情況
試想, 張三和李四都是公司的會計, 張三管考勤, 發現王二上個月遲到了三次, 要扣王二三百元錢, 李四管績效, 要給王二增加一千元獎金, 假設王二工資為八千元.
張三修改王二工資為 8000 - 300 = 7700
update payroll set salary=7700 where username="wang2" and salary=8000
李四修改王二工資為 8000 + 1000 = 9000
update payroll set salary=9000 where username="wang2" and salary=8000
強一致性的關系數據庫使用行級鎖, 張三和李四只有一個會成功, 另一個會修改失敗, 返回給其中一個用戶412錯誤, 讓用戶重新修改. 從而使王二的最終薪水為8000-300+1000=8700
一些不支持強事務的NOSQL存儲, 特別是一些KV系統只能根據key - username來修改數據, 就極有可能出現張三和李四都返回成功, 王二工資變成了7700或9000, 而不是正確的8700, 這時候我們就可以用下面的方法來減少這種情況的發生.
- 更新數據
當服務器發現Http Header 中有 If-Match, 就取出它的值, 與當前資源的Etag 比較, 如果一致的話, 修改數據返回200 OK, 否則返回 412 Precondition Failed
當服務器發現Http Header 中有if-Unmodified-Since, 就取出它的值, 與當前資源的 Last-Modified 進行比較, 如果發現if-Unmodified-Since值大于或等于Last-Modified資源的的話, 修改數據返回200 OK, 否則返回 412 Precondition Failed
- 刪除數據
當服務器發現Http Header 中有 if-Match, 就取出它的值, 與當前資源的Etag 比較, 如果一致的話,刪除數據返回 204 No Content, 否則返回 412 Precondition Failed
當服務器發現Http Header 中有 if-Unmodified-Since, 就取出它的值, 與當前資源的 Last-Modified 進行比較, 如果發現if-Unmodified-Since值大于或等于Last-Modified資源的的話, 修改數據返回 204 No Content, 否則返回 412 Precondition Failed
批量處理
例如我們想一次提交多個請求, 可以用這種方法
Request
POST /api/v1/batch
{
"requests": [
{
"method": "POST",
"path": "/phonenumbers",
“headers”: [ {“Content-Type”: “application/json”}]
"body": {
"number": "86-01012345678",
"type": "mobile"
}
},
{
"method": "POST",
"path": "/telephonydomains/$telephonyDomainID/dialnumbers",
"body": {
"number": "86-01022345678",
"type": "office"
}
}
]
}
Response
HTTP/1.1 200 OK
{
“response” [
{
“status”: 200,
“message”: “OK”
“headers”: [ {“Content-Type”: “application/json”}]
“body”: {}
},
{
“status”: 412,
“message”: “Preconditionl Failed”
“body”: {}
}
]
}
查詢條件超長或者查詢參數有敏感信息
用 POST 來代替 GET , 意謂創建一個查詢
Request:
POST /accounts/queries
{
“userIds”: [111, 222, 333]
}
Response:
HTTP/1.1200 OK
[
…accounts …
]
異步請求
與同步請求不同的是, 不是立即返回結果, 而是先給一個 taskId, 可供稍后查詢結果, 或者在請求時給一個回調URL, 稍后把結果通知回去
Request
POST https://abc.cde.com/api/v1.0/migrations HTTP 1.1
{
pool: "china",
notifyUri: 'https://abc.cde.com/api/v1/migrations/123'
}
Response
{
"status": 'pending',
"taskID": 'f47ac10b-58cc-4372-a567-0e02b2c3d479'
}
實例: 帳號管理的微服務
光說不練假把式, 先拿python 來寫一個微服務原型, 我們平常會使用諸多網站, 帳號密碼經常忘記, 所以讓我們花一點時間寫一個帳號管理的微服務, 基本功能是記錄我們常用的帳號和密碼, 以免遺忘, 一切從簡, 不用id, 而是用sitename 作為主鍵
method | description |
---|---|
GET /accounts | 帳戶列表 |
GET /accounts/<siteName> | 帳戶獲取 |
POST /accounts | 帳戶創建 |
PUT /accounts/<siteName> | 帳戶修改 |
DELETE /accounts/<siteName> | 帳戶刪除 |
- 客戶端用 httpie 來作測試
- 服務器端用 python flask 框架來實現
- 頁面的UI 暫且省略
先安裝python 和 virtualenv
brew install python
brew install pyenv-virtualenv
or
sudo pip install virtualenv
再運行 virtual env
virtualenv venv
source venv/bin/activate
再安裝所需的類庫
pip install flask
pip install flask-httpauth
pip install requests
pip install httpie
為簡單起見, 用 json 文件代替數據庫: account.json
{
"jianshu":{
"userName": "walterfan",
"password": "password",
"siteName": "jianshu",
"siteUrl": "http://www.lxweimin.com/users/e0b365801f48"
},
"weibo":{
"userName": "fanyamin",
"password": "password",
"siteName": "weibo",
"siteUrl": "http://weibo.com/fanyamin"
}
}
源碼如下, 不算空行, 100行之內搞定: account.py, 可讀寫json file, 并對其中的記錄進行增刪改查, 暫不考慮性能和其他異常及并發處理, 差強人意, 僅供演示, 個人日常使用也行
import os
import json
import requests
from flask_httpauth import HTTPBasicAuth
from flask import make_response
from flask import Flask
from flask import request
from werkzeug.exceptions import NotFound, ServiceUnavailable
app = Flask(__name__)
current_path = os.path.dirname(os.path.realpath(__file__))
auth = HTTPBasicAuth()
users = {
"walter": "pass1"
}
json_file = "{}/account.json".format(current_path)
def read_data():
json_fp = open(json_file, "r")
return json.load(json_fp)
def save_data(accounts):
json_fp = open(json_file, "w")
json.dump(accounts, json_fp, sort_keys = True, indent = 4)
@auth.get_password
def get_pw(username):
if username in users:
return users.get(username)
return None
def generate_response(arg, response_code=200):
response = make_response(json.dumps(arg, sort_keys = True, indent=4))
response.headers['Content-type'] = "application/json"
response.status_code = response_code
return response
@app.route("/", methods=['GET'])
@auth.login_required
def index():
return generate_response({
"username": auth.username(),
"description": "account micro service /accounts"
})
@auth.login_required
@app.route("/accounts", methods=['GET'])
def list_account():
accounts = read_data()
return generate_response(accounts)
#Create account
@auth.login_required
@app.route('/accounts', methods=['POST'])
def create_account():
account = request.json
sitename = account["siteName"]
accounts = read_data()
if sitename in accounts:
return generate_response({"error": "conflict"}, 409)
accounts[sitename] = account
save_data(accounts)
return generate_response(account)
#Retrieve account
@auth.login_required
@app.route('/accounts/<sitename>', methods=['GET'])
def retrieve_account(sitename):
accounts = read_data()
if sitename not in accounts:
return generate_response({"error": "not found"}, 404)
return generate_response(accounts[sitename])
#Update account
@auth.login_required
@app.route('/accounts/<sitename>', methods=['PUT'])
def update_account(sitename):
accounts = read_data()
if sitename not in accounts:
return generate_response({"error": "not found"}, 404)
account = request.json
print(account)
accounts[sitename] = account
save_data(accounts)
return generate_response(account)
#Delete account
@auth.login_required
@app.route('/accounts/<sitename>', methods=['DELETE'])
def delete_account(sitename):
accounts = read_data()
if sitename not in accounts:
return generate_response({"error": "not found"}, 404)
del(accounts[sitename])
save_data(accounts)
return generate_response("", 204)
if __name__ == "__main__":
app.run(port=5000, debug=True)
直接運行 python account.py
這個帳戶管理的RESTful 微服務就啟動了, 用 httpie 測試一下
- list accounts
(venv) $ http --json --auth walter:pass GET http://localhost:5000/accounts
HTTP/1.0 200 OK
Content-Length: 347
Content-type: application/json
Date: Sat, 10 Dec 2016 15:43:53 GMT
Server: Werkzeug/0.11.11 Python/3.5.1
{
"jianshu": {
"password": "password",
"siteName": "jianshu",
"siteUrl": "http://www.lxweimin.com/users/e0b365801f48",
"userName": "walterfan"
},
"weibo": {
"password": "password",
"siteName": "weibo",
"siteUrl": "http://weibo.com/fanyamin",
"userName": "fanyamin"
}
}
- create account
http --auth walter:pass --json POST http://localhost:5000/accounts userName=walter password=pass siteName=163 siteUrl=http://163.com
HTTP/1.0 200 OK
Content-Length: 108
Content-type: application/json
Date: Sat, 10 Dec 2016 15:48:59 GMT
Server: Werkzeug/0.11.11 Python/3.5.1
{
"password": "pass",
"siteName": "163",
"siteUrl": "http://163.com",
"userName": "walter"
}
- retrieve account
http --auth walter:pass --json GET http://localhost:5000/accounts/163
HTTP/1.0 200 OK
Content-Length: 108
Content-type: application/json
Date: Sat, 10 Dec 2016 15:49:21 GMT
Server: Werkzeug/0.11.11 Python/3.5.1
{
"password": "pass",
"siteName": "163",
"siteUrl": "http://163.com",
"userName": "walter"
}
- update account
http --auth walter:pass --json PUT http://localhost:5000/accounts/163 userName=walter password=pass123 siteName=163 siteUrl=http://163.com
HTTP/1.0 200 OK
Content-Length: 111
Content-type: application/json
Date: Sat, 10 Dec 2016 15:49:47 GMT
Server: Werkzeug/0.11.11 Python/3.5.1
{
"password": "pass123",
"siteName": "163",
"siteUrl": "http://163.com",
"userName": "walter"
}
- delete account
http --auth walter:pass --json DELETE http://localhost:5000/accounts/163
HTTP/1.0 204 NO CONTENT
Content-Length: 0
Content-type: application/json
Date: Sat, 10 Dec 2016 15:50:18 GMT
Server: Werkzeug/0.11.11 Python/3.5.