4.【Spring Cloud Alibaba】服务容错-sentinel

雪崩效应

image

常见容错方案

  • 超时
  • 限流
  • 仓壁模式
  • 断路器模式
断路器三态转换

image

使用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

流控规则

image

直接

  • 快速失败

  • Warm Up
    image

  • 排队等待
    image

  • 直接拒绝:(RuleConstant.CONTROL_BEHAVIOR_DEFAULT)方式是默认的流量控制方式,当QPS超过任何规则的阈值后,新的请求就会立即拒绝,拒绝方式为抛出FlowException . 这种方式适用于对系统处理能力确切已知的情况下,比如通过压测确定了系统的准确水位时。

  • Warm Up:(RuleConstant.CONTROL_BEHAVIOR_WARM_UP)方式,即预热/冷启动方式。当系统长期处理低水平的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值的上限,给系统一个预热的时间,避免冷系统被压垮。通常冷启动的过程系统允许通过的 QPS 曲线如下图所示:
    image

  • 均速排队:(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER)方式后严格控制请求通过的时间间隔,也即是让请求以均匀的速度通过,对应的是漏桶算法。该方式的作用如下图所示:
    image

这种方式主要用于处理间隔性突发的流量,例如消息队列。想象一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求。

降级规则

image

image

image

image

image

热点规则详解

@GetMapping("test-hot")
@SentinelResource("hot")
public String testHot(
    @RequestParam(required = false) String a,
    @RequestParam(required = false) String b
) {
    return a + " " + b;
}

在sentinel控制台配置热点规则

image

系统规则详解

image

image

image

授权规则

image

使用代码配置流控规则

@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与控制台通信原理剖析

image

sentinel控制台相关配置

image

sentinel Api详解

image

@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

image

为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使用总结

image

sentinel规则持久化01-拉模式

拉模式架构

image

原理简述

  • 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规则持久化-推模式

推模式架构图

image

原理简述

  • 控制台推送规则:
    1. 将规则推送到Nacos或其他远程配置中心
    2. 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

image

集群流控

image

扩展sentinel01-错误页优化

image

@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的值相等,限流规则才生效。

image

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;
    }
}

扩展sentinel03-RESTfulUrl

image

扩展sentinel04-通过现象看本质

image

配置项总结

https://www.imooc.com/article/289562

posted @ 2020-02-25 09:34  程序猿Knight  阅读(1121)  评论(0编辑  收藏  举报