我們對項目模組進行了一定程度的微服務化改造,之前所有模組都放在一個項目裡(一個大檔案夾),線上部署也一樣,這樣的缺點顯而易見。 後面我們按照業務功能拆分成一個個的子模組,然後子模組之間通過RPC架構進行訪問,各個子模組有各自獨立的線上機器叢集、mysql及redis等儲存資源,這樣一個子模組出問題不會影響到其它模組,同時可維護性,擴充性更強。
但現實中每個子模組的服務能力是不同的, 如按子模組拆分之後的架構圖所示,假設到達A模組的QPS為100,A依賴於B,同時每一個A模組到達B模組的請求QPS也為100, 但B模組所能提供的最大QPS能力為50, 如果不進行流量限制,則B模組因為超過負載而流量堆積導致整個系統不可用,我們的動態流量控制系統就是找到子模組的最佳服務能力,即限制A模組到達B模組的流量為50QPS,則至少保證一部分請求是能夠正常進行的,而不會因為一個子服務掛掉而拖跨整個系統。
我們的RPC架構是一個PHP實現的架構,主要支援http協議的訪問。對於一個前端A模組來說,對於依賴於的後端B模組, 需先對B模組進行服務化配置,再按服務名字進行引用訪問,服務配置一般形式如下:
[MODULE-B] ; 服務名字protocol = "http" ;互動協議lb_alg = "random" ; 負載平衡演算法conn_timeout_ms = 1000 ; 連線逾時,所有協議使用, 單位為ms read_timeout_ms = 3000 ; 讀逾時write_timeout_ms = 3000 ; 寫逾時 exe_timeout_ms = 3000 ; 執行逾時host.default[] = "127.0.0.1" ; ip或網域名稱host.default[] = "127.0.0.2" ; ip或網域名稱host.default[] = "127.0.0.3" ; ip或網域名稱port = 80 ; 連接埠domain = 'api.abc.com' ; 網域名稱配置,不作真正解析,作為header host欄位傳給後端
對於要訪問的一個服務模組,部署上一般是一個叢集,我們需要配置機器叢集的所有IP,當然,如果有內部DNS服務,也可以配上叢集的網域名稱。
對於一個RPC架構來說,基本的功能有負載平衡、健全狀態檢查、降級&限流等,我們的流量控制即針對降級&限流功能,在詳細介紹它之前,先說說負載平衡與健全狀態檢查是如何?的,這是流量控制實現的基礎。
負載平衡我們實現了隨機與輪詢演算法,隨機演算法通過在所有IP中隨機選一個即可,比較容易實現,對於輪詢演算法,我們是基於單機輪詢,將上一個選擇的IP序號利用apcu擴充記錄在本地記憶體中,以方便找到下一個要使用的IP序號。
被訪問的機器可能會失敗,我們將失敗的請求IP記錄在redis中,同時分析記錄的失敗日誌來決定是否需要將一個機器IP摘除,即認為這個IP的機器已經掛掉,不能正常提供服務了,這就是健全狀態檢查的功能,我們通過相關服務配置項來介紹下健全狀態檢查的具體功能:
ip_fail_sample_ratio = 1 ; 採樣比例失敗IP記錄採樣比例,我們將失敗的請求記錄在redis中,為防止太多的redis請求,我們可以配一個失敗採樣比例ip_fail_cnt_threshold = 10; IP失敗次數ip_fail_delay_time_s = 2 ; 時間區間ip_fail_client_cnt = 3 ; 失敗的用戶端數不可能一個IP失敗一次就將其從健康IP列表中去掉,只有在有效ip_fail_delay_time_s 時間範圍內,請求失敗了 ip_fail_cnt_threshold 次,並且失敗的用戶端達到ip_fail_client_cnt 個, 才認為其是不健康的IP。 為什麼要添加 ip_fail_client_cnt 這樣一個配置,因為如果只是某一台機器訪問後端某個服務IP失敗,那不一定是服務IP的問題,也可能是訪問用戶端的問題,只有當大多數用戶端都有失敗記錄時才認為是後端服務IP的問題我們將失敗日誌記錄在redis的list表中,並帶上時間戳記,就比較容易統計時間區間內的失敗次數。ip_retry_delay_time_s = 30 ; 檢查失敗IP是否復原間隔時間某個失敗的IP有可能在一定時間內恢複,我們間隔 ip_retry_delay_time_s 長的時間去檢查,如果請求成功,則從失敗的IP列表中去除ip_retry_fail_cnt = 10; 失敗IP如果檢查失敗,記錄的失敗權重值ip_log_ttl_s = 60000; 日誌有效期間時間一般來說只有最近的失敗日誌才有意義,對於曆史的日誌我們將其自動刪除。ip_log_max_cnt = 10000; 記錄的最大日誌量我們用redis記錄失敗日誌,容量有限,我們要設定一個記錄的最大日誌數量,多餘的日誌自動刪除。
在我們的代碼實現中,除了正常的服務IP配置,我們還維護了一個失敗IP列表,這樣通過演算法選IP時先要去掉失敗IP,失敗IP記錄在一個檔案中,同時利用apcu記憶體緩衝加速訪問,這樣我們所有的操作基本是基於記憶體訪問的,不會有效能問題。
我們只有在請求失敗時才會將日誌記錄在redis中,那在什麼時候將失敗的IP找出來呢,這涉及到查詢redis list列表中所有的失敗日誌,同時統計失敗個數,是一個較複雜的操作。我們的實現是多個PHP進程搶佔鎖的方式,誰搶到了就執行分析操作,記錄失敗的IP到檔案中。因為只有一個進程會執行分析操作,所以對正常請求不會有什麼影響。 同時只有在失敗時才會有搶佔鎖的動作,正常情況下基本不會與redis有任何互動,沒有效能損耗。
我們的健全狀態檢查依賴於一個中心化的redis服務,如果它掛了怎麼辦?如果判斷redis服務本身掛掉了,rpc架構會自動關閉健全狀態檢查服務, 不再與redis互動,這樣至少不會影響正常的RPC功能。
在健全狀態檢查實現的基礎上我們可以實現流量控制,即當我們發現大部分或全部IP失敗時,我們可以推斷是因為流量過大導致後端服務響應不過來而請求失敗,這時我們就應該以一定策略限流,一般的實現是直接將流量全部摘除,這有點粗暴,我們的實現是逐步減少流量,直至失敗的IP比例降到一定數值,後面又嘗試逐步增加流量,增加與減少可能是一個迴圈的過程,即是動態流量控制,最終我們會找到一個最佳的流量值。通過相關配置來介紹一下流量控制的功能:
degrade_ip_fail_ratio = 1 ; 服務開始降級時失敗IP比例即失敗的IP比例達到多少時開始降級,即開始減少流量degrade_dec_step = 0.1 ; 每次限流增加多少即每次減少多少比例的流量degrade_stop_ip_ratio = 0.5; 在失敗的IP已降到多少比例時開始停止減少流量,並嘗試增加流量degrade_stop_ttl_s = 10;停止等待多長時間開始嘗試增加流量degrade_step_ttl_s = 10流量增加或減少需要等待的時間。每一次流量增加或減少後,下一步如何做是根據當時失敗的IP比例來決定的,而且會保持當前流量值一段時間,而不是立即做決定。degrade_add_step = 0.1每次增加流量增加的比例值degrade_return = false ; 降級時傳回值降級時我們不會再去訪問後端服務,而是直接給調用方返回一個配置的值。
流量控制的狀態圖描述如下:
如何?控制流程量在一定比例呢? 通過隨機播放,比如獲得一個隨機數並判斷是否落在某個範圍內。 通過限制流量在一個最佳值,在影響最少的使用者情況下讓大部分請求能正常工作,同時流量控制配合監控警示,發現某個模組的流量控制比例在1以下,說明相關模組已是系統的瓶頸,下一步就應該增加硬體資源或者最佳化我們的程式效能了。