Howinfun

原文链接:(万字好文)Dubbo服务熔断与降级的深入讲解&代码实战

一、Dubbo服务降级实战

1 mock 机制

谈到服务降级,Dubbo 本身就提供了服务降级的机制;而 Dubbo 的服务降级机制主要是利用服务消费者的 mock 属性。

服务消费者的 mock 属性有以下三种使用方式,下面将带着例子简单介绍一下。

1.1 服务消费者注册url的mock属性

例子:
mock=return+null,即当服务提供者出现异常(宕机或者业务异常),则返回null给服务消费者。

2021-01-26 09:39:54.631 [main] [INFO ] [o.a.d.r.z.ZookeeperRegistry] [] [] -  [DUBBO] Notify urls for subscribe url consumer://127.0.0.1/com.winfun.service.DubboServiceOne?application=dubbo-service&category=providers,configurators,routers&check=false&dubbo=2.0.2&init=false&interface=com.winfun.service.DubboServiceOne&lazy=true&methods=sayHello&mock=return+null&pid=36270&qos.enable=false&reference.filter=default,dubboLogFilter&release=2.7.7&retries=1&side=consumer&sticky=false&timestamp=1611625194544

1.2 @DubboReference注解或者dubbo:reference标签的mock属性

例子:
mock="return null",即当服务提供者出现异常(宕机或者业务异常),则返回null给服务消费者。

public class HelloController{
    
    @DubboReference(check = false,lazy = true,retries = 1,mock = "return null")
    private DubboServiceOne dubboServiceOne;
    
    //.....
}

1.3 服务消费者mock属性设置为true+Mock实现类

例子:
Mock实现类 为 Dubbo接口 的实现类,并且 Mock实现类 与 Dubbo接口 放同一个项目中。

/**
 *
 * DubboServiceTwo
 * @author winfun
 * @date 2020/10/29 5:00 下午
 **/
public interface DubboServiceTwo {

    /***
     *  say hi
     * @author winfun
     * @param name name
     * @return {@link ApiResult<String> }
     **/
    ApiResult<String> sayHi(String name);
}

/**
 * DubboServiceTwo 降级实现类
 * @author winfun
 * @date 2021/1/26 9:21 上午
 **/
public class DubboServiceTwoMock implements DubboServiceTwo{


    /***
     *  say hi
     * @author winfun
     * @param name name
     * @return {@link ApiResult <String> }
     **/
    @Override
    public ApiResult<String> sayHi(String name) {
        return ApiResult.fail("Mock实现类-服务降级了");
    }
}

2 实战例子:

下面将使用第二种方式来展示 Duboo 自身提供的服务降级机制。

2.1 consumer 代码:

/**
 * Say Hello
 * @author winfun
 * @date 2020/10/29 5:12 下午
 **/
@RestController
public class HelloController {

    @DubboReference(check = false,lazy = true,retries = 1,mock="return {\"code\":1,\"message\":\"熔断限流了\"}")
    private DubboServiceOne dubboServiceOne;

    @GetMapping("/hello/{name}")
    public ApiResult sayHello(@PathVariable("name") String name){
        return this.dubboServiceOne.sayHello(name);
    }
}

2.2 provider 代码:

/**
 * DubboServiceOneImpl
 * @author winfun
 * @date 2020/10/29 5:04 下午
 **/
@DubboService(interfaceClass = DubboServiceOne.class)
public class DubboServiceOneImpl implements DubboServiceOne {
    /***
     *  say hello
     * @author winfun
     * @param name name
     * @return {@link ApiResult<String> }
     **/
    @SneakyThrows
    @Override
    public ApiResult<String> sayHello(String name) {
        // dubbo 接口默认超时时间为1s,我们这里直接休眠5s
        Thread.sleep(5000);
        return ApiResult.success("hello "+name);
    }

2.3 结果分析:

情况一:服务提供者没起来

这个时候服务消费者不会进行重试,只会直接进行服务降级,返回我们设定的 Mock 值。

2021-01-27 18:02:12.288 [http-nio-8081-exec-1] [WARN ] [o.a.d.r.c.s.w.MockClusterInvoker] [/hello/name] [16117417328056102141] -  [DUBBO] fail-mock: sayHello fail-mock enabled , url : zookeeper://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?application=dubbo-service&check=false&dubbo=2.0.2&init=false&interface=com.winfun.service.DubboServiceOne&lazy=true&methods=sayHello&mock=return+%7B%22code%22%3A1%2C%22message%22%3A%22%E7%86%94%E6%96%AD%E9%99%90%E6%B5%81%E4%BA%86%22%7D&pid=51874&qos.enable=false&reference.filter=default,dubboLogFilter,-sentinel.dubbo.consumer.filter&register.ip=172.26.144.16&release=2.7.7&retries=0&side=consumer&sticky=false&timestamp=1611741716665, dubbo version: 2.7.7, current host: 172.26.144.16
org.apache.dubbo.rpc.RpcException: No provider available from registry 127.0.0.1:2181 for service com.winfun.service.DubboServiceOne on consumer 172.26.144.16 use dubbo version 2.7.7, please check status of providers(disabled, not registered or in blacklist).
	//.....省略
2021-01-27 18:02:12.289 [http-nio-8081-exec-1] [INFO ] [o.a.d.r.c.s.w.MockClusterInvoker] [/hello/name] [16117417328056102141] -  [DUBBO] Exception when try to invoke mock. Get mock invokers error for service:com.winfun.service.DubboServiceOne, method:sayHello, will construct a new mock with 'new MockInvoker()'., dubbo version: 2.7.7, current host: 172.26.144.16
org.apache.dubbo.rpc.RpcException: No provider available from registry 127.0.0.1:2181 for service com.winfun.service.DubboServiceOne on consumer 172.26.144.16 use dubbo version 2.7.7, please check status of providers(disabled, not registered or in blacklist).
    //....省略

大家可能会存在一个质疑点:我上面说的是不会进行重试,但是控制台我们看到了两次异常;其实第二次异常,是获取 MockInvoker 导致的,所以大家可以先忽略,下面我会详细分析 MockClusterInvoker 的源码,就知道为啥会有两次异常了。

情况二:服务提供者超时

注意:Dubbo 提供的服务降级机制,不支持对服务提供者业务异常的情况。

这个时候消费者会进行 1+retries 次调用,然后再进行服务降级。

org.apache.dubbo.rpc.RpcException: Failed to invoke the method sayHello in the service com.winfun.service.DubboServiceOne. Tried 2 times of the providers [127.0.0.1:20881] (1/1) from the registry 127.0.0.1:2181 on the consumer 172.26.144.16 using the dubbo version 2.7.7. Last error is: Invoke remote method timeout. method: sayHello, provider: dubbo://127.0.0.1:20881/com.winfun.service.DubboServiceOne?anyhost=true&application=dubbo-service&check=false&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&init=false&interface=com.winfun.service.DubboServiceOne&lazy=true&methods=sayHello&mock=return+%7B%22code%22%3A1%2C%22message%22%3A%22%E7%86%94%E6%96%AD%E9%99%90%E6%B5%81%E4%BA%86%22%7D&pid=52020&qos.enable=false&reference.filter=default,dubboLogFilter,-sentinel.dubbo.consumer.filter&register.ip=172.26.144.16&release=2.7.7&remote.application=dubbo-provider-one&retries=1&service.filter=default,dubboLogFilter&side=consumer&sticky=false&timestamp=1611742232968, cause: org.apache.dubbo.remoting.TimeoutException: Waiting server-side response timeout by scan timer. start time: 2021-01-27 18:10:42.189, end time: 2021-01-27 18:10:43.204, client elapsed: 1 ms, server elapsed: 1014 ms, timeout: 1000 ms, request: Request [id=1, version=2.0.2, twoway=true, event=false, broken=false, data=null], channel: /172.26.144.16:56663 -> /172.26.144.16:20881
	at org.apache.dubbo.rpc.cluster.support.FailoverClusterInvoker.doInvoke(FailoverClusterInvoker.java:113)
//....省略

3 源码分析

关于服务降级的逻辑,主要在 Dubbo 提供的 MockClusterInvoker 类中。

下面我们直接看看 MockClusterInvoker 的源码:

public class MockClusterInvoker<T> implements Invoker<T> {

    // ...省略

    @Override
    public Result invoke(Invocation invocation) throws RpcException {
        Result result = null;
        // 从 consumer 的注册url中获取方法的 mock 属性值
        String value = getUrl().getMethodParameter(invocation.getMethodName(), MOCK_KEY, Boolean.FALSE.toString()).trim();
        // mock 为空或者false,不进行服务降级处理
        if (value.length() == 0 || "false".equalsIgnoreCase(value)) {
            //no mock
            result = this.invoker.invoke(invocation);
        // 是否强行执行服务降级处理
        } else if (value.startsWith("force")) {
            if (logger.isWarnEnabled()) {
                logger.warn("force-mock: " + invocation.getMethodName() + " force-mock enabled , url : " + getUrl());
            }
            //force:direct mock
            result = doMockInvoke(invocation, null);
        } else {
            //fail-mock
            try {
                result = this.invoker.invoke(invocation);

                //fix:#4585
                if(result.getException() != null && result.getException() instanceof RpcException){
                    RpcException rpcException= (RpcException)result.getException();
                    // 如果是业务异常,直接抛出
                    if(rpcException.isBiz()){
                        throw  rpcException;
                    }else {
                        // 服务降级处理
                        result = doMockInvoke(invocation, rpcException);
                    }
                }

            } catch (RpcException e) {
                // 如果是业务异常,直接抛出
                if (e.isBiz()) {
                    throw e;
                }

                if (logger.isWarnEnabled()) {
                    logger.warn("fail-mock: " + invocation.getMethodName() + " fail-mock enabled , url : " + getUrl(), e);
                }
                // 服务降级处理
                result = doMockInvoke(invocation, e);
            }
        }
        return result;
    }

    @SuppressWarnings({"unchecked", "rawtypes"})
    private Result doMockInvoke(Invocation invocation, RpcException e) {
        Result result = null;
        Invoker<T> minvoker;

        // 获取服务降级 Invoker 列表
        List<Invoker<T>> mockInvokers = selectMockInvoker(invocation);
        if (CollectionUtils.isEmpty(mockInvokers)) {
            // 服务降级 Invoker 列表为空,默认使用 Dubbo 提供的 MockInvoker 类
            minvoker = (Invoker<T>) new MockInvoker(getUrl(), directory.getInterface());
        } else {
            minvoker = mockInvokers.get(0);
        }
        try {
            // 服务降级处理
            result = minvoker.invoke(invocation);
        } catch (RpcException me) {
            if (me.isBiz()) {
                result = AsyncRpcResult.newDefaultAsyncResult(me.getCause(), invocation);
            } else {
                throw new RpcException(me.getCode(), getMockExceptionMessage(e, me), me.getCause());
            }
        } catch (Throwable me) {
            throw new RpcException(getMockExceptionMessage(e, me), me.getCause());
        }
        return result;
    }

    //...
}

从上面的源码分析可知:

  • 在 MockClusterInvoker#invoke 方法里面,如果是业务异常是不会做 mock 做处理的。而其他关于 RPC 的异常,是会做 mock 处理的,例如我们这里拿来测试的服务提供者超时异常和服务提供者不存在;

  • 在 doMockInvoke 方法中,首先会调用 selectMockInvoker 方法会尝试获取 MockInvoker,而默认情况下,都是没有的,所以最后会使用 Dubbo 提供的 MockInvoker。

这里也回答了情况一例子中,会抛出两次异常的原因,就是因为获取 MockInvoker 的时候,会调用 selectMockInvoker,而如果服务没启动,那么必须还会再抛一次 No provider 的异常啦~

最后,大家如果有兴趣,可以自行继续研究 MockInvoker 的源码,特别是里面的 invoke 和 parseMockValue 方法,分别是服务降级的逻辑和 Mock 属性内容的解析。

如果不是看了 MockInvoker#parseMockValue 方法,一开始我都不知道如何返回 Json 格式的结果,只会直接返回 null,哈哈哈。所以源码还是得多看!

到此,关于 Dubbo 提供的服务降级的 Mock 机制已经简单地过了一遍,但是我们可以发现有一个问题:

4 Dubbo 服务降级机制的缺陷

4.1 缺陷分析

其实经过上面的例子分析和源码分析,我们会发现一个问题:由于 dubbo 不带熔断机制,所以尽管每次因为 RPC 异常而导致调用失败,也不会进行熔断处理;即不管调用失败多少次,消费者还是会继续进行调用。

其实这样会导致服务的资源浪费:

  • 只要服务提供者出现异常达到一定的次数,其实可以理解为服务提供者短时间内已经不能正常提供服务了,后续再调用也是浪费资源。
  • 如果是上述的超时问题,消费者还会进行 1+retires 次的 RPC 调用,这样就更加浪费资源了。

所以,为 dubbo 配一个熔断机制是非常有必要的了。

二 熔断机制

我个人对熔断的理解:

如果当调用失败达到指定的次数,则将熔断器打开一段时间,即将请求链路断开;在指定时间内,都不再让消费者向提供者发送请求;当熔断时间到了,就将熔断器设置为半打开的状态,此时消费者可以往提供者发送请求,并统计成功次数,如果达到指定的成功次数,熔断器则变为关闭状态,即将请求链路打开,否则熔断器又变回打开状态。

所以说,只要给dubbo加上熔断机制,不管是业务错误还是请求超时,只要时间内达到了一定的次数就做上述的熔断处理,这样就可以防止没有必要的调用。

熔断框架比较:https://www.icode9.com/content-4-620551.html

1 引入 Sentinel

经过上面的对比,我个人是建议使用 Sentinel 的,因为它提供了更加灵活的使用方式,并且支持更多的规则,还提供了一个易用强大的控制台。

Sentinel 提供了三大接入方式:利用 sentinel-core 组件进行硬代码、利用 sentinel-annotation-aspectj 组件提供的注解功能、各种主流框架的接入方式。

下面我将分别使用三种方式为 Dubbo接口 接入限流和熔断的机制。

在接入之前,我们需要注意的一个点是:要为 Dubbo接口 接入熔断&限流等机制,必须要为 Dubbo接口 指定资源,并且为资源初始化规则;否则只是引入了组件,也无法使用 Sentinel 提供的限流&熔断功能。

1.1 方式一:sentinel-core组件

引入依赖:
引入 sentinel-core 依赖,直接植入硬代码进行熔断限流。

<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-core</artifactId>
    <version>1.8.0</version>
</dependency>

Controller 代码:

/**
 * Say Hello
 * @author winfun
 * @date 2020/10/29 5:12 下午
 **/
@Slf4j
@RequestMapping("/hello")
@RestController
public class HelloController {

    public static final String RESOURCE_NAME = "dubboServiceOne";
    @DubboReference(check = false,lazy = true,retries = 0)
    private DubboServiceOne dubboServiceOne;

    /**
     * 初始化流控规则和熔断规则
     * ps:因为我们没有接入 Sentinel Dashboard,所以得自己在代码里面设置好
     */
    static{
        // 初始化流控规则
        final List<FlowRule> flowRules = new ArrayList<>();
        final List<DegradeRule> degradeRules = new ArrayList<>();
        // 限流规则
        final FlowRule flowRule = new FlowRule();
        flowRule.setResource(RESOURCE_NAME);
        flowRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
        // 1 QPS
        flowRule.setCount(1);
        flowRules.add(flowRule);
        // 熔断规则
        final DegradeRule degradeRule = new DegradeRule();
        degradeRule.setResource(RESOURCE_NAME);
        degradeRule.setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_COUNT);
        // 2个异常数
        degradeRule.setCount(2);
        // 时间窗口长度,单位为秒
        degradeRule.setTimeWindow(5);
        // 最小请求数
        degradeRule.setMinRequestAmount(5);
        // 熔断时长:当5秒内,10个请求里面出现2个异常,则进行熔断,熔断时长为10s
        degradeRule.setStatIntervalMs(10000);
        degradeRules.add(degradeRule);
        FlowRuleManager.loadRules(flowRules);
        DegradeRuleManager.loadRules(degradeRules);
    }

    @GetMapping("/{name}")
    public ApiResult sayHello(@PathVariable("name") final String name){
        final String hello = this.sayHelloByDubbo2Code(name);
        return ApiResult.success(hello);
    }


    /***
     * 接入Sentinel方式:植入硬代码进行熔断限流
     * @author winfun
     * @param name name
     * @return {@link ApiResult<String> }
     **/
    private ApiResult<String> sayHelloByDubbo2Code(final String name) {

        ApiResult<String> result;
        Entry entry = null;
        try {
            entry = SphU.entry(RESOURCE_NAME);
            result = this.dubboServiceOne.sayHello(name);
        }  catch (BlockException e) {
            if (e instanceof DegradeException){
                log.error("资源:{} 被熔断了,message is {}",RESOURCE_NAME,e.getMessage());
                result = ApiResult.fail("hello fallback");
            }else {
                log.error("资源:{} 被流控了",RESOURCE_NAME);
                result = ApiResult.fail("hello block");
            }
        } catch (Exception e){
            log.error("业务处理发生异常,exception is {}",e.getMessage());
            // 若需要配置降级规则,需要通过这种方式记录业务异常
            Tracer.traceEntry(e, entry);
            result = ApiResult.fail("exception");
            //throw new RuntimeException("业务处理发生异常");
        } finally {
            // 务必保证 exit,务必保证每个 entry 与 exit 配对
            if (entry != null) {
                entry.exit();
            }
        }
        return result;
    }
}

单元测试:
利用 MockMvc 来进行接口测试,循环调用。

@AutoConfigureMockMvc
@SpringBootTest(classes = DubboServiceApplication.class)
@RunWith(SpringRunner.class)
public class ServiceOneApplicationTests {

    @Autowired
    private MockMvc mockMvc;

    @SneakyThrows
    @Test
    public void sayHello(){
        for (int i = 0; i < 100; i++) {
            // 休眠500毫秒,即1秒两次调用,可以触发流控规则
            Thread.sleep(500);
            MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get("/hello/winfun"))
                    .andReturn();
            System.out.println(result.getResponse().getContentAsString());
        }
    }
}

在看单元测试的结果前,我们先分析一下我们的流控&熔断配置:

  • 限流,我们是限制每秒只能有1个请求
  • 熔断:在5秒内,如果5个请求中有两个请求是发生异常的,则打开熔断器,熔断时长为10s。
  • 熔断中,当超过熔断时长了,接下来一个请求如果还是发生异常,则继续打开熔断器,否则就是关闭熔断器,恢复正常的请求链路。

单元测试中,我们是不会开启服务提供者的,先看看控制台打印的日志信息:

2021-01-27 11:42:18.568 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 发生异常,message is No provider available from registry 127.0.0.1:2181 for service com.winfun.service.DubboServiceOne on consumer 127.0.0.1 use dubbo version 2.7.7, please check status of providers(disabled, not registered or in blacklist).
{"code":1,"message":"exception","data":null,"traceId":null}
2021-01-27 11:42:19.147 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 被流控了
{"code":1,"message":"hello block","data":null,"traceId":null}
2021-01-27 11:42:19.653 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 发生异常,message is No provider available from registry 127.0.0.1:2181 for service com.winfun.service.DubboServiceOne on consumer 127.0.0.1 use dubbo version 2.7.7, please check status of providers(disabled, not registered or in blacklist).
{"code":1,"message":"exception","data":null,"traceId":null}
2021-01-27 11:42:20.160 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 被流控了
{"code":1,"message":"hello block","data":null,"traceId":null}
2021-01-27 11:42:20.666 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 发生异常,message is No provider available from registry 127.0.0.1:2181 for service com.winfun.service.DubboServiceOne on consumer 127.0.0.1 use dubbo version 2.7.7, please check status of providers(disabled, not registered or in blacklist).
{"code":1,"message":"exception","data":null,"traceId":null}
2021-01-27 11:42:21.172 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 被流控了
{"code":1,"message":"hello block","data":null,"traceId":null}
2021-01-27 11:42:21.681 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 发生异常,message is No provider available from registry 127.0.0.1:2181 for service com.winfun.service.DubboServiceOne on consumer 127.0.0.1 use dubbo version 2.7.7, please check status of providers(disabled, not registered or in blacklist).
{"code":1,"message":"exception","data":null,"traceId":null}
2021-01-27 11:42:22.190 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 被流控了
{"code":1,"message":"hello block","data":null,"traceId":null}
2021-01-27 11:42:22.695 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 发生异常,message is No provider available from registry 127.0.0.1:2181 for service com.winfun.service.DubboServiceOne on consumer 127.0.0.1 use dubbo version 2.7.7, please check status of providers(disabled, not registered or in blacklist).
{"code":1,"message":"exception","data":null,"traceId":null}
2021-01-27 11:42:23.202 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 被流控了
{"code":1,"message":"hello block","data":null,"traceId":null}
2021-01-27 11:42:23.709 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 发生异常,message is No provider available from registry 127.0.0.1:2181 for service com.winfun.service.DubboServiceOne on consumer 127.0.0.1 use dubbo version 2.7.7, please check status of providers(disabled, not registered or in blacklist).
{"code":1,"message":"exception","data":null,"traceId":null}
2021-01-27 11:42:24.215 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 被流控了
{"code":1,"message":"hello block","data":null,"traceId":null}
2021-01-27 11:42:24.724 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 发生异常,message is No provider available from registry 127.0.0.1:2181 for service com.winfun.service.DubboServiceOne on consumer 127.0.0.1 use dubbo version 2.7.7, please check status of providers(disabled, not registered or in blacklist).
{"code":1,"message":"exception","data":null,"traceId":null}
2021-01-27 11:42:25.232 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 被流控了
{"code":1,"message":"hello block","data":null,"traceId":null}
2021-01-27 11:42:25.738 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 被熔断了,message is null
{"code":1,"message":"hello fallback","data":null,"traceId":null}
2021-01-27 11:42:26.247 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 被熔断了,message is null
{"code":1,"message":"hello fallback","data":null,"traceId":null}
2021-01-27 11:42:26.757 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 被熔断了,message is null
{"code":1,"message":"hello fallback","data":null,"traceId":null}
2021-01-27 11:42:27.265 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 被熔断了,message is null
{"code":1,"message":"hello fallback","data":null,"traceId":null}
2021-01-27 11:42:27.769 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 被熔断了,message is null
{"code":1,"message":"hello fallback","data":null,"traceId":null}
2021-01-27 11:42:28.274 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 被熔断了,message is null
{"code":1,"message":"hello fallback","data":null,"traceId":null}
2021-01-27 11:42:28.779 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 被熔断了,message is null
{"code":1,"message":"hello fallback","data":null,"traceId":null}
2021-01-27 11:42:29.284 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 被熔断了,message is null
{"code":1,"message":"hello fallback","data":null,"traceId":null}
2021-01-27 11:42:29.790 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 发生异常,message is No provider available from registry 127.0.0.1:2181 for service com.winfun.service.DubboServiceOne on consumer 127.0.0.1 use dubbo version 2.7.7, please check status of providers(disabled, not registered or in blacklist).
{"code":1,"message":"exception","data":null,"traceId":null}
2021-01-27 11:42:30.299 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 被流控了
{"code":1,"message":"hello block","data":null,"traceId":null}
2021-01-27 11:42:30.803 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 被熔断了,message is null
{"code":1,"message":"hello fallback","data":null,"traceId":null}
2021-01-27 11:42:31.309 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 被熔断了,message is null
{"code":1,"message":"hello fallback","data":null,"traceId":null}
2021-01-27 11:42:31.814 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 被熔断了,message is null
{"code":1,"message":"hello fallback","data":null,"traceId":null}
// .....

总结:
我们可以看到,控制台的日志打印是符合我们的预期的:

  • 限流:1秒内会调用两次接口,所以会每两次调用都会触发限流规则。
  • 熔断:服务提供者未启动,即每次 RPC 调用都会出现异常,所以时间窗口内出现一定数量的异常调用,触发了熔断规则。

并且,流控规则的优先级优先于熔断规则。

1.2 方式二:使用注解 @SentinelResource

引入依赖:
引入 sentinel-annotation-aspectj 依赖。

<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-transport-simple-http</artifactId>
    <version>1.8.0</version>
</dependency>

注入切面:
手动注入 SentinelResourceAspect,因为 Sentinel 提供的这个切面,是没有 @Component 等注解的,所以还是得自己注入到 Spring 容器中。

/**
 * 需要引入一下,因为 Sentile 提供的这个切面是没有加 @Component 注解的
 * @return
 */
@Bean
public SentinelResourceAspect sentinelResourceAspect() {
    return new SentinelResourceAspect();
}

我们使用这个 @SentinelResource 注解,可直接在 Controller 的方法加注解,也可以自己再弄一个 Service 来加注解,这两个都可以.
因为我们的 Controller 层就是只调用了 Dubbo 接口,所以就直接在 Controller 的方法上面加注解了,而如果业务是非常多的,那就可以考虑单独弄个 Service 来给 Dubbo 接口再包一层。

Spring AOP 同时支持 JDK 和 CGLIB 动态代理,SpringBoot 2.x 默认使用 CGLIB 动态代理。

Controller 代码:

/**
 * Say Hello
 * @author winfun
 * @date 2020/10/29 5:12 下午
 **/
@Slf4j
@RequestMapping("/hello")
@RestController
public class HelloController {

    public static final String RESOURCE_NAME = "dubboServiceOne";
    @DubboReference(check = false,lazy = true,retries = 0)
    private DubboServiceOne dubboServiceOne;

