"1. 為什麼要分散式" "2. 分散式架構帶來的挑戰" "3. 提高可靠性的設計" "3.1 監控設計" "3.2 一致性設計" "3.3 重試設計" "3.4 熔斷設計" "3.5 限流設計" "3.6 降級設計" "4. 提高性能的設計" "4.1 緩存設計" "4.2 非同步設計" "4.3 ...
1. 為什麼要分散式
隨著信息化的推進, 不論是 PC 還是 mobile 的用戶量都在激增, 單機的性能和可靠性是不可能滿足用戶的增長需求的. 為什麼分散式? 簡單來說, 不用分散式, 沒有更好的方法來解決不斷增長的用戶訪問需求.
分散式是趨勢, 對分散式架構中的機制有所瞭解, 是實施分散式架構的前提. 本文先簡要介紹分散式架構中一些的重要的概念和機制, 後續會對其中的機制進行更細緻的講解, 並給出實現示例.
2. 分散式架構帶來的挑戰
實施分散式之後, 首先面對的就是服務數量的增加, 由此給 監控 和 調度 帶來了新的挑戰. 分散式的複雜性肯定比單機要高, 由此又帶來 可靠性 和 性能 上的挑戰.
3. 提高可靠性的設計
提高可靠性, 先得知道可靠性是怎麼計算的, 一般有 2 個指標 MTTF 和 MTTR
- MTTF: mean time to failure 平均故障前時間, 註意, 這裡是 故障前 時間, 不是 故障 時間
- MTTR: mean time to recovery 平均修複時間
系統可用性計算方法 MTTF/(MTTF+MTTR)
提高系統的可靠性, 有 2 種方式, 一種是做到沒有故障, 一種做到故障發生時, 系統依然能夠工作. 顯然第一種方式就像寫出沒有 BUG 的程式一樣不可能做到, 所以, 應該把故障也當成正常的業務邏輯來處理, 只要故障有了應對之策, 那麼對系統的損害就微乎其微, 至少損害是可控的.
3.1 監控設計
監控是可靠性的前提, 沒有監控, 無法在第一時間發現問題, 更別說預防問題的發生了. 監控也是分層的:
- 基礎層: CPU, 記憶體, 網路吞吐, 磁碟 等
- 中間層: nginx, redis, 消息隊列, 資料庫 等
- 應用層: HTTP 響應時間, 返回碼, API 調用鏈路, 客戶端訪問信息 等
3.2 一致性設計
一致性是單機應用改造成分散式之後, 首先面對的問題. 一致性有強一致性(ACID)和最終一致性(BASE) 2 種. 現實中, 要求強一致性的場景其實遠沒有我們想象的那麼多, 在分散式系統中, 很多時候只需要最終一致性(BASE)即可.
- ACID: 原子性(Atomcity), 一致性(Consistency), 隔離性(Isolation,又稱獨立性), 持久性(Durability)
- BASE: 基本可用(Basic Availability), 軟狀態(Soft-state), 最終一致性(Eventual Consistency)
ACID 是真正的強調一致性, BASE 其實是強調可用性
3.3 重試設計
分散式系統中, 存在很多服務之間的調用, 頻繁的互相調用中, 經常會發生些意料之前的間歇性錯誤. 有些錯誤在響應端是會自動恢復的, 所以請求端不用對每個錯誤都直接返回給客戶端, 有時需要再重試幾次, 等待響應端的恢復.
當然, 重試要看情況, 調用返回超時, 或者返回可以重試的錯誤(比如, 繁忙中, 維護中, 資源不足等), 那麼就可以重試幾次. 如果返回 http 503 等, 這些可能觸發了響應端的 BUG, 或者響應端服務已經不正常了, 就沒有必要重試了.
重試也有策略, 不是一味不停的反覆調用, 這樣可能會給響應端帶來更大的壓力, 讓其更難自動恢復. 重試的策略有多種, 根據實際情況, 可以讓重試的時間間隔越來越大, 或者對重試次數做限制.
3.4 熔斷設計
重試是請求端的機制, 熔斷是響應端的機制, 目的是為了防止響應端的進一步惡化. 熔斷是響應端保護自己的一種機制, 也就是在不堪重負的時候, 斷開自己和外部的聯繫, 防止進一步惡化(就像家裡保護電器的保險絲)
熔斷類似保險絲, 但也有不同, 它不僅僅只有 斷開 和 連通 2 種狀態, 還可以有 半開 的狀態, 也就是限制請求的流量, 只處理有限的請求, 直至服務恢復. 當響應端的熔斷斷開的時候, 應該通知請求端, 停止重試
3.5 限流設計
限流的目的是通過對併發訪問進行限速, 讓服務端能夠響應更多的請求, 不至於在峰值被壓死 限流的演算法有: 計數器方式, 隊列演算法(請求速度波動, 處理速度勻速), 漏斗演算法, 令牌桶演算法
3.6 降級設計
降級是在系統應對突發情況時, 降低損失的一個方案 主要的降級方式有:
- 降低一致性: 從強一致性變為最終一致性
- 停止次要功能: 停止訪問不重要的功能, 從而釋放出更多的資源
- 簡化功能: 把一些功能簡化掉. 比如, 簡化業務流程, 或是不再返回全量數據, 只返回部分數據
4. 提高性能的設計
採用分散式設計之後, 不僅不會降低性能, 反而在分散式環境下, 有了更多的手段來提高性能.
4.1 緩存設計
緩存可以有效的提高 I/O, 是提高性能的有效手段之一. 在分散式環境下, 除了性能, 緩存的策略還要考慮一致性的問題, 一般有 3 種常用策略:
- Cache Aside 更新模式 失效 先從 Cache 取數據, 沒有則從資料庫獲取, 成功後, 放入 Cache 命中 從 Cache 中取數據, 然後返回 更新 先把數據存入資料庫中, 成功後, 讓緩存失效
- Read/Write Through 更新模式, 與 Cache Aside 相比, 應用不用再關心數據放在緩存還是資料庫中了 Read Through 查詢時如果未命中, 則更新緩存, 但是更新緩存的動作由緩存服務來完成, 應用本身不用關心 Write Through 如果未命中, 直接更新資料庫, 然後返回. 如果命中, 則更新緩存, 然後由緩存服務自己去更新資料庫
- Write Behind Caching 更新模式 在更新數據的時候,只更新緩存,不更新資料庫,而我們的緩存會非同步地批量更新資料庫 因為 I/O 是非同步處理的, 所有更新操作會非常快, 但是帶來的問題是, 數據不是強一致性的, 有數據丟失的風險
緩存的設計中, 要考慮爬蟲的影響, 因為爬蟲有可能會改變緩存數據的優先順序, 讓真正有用的數據被緩存清除.
4.2 非同步設計
同步調用 影響吞吐量, 消耗系統資源, 只能一對一, 有多米諾骨牌效應, 而 非同步調用 能有效提高系統的吞吐量. 採用非同步設計, 服務解耦之後, 一定要有 監控, 否則出了問題無法及時對應.
非同步調用的方式主要有:
- 請求響應方式: 給請求中加入回調, 響應結束後出發回調
- 訂閱方式: 接收方訂閱發送方的消息, 收到消息就放入處理隊列中, 逐步處理
- Broker 方式: 接收方和發送方互相看不到對方, 發送方發送消息到 broker, 接受方從 broker 中獲取消息來處理, 這種方式的好處是將服務徹底解耦
非同步處理的事務一致性一般不是強一致性, 而是最終一致性, 非同步處理可以和事件溯源(Event Sourcing)結合, 非同步處理 + 事件溯源的方式,可以很好地讓我們的整個系統進行任務的統籌安排、批量處理,可以讓整體處理過程達到性能和資源的最大化利用
4.3 資料庫設計
分散式環境中, 不論是 緩存, 還是 非同步, 最終數據還是要持久化到資料庫中, 如果資料庫慢的話, 前面做的再好也效果有限. 提高資料庫性能的方式主要有:
- 讀寫分離(CQRS): 將 Command 和 Query 分開, 如果 Command 操作變為 Event Sourcing, 就可以把寫操作也簡化掉, 也變成無狀態的, 大幅降低寫操作的副作用, 以得到更大的併發和性能
- 分庫分表(Sharding): 一般來說, 資料庫最大的性能問題有 2 個, 一個是對資料庫的操作, 一個是資料庫中數據的大小 分庫的策略可以按地理位置, 按日期或者某個範圍分, 或是按一種哈希散列演算法. 資料庫分片必須考慮業務, 從業務的角度入手, 而不是從技術的角度入手 只考慮業務分片, 不要考慮哈希散列的方式分片
- 數據分片有水平分片和垂直分片 2 種, 水平分片就是分庫, 垂直分片則是分表, 把一張表中的一些欄位放到一張表中,另一些欄位放到另一張表中
5. 分散式架構的部署
分散式環境下, 部署升級的方式也和單機應用不一樣, 更加靈活, 主要有:
- 停機部署: 現有服務停機, 部署新版本之後再啟動
- 藍綠部署: 分別有 stage/prod 2 套環境, 升級 stage 之後, 將流量切換到 stage, stage 變為 prod, prod 作為下一次升級的 stage 使用物理機的話, 2 套環境會有資源浪費, 虛擬機的話隨時回收會好些. 如果服務中有狀態的話, 比如緩存之類的, 那麼停機部署和藍綠部署都會有問題
- 滾動部署: 逐步替換應用的所有實例, 來緩慢發佈一個新版本, 這種方式對於有狀態的服務也是比較友好的 這種方式的問題是新舊版本同時線上, 如果有問題, 回滾麻煩, 而且 2 個版本同時在會帶來相容性的問題
- 灰度部署(金絲雀部署): 將生產環境的流量逐步切換到新環境, 可以按流量分配, 先切 10%, 沒有問題, 再加, 有問題回滾. 在多租戶環境, 也可以按租戶切換流量.
- A/B 測試: 這種方式同時上線 2 個版本, 然後比較可用性, 受歡迎程度, 可見性等 藍綠部署是為了不停機, 灰度部署是對新版本的質量沒信心. 而 A/B 測試是對新版的功能沒信心