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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM