雪崩效應
常見容錯方案
- 超時
- 限流
- 倉壁模式
- 斷路器模式
斷路器三態轉換
使用Sentinel實現容錯
什么是Sentinel
https://github.com/alibaba/Sentinel
輕量級的流量控制、熔斷降級 Java 庫
pom.xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
搭建Sentinel控制台
下載控制台
https://github.com/alibaba/Sentinel/releases
項目整合sentinel控制台
application.yml
spring:
cloud:
sentinel:
filter:
# 打開/關閉掉對Spring MVC端點的保護
enabled: true
transport:
# 指定sentinel 控制台的地址
dashboard: localhost:8080
流控規則
直接
-
快速失敗
-
Warm Up
-
排隊等待
-
直接拒絕:(RuleConstant.CONTROL_BEHAVIOR_DEFAULT)方式是默認的流量控制方式,當QPS超過任何規則的閾值后,新的請求就會立即拒絕,拒絕方式為拋出FlowException . 這種方式適用於對系統處理能力確切已知的情況下,比如通過壓測確定了系統的准確水位時。
-
Warm Up:(RuleConstant.CONTROL_BEHAVIOR_WARM_UP)方式,即預熱/冷啟動方式。當系統長期處理低水平的情況下,當流量突然增加時,直接把系統拉升到高水位可能瞬間把系統壓垮。通過"冷啟動",讓通過的流量緩慢增加,在一定時間內逐漸增加到閾值的上限,給系統一個預熱的時間,避免冷系統被壓垮。通常冷啟動的過程系統允許通過的 QPS 曲線如下圖所示:
-
均速排隊:(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER)方式后嚴格控制請求通過的時間間隔,也即是讓請求以均勻的速度通過,對應的是漏桶算法。該方式的作用如下圖所示:
這種方式主要用於處理間隔性突發的流量,例如消息隊列。想象一下這樣的場景,在某一秒有大量的請求到來,而接下來的幾秒則處於空閑狀態,我們希望系統能夠在接下來的空閑期間逐漸處理這些請求,而不是在第一秒直接拒絕多余的請求。
降級規則
熱點規則詳解
@GetMapping("test-hot")
@SentinelResource("hot")
public String testHot(
@RequestParam(required = false) String a,
@RequestParam(required = false) String b
) {
return a + " " + b;
}
在sentinel控制台配置熱點規則
系統規則詳解
授權規則
使用代碼配置流控規則
@GetMapping("test-add-flow-rule")
public String testHot() {
this.initFlowQpsRule();
return "success";
}
private void initFlowQpsRule() {
List<FlowRule> rules = new ArrayList<>();
FlowRule rule = new FlowRule("/shares/1");
// set limit qps to 20
rule.setCount(20);
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule.setLimitApp("default");
rules.add(rule);
FlowRuleManager.loadRules(rules);
}
sentinel與控制台通信原理剖析
sentinel控制台相關配置
sentinel Api詳解
@GetMapping("/test-sentinel-api")
public String testSentinelAPI(
@RequestParam(required = false) String a) {
String resourceName = "test-sentinel-api";
ContextUtil.enter(resourceName, "test-wfw");
// 定義一個sentinel保護的資源,名稱是test-sentinel-api
Entry entry = null;
try {
entry = SphU.entry(resourceName);
// 被保護的業務邏輯
if (StringUtils.isBlank(a)) {
throw new IllegalArgumentException("a不能為空");
}
return a;
}
// 如果被保護的資源被限流或者降級了,就會拋BlockException
catch (BlockException e) {
log.warn("限流,或者降級了", e);
return "限流,或者降級了";
} catch (IllegalArgumentException e2) {
// 統計IllegalArgumentException【發生的次數、發生占比...】
Tracer.trace(e2);
return "參數非法!";
} finally {
if (entry != null) {
// 退出entry
entry.exit();
}
ContextUtil.exit();
}
}
@SentinelResource注解詳解
@GetMapping("/test-sentinel-resource")
@SentinelResource(
value = "test-sentinel-api",
blockHandler = "block",
blockHandlerClass = TestControllerBlockHandlerClass.class,
fallback = "fallback"
)
public String testSentinelResource(@RequestParam(required = false) String a) {
if (StringUtils.isBlank(a)) {
throw new IllegalArgumentException("a cannot be blank.");
}
return a;
}
/**
* 1.5 處理降級
* - sentinel 1.6 可以處理Throwable
*
* @param a
* @return
*/
public String fallback(String a) {
return "限流,或者降級了 fallback";
}
TestControllerBlockHandlerClass
@Slf4j
public class TestControllerBlockHandlerClass {
/**
* 處理限流或者降級
*
* @param a
* @param e
* @return
*/
public static String block(String a, BlockException e) {
log.warn("限流,或者降級了 block", e);
return "限流,或者降級了 block";
}
}
RestTemplate整合sentinel
// 在spring容器中,創建一個對象,類型RestTemplate;名稱/ID是:restTemplate
// <bean id="restTemplate" class="xxx.RestTemplate"/>
@Bean
@LoadBalanced
@SentinelRestTemplate
public RestTemplate restTemplate() {
RestTemplate template = new RestTemplate();
template.setInterceptors(
Collections.singletonList(
new TestRestTemplateTokenRelayInterceptor()
)
);
return template;
}
@Autowired
private RestTemplate restTemplate;
@GetMapping("/test-rest-template-sentinel/{userId}")
public UserDTO test(@PathVariable Integer userId) {
return this.restTemplate
.getForObject(
"http://user-center/users/{userId}",
UserDTO.class, userId);
}
關閉@SentinelRestTemplate注解
resttemplate:
sentinel:
# 設置成false,表示關閉@SentinelRestTemplate注解
enabled: true
Feign整合sentinel
為feign整合sentinel
feign:
sentinel:
# 為feign整合sentinel
enabled: true
代碼驗證
不能獲取異常
@FeignClient(name = "user-center",
fallback = UserCenterFeignClientFallback.class,
)
public interface UserCenterFeignClient {
/**
* http://user-center/users/{id}
*
* @param id
* @return
*/
@GetMapping("/users/{id}")
UserDTO findById(@PathVariable Integer id);
}
private final ShareMapper shareMapper;
private final UserCenterFeignClient userCenterFeignClient;
public ShareDTO findById(Integer id) {
// 獲取分享詳情
Share share = this.shareMapper.selectByPrimaryKey(id);
// 發布人id
Integer userId = share.getUserId();
// 1. 代碼不可讀
// 2. 復雜的url難以維護:https://user-center/s?ie={ie}&f={f}&rsv_bp=1&rsv_idx=1&tn=baidu&wd=a&rsv_pq=c86459bd002cfbaa&rsv_t=edb19hb%2BvO%2BTySu8dtmbl%2F9dCK%2FIgdyUX%2BxuFYuE0G08aHH5FkeP3n3BXxw&rqlang=cn&rsv_enter=1&rsv_sug3=1&rsv_sug2=0&inputT=611&rsv_sug4=611
// 3. 難以相應需求的變化,變化很沒有幸福感
// 4. 編程體驗不統一
UserDTO userDTO = this.userCenterFeignClient.findById(userId);
ShareDTO shareDTO = new ShareDTO();
// 消息的裝配
BeanUtils.copyProperties(share, shareDTO);
shareDTO.setWxNickname(userDTO.getWxNickname());
return shareDTO;
}
UserCenterFeignClientFallback
@Component
public class UserCenterFeignClientFallback implements UserCenterFeignClient {
@Override
public UserDTO findById(Integer id) {
UserDTO userDTO = new UserDTO();
userDTO.setWxNickname("流控/降級返回的用戶");
return userDTO;
}
}
可以捕獲異常
@FeignClient(name = "user-center",
fallbackFactory = UserCenterFeignClientFallbackFactory.class
)
public interface UserCenterFeignClient {
/**
* http://user-center/users/{id}
*
* @param id
* @return
*/
@GetMapping("/users/{id}")
UserDTO findById(@PathVariable Integer id);
}
private final ShareMapper shareMapper;
private final UserCenterFeignClient userCenterFeignClient;
public ShareDTO findById(Integer id) {
// 獲取分享詳情
Share share = this.shareMapper.selectByPrimaryKey(id);
// 發布人id
Integer userId = share.getUserId();
// 1. 代碼不可讀
// 2. 復雜的url難以維護:https://user-center/s?ie={ie}&f={f}&rsv_bp=1&rsv_idx=1&tn=baidu&wd=a&rsv_pq=c86459bd002cfbaa&rsv_t=edb19hb%2BvO%2BTySu8dtmbl%2F9dCK%2FIgdyUX%2BxuFYuE0G08aHH5FkeP3n3BXxw&rqlang=cn&rsv_enter=1&rsv_sug3=1&rsv_sug2=0&inputT=611&rsv_sug4=611
// 3. 難以相應需求的變化,變化很沒有幸福感
// 4. 編程體驗不統一
UserDTO userDTO = this.userCenterFeignClient.findById(userId);
ShareDTO shareDTO = new ShareDTO();
// 消息的裝配
BeanUtils.copyProperties(share, shareDTO);
shareDTO.setWxNickname(userDTO.getWxNickname());
return shareDTO;
}
UserCenterFeignClientFallbackFactory
@Component
@Slf4j
public class UserCenterFeignClientFallbackFactory implements FallbackFactory<UserCenterFeignClient> {
@Override
public UserCenterFeignClient create(Throwable cause) {
return new UserCenterFeignClient() {
@Override
public UserDTO findById(Integer id) {
log.warn("遠程調用被限流/降級了", cause);
UserDTO userDTO = new UserDTO();
userDTO.setWxNickname("流控/降級返回的用戶");
return userDTO;
}
};
}
}
sentinel使用總結
sentinel規則持久化01-拉模式
拉模式架構
原理簡述
- FileRefreshableDataSource 定時從指定文件中讀取規則JSON文件【圖中的本地文件】,如果發現文件發生變化,就更新規則緩存。
- FileWritableDataSource 接收控制台規則推送,並根據配置,修改規則JSON文件【圖中的本地文件】。
編寫
3.1 加依賴
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-extension</artifactId>
</dependency>
寫代碼
FileDataSourceInit
/**
* 拉模式規則持久化
*
* @author itmuch.com
*/
public class FileDataSourceInit implements InitFunc {
@Override
public void init() throws Exception {
// TIPS: 如果你對這個路徑不喜歡,可修改為你喜歡的路徑
String ruleDir = System.getProperty("user.home") + "/sentinel/rules";
String flowRulePath = ruleDir + "/flow-rule.json";
String degradeRulePath = ruleDir + "/degrade-rule.json";
String systemRulePath = ruleDir + "/system-rule.json";
String authorityRulePath = ruleDir + "/authority-rule.json";
String paramFlowRulePath = ruleDir + "/param-flow-rule.json";
this.mkdirIfNotExits(ruleDir);
this.createFileIfNotExits(flowRulePath);
this.createFileIfNotExits(degradeRulePath);
this.createFileIfNotExits(systemRulePath);
this.createFileIfNotExits(authorityRulePath);
this.createFileIfNotExits(paramFlowRulePath);
// 流控規則
ReadableDataSource<String, List<FlowRule>> flowRuleRDS = new FileRefreshableDataSource<>(
flowRulePath,
flowRuleListParser
);
// 將可讀數據源注冊至FlowRuleManager
// 這樣當規則文件發生變化時,就會更新規則到內存
FlowRuleManager.register2Property(flowRuleRDS.getProperty());
WritableDataSource<List<FlowRule>> flowRuleWDS = new FileWritableDataSource<>(
flowRulePath,
this::encodeJson
);
// 將可寫數據源注冊至transport模塊的WritableDataSourceRegistry中
// 這樣收到控制台推送的規則時,Sentinel會先更新到內存,然后將規則寫入到文件中
WritableDataSourceRegistry.registerFlowDataSource(flowRuleWDS);
// 降級規則
ReadableDataSource<String, List<DegradeRule>> degradeRuleRDS = new FileRefreshableDataSource<>(
degradeRulePath,
degradeRuleListParser
);
DegradeRuleManager.register2Property(degradeRuleRDS.getProperty());
WritableDataSource<List<DegradeRule>> degradeRuleWDS = new FileWritableDataSource<>(
degradeRulePath,
this::encodeJson
);
WritableDataSourceRegistry.registerDegradeDataSource(degradeRuleWDS);
// 系統規則
ReadableDataSource<String, List<SystemRule>> systemRuleRDS = new FileRefreshableDataSource<>(
systemRulePath,
systemRuleListParser
);
SystemRuleManager.register2Property(systemRuleRDS.getProperty());
WritableDataSource<List<SystemRule>> systemRuleWDS = new FileWritableDataSource<>(
systemRulePath,
this::encodeJson
);
WritableDataSourceRegistry.registerSystemDataSource(systemRuleWDS);
// 授權規則
ReadableDataSource<String, List<AuthorityRule>> authorityRuleRDS = new FileRefreshableDataSource<>(
flowRulePath,
authorityRuleListParser
);
AuthorityRuleManager.register2Property(authorityRuleRDS.getProperty());
WritableDataSource<List<AuthorityRule>> authorityRuleWDS = new FileWritableDataSource<>(
authorityRulePath,
this::encodeJson
);
WritableDataSourceRegistry.registerAuthorityDataSource(authorityRuleWDS);
// 熱點參數規則
ReadableDataSource<String, List<ParamFlowRule>> paramFlowRuleRDS = new FileRefreshableDataSource<>(
paramFlowRulePath,
paramFlowRuleListParser
);
ParamFlowRuleManager.register2Property(paramFlowRuleRDS.getProperty());
WritableDataSource<List<ParamFlowRule>> paramFlowRuleWDS = new FileWritableDataSource<>(
paramFlowRulePath,
this::encodeJson
);
ModifyParamFlowRulesCommandHandler.setWritableDataSource(paramFlowRuleWDS);
}
private Converter<String, List<FlowRule>> flowRuleListParser = source -> JSON.parseObject(
source,
new TypeReference<List<FlowRule>>() {
}
);
private Converter<String, List<DegradeRule>> degradeRuleListParser = source -> JSON.parseObject(
source,
new TypeReference<List<DegradeRule>>() {
}
);
private Converter<String, List<SystemRule>> systemRuleListParser = source -> JSON.parseObject(
source,
new TypeReference<List<SystemRule>>() {
}
);
private Converter<String, List<AuthorityRule>> authorityRuleListParser = source -> JSON.parseObject(
source,
new TypeReference<List<AuthorityRule>>() {
}
);
private Converter<String, List<ParamFlowRule>> paramFlowRuleListParser = source -> JSON.parseObject(
source,
new TypeReference<List<ParamFlowRule>>() {
}
);
private void mkdirIfNotExits(String filePath) throws IOException {
File file = new File(filePath);
if (!file.exists()) {
file.mkdirs();
}
}
private void createFileIfNotExits(String filePath) throws IOException {
File file = new File(filePath);
if (!file.exists()) {
file.createNewFile();
}
}
private <T> String encodeJson(T t) {
return JSON.toJSONString(t);
}
}
配置
在項目的 resources/META-INF/services 目錄下創建文件,名為 com.alibaba.csp.sentinel.init.InitFunc ,內容為:
# 改成上面FileDataSourceInit的包名類名全路徑即可。
com.itmuch.contentcenter.FileDataSourceInit
優缺點分析
優點
- 簡單易懂
- 沒有多余依賴(比如配置中心、緩存等)
缺點
- 由於規則是用 FileRefreshableDataSource 定時更新的,所以規則更新會有延遲。如果FileRefreshableDataSource定時時間過大,可能長時間延遲;如果FileRefreshableDataSource過小,又會影響性能;
- 規則存儲在本地文件,如果有一天需要遷移微服務,那么需要把規則文件一起遷移,否則規則會丟失。
sentinel規則持久化-推模式
推模式架構圖
原理簡述
- 控制台推送規則:
- 將規則推送到Nacos或其他遠程配置中心
- Sentinel客戶端鏈接Nacos,獲取規則配置;並監聽Nacos配置變化,如發生變化,就更新本地緩存(從而讓本地緩存總是和Nacos一致)
- 控制台監聽Nacos配置變化,如發生變化就更新本地緩存(從而讓控制台本地緩存總是和Nacos一致)
微服務改造
加依賴
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
添加配置
spring:
cloud:
sentinel:
datasource:
# 名稱隨意
flow:
nacos:
server-addr: localhost:8848
dataId: ${spring.application.name}-flow-rules
groupId: SENTINEL_GROUP
# 規則類型,取值見:
# org.springframework.cloud.alibaba.sentinel.datasource.RuleType
rule-type: flow
degrade:
nacos:
server-addr: localhost:8848
dataId: ${spring.application.name}-degrade-rules
groupId: SENTINEL_GROUP
rule-type: degrade
system:
nacos:
server-addr: localhost:8848
dataId: ${spring.application.name}-system-rules
groupId: SENTINEL_GROUP
rule-type: system
authority:
nacos:
server-addr: localhost:8848
dataId: ${spring.application.name}-authority-rules
groupId: SENTINEL_GROUP
rule-type: authority
param-flow:
nacos:
server-addr: localhost:8848
dataId: ${spring.application.name}-param-flow-rules
groupId: SENTINEL_GROUP
rule-type: param-flow
推模式改造詳情請查看如下地址
https://www.imooc.com/article/289464
在生產環境使用
https://help.aliyun.com/document_detail/90323.html
集群流控
擴展sentinel01-錯誤頁優化
@Component
public class MyUrlBlockHandler implements UrlBlockHandler {
@Override
public void blocked(HttpServletRequest request, HttpServletResponse response, BlockException ex) throws IOException {
ErrorMsg msg = null;
if (ex instanceof FlowException) {
msg = ErrorMsg.builder()
.status(100)
.msg("限流了")
.build();
} else if (ex instanceof DegradeException) {
msg = ErrorMsg.builder()
.status(101)
.msg("降級了")
.build();
} else if (ex instanceof ParamFlowException) {
msg = ErrorMsg.builder()
.status(102)
.msg("熱點參數限流")
.build();
} else if (ex instanceof SystemBlockException) {
msg = ErrorMsg.builder()
.status(103)
.msg("系統規則(負載/...不滿足要求)")
.build();
} else if (ex instanceof AuthorityException) {
msg = ErrorMsg.builder()
.status(104)
.msg("授權規則不通過")
.build();
}
// http狀態碼
response.setStatus(500);
response.setCharacterEncoding("utf-8");
response.setHeader("Content-Type", "application/json;charset=utf-8");
response.setContentType("application/json;charset=utf-8");
// spring mvc自帶的json操作工具,叫jackson
new ObjectMapper()
.writeValue(
response.getWriter(),
msg
);
}
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
class ErrorMsg {
private Integer status;
private String msg;
}
擴展sentinel02-實現區分來源
限流規則里的來源值要和請求參數origin的值相等,限流規則才生效。
import com.alibaba.csp.sentinel.adapter.servlet.callback.RequestOriginParser;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
@Component
public class MyRequestOriginParser implements RequestOriginParser {
@Override
public String parseOrigin(HttpServletRequest request) {
// 從請求參數中獲取名為 origin 的參數並返回
// 如果獲取不到origin參數,那么就拋異常
String origin = request.getParameter("origin");
if (StringUtils.isBlank(origin)) {
throw new IllegalArgumentException("origin must be specified");
}
return origin;
}
}