close

互聯網+ 時代,業務數字化已經蔓延到你能想到的各個行業。各種業務功能、營銷玩法越來越多,系統也越來越複雜。

面對不斷複雜的業務系統,腦子越來越不夠用了。

於是聰明的人們提出了微服務的設計思想。

本着複雜的事情簡單化的原則,我們將一個大的系統拆分成若干個子系統,每個子系統職責單一。按 DDD 的設計理念,承載一個子域的業務建設。

於是,人們可以將精力聚焦,專心完成某一個業務點的深度建設。

多個微服務系統之間通過 RPC 框架(如 Dubbo、Spring Cloud、gRPC 等)完成了串聯,但隨着調用量越來越大,人們發現服務與服務之間的穩定性變得越來越重要。

舉個例子:

Service D 掛了,響應很慢;
Service G 和 Service F 都依賴 Service D 也會受到牽連,對外響應也會變慢;
影響層層向上傳遞,Service A 和 Service B 也會被拖垮;
最後引發雪崩效應,系統的故障影響面會越來越大。

為了解決這種問題,我們需要引入熔斷機制。「當斷則斷,不受其亂。當斷不斷,必受其難」。

什麼是熔斷?

熔斷,其實是對調用鏈路中某個資源出現不穩定狀態時(如調用超時或異常比例升高),對這個資源的調用進行限制,讓請求快速失敗。避免影響到其它的資源而導致級聯錯誤。

當資源被降級後,在接下來的降級時間窗口內,對該資源的調用都自動熔斷(默認是拋出 BlockException)。

目前市面上的熔斷框架很多,如 Sentinel、Hystrix、Resilience4j 等,這些框架的設計理念都差不多。

本文重點講下 Sentinel 是如何在項目中使用的。

Sentinel (分布式系統的流量防衛兵)是阿里開源的一套用於服務容錯的綜合性解決方案。它以流量為切入點,從流量控制、熔斷降級、系統負載保護等多個維度來保護服務的穩定性。

核心分為兩部分:

核心庫(Java 客戶端):能夠運行在所有 Java 環境,對 Dubbo 、Spring Cloud 等框架也有較好的支持。

控制台(Dashboard):基於 Spring Boot 開發,打包後可以直接運行。

Sentinel 熔斷種類:

RT 響應時間
異常數
異常比例
Sentinel 安裝

首先,官網下載 Sentinel 控制台安裝包。

下載地址:https://github.com/alibaba/Sentinel/releases

下載 jar 包後,打開終端運行命令:

java -Dserver.port=8180 -Dcsp.sentinel.dashboard.server=localhost:8180 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.8.1.jar

登錄 Sentinal 控制台。

默認用戶和密碼都是 Sentinel。登錄成功後的界面如下,先來個直觀感受。

控制台配置熔斷規則:

這裡表示熔斷策略選擇慢調用比例,響應時間超過 200 毫秒則標記為慢請求。如果在一個 1000ms 的統計周期內(可自行調整)。慢請求比例超過 30% 且數量超過 3 個,則對後續請求進行熔斷,熔斷時長為 10 秒鐘。10 秒以後恢復正常。

註解式接入

接入非常簡單,只需要提前在控制台配置好資源規則,然後在代碼中添加@SentinelResource 註解即可。

// 資源名稱為 handle1 @RequestMapping("/handle1")@SentinelResource(value = "handle1", blockHandler = "blockHandlerTestHandler")public String handle1(String params) { // 業務邏輯處理 return "success";}//接口方法handle1的兜底方法public String blockHandlerTestHandler(String params, BlockException blockException) { return "兜底返回";}

達到閾值後,系統的默認提示是一段英文,很不友好,我們可以自定義兜底方法。在 @SentinelResource 註解中進一步配置 blockHandler、fallback 屬性字段。

blockHandler:主觀層面,如果被限流或熔斷,則調用該方法,進行兜底處理.

fallback:對業務的異常兜底,比如,執行過程中拋了各種Exception,則調用該方法,進行兜底處理。


通過上面兩層兜底,可以讓 Sentinel 框架更加人性化,體驗更好。

注意:註解式開發,需要添加在方法上,作用域範圍相對固定。下面的項目實戰中,我們也可以採用顯式形式,可以靈活圈定代碼塊範圍。

項目實戰

我們這邊有個項目,考慮到客戶的部署成本,想做一個輕量級方案,需求如下:

既想引入框架的熔斷功能,又不想部署控制台。
攔截點相對收攏,類似 Dubbo 消費端遠程訪問一樣,在代理類的遠程通訊位置做攔截處理。

概要方案—流程圖

1) 通過 Proxy.newProxyInstance 為所有的接口創建了代理子類。

2) 所有對代理子類的方法調用全部收攏到 InvocationHandler。

3)將類名和方法名做一個拼接,然後去熔斷規則表查詢,看是否配置了規則。

4)如果沒有,那麼走常規則遠程調用邏輯。

5)如果有,將遠程調用邏輯納入 Sentinel 的監控管轄。

6)如果觸發了 熔斷機制,則直接拋出 BlockException ,上層業務攔截異常,做特殊處理,比如:修飾下給用戶更合適的文案提示。

熔斷狀態機

