統一降級策略
在我們之前的博客裏介紹了dubbo如何整合sentinel進行限流。既然用到了限流,那麼每次讓被限流的服務都拋一個異常讓調用端自己處理顯然不是優雅的解決方案,在者一些帶有業務含義的降級策略只有在服務提供方纔能完成,這時候我們就需要引入sentinel的降級能力。sentinel官方文檔推薦的做法是使用註解。這種方法比較靈活但缺點是降級的代碼比較分散,代碼量也比較多,這時候我們需要一種全量的降級策略,將降級的邏輯集中起來,或者極端情況下一個provider的所有服務都可以使用同一個降級策略(比如返回null)。官方文檔裏並沒有提及這樣的降級方法,下面我們來探索如何實現上述邏輯。
從源碼入手
解決問題的最好方式就是從源碼入口,dubbo的擴展點在於filter,sentinel也是通過一個filter與dubbo做了整合
com.alibaba.csp.sentinel.adapter.dubbo.SentinelDubboProviderFilter
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
// Get origin caller.
String application = DubboUtils.getApplication(invocation, "");
Entry interfaceEntry = null;
Entry methodEntry = null;
try {
String resourceName = DubboUtils.getResourceName(invoker, invocation);
String interfaceName = invoker.getInterface().getName();
// Only need to create entrance context at provider side, as context will take effect
// at entrance of invocation chain only (for inbound traffic).
ContextUtil.enter(resourceName, application);
interfaceEntry = SphU.entry(interfaceName, ResourceTypeConstants.COMMON_RPC, EntryType.IN);
methodEntry = SphU.entry(resourceName, ResourceTypeConstants.COMMON_RPC,
EntryType.IN, invocation.getArguments());
Result result = invoker.invoke(invocation);
if (result.hasException()) {
Throwable e = result.getException();
// Record common exception.
Tracer.traceEntry(e, interfaceEntry);
Tracer.traceEntry(e, methodEntry);
}
return result;
} catch (BlockException e) {
return DubboFallbackRegistry.getProviderFallback().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();
}
ContextUtil.exit();
}
}
可以看到invoke方法主體已經包含了限流相關的邏輯,不過這不是我們這篇博客的重點,我們的重點在於catch到BlockException之後的處理邏輯(*com.alibaba.csp.sentinel.slots.block.BlockException是sentinel所有限流相關異常的父類)
return DubboFallbackRegistry.getProviderFallback().handle(invoker, invocation, e);
sentinel通過從DubboFallbackRegistry獲取到dubbo Provider對應的fallback類,然後調用handle方法進行處理,看到這我們已經找到了擴展的入口。
繼續閱讀DubboFallbackRegistry源碼:
com.alibaba.csp.sentinel.adapter.dubbo.fallback.DubboFallbackRegistry
public final class DubboFallbackRegistry {
private static volatile DubboFallback consumerFallback = new DefaultDubboFallback();
private static volatile DubboFallback providerFallback = new DefaultDubboFallback();
public static DubboFallback getConsumerFallback() {
return consumerFallback;
}
public static void setConsumerFallback(DubboFallback consumerFallback) {
AssertUtil.notNull(consumerFallback, "consumerFallback cannot be null");
DubboFallbackRegistry.consumerFallback = consumerFallback;
}
public static DubboFallback getProviderFallback() {
return providerFallback;
}
public static void setProviderFallback(DubboFallback providerFallback) {
AssertUtil.notNull(providerFallback, "providerFallback cannot be null");
DubboFallbackRegistry.providerFallback = providerFallback;
}
private DubboFallbackRegistry() {}
}
DubboFallbackRegistry裏維護了兩個被volatile修飾的靜態DubboFallback,一個用於dubbo provider,一個用於dubbo consumer(既然有了consumer的DubboFallback,我們也可以在服務端做降級,這樣不同的服務調用方可以根據自己的情況時有不同的降級策略,更加靈活)。
com.alibaba.csp.sentinel.adapter.dubbo.fallback.DubboFallback
@FunctionalInterface
public interface DubboFallback {
/**
* Handle the block exception and provide fallback result.
*
* @param invoker Dubbo invoker
* @param invocation Dubbo invocation
* @param ex block exception
* @return fallback result
*/
Result handle(Invoker<?> invoker, Invocation invocation, BlockException ex);
}
DubboFallback是一個接口,只有一個handle方法需要實現。在我們實現自己的統一降級策略之前,我們可以先看看sentinel默認的降級策略
public class DefaultDubboFallback implements DubboFallback {
@Override
public Result handle(Invoker<?> invoker, Invocation invocation, BlockException ex) {
// Just wrap and throw the exception.
throw new SentinelRpcException(ex);
}
}
就是簡單的將異常包裝類一下拋出。接下來我們看怎麼實現自己的降級策略
自定義統一的降級策略
從之前的源碼閱讀我們已經看到了providerFallback是靜態的了,所以我們只需要實現一個DubboFallback去替換DubboFallbackRegistry中的providerFallback即可:
DubboFallbackRegistry.setProviderFallback(new DubboFallback() {
@Override
public Result handle(Invoker<?> invoker, Invocation invocation, BlockException ex) {
log.info("捕獲到block異常,降級處理", ex);
return AsyncRpcResult.newDefaultAsyncResult("服務器處理不過來啦", invocation);
}
});
邏輯很簡單,打印一行日誌然後告訴服務調用方服務器無法處理更多的請求,這裏有幾個需要注意的地方
- 我用於測試的rpc方法的返回值是String類型的,所以降級的返回"服務器處理不過來啦"這個字符串不會出問題,實際情況可以統一返回null或者所有dubbo方法返回值的父類(如果dubbo使用了統一的返回值規範的話)。
- 不同版本的dubbo自定義org.apache.dubbo.rpc.Result的方法不一致,根據自己使用的版進行構建。
- 因爲入參中有invocation,我們可以獲取到調用服務的信息,這裏可以根據攜帶的信息對某些或某類做統一的特殊處理,這也是實際業務開發中最常見的情況。
最後看下實際效果
降級策略
{
"resource": "org.apache.dubbo.demo.DemoService:sayHello(java.lang.String)",
"controlBehavior": 0,
"count": 5.0,
"grade": 1,
"limitApp": "default",
"strategy": 0
}
服務方同時發起50次調用的結果
[24/10/19 13:26:24:280 CST] Thread-25 INFO consumer.Application: result: Hello dubbo, response from provider: 10.242.187.173:20880
[24/10/19 13:26:24:280 CST] Thread-29 INFO consumer.Application: result: Hello dubbo, response from provider: 10.242.187.173:20880
[24/10/19 13:26:24:283 CST] Thread-39 INFO consumer.Application: result: Hello dubbo, response from provider: 10.242.187.173:20880
[24/10/19 13:26:24:280 CST] Thread-49 INFO consumer.Application: result: Hello dubbo, response from provider: 10.242.187.173:20880
[24/10/19 13:26:24:280 CST] Thread-48 INFO consumer.Application: result: Hello dubbo, response from provider: 10.242.187.173:20880
[24/10/19 13:26:24:295 CST] Thread-16 INFO consumer.Application: result: 服務器處理不過來啦
[24/10/19 13:26:24:295 CST] Thread-15 INFO consumer.Application: result: 服務器處理不過來啦
[24/10/19 13:26:24:296 CST] Thread-51 INFO consumer.Application: result: 服務器處理不過來啦
[24/10/19 13:26:24:296 CST] Thread-21 INFO consumer.Application: result: 服務器處理不過來啦
[24/10/19 13:26:24:297 CST] Thread-37 INFO consumer.Application: result: 服務器處理不過來啦
[24/10/19 13:26:24:298 CST] Thread-13 INFO consumer.Application: result: 服務器處理不過來啦
[24/10/19 13:26:24:300 CST] Thread-7 INFO consumer.Application: result: 服務器處理不過來啦
[24/10/19 13:26:24:302 CST] Thread-28 INFO consumer.Application: result: 服務器處理不過來啦
[24/10/19 13:26:24:302 CST] Thread-33 INFO consumer.Application: result: 服務器處理不過來啦
[24/10/19 13:26:24:304 CST] Thread-34 INFO consumer.Application: result: 服務器處理不過來啦
[24/10/19 13:26:24:305 CST] Thread-43 INFO consumer.Application: result: 服務器處理不過來啦
[24/10/19 13:26:24:306 CST] Thread-11 INFO consumer.Application: result: 服務器處理不過來啦
[24/10/19 13:26:24:306 CST] Thread-10 INFO consumer.Application: result: 服務器處理不過來啦
[24/10/19 13:26:24:307 CST] Thread-53 INFO consumer.Application: result: 服務器處理不過來啦
[24/10/19 13:26:24:314 CST] Thread-50 INFO consumer.Application: result: 服務器處理不過來啦
[24/10/19 13:26:24:316 CST] Thread-6 INFO consumer.Application: result: 服務器處理不過來啦
[24/10/19 13:26:24:316 CST] Thread-18 INFO consumer.Application: result: 服務器處理不過來啦
[24/10/19 13:26:24:316 CST] Thread-4 INFO consumer.Application: result: 服務器處理不過來啦
[24/10/19 13:26:24:316 CST] Thread-31 INFO consumer.Application: result: 服務器處理不過來啦
[24/10/19 13:26:24:318 CST] Thread-30 INFO consumer.Application: result: 服務器處理不過來啦
[24/10/19 13:26:24:318 CST] Thread-35 INFO consumer.Application: result: 服務器處理不過來啦
[24/10/19 13:26:24:319 CST] Thread-52 INFO consumer.Application: result: 服務器處理不過來啦
[24/10/19 13:26:24:319 CST] Thread-24 INFO consumer.Application: result: 服務器處理不過來啦
[24/10/19 13:26:24:319 CST] Thread-14 INFO consumer.Application: result: 服務器處理不過來啦
[24/10/19 13:26:24:320 CST] Thread-8 INFO consumer.Application: result: 服務器處理不過來啦
[24/10/19 13:26:24:320 CST] Thread-40 INFO consumer.Application: result: 服務器處理不過來啦
[24/10/19 13:26:24:320 CST] Thread-47 INFO consumer.Application: result: 服務器處理不過來啦
[24/10/19 13:26:24:320 CST] Thread-26 INFO consumer.Application: result: 服務器處理不過來啦
[24/10/19 13:26:24:321 CST] Thread-12 INFO consumer.Application: result: 服務器處理不過來啦
[24/10/19 13:26:24:321 CST] Thread-54 INFO consumer.Application: result: 服務器處理不過來啦
[24/10/19 13:26:24:322 CST] Thread-20 INFO consumer.Application: result: 服務器處理不過來啦
[24/10/19 13:26:24:322 CST] Thread-19 INFO consumer.Application: result: 服務器處理不過來啦
[24/10/19 13:26:24:321 CST] Thread-44 INFO consumer.Application: result: 服務器處理不過來啦
[24/10/19 13:26:24:322 CST] Thread-45 INFO consumer.Application: result: 服務器處理不過來啦
[24/10/19 13:26:24:322 CST] Thread-3 INFO consumer.Application: result: 服務器處理不過來啦
[24/10/19 13:26:24:323 CST] Thread-38 INFO consumer.Application: result: 服務器處理不過來啦
[24/10/19 13:26:24:323 CST] Thread-23 INFO consumer.Application: result: 服務器處理不過來啦
[24/10/19 13:26:24:323 CST] Thread-42 INFO consumer.Application: result: 服務器處理不過來啦
[24/10/19 13:26:24:324 CST] Thread-5 INFO consumer.Application: result: 服務器處理不過來啦
[24/10/19 13:26:24:324 CST] Thread-17 INFO consumer.Application: result: 服務器處理不過來啦
[24/10/19 13:26:24:324 CST] Thread-32 INFO consumer.Application: result: 服務器處理不過來啦
[24/10/19 13:26:24:325 CST] Thread-36 INFO consumer.Application: result: 服務器處理不過來啦
[24/10/19 13:26:24:325 CST] Thread-27 INFO consumer.Application: result: 服務器處理不過來啦
[24/10/19 13:26:24:325 CST] Thread-22 INFO consumer.Application: result: 服務器處理不過來啦
[24/10/19 13:26:24:326 CST] Thread-9 INFO consumer.Application: result: 服務器處理不過來啦
只有5個請求被正常響應,其它的都按照我們自定義的降級策略返回了。