【案例分析】分布式系统的接口幂等性设计!
概念
冪等性, Idempotence, 這個詞來源自數學領域, 百科 上一元運算的冪等性解釋如下:設 f 為一由 {x} 映射至 {x} 的一元運算, 則 f 為冪等的, 當對于所有在 {x} 內的 x: ?f(f(x)) = f(x) 。特別的是,恒等函數一定是冪等的,且任一常數函數也都是冪等的。
冪等性衍生到軟件工程中, 它的語義是指: 函數/接口可以使用相同的參數重復執(zhí)行, 不應該影響系統(tǒng)狀態(tài), 也不會對系統(tǒng)造成改變 .
一個簡答的例子:?查詢接口 GetFoo(), 不管調用多少次, 都不會破壞當前的系統(tǒng)/內存, 這就是一個冪等操作. 當然, 系統(tǒng)內部產生的日志這些細節(jié)不要在意.
在 HTTP/1.1 規(guī)范中, 冪等性有類似的明確定義: ?Methods can also have the property of “idempotence” in that (aside from error or expiration issues) the side-effects of N > 0 identical requests is the same as for a single request.
從語義上不難看出, HTTP GET 是一個清晰的冪等操作, HTTP DELETE/POST 是非冪等的, HTTP PUT 也是冪等的, 因為對同一個 URI 進行多次 PUT 的 side-effetcs 是一致的.
在分布式系統(tǒng)中, 由于分布式天然特性的時序問題, 以及網絡的不可靠性(機器、機架、機房故障, 電纜被挖斷等等), 重復請求很常見, 接口冪等性設計就顯得尤為重要 .
案例分析
舉一個游戲領域中的案例:
玩家 Jack 花費點券購買道具, 調用后端 shop_svr 集群的 rpc 接口 buy_commodity(commodity_id) .?
由于網絡延遲, 或者系統(tǒng)負載比較高, shop_svr 沒來得及返回, 總之, 第一次調用超時了沒返回.?
Jack 見一直木有反映, 又點了一次購買按鈕.?
網絡恢復了, shop_svr 連續(xù)收到兩次 buy_commodity(commodity_id) 請求.?
好吧, Jack 本來只想花 100 點券買個小喇叭, 系統(tǒng)硬是讓他買了倆, 難怪都說 XX 游戲坑錢了……?
上面錯誤的示例只是扯個蛋, 咳咳…… 從這個問題中可以折射出幾點系統(tǒng)設計的問題:
buy_commodity() 接口不符合冪等性 , 當重復操作時, 對整個系統(tǒng)產生了影響, 玩家 A 被多扣了點券, 在網游業(yè)務中, 一旦涉及到錢這種敏感數據, 往往就不妙了.
shop_svr 的消息處理做的不夠完善, 當它收到延遲了許久的消息時, 應該及早拒絕, 返回失敗, 不僅是為了避免重復調用, 更重要的是保證 shop_svr 不會過載而導致整個系統(tǒng)雪崩 (不過這又是另外個話題, 不在此贅述).
那么,怎么完善 buy_commodity() 接口的冪等性呢?
借鑒銀行等金融系統(tǒng)的做法, 引入 票據 (token) 是個不錯的主意:
Jack 花費點券購買道具, 先到 shop_svr 中去申請交易票據 token.?
shop_svr 生成唯一 token, 并記錄到 DB.?
Jack 拿到 token, 調用接口 buy_commodity(token, commodity_id) 購買.?
由于網絡延遲, 或者系統(tǒng)負載比較高, shop_svr 沒來得及返回, 總之, 第一次調用超時了沒返回.?
Jack 重試購買, 仍然調用接口 buy_commodity(token, commodity_id) .?
shop_svr 收到第一次 buy_commodity() 請求, 驗證 token 之后完成購買行為,再將 token 標記為已執(zhí)行, 這是個 原子行為 .?
shop_svr 收到第二次 buy_commodity() 請求, 驗證 token 失敗, 丟棄消息.?
票據 (token) 機制, 保證了 buy_commodity() 接口的冪等性 , 同樣的請求, 并不會對系統(tǒng)造成額外的 side-effects, 即多次調用預期保持一致, 問題解決!
PS: 按照上面的描述, DB 層保證 “驗證 token”, “加道具扣點券”, “標記 token” 這三步操作的原子性, 這并不是一個很容易的事情
所以實際中往往妥協(xié)為: 先 “驗證并標記 token” , 再 “加道具扣點券” 這兩步操作:
第一步操作可以通過 SQL 的條件更新, 或者帶版本號寫(部分 NoSQL 支持)來實現, 這是冪等性操作.?
如果第一步成功, 第二步失敗, 可以直接認為操作失敗, 但并不會破壞接口的冪等性.?
大部分的網游服務器, 是極其注重數據強一致性的, 但能容忍一定的可用性缺失.?
例如: 玩家能接受每周的例行停服維護時間, 能接受某次點擊服務器返回失敗, 但是很難接受數據被篡改乃至回檔, 這也是上面 DB 操作可以妥協(xié)的根本原因.
擴展
But, 問題真的完美解決了么?
再擴展一下上面的例子, 現在游戲火了, 為了響應迅速增大的并發(fā)請求, 游戲服務都做了擴展, 無狀態(tài)的 shop_svr 也平行擴展為一個集群
玩家的每次 buy_commodity() 請求都被負載均衡器路由到不同的 shop_svr 處理, 以 平攤系統(tǒng)負載 , 一切都看上去很好.
Jack 吃了一個禮拜泡面終于攢了 20000 點券, 準備買個”趙云-子龍”的皮膚, Jack 滿心期待的點下了”購買”按鈕, 額, 居然又沒反應… 點了幾下都如此
納悶兒的 Jack 順手點了下隔壁的”閉月之顏-貂蟬”皮膚, 彈窗提示:”購買成功”, 這…… Jack 哭了.
我們來回顧一下, 應該是如此的流程: 托分布式系統(tǒng)的福, 第二個請求 buy_commodity(token_2, “閉月之顏-貂蟬”) 后發(fā)而先至, 被優(yōu)先處理
當第一個請求 buy_commodity(pay_token_1, “趙云-子龍”) 在之后到達時, Jack 的點券已經被扣完了,扣完了……
這個問題跟冪等性本身無關, 從系統(tǒng)的行為來看, 也是符合強一致性 的, 只是在時序上沒能符合 Jack 的預期, 帶來了體驗上的心理落差.
解決之道:?
配置 shop_svr 集群前端的 負載均衡器 , 通過一定的 路由算法 保證 Jack 的請求消息路由到固定某個 shop_svr_j 上處理.
同時, 請求消息的傳遞通過 消息隊列 (TCP 也是個樸素的實現) 來 保證順序 , 這樣, Jack 先發(fā)的請求 request 1 一定在后發(fā)的請求 request 2 之前到達, 并被處理, 從而避免時序的影響.
總結
以上是生活随笔為你收集整理的【案例分析】分布式系统的接口幂等性设计!的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: zookeeper 都有哪些使用场景?
- 下一篇: 大剑无锋之二分搜索、二分搜索时间复杂度、