学习笔记(Sentinel)

分布式流量防护

在分布式系统中,服务之间的相互调用会生成分布式流量。如何通过组件进行流量防护,并有效控制流量,是分布式系统的技术挑战之一

什么是服务雪崩

假设我有一个微服务系统,这个系统内包含了 ABCD 四个微服务,这四个服务都是以集群模式构建的,微服务之间相互调用,因为调用链中的一个服务故障,引起整个链路都无法访问的情况就是服务雪崩
在这里插入图片描述

安装软件

下载地址:https://github.com/alibaba/Sentinel/releases

直接下载

wget https://github.com/alibaba/Sentinel/releases/download/1.8.7/sentinel-dashboard-1.8.7.jar

注意事项:Sentinel 控制台目前仅支持单机部署。Sentinel 控制台项目提供 Sentinel 功能全集示例,不作为开箱即用的生产环境控制台,若希望在生产环境使用请根据文档自行进行定制和改造

// 前台运行
java -Dserver.port=3000 -Dcsp.sentinel.dashboard.server=111.180.198.78:3000 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.8.7.jar


// 后台运行
nohup java -server -Xms64m -Xmx256m -Dserver.port=3000 -Dcsp.sentinel.dashboard.server=111.180.198.78:3000 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.8.7.jar >> /root/temp/Sentinel/sentinel.log 2>&1 &

在浏览器输入 111.180.198.78:3000 即可,登录用户名密码都是 sentinel

搭建项目

第一步:导入依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.10</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>org.example</groupId>
    <artifactId>sentinel-basic</artifactId>
    <version>1.0.0</version>
    <name>sentinel-basic</name>
    <description>sentinel-basic</description>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
            <version>2021.0.6.0</version>
        </dependency>


        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.example</groupId>
                <artifactId>sentinel</artifactId>
                <version>1.0.0</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

第二步:修改配置文件

server:
  port: 8080

spring:
  application:
    name: sentinel-basic
  cloud:
    sentinel:
      transport:
        # Sentinel 控制台地址
        dashboard: 192.168.0.104:8858

第三步:配置主启动类

package org.example;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@Slf4j
@SpringBootApplication
public class SentinelBasicApplication {

    public static void main(String[] args) {
        SpringApplication.run(SentinelBasicApplication.class, args);
        log.info("********************* 应用启动成功 *********************");
    }

}

第四步:创建测试控制器

package org.example.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class SentinelController {

    @RequestMapping("/sentinel")
    public Map<String, String> sentinelController(){
        Map<String, String> map = new HashMap<>();
        map.put("sentinel", "sentinel success");
        return map;
    }

    @RequestMapping("/relation")
    public Map<String, String> relationController(){
        Map<String, String> map = new HashMap<>();
        map.put("relation", "relation success");
        return map;
    }

}

第五步:测试即可,Sentinel1 是懒加载的,只有访问了控制台里才有路径信息

在这里插入图片描述

流量控制概述

监控应用流量的 QPS(每秒能够处理的请求数) 或并发线程数,当达到指定的阈值时对流量进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性
在这里插入图片描述

流控控制规则

在这里插入图片描述

  • 直接:直接作用于当前资源,如果访问压力大于某个阈值,后续请求将被直接拦下来
  • 关联:统计与当前资源相关的另一个资源,另一个资源触发阈值时,对当前资源限流
  • 链路:当指定链路上的访问量⼤于某个阈值时,对当前资源进⾏限流,这⾥的指定链路是细化到 API 级别的限流维度
  • 快速失败:默认的流量控制方式,当QPS超过任意规则的阈值后,新的请求就会被立即拒绝,拒绝方式为抛出FlowException
  • Warm Up:即预热/冷启动方式。当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压爆
  • 排队等待:匀速排队方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法 。这种方式主要用于处理间隔性突发的流量

流控模式(直接模式)

概述:当 QPS 超过某个阈值的时候,则采取措施进行流量控制

在这里插入图片描述

如图所示:当每秒的请求数超过 3 时就会触发流控规则,使用 ApiFox 进行压力测试,失败返回:Blocked by Sentinel (flow limiting)

