Dubbo之服务降级分析

当我们遇到高并发下的流量激增问题时,常常提到降级熔断限流的概念。我先简单讲解一下这三个概念的定义。

相关概念

降级
降级也就是服务降级,当我们的服务器压力剧增时,为了保证核心功能的可用性,而选择性的降低一些功能的可用性,或者直接关闭该功能。例如在双十一凌晨抢购的时候,流量压力是很大的,为了保证订单的正常支付,在凌晨1-2点左右的订单修改功能是关闭的。
熔断
降级一般是指我们自身的系统出现了故障而降级。而熔断一般是指依赖的外部接口出现故障的情况断绝和外部接口的关系。
例如你的A服务里的一个功能依赖B服务,这时B服务出现问题了,返回的很慢。而越是庞大的系统,上下游的调用链就会越长,而如果在一个很长的调用链中,某一个服务由于某种原因导致响应时间过长,或者完全无响应,那么就可能把整个分布式系统都拖垮。这种情况下可能会因为这个功能而拖慢A服务里面的所有功能,严重的时候会导致服务雪崩现象。

在分布式系统中,为了保证整体服务的可用性和一致性,很多系统会采用重试机制,在一些因为网络抖动原因发生失败的可以采用这种措施。

但是有些情况,并不适合用重试机制,反而进一步损害了系统的性能。比如说下游系统因为请求量太大,导致CPU已经被打满,或者数据库连接池被占满,这时候上游系统调用下游系统获取不导信息就会不断重试,这种情况下的重试很造成下游系统的崩溃。

在分布式系统中,大多数的服务雪崩也是因为不断重试导致的,这种重试有可能是框架级别的自动重试、有可能是代码级别的重试逻辑、还有可能是用户的主动重试。

那我们可以利用熔断器模式来解决这一现象。一个典型的熔断器有三种状态:

  1. 关闭

熔断器默认情况是关闭的。熔断器本身带有计数功能,每当错误发生一次,计数器就会进行累加,到了一定的次数熔断器就会开启,同时开启一个计时器,一旦时间到了就会切换成半开启的状态。

  1. 开启

在开启的状态下任何请求都会直接被拒绝并且抛出异常信息。

  1. 半开启

熔断器会允许部分的请求,如果这些请求都成功通过,那么就说明错误已经不存在了,则会被切换到关闭状态并重置计数。倘若请求中有任何一个发生失败,就会恢复到开启状态,并且重置计时,给予系统一段休息时间。

漫话:如何给女朋友解释什么是熔断?
限流
这个很容易理解,就是通过对系统的评估,只允许一定数量的请求进入,剩余的请求拒绝执行。 比如在秒杀系统中,要秒杀100份商品,我只允许前2000个请求进入,剩余的直接拦截拒接。

现在我们已经讲解了这三个概念,那接下来先开始讲解dubbo是如何降级的

Dubbo服务降级

Dubbo可以通过服务降级功能临时屏蔽某个出错的非关键性服务,并定义降级后的返回策略。 我们可以向注册中心写入动态配置覆盖规则:

RegistryFactory registryFactory = ExtensionLoader.getExtensionLoader(RegistryFactory.class).getAdaptiveExtension();
Registry registry = registryFactory.getRegistry(URL.valueOf("zookeeper://10.20.153.10:2181"));
registry.register(URL.valueOf("override://0.0.0.0/com.foo.BarService?category=configurators&dynamic=false&application=foo&mock=force:return+null"));

也可以通过配置文件进行定义。

<dubbo:reference id="iUser" interface="com.dubbosample.iface.IUser"  timeout="1000" check="false" mock="return null">

也有这几种形式:

<dubbo:reference mock="true"  .../>
<dubbo:reference mock="com.xxxx" .../>
<dubbo:reference mock="return null" .../>
<dubbo:reference mock="throw xxx" .../>
<dubbo:reference mock="force:return .." .../>
<dubbo:reference mock="force:throw ..." .../>

其中,最主要的两种形式是:
1) mock='force:return+null'表示消费对该服务的方法调用都直接返回null值,不发起远程调用。用来屏蔽不重要服务不可用时对调用方的影响。
2) 还可以改为mock=fail:return+null表示消费方对该服务的方法调用在失败后,再返回null。用来容忍不重要服务不稳定时对调用方的影响。

具体代码

阅读过源码的知道Dubbo的远程调用是从一个代理Proxy开始的,首先将运行时参数存储在数组中,然后调用InvocationHandler接口来实现类的invoke方法。下面是一个动态生成的一个代理类例子。

public class proxy0 implements ClassGenerator.DC, EchoService, DemoService {
    // 方法数组
    public static Method[] methods;
    private InvocationHandler handler;

    public proxy0(InvocationHandler invocationHandler) {
        this.handler = invocationHandler;
    }

    public proxy0() {
    }

    public String sayHello(String string) {
        // 将参数存储到 Object 数组中
        Object[] arrobject = new Object[]{string};
        // 调用 InvocationHandler 实现类的 invoke 方法得到调用结果
        Object object = this.handler.invoke(this, methods[0], arrobject);
        // 返回调用结果
        return (String)object;
    }
    public Object $echo(Object object) {
        Object[] arrobject = new Object[]{object};
        Object object2 = this.handler.invoke(this, methods[1], arrobject);
        return object2;
    }
}

InvokerInvocationHandler中的invoker成员变量为MockClusterInvoker,它来处理服务降级的逻辑。

public class InvokerInvocationHandler implements InvocationHandler {

    private final Invoker<?> invoker;

    public InvokerInvocationHandler(Invoker<?> handler) {
        this.invoker = handler;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        String methodName = method.getName();
        Class<?>[] parameterTypes = method.getParameterTypes();
        
        // 拦截定义在 Object 类中的方法(未被子类重写),比如 wait/notify
        if (method.getDeclaringClass() == Object.class) {
            return method.invoke(invoker, args);
        }
        
        // 如果 toString、hashCode 和 equals 等方法被子类重写了,这里也直接调用
        if ("toString".equals(methodName) && parameterTypes.length == 0) {
            return invoker.toString();
        }
        if ("hashCode".equals(methodName) && parameterTypes.length == 0) {
            return invoker.hashCode();
        }
        if ("equals".equals(methodName) && parameterTypes.length == 1) {
            return invoker.equals(args[0]);
        }
        
        // 将 method 和 args 封装到 RpcInvocation 中,并执行后续的调用
        return invoker.invoke(new RpcInvocation(method, args)).recreate();
    }
}

MockClusterInvoker中,从no mock(正常情况),force:direct mock(屏蔽),fail-mock(容错)三种情况我们也可以看出,普通情况是直接调用,容错的情况是调用失败后,返回一个设置的值.而屏蔽就很暴力了,直接连调用都不调用,就直接返回一个之前设置的值.
从下面的注释中可以看出,如果没有降级,会执行this.invoker.invoke(invocation)方法进行远程调动,默认类是FailoverClusterInvoker,它会执行集群模块的逻辑,主要是调用Directory#list方法获取所有该服务提供者的地址列表,然后将多个服务提供者聚合成一个Invoker, 并调用 Router 的 route 方法进行路由,过滤掉不符合路由规则的 Invoker。当 FailoverClusterInvoker 拿到 Directory 返回的 Invoker 列表后,它会通过 LoadBalance 从 Invoker 列表中选择一个 Invoker。最后 FailoverClusterInvoker 会将参数传给 LoadBalance 选择出的 Invoker 实例的 invoke 方法,进行真正的远程调用。

public class MockClusterInvoker<T> implements Invoker<T> {
    
    private final Invoker<T> invoker;
    
    public Result invoke(Invocation invocation) throws RpcException {
        Result result = null;

        // 获取 mock 配置值
        String value = directory.getUrl().getMethodParameter(invocation.getMethodName(), Constants.MOCK_KEY, Boolean.FALSE.toString()).trim();
        if (value.length() == 0 || value.equalsIgnoreCase("false")) {
            // 无 mock 逻辑,直接调用其他 Invoker 对象的 invoke 方法,
            // 比如 FailoverClusterInvoker
            result = this.invoker.invoke(invocation);
        } else if (value.startsWith("force")) {
            // force:xxx 直接执行 mock 逻辑,不发起远程调用
            result = doMockInvoke(invocation, null);
        } else {
            // fail:xxx 表示消费方对调用服务失败后,再执行 mock 逻辑,不抛出异常
            try {
                // 调用其他 Invoker 对象的 invoke 方法
                result = this.invoker.invoke(invocation);
            } catch (RpcException e) {
                if (e.isBiz()) {
                    throw e;
                } else {
                    // 调用失败,执行 mock 逻辑
                    result = doMockInvoke(invocation, e);
                }
            }
        }
        return result;
    }
    
    // 省略其他方法
}

参考文章:
服务降级
dubbo源码解析-逻辑层设计之服务降级
远程调用
面试官:说说降级、熔断、限流

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容