前言
在服務提供者不可用的時候,會出現大量重試的情況:使用者重試、程式碼邏輯重試,這些重試最終導致:進-步加大請求流量。 所以歸根結底導致雪崩效應的最根本原因是:大量請求執行緒同步等待造成
的資源耗盡。當服務呼叫者使用同步呼叫時,會產生大量的等待執行緒佔用系統資源。一-旦執行緒 資源被耗盡服務呼叫者提供的服務也將處於不可用狀態,於是服務雪崩效應產生了。
解決方案
穩定性(Reliability)、恢復性(Resilience)
常見的容錯機制:
- 超時機制
在不做任何處理的情況下,服務提供者不可用會導致消費者請求執行緒強制等待,而造成系統資源耗盡。加入超時機制,一旦超時,就釋放資源。由於釋放資源速度較快,一定程度上可以抑制資源耗盡
的問題。
- 限流機制
例如一個服務的 QPS 為500,當有 800 個 QPS訪問時,300 個 QPS 直接拒絕請求
- 隔離
原理:使用者的請求將不再直接訪問服務,而是透過執行緒池中的空閒執行緒來訪問服務,如果執行緒池已滿,則會進行降級處理,使用者的請求不會被阻塞,至少可以看到一個執行結果(例如返回友好的提示資訊),而不是無休止的等待或者看到系統崩潰。
- 訊號隔離
制併發訪問,防止阻塞擴散,與執行緒隔離最大不同在於執行依賴程式碼的執行緒依然是請求執行緒,該執行緒需要透過訊號申請,如果客戶端是可信的且可以快速返回,可以使用訊號隔離替換執行緒隔離,降低開銷。訊號量的大小可以動態調整,執行緒池大小不可以。
- 服務熔斷
`遠端服務不穩定或網路抖動時暫時關閉,就叫服務熔斷。`
現實世界的斷路器大家肯定都很瞭解,斷路器實時監控電路的情況,如果發現電路電流異常,就會跳閘,從而防止電路被燒燬。
軟體世界的斷路器可以這樣理解:實時監測應用,如果發現在一定時間內失敗次數/失敗率達到一 定閾值,就“跳閘”,斷路器開啟,此時,請求直接返回,而不去呼叫原本呼叫的邏輯。跳閘一段時間後(例如10秒),斷路器會進入半開狀態,這是一個瞬間態,此時允許一次請求呼叫該調的邏輯,如果成功,則斷路器關閉,應用正常呼叫;如果呼叫依然不成功,斷路器繼續回到開啟狀態,過段時間再進入半開狀態嘗試透過“跳閘”, 應用可以保護自己,而且避免浪費資源;而透過半開的設計,可實現應用的“自我修復”。
所以,同樣的道理,`當依賴的服務有大量超時時,在讓新的請求去訪問根本沒有意義,只會無畏的消耗現有資源`。比如我們設定了超時時間為1s,如果短時間內有大量請求在1s內都得不到響應,就意味著這個服務出現了異常,此時就沒有必要再讓其他的請求去訪問這個依賴了,這個時候就應該使用斷路器避免資源浪費。
- 服務降級
有服務熔斷,必然要有服務降級。
所謂降級,就是當某個服務熔斷之後,服務將不再被呼叫,此時客戶端可以自己準備一個本地的fallback(回退)回撥,返回一個預設值。例如: (備用介面/快取/mock資料)。這樣做,雖然服務水平下降,但好歹可用,比直接掛掉要強,當然這也要看適合的業務場景。
一、什麼是 Sentinel
Sentinel 是阿里巴巴開源的,面向分散式架構的高可用防護元件,是流量防衛兵。提供了多維度的流控降級能力,以不同的執行指標為基準,例如可以針對 QPS,併發執行緒數,異常,響應時間,系統負載等不同執行指標進行自動降級,排隊,快速失敗。同時也提供了秒級實時監控與動態規劃管理。
隨著微服務的流行,服務和服務之間的穩定性變得越來越重要。Sentinel 是面向分散式服務架構的流量控制元件,主要以流量為切入點從限流、流量整形、熔斷降級、系統負載保護、熱點防護等多個維度來幫助開發者保障微服務的穩定性。
Sentinel 具有以下特徵
- 豐富的應用場景: Sentinel 承接了阿里巴巴近10年的雙十一大促流量的核心場景,例如秒殺(即突發流量控制在系統容量可以承受的範圍)、訊息削峰填谷、實時熔斷下游不可用應用等。
- 完備的實時監控: Sentinel 同時提供實時的監控功能。您可以在控制檯中看到接入應用的單臺機器秒級資料,甚至500臺以下規模的叢集的彙總執行情況。
- 廣泛的開源生態: Sentinel 提供開箱即用的與其它開源框架/庫的整合模組,例如與Spring Cloud、Dubbo、 gRPC的整合。您只需要引入相應的依賴並進行簡單的配置即可快速地接入Sentinel。
- 完善的SPI擴充套件點: Sentinel 提供簡單易用、完善的SPI擴充套件點。您可以透過實現擴充套件點,快速的定製邏輯。例如定製規則管理、適配資料來源等。
Sentinel | Hystrix | |
---|---|---|
隔離策略 | 訊號量隔離 | 執行緒池隔離/訊號量隔離 |
熔斷降級策略 | 基於響應時間或失敗比例 | 基於失敗比率 |
實時指標實現 | 滑動視窗 | 滑動視窗(基於RxJava) |
規則配置 | 支援多種資料來源 | 支援多種資料來源 |
擴充性 | 多個擴充性 | 外掛的形式 |
基於註解的支援 | 支援 | 支援 |
限流 | 基於 QPS,支援基於呼叫關係的限流 | 有限的支援 |
流量整形 | 支援慢啟動、勻速器模式 | 不支援 |
系統負載保護 | 支援 | 不支援 |
控制檯 | 開箱即用,可配置規則,檢視秒級監控,機器發現等 | 不完善 |
常見框架的適配 | Servlet、SpringCloud、Dubbo、GRpc | Servlet、SpringCloud Netflix |
二、Sentinel 流控體驗
2.1、Sentinel 快速開始
引入依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--sentinel 核心庫-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-core</artifactId>
<version>1.8.4</version>
</dependency>
controller 層程式碼,定義資源,一般資源名稱和介面名稱保持一致,並且為資源設定流控規則
@RestController
@Slf4j
public class HelloController {
public static final String RESOURCE_NAME = "hello";
public static final String USER_RESOURCE_NAME = "user";
public static final String DEGRADE_RESOURCE_NAME = "degrade";
// 進行流控
@RequestMapping("/hello")
public String hello() {
Entry entry = null;
try {
// sentinel 針對資源進行限制的
entry = SphU.entry(RESOURCE_NAME);
// 被保護的業務邏輯
String str = "hello world";
log.info("======"+str+"======");
return str;
} catch (BlockException e) {
// 資源訪問阻止,被限流或降級
// 進行相應的處理操作
log.info("block!");
return "被流控了!";
} catch (Exception e) {
// 若需要配置降級規則,需要透過這種方式記錄業務異常
Tracer.traceEntry(e,entry);
} finally {
if (entry != null) {
entry.exit();
}
}
return null;
}
/**
* 定義規則
*
* spring 的初始化方法
*/
@PostConstruct // init method
public static void initFlowRules() {
// 流控規則
List<FlowRule> rules = new ArrayList<>();
// 流控
FlowRule rule = new FlowRule();
// 設定受保護的資源,為哪個資源進行流控
rule.setResource(RESOURCE_NAME);
// 設定流控規則,qps 每秒訪問數
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
// 設定受保護的資源閾值
// Set limit QPS to 1
rule.setCount(1);
rules.add(rule);
// 載入配置好的規則
FlowRuleManager.loadRules(rules);
}
}
請求http://localhost:8060/hello
,當請求過於頻繁時,被流控
2.2、@SentinelResource
上面的程式碼雖然實現了流控,但是我們可以發現,Sentinel 與程式碼的耦合性太強了,使用@SentinelResource
註解
1、引入依賴
<!--如果要使用 @SentinelResource-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-annotation-aspectj</artifactId>
<version>1.8.4</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</dependency>
2、設定 bean
// 註解支援的配置 Bean
@Bean
public SentinelResourceAspect sentinelResourceAspect() {
return new SentinelResourceAspect();
}
3、資源限流設定
@RestController
@Slf4j
public class HelloController {
public static final String RESOURCE_NAME = "hello";
public static final String USER_RESOURCE_NAME = "user";
public static final String DEGRADE_RESOURCE_NAME = "degrade";
/**
* 定義規則
* <p>
* spring 的初始化方法
*/
@PostConstruct // init method
public static void initFlowRules() {
// 流控規則
List<FlowRule> rules = new ArrayList<>();
// 透過 @SentinelResource 來定義資源並配置剪輯和流控的處理方法
FlowRule rule2 = new FlowRule();
// 設定受保護的資源
rule2.setResource(USER_RESOURCE_NAME);
// 設定流控規則 QPS
rule2.setGrade(RuleConstant.FLOW_GRADE_QPS);
// 設定受保護的資源的閾值
rule2.setCount(1);
rules.add(rule2);
// 載入配置好的規則
FlowRuleManager.loadRules(rules);
}
/**
* @SentinelResource 改善介面中資源定義和被流控降級後的處理方法
* 怎麼使用
* 1、新增依賴
* 2、配置Bean SentinelResourceAspect
* value 定義資源
* blockHandler 設定流控降級後的處理方法(預設該方法必須設定在一個同一個類中)
* 如果不想在同一個類中,可以設定 blockHandlerClass,並且方法必須是靜態方法
* fallback 當介面出現異常,就可以交給 fallback 指定的方法進行處理
*/
@RequestMapping("/user")
@SentinelResource(value = USER_RESOURCE_NAME, blockHandlerClass = User.class, blockHandler = "blockHandlerForGetUser")
public User getUser(String id) {
return new User("hudu");
}
}
4、pojo 類
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private String username;
/**
* 注意
* 1、一定要 public
* 2、返回值一定要和原方法保持一致,包含原方法的引數
* 3、可以在引數最後新增 BlockException 可以區分是什麼規則的處理方法
*/
public static User blockHandlerForGetUser(String id, BlockException ex) {
ex.printStackTrace();
return new User("流控!");
}
}
當訪問頻繁時,被流控了
fallback 測試
/**
* @SentinelResource 改善介面中資源定義和被流控降級後的處理方法
* 怎麼使用
* 1、新增依賴
* 2、配置Bean SentinelResourceAspect
* value 定義資源
* blockHandler 設定流控降級後的處理方法(預設該方法必須設定在一個同一個類中)
* 如果不想在同一個類中,可以設定 blockHandlerClass,並且方法必須是靜態方法
* fallback 當介面出現異常,就可以交給 fallback 指定的方法進行處理
*
* blockHandler 和 fallback 同時指定了,則 blockHandler 優先順序更高
*
* 可以透過 exceptionsToIgnore 排除不處理的異常
*/
@RequestMapping("/user")
@SentinelResource(value = USER_RESOURCE_NAME,
blockHandlerClass = User.class, blockHandler = "blockHandlerForGetUser",
fallbackClass = User.class, fallback = "fallbackForGetUser",
exceptionsToIgnore = {ArithmeticException.class})
public User getUser(String id) {
int a = 1 / 0;
return new User("hudu");
}
pojo 類程式碼
public static User fallbackForGetUser(String id,Throwable e) {
e.printStackTrace();
return new User("異常處理");
}
再次請求介面,發現已經被異常處理的方法處理了
當我們重新整理頻率過高時,會發現介面被流控了,當 blockHandler 和 fallback 同時指定了,則 blockHandler 優先順序更高。
三、Sentinel 降級體驗
設定降級規則
@PostConstruct
public void initDegradeRule() {
// 降級規則
List<DegradeRule> degradeRules = new ArrayList<>();
DegradeRule degradeRule = new DegradeRule();
degradeRule.setResource(DEGRADE_RESOURCE_NAME);
// 設定降級規則策略,異常數
degradeRule.setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_COUNT);
// 設定閾值,異常數 2
degradeRule.setCount(2);
// 觸發熔斷的最小請求數量
degradeRule.setMinRequestAmount(2);
// 統計時長,單位 ms,預設 1000 一秒
degradeRule.setStatIntervalMs(60*1000);
// 一分鐘內:執行了2次,出現了2次異常,就會觸發熔斷
// 熔斷持續的時長(時間視窗)單位 s,
// 一旦觸發了熔斷,再次請求對應的介面就會直接呼叫 降級方法
// 熔斷時長多了之後,進入半開狀態,恢復介面請求呼叫,如果第一次請求就異常,再次熔斷,不會根據設定的條件進行判定
degradeRule.setTimeWindow(10);
degradeRules.add(degradeRule);
DegradeRuleManager.loadRules(degradeRules);
}
@RequestMapping("/degrade")
@SentinelResource(value = DEGRADE_RESOURCE_NAME,entryType = EntryType.IN,blockHandlerClass = User.class,blockHandler = "blockHandlerForFb")
public User degrade(String id) {
// 異常數
throw new RuntimeException("異常");
}
public static User blockHandlerForFb(String id, BlockException ex) {
return new User("降級");
}
請求http://localhost:8060/degrade
介面第四次,發現被降級
在等十秒,可以請求,但是異常之後直接熔斷降級。
流控規則一般會設定在服務提供方,熔斷降級規則一般設定在服務消費端
本作品採用《CC 協議》,轉載必須註明作者和本文連結