    /**
     * 初始化流控规则和熔断规则
     * ps:因为我们没有接入 Sentinel Dashboard,所以得自己在代码里面设置好
     */
    static{
        // 初始化流控规则
        final List<FlowRule> flowRules = new ArrayList<>();
        final List<DegradeRule> degradeRules = new ArrayList<>();
        // 限流规则
        final FlowRule flowRule = new FlowRule();
        flowRule.setResource(RESOURCE_NAME);
        flowRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
        // 1 QPS
        flowRule.setCount(1);
        flowRules.add(flowRule);
        // 熔断规则
        final DegradeRule degradeRule = new DegradeRule();
        degradeRule.setResource(RESOURCE_NAME);
        degradeRule.setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_COUNT);
        // 2个异常数
        degradeRule.setCount(1);
        // 时间窗口长度,单位为秒
        degradeRule.setTimeWindow(5);
        // 最小请求数
        degradeRule.setMinRequestAmount(5);
        // 熔断时长:当5秒内,10个请求里面出现2个异常,则进行熔断,熔断时长为10s
        degradeRule.setStatIntervalMs(10000);
        degradeRules.add(degradeRule);

        FlowRuleManager.loadRules(flowRules);
        DegradeRuleManager.loadRules(degradeRules);
    }

    @GetMapping("/{name}")
    @SentinelResource(value=RESOURCE_NAME,fallback = "sayHelloFallback",blockHandler = "sayHelloBlock")
    public ApiResult sayHello(@PathVariable("name") final String name){
        String hello = this.dubboServiceOne.sayHello(name);
        return ApiResult.success(hello);
    }

    /**
     * Fallback 函数,函数签名与原函数一致或加一个 Throwable 类型的参数.
     * @param name
     * @param throwable
     * @return {@link ApiResult<String> }
     */
    public ApiResult<String> sayHelloFallback(final String name, final Throwable throwable){
        log.error("资源:{} 发生异常,message is {}",RESOURCE_NAME,throwable.getMessage());
        return ApiResult.fail("hello exception");
    }

    /**
     * BlockHandler 函数
     * blockHandler 函数访问范围需要是 public,返回类型需要与原方法相匹配,参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为 BlockException
     * @param name
     * @param e
     * @return {@link ApiResult<String> }
     */
    public ApiResult<String> sayHelloBlock(final String name, final BlockException e){
        ApiResult<String> result;
        if (e instanceof DegradeException){
            log.error("资源:{} 被熔断了,message is {}",RESOURCE_NAME,e.getMessage());
            result = ApiResult.fail("hello fallback");
        }else {
            log.error("资源:{} 被流控了",RESOURCE_NAME);
            result = ApiResult.fail("hello block");
        }
        return result;
    }
}

bolckHandler 和 fallbackHandler 的返回值都要原方法一致。

单元测试:

@AutoConfigureMockMvc
@SpringBootTest(classes = DubboServiceApplication.class)
@RunWith(SpringRunner.class)
public class ServiceOneApplicationTests {

    @Autowired
    private MockMvc mockMvc;

    @SneakyThrows
    @Test
    public void sayHello(){
        for (int i = 0; i < 100; i++) {
            // 休眠500毫秒,即1秒两次调用,可以出发流控规则
            Thread.sleep(500);
            MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get("/hello/winfun"))
                    .andReturn();
            System.out.println(result.getResponse().getContentAsString());
        }
    }

}

控制台打印:可以看到和上面使用 Sentinel-core 植入硬代码的打印是一样的

2021-01-27 11:43:36.995 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 发生异常,message is No provider available from registry 127.0.0.1:2181 for service com.winfun.service.DubboServiceOne on consumer 127.0.0.1 use dubbo version 2.7.7, please check status of providers(disabled, not registered or in blacklist).
{"code":1,"message":"hello exception","data":null,"traceId":null}
2021-01-27 11:43:37.560 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 发生异常,message is No provider available from registry 127.0.0.1:2181 for service com.winfun.service.DubboServiceOne on consumer 127.0.0.1 use dubbo version 2.7.7, please check status of providers(disabled, not registered or in blacklist).
{"code":1,"message":"hello exception","data":null,"traceId":null}
2021-01-27 11:43:38.098 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 被流控了
{"code":1,"message":"hello block","data":null,"traceId":null}
2021-01-27 11:43:38.605 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 发生异常,message is No provider available from registry 127.0.0.1:2181 for service com.winfun.service.DubboServiceOne on consumer 127.0.0.1 use dubbo version 2.7.7, please check status of providers(disabled, not registered or in blacklist).
{"code":1,"message":"hello exception","data":null,"traceId":null}
2021-01-27 11:43:39.114 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 被流控了
{"code":1,"message":"hello block","data":null,"traceId":null}
2021-01-27 11:43:39.620 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 发生异常,message is No provider available from registry 127.0.0.1:2181 for service com.winfun.service.DubboServiceOne on consumer 127.0.0.1 use dubbo version 2.7.7, please check status of providers(disabled, not registered or in blacklist).
{"code":1,"message":"hello exception","data":null,"traceId":null}
2021-01-27 11:43:40.129 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 被流控了
{"code":1,"message":"hello block","data":null,"traceId":null}
2021-01-27 11:43:40.638 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 发生异常,message is No provider available from registry 127.0.0.1:2181 for service com.winfun.service.DubboServiceOne on consumer 127.0.0.1 use dubbo version 2.7.7, please check status of providers(disabled, not registered or in blacklist).
{"code":1,"message":"hello exception","data":null,"traceId":null}
2021-01-27 11:43:41.144 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 被流控了
{"code":1,"message":"hello block","data":null,"traceId":null}
2021-01-27 11:43:41.651 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 发生异常,message is No provider available from registry 127.0.0.1:2181 for service com.winfun.service.DubboServiceOne on consumer 127.0.0.1 use dubbo version 2.7.7, please check status of providers(disabled, not registered or in blacklist).
{"code":1,"message":"hello exception","data":null,"traceId":null}
2021-01-27 11:43:42.160 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 被流控了
{"code":1,"message":"hello block","data":null,"traceId":null}
2021-01-27 11:43:42.668 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 发生异常,message is No provider available from registry 127.0.0.1:2181 for service com.winfun.service.DubboServiceOne on consumer 127.0.0.1 use dubbo version 2.7.7, please check status of providers(disabled, not registered or in blacklist).
{"code":1,"message":"hello exception","data":null,"traceId":null}
2021-01-27 11:43:43.173 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 被流控了
{"code":1,"message":"hello block","data":null,"traceId":null}
2021-01-27 11:43:43.679 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 发生异常,message is No provider available from registry 127.0.0.1:2181 for service com.winfun.service.DubboServiceOne on consumer 127.0.0.1 use dubbo version 2.7.7, please check status of providers(disabled, not registered or in blacklist).
{"code":1,"message":"hello exception","data":null,"traceId":null}
2021-01-27 11:43:44.185 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 被流控了
{"code":1,"message":"hello block","data":null,"traceId":null}
2021-01-27 11:43:44.691 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 发生异常,message is No provider available from registry 127.0.0.1:2181 for service com.winfun.service.DubboServiceOne on consumer 127.0.0.1 use dubbo version 2.7.7, please check status of providers(disabled, not registered or in blacklist).
{"code":1,"message":"hello exception","data":null,"traceId":null}
2021-01-27 11:43:45.198 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 被流控了
{"code":1,"message":"hello block","data":null,"traceId":null}
2021-01-27 11:43:45.704 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 被熔断了,message is null
{"code":1,"message":"hello fallback","data":null,"traceId":null}
2021-01-27 11:43:46.208 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 被熔断了,message is null
{"code":1,"message":"hello fallback","data":null,"traceId":null}
2021-01-27 11:43:46.714 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 被熔断了,message is null
{"code":1,"message":"hello fallback","data":null,"traceId":null}
2021-01-27 11:43:47.222 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 被熔断了,message is null
{"code":1,"message":"hello fallback","data":null,"traceId":null}
2021-01-27 11:43:47.729 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 被熔断了,message is null
{"code":1,"message":"hello fallback","data":null,"traceId":null}
2021-01-27 11:43:48.233 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 被熔断了,message is null
{"code":1,"message":"hello fallback","data":null,"traceId":null}
2021-01-27 11:43:48.736 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 被熔断了,message is null
{"code":1,"message":"hello fallback","data":null,"traceId":null}
2021-01-27 11:43:49.247 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 被熔断了,message is null
{"code":1,"message":"hello fallback","data":null,"traceId":null}
2021-01-27 11:43:49.758 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 发生异常,message is No provider available from registry 127.0.0.1:2181 for service com.winfun.service.DubboServiceOne on consumer 127.0.0.1 use dubbo version 2.7.7, please check status of providers(disabled, not registered or in blacklist).
{"code":1,"message":"hello exception","data":null,"traceId":null}
2021-01-27 11:43:50.266 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 被流控了
{"code":1,"message":"hello block","data":null,"traceId":null}
2021-01-27 11:43:50.773 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 被熔断了,message is null
{"code":1,"message":"hello fallback","data":null,"traceId":null}
2021-01-27 11:43:51.277 [main] [ERROR] [c.w.c.HelloController] [] [] - 资源:dubboServiceOne 被熔断了,message is null
{"code":1,"message":"hello fallback","data":null,"traceId":null}
// .....