在这里插入图片描述

流控模式(关联模式)

概述:当关联的资源达到阈值时,就限流自己

在这里插入图片描述
/sentinel 资源关联了 /relation 资源,当 /relation 资源的 QPS 达到 3 后,/sentinel 就会被限流

在这里插入图片描述

流控模式(链路模式)

概述:链路流控模式指的是,当从某个接口过来的资源达到限流条件时,开启限流。它的功能有点类似于针对来源配置项,区别在于:针对来源是针对上级微服务,而链路流控是针对上级接口,也就是说它的粒度更细

比如说,项目中的两个资源都用到了同一个 Service 下的服务,我们希望走 /sentinel 资源的超过阈值就限流,/relation 资源不进行限流

添加的相关代码,需要使用 @SentinelResource() 注解来标记资源,必须要配置文件里添加 web-context-unify: false

package org.example.service;

public interface LinkService {

    String linkService();

}

package org.example.service.impl;

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import org.example.service.LinkService;
import org.springframework.stereotype.Service;

@Service
public class LinkServiceImpl implements LinkService {

    @SentinelResource(value = "goods")
    @Override
    public String linkService() {
        return "goods";
    }

}

package org.example.controller;

import org.example.service.LinkService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@RestController
public class SentinelController {

    @Autowired
    private LinkService linkService;

    @RequestMapping("/sentinel")
    public Map<String, String> sentinelController(){
        linkService.linkService();
        Map<String, String> map = new HashMap<>();
        map.put("sentinel", "sentinel success");
        return map;
    }

    @RequestMapping("/relation")
    public Map<String, String> relationController(){
        linkService.linkService();
        Map<String, String> map = new HashMap<>();
        map.put("relation", "relation success");
        return map;
    }

}

server:
  port: 8080

spring:
  application:
    name: sentinel-basic
  cloud:
    sentinel:
      transport:
        # Sentinel 控制台地址
        dashboard: 192.168.0.104:8858
        # sentnel api端口号 默认 8719
        port: 8719
      # 关闭context整合
      web-context-unify: false

在这里插入图片描述
测试结果

在这里插入图片描述

流控控制效果

流控效果(预热)

warm up 也叫预热模式,是应对服务冷启动的一种方案,下图所表示的含义是,当请求量大量到来时,在5秒的时间内,慢慢的把 QPS 提高到 10

在这里插入图片描述
添加控制器,然后使用 ApiFox 测试200个请求即可

package org.example.controller;

import org.example.service.LinkService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@RestController
public class SentinelController {

    ......

    @RequestMapping("/warmup")
    public Map<String, String> warmupController(){
        Map<String, String> map = new HashMap<>();
        map.put("warmup", "warmup success");
        return map;
    }

}

热点参数限流

热点即经常访问的数据,给 /hot 这个资源添加热点参数限流,规则如下:

  • 默认的热点参数规则是每1秒请求量不超过2
  • 给102这个参数设置例外:每1秒请求量不超过4
  • 给103这个参数设置例外:每1秒请求量不超过10

添加控制器代码,必须用 @SentinelResource("hot") 标记资源才可以

package org.example.controller;

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import org.example.service.LinkService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@RestController
public class SentinelController {

    ......
    
    @SentinelResource("hot")
    @RequestMapping("/hot")
    public Map<String, String> hotController(Long id) {
        Map<String, String> map = new HashMap<>();
        map.put("hot", "success" + id);
        return map;
    }

}

对于特定的资源,点击热点进行设置,这里的参数索引指的是对控制器的第几个参数进行限流,然后去热点规则里点击编辑进行热点数据的设置,最后自己测试就可以了

在这里插入图片描述
在这里插入图片描述

线程隔离

限流是一种预防措施,虽然限流可以尽量避免因高并发而引起的服务故障,但服务还会因为其它原因而故障。而要将这些故障控制在一定范围,避免雪崩,就要靠线程隔离(舱壁模式)和熔断降级手段了

线程隔离就是允许的并发访问的线程数

在这里插入图片描述

熔断降级

