• 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

  • 相关阅读:
    HttpServletRequest对象(一)
    HttpServletResponse对象(二)
    HttpServletResponse对象(一)
    Servlet路径跳转问题
    sevlet的url-pattern设置
    java中使用相对路径读取文件的写法总结 ,以及getResourceAsStream() (转)
    创建第一个servlet程序--HelloServlet
    Eclipse创建一个JAVA WEB项目
    Servlet学习(二):ServletConfig获取参数;ServletContext应用:请求转发,参数获取,资源读取;类装载器读取文件
    Centos7默认安装的docker版本说明
  • 原文地址:https://www.cnblogs.com/xjknight/p/12349102.html
Copyright © 2020-2023  润新知