深度解析 dubbo 服务调用异常:The service using threads greater than <dubbo:service executes=“128“ /> limited

2 篇文章 0 订阅

背景介绍

服务在上线一段时间后,就出现了 Dubbo 调用异常,异常信息为: The service using threads greater than <dubbo:service executes=“128” /> limited。这样就导致功能不可用,由于该接口在实际中的并发量并不高,因此很诡异,在经过一段分析后,最终得以解决。

问题分析

dubbo:service executes=“128” 表示:服务提供者每服务每方法最大可并行执行请求数,可以通过 dubbo.provider.executes=128 或者 dubbo.service.executes=128 来配置,区别在于应用范围不同。也就是说某一个方法的并行请求数已经达到了 128 个,但是实际情况下该接口并没有太大的并发,而且后面的每次请求都会失败。由于是对外暴露是 HTTP 接口,通过 kong 的请求日志也验证了并发量也并没有达到 128。于是就要从线程、堆 dump 文件和源码等方面详细的分析了。

线程 dump 文件分析

看服务线程 dump 文件的目的是为了看是否有线程在这个方法上处于阻塞或运行状态。通过命令: jstack pid 输出线程信息,经过查看发现并没有线程在这个方法上阻塞或运行,因此也印证了问题并不是由于并发请求数超过了128 导致的。

源码分析

在线程信息上并不能找到太多有用信息,因此要深入源码解析看异常是如何产生的。通过异常信息可以定位到源码类 ExecuteLimitFilter 中的 invoke 方法:

public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        URL url = invoker.getUrl();
        String methodName = invocation.getMethodName();
        int max = url.getMethodParameter(methodName, EXECUTES_KEY, 0);
        if (!RpcStatus.beginCount(url, methodName, max)) {
            throw new RpcException(RpcException.LIMIT_EXCEEDED_EXCEPTION,
                    "Failed to invoke method " + invocation.getMethodName() + " in provider " +
                            url + ", cause: The service using threads greater than <dubbo:service executes=\"" + max +
                            "\" /> limited.");
        }

        invocation.put(EXECUTELIMIT_FILTER_START_TIME, System.currentTimeMillis());
        try {
            return invoker.invoke(invocation);
        } catch (Throwable t) {
            if (t instanceof RuntimeException) {
                throw (RuntimeException) t;
            } else {
                throw new RpcException("unexpected exception when ExecuteLimitFilter", t);
            }
        }
    }

具体的判断是在 RpcStatus.beginCount(url, methodName, max) 语句,再进入具体的方法:

public static boolean beginCount(URL url, String methodName, int max) {
        max = (max <= 0) ? Integer.MAX_VALUE : max;
        RpcStatus appStatus = getStatus(url);
        RpcStatus methodStatus = getStatus(url, methodName);
        if (methodStatus.active.get() == Integer.MAX_VALUE) {
            return false;
        }
        if (methodStatus.active.incrementAndGet() > max) {
            methodStatus.active.decrementAndGet();
            return false;
        } else {
            appStatus.active.incrementAndGet();
            return true;
        }
    }

通过代码我们可以得出,程序主要是判断 AtomicInteger active 变量的值是否大于设置的阈值,如果大于的话就返回 false,上层调用就会抛出 The service using threads greater than <dubbo:service executes=“128“ /> limited 异常。

既然方法调用的并发数并没有超过设定值,那么就有可能是方法正在请求数 active 变量出了问题,就需要查看下 active 值现在是多少。

堆转储快照 dump 文件分析

要想查看 active 的值,就需要堆转储快照 dump 文件,通过命令 jmap -dump:format=b,file=eclipes.bin pid 导出 dump 文件。然后使用 Eclipse 的 MAT(Memory Analyzer Tool) 工具分析文件。

先通过 RpcStatus 类名搜索,可以看到下图中左边类中的两个静态变量 METHOD_STATISTICS、SERVICE_STATISTICS。 METHOD_STATISTICS map中就存着每个方法调用状态。
在这里插入图片描述然后在 METHOD_STATISTICS 右键 -> List objects -> with outgoing references 查看变量的具体内容,再通过方法名找到对应的方法的 active,如下:

在这里插入图片描述从图中看出 active = 128,确实已经达到设置的阈值了,因此调用方法会直接抛出异常。那问题是为什么这个值没有降下去呢?再看代码,每次方法调用完都会调用 endCount 方法来减少 active 值。

private static void endCount(RpcStatus status, long elapsed, boolean succeeded) {
        status.active.decrementAndGet();
        status.total.incrementAndGet();
        status.totalElapsed.addAndGet(elapsed);
        if (status.maxElapsed.get() < elapsed) {
            status.maxElapsed.set(elapsed);
        }
        if (succeeded) {
            if (status.succeededMaxElapsed.get() < elapsed) {
                status.succeededMaxElapsed.set(elapsed);
            }
        } else {
            status.failed.incrementAndGet();
            status.failedElapsed.addAndGet(elapsed);
            if (status.failedMaxElapsed.get() < elapsed) {
                status.failedMaxElapsed.set(elapsed);
            }
        }
    }

active 值没有减少就有可能是没有调用 endCount 方法,没有调用 endCount 方法就可能是 RPC 远程调用整个流程没有走完。再分析业务代码后找到了问题的关键,服务提供方是异步处理请求的,在接收到 RPC 远程请求后,会存储异步上下文 AsyncContext,然后等待获取结果后才异步写回请求放。如果结果一直没有得到,超过了一定的时间会清除 AsyncContext。问题就出在了清除 AsyncContext 缓存这一步,清除缓存时要写结果回去 asyncContext.write(object); ,然后就会调用到 endCount 方法。

总结

经过上面的一系列分析,最终发现清除上下文时要写结果来完成一次完整的链路调用。这也提醒我们在做相关资源的缓存时,清除缓存前要做好相关的结尾处理。

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值