我們對專案模組進行了一定程度的微服務化改造,之前所有模組都放在一個專案裡(一個大資料夾),線上部署也一樣,這樣的缺點顯而易見。 後面我們按照業務功能拆分成一個個的子模組,然後子模組之間通過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以下,說明相關模組已是系統的瓶頸,下一步就應該增加硬體資源或優化我們的程式效能了。
相關推薦:
以上是PHP中RPC框架基於Redis實現流量控制系統的詳細內容。更多資訊請關注PHP中文網其他相關文章!