熔断降级是解决雪崩问题的重要手段。其思路是由断路器统计服务调用的异常比例、慢请求比例,如果超出阈值则会熔断该服务。即拦截访问该服务的一切请求;而当服务恢复时,断路器会放行访问该服务的请求

断路器控制熔断和放行是通过状态机来完成的
在这里插入图片描述

状态机包括三个状态

  • closed:关闭状态,断路器放行所有请求,并开始统计异常比例、慢请求比例。超过阈值则切换到open状态
  • open:打开状态,服务调用被熔断,访问被熔断服务的请求会被拒绝,快速失败,直接走降级逻辑。Open状态5秒后会进入half-open状态
  • half-open:半开状态,放行一次请求,根据执行结果来判断接下来的操作。 请求成功:则切换到closed状态 请求失败:则切换到open状态

熔断降级策略

  • 慢调用:业务的响应时长(RT)大于指定时长的请求认定为慢调用请求。在指定时间内,如果请求数量超过设定的最小数量,慢调用比例大于设定的阈值,则触发熔断

  • 异常比例,异常数:统计指定时间内的调用,如果调用次数超过指定请求数,并且出现异常的比例达到设定的比例阈值(或超过指定异常数),则触发熔断

慢调用

下图表示:超过 50ms 的请求都会被认为是慢请求,1 秒内 5 个请求数的异常比例达到百分之40,断路器打开(保险丝跳闸)微服务不可用,保险丝跳闸断电了。过 5 秒钟由断路器又打开状态变为半开状态放一部分请求进来

相关控制器代码

package org.example.controller;

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import org.example.service.LinkService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@RestController
public class SentinelController {

	......

    @RequestMapping("/slow")
    public Map<String, String> slowController() throws InterruptedException {
        Thread.sleep(100);
        Map<String, String> map = new HashMap<>();
        map.put("slow", "success");
        return map;
    }

}

在这里插入图片描述

异常比例

当资源每秒报错异常总数占通过量的比值超过阈值之后,资源进入降级状态。异常比率的阈值范围是 [0.0,1.0]

在 1 秒内的 5 次请求中,只要异常比例超过 0.4,也就是有 2 次以上的异常,就会触发熔断

在这里插入图片描述

异常数

异常数:当资源近 1 分钟的异常数目超过阈值之后会进行熔断

在 1 秒内超过有 5 个异常就进入熔断

在这里插入图片描述

授权规则

授权规则可以对调用方的来源做控制,有白名单和黑名单两种方式

  • 白名单:来源(origin)在白名单内的调用者允许访问
  • 黑名单:来源(origin)在黑名单内的调用者不允许访问

比如我们有过一个需求:我们允许请求从 Higress 网关到服务访问,不允许浏览器访问服务,那么白名单中就要填写网关的来源名称(origin)

Sentinel 是通过 RequestOriginParser 这个接口的 parseOrigin 来获取请求的来源的,定义接口和实现类

package org.example.service;

import javax.servlet.http.HttpServletRequest;

public interface RequestOriginParser {

    /**
     * 从请求request对象中获取origin,获取方式自定义
     */
    String parseOrigin(HttpServletRequest request);

}

package org.example.service.impl;

import org.example.service.RequestOriginParser;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletRequest;

@Component
public class HeaderOriginParser implements RequestOriginParser {

    @Override
    public String parseOrigin(HttpServletRequest request) {
        // 1.获取请求头
        String origin = request.getHeader("origin");
        // 2.非空判断
        if (StringUtils.isEmpty(origin)) {
            origin = "blank";
        }
        return origin;
    }

}

然后给网关添加请求头:既然获取请求 origin 的方式是从 reques-header 中获取 origin 值,我们必须让所有从网关路由到微服务的请求都带上 origin 头(在路由设置就可以了)
在这里插入图片描述
然后去相应的资源配置授权规则,点击授权,然后就可以进行测试了

在这里插入图片描述

系统自适应限流

Sentinel 的系统保护规则是从应用级别的入口流量进行控制,从单台机器的 load、CPU 使用率、平均 RT、入口 QPS 和并发线程数等几个维度监控应用指标,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性