核心的代碼邏輯,請繼續往下看。

首先,引入 Sentinel 的依賴包:

<!-- 限流、熔斷框架 --><dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-core</artifactId> <version>1.8.3</version></dependency>

熔斷規則表設計:

CREATE TABLE `degrade_rule` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵', `resource_name` varchar(256) NOT NULL COMMENT '資源名稱', `count` double NOT NULL COMMENT '慢調用時長,單位 毫秒', `slow_ratio_threshold` double NOT NULL COMMENT '慢調用比例閾值', `min_request_amount` int NOT NULL COMMENT '熔斷觸發的最小請求數', `stat_interval` int NOT NULL COMMENT '統計時長,單位 毫秒', `time_window` int NOT NULL COMMENT '熔斷時長,單位為 s', `created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間', `updated_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改時間', PRIMARY KEY (`id`) USING BTREE, UNIQUE KEY `uk_resource_name` (`resource_name`)) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb3 COMMENT='熔斷規則表';

由於放棄了部署控制台,我們只能自己管理熔斷規則的各個屬性值。可以按企業內部管理後颱風格,開發頁面管理這些規則。

當然,早期可以採用更簡單粗暴方式,在數據庫表手動初始化數據。如果要調整規則,走 SQL 訂正。

為了儘可能實時感知規則表數據變更開發了定時任務,每 10 秒運行一次。

@Scheduled(cron = "0/10 * * * * ? ")public void loadDegradeRule() { List<DegradeRuleDO> degradeRuleDOList = degradeRuleDao.queryAllRule(); if (CollectionUtils.isEmpty(degradeRuleDOList)) { return; } String newMd5Hex = DigestUtils.md5Hex(JSON.toJSONString(degradeRuleDOList)); if (StringUtils.isBlank(newMd5Hex) || StringUtils.equals(lastMd5Hex, newMd5Hex)) { return; } List<DegradeRule> rules = null; List<String> resourceNameList = new ArrayList<>(); rules = degradeRuleDOList.stream().map(degradeRuleDO -> { //資源名,即規則的作用對象 DegradeRule rule = new DegradeRule(degradeRuleDO.getResourceName()) // 熔斷策略,支持慢調用比例/異常比例/異常數策略 .setGrade(CircuitBreakerStrategy.SLOW_REQUEST_RATIO.getType()) //慢調用比例模式下為慢調用臨界 RT(超出該值計為慢調用);異常比例/異常數模式下為對應的閾值 .setCount(degradeRuleDO.getCount()) // 熔斷時長,單位為 s .setTimeWindow(degradeRuleDO.getTimeWindow()) // 慢調用比例閾值 .setSlowRatioThreshold(degradeRuleDO.getSlowRatioThreshold()) //熔斷觸發的最小請求數,請求數小於該值時即使異常比率超出閾值也不會熔斷 .setMinRequestAmount(degradeRuleDO.getMinRequestAmount()) //統計時長(單位為 ms) .setStatIntervalMs(degradeRuleDO.getStatInterval()); resourceNameList.add(degradeRuleDO.getResourceName()); return rule; }).collect(Collectors.toList()); if (CollectionUtils.isNotEmpty(rules)) { DegradeRuleManager.loadRules(rules); ConsumerProxyFactory.resourceNameList = resourceNameList; lastMd5Hex = newMd5Hex; } log.error("[DegradeRuleConfig] 熔斷規則加載: " + rules);}

考慮到規則變更頻率不會很高,沒有必要每次都DegradeRuleManager.loadRules重新加載規則。這裡設計了個小竅門。

DigestUtils.md5Hex(JSON.toJSONString(degradeRuleDOList));

對查詢的規則內容 JSON 序列化,然後計算其 MD5 摘要,如果跟上一次的結果一致,說明這期間沒有變更,直接 return 不做處理。

定義子類,實現了 InvocationHandler 接口。通過 Proxy.newProxyInstance 為目標接口創建一個代理子類。

這樣,每次調用接口方法,實際都是在調用 invoke 方法。

@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Class<?> clazz = proxy.getClass().getInterfaces()[0]; String urlCode = clazz.getName() + "#" + method.getName(); if (resourceNameList.contains(urlCode)) { // 增加熔斷處理 Entry entry = null; try { entry = SphU.entry(urlCode); // 遠程網絡調用,獲取結果 responseString = HttpClientUtil.postJsonRequest(url, header, body); } catch (BlockException blockException) { // 觸發熔斷 log.error("degrade trigger ! remote url :{} ", urlCode); throw new DegradeBlockExcetion(urlCode); } finally { if (entry != null) { entry.exit(); } } } else { // 常規處理,不走熔斷判斷邏輯 // 省略 } }

實驗結果:

- EOF -

推薦閱讀點擊標題可跳轉

1、面試官:熔斷降級原理是什麼?

2、HttpClient 連接池設置引發的一次雪崩

3、東漢末年,他們把「服務雪崩」玩到了極致(乾貨)

看完本文有收穫?請轉發分享給更多人

關注「ImportNew」,提升Java技能

點讚和在看就是最大的支持❤️

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 鑽石舞台 的頭像
    鑽石舞台

    鑽石舞台

    鑽石舞台 發表在 痞客邦 留言(0) 人氣()