1.3 sentinel-apache-dubbo-adapter 组件

1.3.1 简单介绍

虽然使用 @SentinelResource 注解避免了 sentinel-core 那样的硬代码,但是对 Dubbo接口 增加熔断机制还是比较麻烦的,需要对每个业务方法增加注解;但是别慌,Sentinel 针对 Dubbo 提供了组件 sentinel-apache-dubbo-adapter,只要引入这个依赖,即可对所有 Dubbo 接口进行熔断和流控。

原理:

它的原理还是比较简单的,利用的是 Dubbo 提供的 Filter 机制,为服务提供者和服务消费者分别建立了 SentinelDubboProviderFilter 和 SentinelDubboConsumerFilter 过滤器。在过滤器的 invoke 方法里面就植入了 Sentinel 的代码,大概如下:

private Result syncInvoke(Invoker<?> invoker, Invocation invocation) {
    Entry interfaceEntry = null;
    Entry methodEntry = null;
    String prefix = DubboAdapterGlobalConfig.getDubboConsumerResNamePrefixKey();
    String interfaceResourceName = getInterfaceName(invoker, prefix);
    String methodResourceName = getMethodName(invoker, invocation, prefix);
    try {
        interfaceEntry = SphU.entry(interfaceResourceName, ResourceTypeConstants.COMMON_RPC, EntryType.OUT);
        methodEntry = SphU.entry(methodResourceName, ResourceTypeConstants.COMMON_RPC, EntryType.OUT,
            invocation.getArguments());
        Result result = invoker.invoke(invocation);
        if (result.hasException()) {
            Tracer.traceEntry(result.getException(), interfaceEntry);
            Tracer.traceEntry(result.getException(), methodEntry);
        }
        return result;
    } catch (BlockException e) {
        return DubboAdapterGlobalConfig.getConsumerFallback().handle(invoker, invocation, e);
    } catch (RpcException e) {
        Tracer.traceEntry(e, interfaceEntry);
        Tracer.traceEntry(e, methodEntry);
        throw e;
    } finally {
        if (methodEntry != null) {
            methodEntry.exit(1, invocation.getArguments());
        }
        if (interfaceEntry != null) {
            interfaceEntry.exit();
        }
    }
}

从上面源码我们可以发现,这样会有一个小问题;如果使用 sentinel-apache-dubbo-adapter 组件,Sentinel 无法将 No provider xxxx 这个异常作为熔断机制的一个异常点,因为过滤器的 invoke 方法是需要先获取到 Invoker 的,所以如果服务提供者未启动,那么在进入过滤器之前,就已经抛出异常了。至于要怎么解决,只需要配合上面介绍的 Dubbo 自身提供的服务降级机制即可。

初始化规则:
使用之前,我们还需要注意如何初始化规则,sentinel-apache-dubbo-adapter 组件对 Dubbo 接口的资源名定义有自己的一套规则:

  • 服务接口:resourceName 为 接口全限定名,如 com.alibaba.csp.sentinel.demo.dubbo.FooService
  • 服务方法:resourceName 为 接口全限定名:方法签名,如 com.alibaba.csp.sentinel.demo.dubbo.FooService:sayHello(java.lang.String)

全局 fallback 函数:
Sentinel Dubbo Adapter 还支持配置全局的 fallback 函数,可以在 Dubbo 服务被限流/降级/负载保护的时候进行相应的 fallback 处理。用户只需要实现自定义的 DubboFallback 接口,并通过 DubboFallbackRegistry 注册即可。默认情况会直接将 BlockException 包装后抛出。

1.3.2 接入

引入依赖:

<!-- 适配 dubbo 框架,里面带了 sentinel-core -->
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-apache-dubbo-adapter</artifactId>
    <version>1.8.0</version>
</dependency>

Controller:
因为我们只是对 sentinel-apache-dubbo-adapter 组件做测试,所以我们只针对 Dubbo 接口初始化了流控规则和熔断规则;业务上,可以更细粒度地对 Dubbo 接口的方法进行流控规则和熔断规则的配置。

/**
 * Say Hello
 * @author winfun
 * @date 2020/10/29 5:12 下午
 **/
@Slf4j
@RestController
public class HelloController {