系统规则支持以下模式(在系统规则里新增系统规则即可):

  • Load 自适应(仅对 Linux/Unix-like 机器生效):系统的 load1 作为启发指标,进行自适应系统保护。当系统 load1 超过设定的启发值,且系统当前的并发线程数超过估算的系统容量时才会触发系统保护(BBR 阶段)。系统容量由系统的 maxQps * minRt 估算得出。设定参考值一般是 CPU cores * 2.5。
  • CPU usage:当系统 CPU 使用率超过阈值即触发系统保护(取值范围 0.0-1.0),比较灵敏。
  • 平均 RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
  • 并发线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
  • 入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护

在这里插入图片描述
在这里插入图片描述

@SentinelResource

服务降级功能,但是只是限制后,返回不可控的结果肯定是不行的,我们还要保证调用者在调用那些被限制的服务时候,不管是不是被限制,都要让他们拿到一个合理的结果,而不是扔回去一个异常就完事了

Sentinel 提供了这样的功能,让我们可以另外定义一个方法来代替被限制或异常服务返回数据,这就是 fallback 和 blockHandler

  • fallback:针对Java本身出现的异常进行处理的对应属性
  • blockHandler:针对违反Sentinel控制台配置规则时触发 BlockException 异常时对应处理的属性

只配置fallback

放在一起,注意方法的返回值必须一致才行,当出现异常时就返回下面的东西

package org.example.controller;

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import org.example.service.LinkService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@RestController
public class SentinelController {

	......

    @SentinelResource(value = "testFallback", fallback = "testFallback")
    @RequestMapping("/fallback")
    public Map<String, String> fallbackController(String id) {
        if (id.equals("1")) {
            throw new RuntimeException("出异常了");
        }
        Map<String, String> map = new HashMap<>();
        map.put("exception", "success");
        return map;
    }

    // 降级  
    public Map<String, String> testFallback(String id, Throwable e) {
        Map<String, String> map = new HashMap<>();
        map.put("exception", "服务繁忙请稍后在试");
        return map;
    }

}

放在外面,现在 fallback 包下创建类,然后再去控制器修改

package org.example.fallback;

import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;

@Component
public class TestFallback {

    // 降级
    public static Map<String, String> testFallback(String id, Throwable e) {
        Map<String, String> map = new HashMap<>();
        map.put("exception", "服务繁忙请稍后在试");
        return map;
    }

}

package org.example.controller;

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import org.example.fallback.TestFallback;
import org.example.service.LinkService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@RestController
public class SentinelController {

	......

    @SentinelResource(value = "testFallback", fallbackClass = TestFallback.class, fallback = "testFallback")
    @RequestMapping("/fallback")
    public Map<String, String> fallbackController(String id) {
        if (id.equals("1")) {
            throw new RuntimeException("出异常了");
        }
        Map<String, String> map = new HashMap<>();
        map.put("exception", "success");
        return map;
    }

}

只配置blockHandler

超出流量限制的部分是否会进入到 blockHandler 的方法,要注意是超出流量限制的请求调用,会进入 blockHandler 方法

package org.example.handler;

import com.alibaba.csp.sentinel.slots.block.BlockException;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;

@Component
public class TestBlockHander {

    // 降级
    public static Map<String, String> testBlockHander(String id, BlockException e) {
        Map<String, String> map = new HashMap<>();
        map.put("exception", "被限流了");
        return map;
    }

}

package org.example.controller;

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import org.example.fallback.TestFallback;
import org.example.handler.TestBlockHander;
import org.example.service.LinkService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@RestController
public class SentinelController {

	......

    @SentinelResource(value = "testBlockHander", blockHandlerClass = TestBlockHander.class, blockHandler = "testBlockHander")
    @RequestMapping("/blockhander")
    public Map<String, String> blockhanderController(String id) {
        if (id.equals("1")) {
            System.out.println(10 / 0);
        }
        Map<String, String> map = new HashMap<>();
        map.put("exception", "success");
        return map;
    }

}

都配置

都加上就可以了

  • 12
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值