英文名为cascading failure,也叫级联失效,级联故障;每个微服务并不是100%可用,网络也有可能出问题,如有一个高并发的微服务系统,如下图,包含四个微服务,开始都为正常,在某个时间点,当A挂了,而此系统为高并发系统,B服务疯狂调用A服务,而A挂了,B发往A的请求就会强制等待,知道请求超时,在java程序中,一次请求往往对应一个线程,请求强制等待,线程就会强制阻塞,一直等到线程超时,才会被释放,在高并发情况下,阻塞线程越来越多,而线程对应的又是服务器计算资源,如不做任务处理,随着积累,B服务将无法创建线程,B服务也挂了,同理,C、D也疯狂请求B,C、D也会因为同样原因而不可用,这就是雪崩效应;导致雪崩效应的原因就是服务消费者未做好容错措施
1、业界常用容错方案
PS:四种方案思想,超时,释放够快,就不那么容易死;限流,只有一碗饭量,哪怕给三碗,也只吃一碗;仓壁模式,不把鸡蛋放一个篮子里,你有你的线程池,我有我的线程池;断路器模式,监控+开关,当监控API达到一定预值,就跳闸
轻量级的流量控制、熔断降级java库
1、整合sentinel
<!-- 容错服务组件 sentinel依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
<version>0.2.0.RELEASE</version>
</dependency>
<!-- 添加 actuator依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
2、Sentinel控制台
2、降级规则(断路器模式)
3、热点规则(特殊的流控规则):可对参数限流,亦或是对参数的值进行限流;适用于某些参数传递频繁,但有希望提高接口可靠性的场景;热点参数必须是基本类型或者String,否则不会生效
5、授权规则:对消费者进行授权控制,白名单,则是允许test微服务访问内容中心 /shares/1 API,黑名单则是不允许访问
Alibaba Sentinel 规则参数总结
控制台是如何获取到微服务的监控信息?用控制台配置规则时,控制台是如何将规则发送到各个微服务?
PS:代码示例
@GetMapping("/test-sentinel-api")
public String testSentinelApi(@RequestParam String a){
//定义一个sentinel保护资源,名称为test-sentinel-api
String resourceName = "test-sentinel-api";
//标记资源
ContextUtil.enter(resourceName,"test-wfw");
Entry entry = null;
try {
entry = SphU.entry(resourceName);
if(StringUtils.isBlank(a)){
throw new IllegalArgumentException("a不能为空!");
}
return a;
} catch (BlockException e) {
e.printStackTrace();
return "限流或者降级了";
}catch (IllegalArgumentException e2){
Tracer.trace(e2);
return "非法参数";
}finally {
if(entry != null){
entry.exit();
}
ContextUtil.exit();
}
}
PS:让代码更加优雅,参考手记
PS:代码示例
/**
* sentinel 注解
* @param a
* @return
*/
@GetMapping("/testSentinelResource")
@SentinelResource(
value = "test-sentinel-resource",
blockHandler = "block",
blockHandlerClass = TestControllerBlockHandler.class,
fallback = "fallback",
defaultFallback = "defaultFallback",
fallbackClass = TestControllerFallback.class
)
public String testSentinelResource(@RequestParam String a){
if(a.equals("0")){
throw new IllegalArgumentException("a不能为0!");
}
return a;
}
public class TestControllerFallback {
public static String defaultFallback(String a,Throwable throwable){
return "降级,或者其他异常:" + throwable.getMessage();
}
public static String fallback(String a,Throwable throwable){
return "指定异常:" + throwable.getMessage();
}
}
public class TestControllerBlockHandler {
public static String block(String a, BlockException e){
return "限流或者降级了,block";
}
}
//在spring容器中,创建一个对象,类型RestTemplate,名称/id为方法名
//相当于传统
@Bean
@LoadBalanced
@SentinelRestTemplate(
blockHandler = "block",
blockHandlerClass = TestControllerBlockHandler.class,
fallback = "fallback",
fallbackClass = TestControllerFallback.class
)
public RestTemplate restTemplate(){
return new RestTemplate();
}
#Template整合sentinel 开关
resttemplate:
sentinel:
enabled: true
package com.hzb2i.contentcenter.feignClient;
import com.hzb2i.feignapi.domain.dto.user.UserDto;
import org.springframework.stereotype.Component;
@Component
public class UserCenterFeignClientFallback implements UserCenterFeignClient{
@Override
public UserDto findById(Integer id) {
UserDto userDto = new UserDto();
userDto.setWxNickname("默认用户");
return userDto;
}
@Override
public UserDto query(Integer id, String userName) {
UserDto userDto = new UserDto();
userDto.setWxNickname("默认用户");
return userDto;
}
}
package com.hzb2i.contentcenter.feignClient;
import com.hzb2i.feignapi.domain.dto.user.UserDto;
import feign.hystrix.FallbackFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class UserCenterFeignClientFallbackFactory implements FallbackFactory<UserCenterFeignClient> {
@Override
public UserCenterFeignClient create(Throwable throwable) {
return new UserCenterFeignClient() {
@Override
public UserDto findById(Integer id) {
log.error("限流或降级:" + throwable);
UserDto userDto = new UserDto();
userDto.setWxNickname("默认用户");
return userDto;
}
@Override
public UserDto query(Integer id, String userName) {
log.error("限流或降级:" + throwable);
UserDto userDto = new UserDto();
userDto.setWxNickname("默认用户");
return userDto;
}
};
}
}
从测试反馈,微服务每次重启,规则就消失了,每次都需要重新配置,不适用于生产,需进行规则持久化
PS:生产环境使用sentinel
@Component
public class MyUrlBlockHandler implements BlockExceptionHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e) throws Exception {
ErrMsg msg = null;
if(e instanceof FlowException){
//限流异常
msg = ErrMsg.builder().status("100").msg("限流了").build();
}
else if(e instanceof DegradeException){
//降级异常
msg = ErrMsg.builder().status("101").msg("降级了").build();
}
else if(e instanceof ParamFlowException){
//热点异常
msg = ErrMsg.builder().status("102").msg("热点参数限流").build();
}
else if(e instanceof SystemBlockException){
//系统异常
msg = ErrMsg.builder().status("100").msg("系统规则不满足").build();
}
else if(e instanceof AuthorityException){
//权限异常
msg = ErrMsg.builder().status("100").msg("授权规则不通过").build();
}
//http状态码
httpServletResponse.setStatus(500);
httpServletResponse.setCharacterEncoding("utf-8");
httpServletResponse.setHeader("Content-type","application/json;charset=utf-8");
httpServletResponse.setContentType("application/json;charset=utf-8");
new ObjectMapper().writeValue(
httpServletResponse.getWriter(),
msg
);
}
}
@Data
@Builder
@AllArgsConstructor
class ErrMsg{
private String status;
private String msg;
}
@Component
public class MyRequestOriginParser implements RequestOriginParser {
@Override
public String parseOrigin(HttpServletRequest httpServletRequest) {
String origin = httpServletRequest.getParameter("origin");
if(StringUtils.isBlank(origin)){
throw new IllegalArgumentException("origin must be special!");
}
return origin;
}
}
测试:针对browser进行流控,访问地址参数带?origin=browser则会被限流,其他传值则不会限流
PS:不建议将origin参数放在url地址中,可放在header头部里
@Slf4j
@Component
public class MyUrlCleaner implements UrlCleaner {
@Override
public String clean(String s) {
log.info(s);
String[] split = s.split("/");
return Arrays.stream(split).map(string -> {
if(StringUtils.equals(string,"{shareId}")){
//if(NumberUtils.isNumber(string)){
return "{number}";
}
return string;
}).reduce((a,b) -> a + "/" + b).orElse("");
}
}