    public static final String DUBBO_INTERFACE_RESOURCE_NAME = "com.winfun.service.DubboServiceOne";
    @DubboReference(check = false,lazy = true,retries = 0)
    private DubboServiceOne dubboServiceOne;

    /**
     * 初始化流控规则和熔断规则
     * ps:因为我们没有接入 Sentinel Dashboard,所以得自己在代码里面设置好
     */
    static{
        /**
         * 给dubbo接口设置流控,维度:接口或方法
         * 以 DubboServiceOne 接口为例子:
         * 接口:com.winfun.service.DubboServiceOne
         * 方法:com.winfun.service.DubboServiceOne:sayHello(java.lang.String)
         */
        // 限流规则
        final FlowRule dobboInterfaceFlowRule = new FlowRule();
        dobboInterfaceFlowRule.setResource(DUBBO_INTERFACE_RESOURCE_NAME);
        dobboInterfaceFlowRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
        // 1 QPS
        dobboInterfaceFlowRule.setCount(1);
        flowRules.add(dobboInterfaceFlowRule);
        // 熔断规则
        final DegradeRule dubboInterfaceDegradeRule = new DegradeRule();
        dubboInterfaceDegradeRule.setResource(DUBBO_INTERFACE_RESOURCE_NAME);
        dubboInterfaceDegradeRule.setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_COUNT);
        // 2个异常数
        dubboInterfaceDegradeRule.setCount(2);
        // 时间窗口长度,单位为秒
        dubboInterfaceDegradeRule.setTimeWindow(5);
        // 最小请求数
        dubboInterfaceDegradeRule.setMinRequestAmount(5);
        // 熔断时长:当5秒内,10个请求里面出现2个异常,则进行熔断,熔断时长为10s
        dubboInterfaceDegradeRule.setStatIntervalMs(10000);
        degradeRules.add(dubboInterfaceDegradeRule);
        FlowRuleManager.loadRules(flowRules);
        DegradeRuleManager.loadRules(degradeRules);
        // 全局 Fallback 函数
        DubboAdapterGlobalConfig.setConsumerFallback(new DefaultDubboFallback());
    }

    @GetMapping("/hello/{name}")
    public ApiResult sayHello(@PathVariable("name") final String name){
        return this.dubboServiceOne.sayHello(name);
    }
}

单元测试:

@Slf4j
@AutoConfigureMockMvc
@SpringBootTest(classes = DubboServiceApplication.class)
@RunWith(SpringRunner.class)
public class ServiceOneApplicationTests {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void sayHello2(){
        for (int i = 0; i < 100; i++) {
            try {
                // 休眠500毫秒,即1秒两次调用,可以出发流控规则
                Thread.sleep(500);
                MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get("/hello/winfun"))
                        .andReturn();
                log.info("业务处理成功,结果:{}", result.getResponse().getContentAsString());
            } catch (Exception e) {
                log.error("业务处理发生异常,exception is {}", e.getMessage());
            }
        }
    }
}

控制台打印:由于我这次 Dubbo 的限流和熔断是基于 Trace 组件来搭建的,所以日志会有点多,下面在做单元测试时,会将Trace的log信息去除掉;由于配置和 Sentinel-core 例子、@SentinelResource 例子的配置是一样的,因此我们可以发现日志如出一辙。

