(萬字好文)Dubbo服務熔斷與降級的深入講解&程式碼實戰

不送花的程式猿發表於2021-01-30

原文連結:(萬字好文)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 的服務降級和熔斷機制,已經全部介紹完畢,配合上例子和原始碼的講解,大家應該會有一定程度上的理解了。

在分散式系統和微服務架構中,服務間堅總是互相依賴,為服務加入熔斷和降級是必不可少的,希望大家能一起研究研究,如果有什麼疑惑或者建議,可評論在下方,歡迎點贊收藏~

相關文章