2021-01-27 14:49:56.883 [main] [ERROR] [c.w.s.ServiceOneApplicationTests] [] [] - 业务处理发生异常,exception is Request processing failed; nested exception is java.lang.Exception: Exception 来搞事了
2021-01-27 14:49:57.428 [main] [ERROR] [c.w.f.DefaultDubboFallback] [] [] - 资源:com.winfun.service.DubboServiceOne 被流控了
{"code":1,"message":"hello block","data":null,"traceId":null}
2021-01-27 14:49:58.059 [main] [ERROR] [c.w.s.ServiceOneApplicationTests] [] [] - 业务处理发生异常,exception is Request processing failed; nested exception is java.lang.Exception: Exception 来搞事了
2021-01-27 14:49:58.562 [main] [ERROR] [c.w.f.DefaultDubboFallback] [] [] - 资源:com.winfun.service.DubboServiceOne 被流控了
{"code":1,"message":"hello block","data":null,"traceId":null}
2021-01-27 14:49:59.079 [main] [ERROR] [c.w.s.ServiceOneApplicationTests] [] [] - 业务处理发生异常,exception is Request processing failed; nested exception is java.lang.Exception: Exception 来搞事了
2021-01-27 14:49:59.582 [main] [ERROR] [c.w.f.DefaultDubboFallback] [] [] - 资源:com.winfun.service.DubboServiceOne 被流控了
{"code":1,"message":"hello block","data":null,"traceId":null}
2021-01-27 14:50:00.098 [main] [ERROR] [c.w.s.ServiceOneApplicationTests] [] [] - 业务处理发生异常,exception is Request processing failed; nested exception is java.lang.Exception: Exception 来搞事了
2021-01-27 14:50:00.601 [main] [ERROR] [c.w.f.DefaultDubboFallback] [] [] - 资源:com.winfun.service.DubboServiceOne 被流控了
{"code":1,"message":"hello block","data":null,"traceId":null}
2021-01-27 14:50:01.113 [main] [ERROR] [c.w.s.ServiceOneApplicationTests] [] [] - 业务处理发生异常,exception is Request processing failed; nested exception is java.lang.Exception: Exception 来搞事了
2021-01-27 14:50:01.617 [main] [ERROR] [c.w.f.DefaultDubboFallback] [] [] - 资源:com.winfun.service.DubboServiceOne 被流控了
{"code":1,"message":"hello block","data":null,"traceId":null}
2021-01-27 14:50:02.129 [main] [ERROR] [c.w.s.ServiceOneApplicationTests] [] [] - 业务处理发生异常,exception is Request processing failed; nested exception is java.lang.Exception: Exception 来搞事了
2021-01-27 14:50:02.633 [main] [ERROR] [c.w.f.DefaultDubboFallback] [] [] - 资源:com.winfun.service.DubboServiceOne 被流控了
{"code":1,"message":"hello block","data":null,"traceId":null}
2021-01-27 14:50:03.147 [main] [ERROR] [c.w.s.ServiceOneApplicationTests] [] [] - 业务处理发生异常,exception is Request processing failed; nested exception is java.lang.Exception: Exception 来搞事了
2021-01-27 14:50:03.653 [main] [ERROR] [c.w.f.DefaultDubboFallback] [] [] - 资源:com.winfun.service.DubboServiceOne 被流控了
{"code":1,"message":"hello block","data":null,"traceId":null}
2021-01-27 14:50:04.170 [main] [ERROR] [c.w.s.ServiceOneApplicationTests] [] [] - 业务处理发生异常,exception is Request processing failed; nested exception is java.lang.Exception: Exception 来搞事了
2021-01-27 14:50:04.676 [main] [ERROR] [c.w.f.DefaultDubboFallback] [] [] - 资源:com.winfun.service.DubboServiceOne 被流控了
{"code":1,"message":"hello block","data":null,"traceId":null}
2021-01-27 14:50:05.183 [main] [ERROR] [c.w.f.DefaultDubboFallback] [] [] - 资源:com.winfun.service.DubboServiceOne 被熔断了,message is null
{"code":1,"message":"hello fallback","data":null,"traceId":null}
2021-01-27 14:50:05.688 [main] [ERROR] [c.w.f.DefaultDubboFallback] [] [] - 资源:com.winfun.service.DubboServiceOne 被熔断了,message is null
{"code":1,"message":"hello fallback","data":null,"traceId":null}
2021-01-27 14:50:06.194 [main] [ERROR] [c.w.f.DefaultDubboFallback] [] [] - 资源:com.winfun.service.DubboServiceOne 被熔断了,message is null
{"code":1,"message":"hello fallback","data":null,"traceId":null}
2021-01-27 14:50:06.700 [main] [ERROR] [c.w.f.DefaultDubboFallback] [] [] - 资源:com.winfun.service.DubboServiceOne 被熔断了,message is null
{"code":1,"message":"hello fallback","data":null,"traceId":null}
2021-01-27 14:50:07.207 [main] [ERROR] [c.w.f.DefaultDubboFallback] [] [] - 资源:com.winfun.service.DubboServiceOne 被熔断了,message is null
{"code":1,"message":"hello fallback","data":null,"traceId":null}
2021-01-27 14:50:07.712 [main] [ERROR] [c.w.f.DefaultDubboFallback] [] [] - 资源:com.winfun.service.DubboServiceOne 被熔断了,message is null
{"code":1,"message":"hello fallback","data":null,"traceId":null}
2021-01-27 14:50:08.220 [main] [ERROR] [c.w.f.DefaultDubboFallback] [] [] - 资源:com.winfun.service.DubboServiceOne 被熔断了,message is null
{"code":1,"message":"hello fallback","data":null,"traceId":null}
2021-01-27 14:50:08.726 [main] [ERROR] [c.w.f.DefaultDubboFallback] [] [] - 资源:com.winfun.service.DubboServiceOne 被熔断了,message is null
{"code":1,"message":"hello fallback","data":null,"traceId":null}
2021-01-27 14:50:09.246 [main] [ERROR] [c.w.s.ServiceOneApplicationTests] [] [] - 业务处理发生异常,exception is Request processing failed; nested exception is java.lang.Exception: Exception 来搞事了
2021-01-27 14:50:09.749 [main] [ERROR] [c.w.f.DefaultDubboFallback] [] [] - 资源:com.winfun.service.DubboServiceOne 被流控了
{"code":1,"message":"hello block","data":null,"traceId":null}
2021-01-27 14:50:10.255 [main] [ERROR] [c.w.f.DefaultDubboFallback] [] [] - 资源:com.winfun.service.DubboServiceOne 被熔断了,message is null
{"code":1,"message":"hello fallback","data":null,"traceId":null}
2021-01-27 14:50:10.762 [main] [ERROR] [c.w.f.DefaultDubboFallback] [] [] - 资源:com.winfun.service.DubboServiceOne 被熔断了,message is null
{"code":1,"message":"hello fallback","data":null,"traceId":null}
2021-01-27 14:50:11.270 [main] [ERROR] [c.w.f.DefaultDubboFallback] [] [] - 资源:com.winfun.service.DubboServiceOne 被熔断了,message is null
{"code":1,"message":"hello fallback","data":null,"traceId":null}
// .....

到此,关于 Sentinel 提供的三种接入方式,我们都已经尝试了;只不过对于熔断机制的配置还没全部完一遍,我们上面例子仅仅是利用了 异常数的机制,其实还有慢调用机制和异常数比例机制,当然了,这些机制其实在代码层面的玩法都是一样的,大家可以自行尝试。

当然了,除了流控和熔断,Sentinel 还提供了多种机制,例如热点参数限流、系统自适应限流,黑白名单控制和实时监控数据等等,感兴趣的可以去研究一下,这里不做详细介绍和使用。

但是到这里还没结束,因为我们可以发现,我们需要在代码层面为每一个需要限流或者熔断的接口初始化限流规则或熔断规则,所以这个是非常麻烦的,不过不用怕,Sentinel 还提供了 Sentinel控制台 组件,我们可以接入这个组件,例如它来给资源配置流控、熔断和热点等规则。

2 部署控制台

2.1 项目接入

这个不用多详细介绍,直接看官方文档即可:
启动控制台

主要是先获取到 Sentinel-Dashboard 这个组件的 jar 包,然后启动即可;因为 Sentinel-Dashboard 这个项目是一个 Spring Boot 项目,我们直接打成 Jar 包,利用 java -jar 命令启动即可,没有任何难度。

接着需要注意的是,Sentinel 1.6.0 之后,Sentinel控制台增加了登录校验,默认账号和密码都是 sentinel。

2.2 客户端接入

这个也不用多详细介绍,直接看官方文档即可:
客户端接入控制台

客户端接入主要就是引入依赖,然后增加启动命令即可,也是非常的简单。

但是要注意的一个点是:如果是 Spring Cloud 项目,可以在配置文件配置 Sentinel控制台 的配置;但如果是 Spring Boot+Dubbo的项目,一定要按照文档的走,利用 JVM 参数来指定 Sentinel控制台 的配置。

接着,不是项目一启动,就会在 Sentinel控制台 为所有 Dubbo接口 创建对应的资源,而是当第一次成功调用 Dubbo接口 后,才会在 Sentinel控制台 初始化对应的资源;并且,只有当服务被第一次成功调用接口,才会在 Sentinel控制台 显示。

2.3 为资源配置规则

当资源创建后,我们可以在 簇点链路中,为资源新建流控、熔断等规则。
在这里插入图片描述

三、总结

到此,关于 Dubbo 的服务降级和熔断机制,已经全部介绍完毕,配合上例子和源码的讲解,大家应该会有一定程度上的理解了。

在分布式系统和微服务架构中,服务间坚总是互相依赖,为服务加入熔断和降级是必不可少的,希望大家能一起研究研究,如果有什么疑惑或者建议,可评论在下方,欢迎点赞收藏~

分类:

技术点:

Dubbo

相关文章: