踩坑篇:记一次dubbo异步调用超时引发的血案

背景
新接业务需求要求响应时效要求控制在5s(md5撞库时效1.2s,业务流转时效3.8s),上线后直接导致生产事故(甲方爸爸反馈存在大量超时)。

环境
dubbo 2.6.0

问题分析
超时问题,首先就想到两处与时效相关的配置,其中,业务流转部分长期稳定运行,时效控制方面也几乎没有出现过问题,md5撞库时效则是在此次需求中新接入的需求,故重点排查md5撞库部分的时效控制,再排查业务流转部分。

问题排查

(ApiCommonAspect.java:204) requestId:539177628, product:67941, costL:6055ms
(ApiCommonAspect.java:204) requestId:539177695, product:67941, costL:5028ms
(ApiCommonAspect.java:204) requestId:539177767, product:67941, costL:6191ms
(ApiCommonAspect.java:204) requestId:539177769, product:67941, costL:5236ms
(ApiCommonAspect.java:204) requestId:539178024, product:67941, costL:6283ms
(ApiCommonAspect.java:204) requestId:539178073, product:67941, costL:6104ms
(ApiCommonAspect.java:204) requestId:539178098, product:67941, costL:5997ms
(ApiCommonAspect.java:204) requestId:539178116, product:67941, costL:6064ms
(ApiCommonAspect.java:204) requestId:539178170, product:67941, costL:6128ms

先拉取所有的请求记录id,通过请求id去查找响应的日志记录,发现最长超时时间不超过6.3秒,1.2和1.3似乎有什么联系,嗯~继续往下走
在这里插入图片描述
在通过RPC异步调用的md5的时候,发现了问题所在,整个调用耗时2.4s,调用时间翻倍,但是明明设置的时间是1.2s,为什么会翻倍!细思极恐!

public Object get(int timeout) throws RemotingException {
        if (timeout <= 0) {
            timeout = Constants.DEFAULT_TIMEOUT;
        }
        if (!isDone()) {
            long start = System.currentTimeMillis();
            lock.lock();
            try {
                while (!isDone()) {
                    done.await(timeout, TimeUnit.MILLISECONDS);
                    if (isDone() || System.currentTimeMillis() - start > timeout) {
                        break;
                    }
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                lock.unlock();
            }
            if (!isDone()) {
                throw new TimeoutException(sent > 0, channel, getTimeoutMessage(false));
            }
        }
        return returnFromResponse();
    }

最终在dubbo的默认get方法中找到了答案,在第1次dubbo等待时间结束的时候会与当前时间进行一个时间判断,默认的时间单位是ms,当其调用结束或者大于其等待时间跳出循环否则继续进行等待,这段逻辑存在一个致命的问题,在计算机系统中1ms也是一个很大的单位,计算机可以在这1ms的时间中做很多事情,所以在进行时间判断的时候还没有1ms,即判断结果为false,进入第2次等待,导致整个dubbo调用时间翻倍

解决方案

private static class RemotingInvocationTimeoutScan implements Runnable {

        public final int DURATION = 30;

        private Object getFiledValue(Class clazz, DefaultFuture future, String property) throws NoSuchFieldException, IllegalAccessException {
            Field field = clazz.getDeclaredField(property);
            field.setAccessible(true);
            return field.get(future);
        }

        @Override
        @SuppressWarnings("squid:S2189")
        public void run() {
            while (true) {
                try {
                    Iterator<DefaultFuture, Long>> iterator = FUTURES.entrySet().iterator();
                    while (iterator.hasNext()) {
                        Map.Entry<DefaultFuture, Long> entry = iterator.next();

                        DefaultFuture future = entry.getKey();
                        if (future == null || future.isDone()) {
                            //调用成功,释放future
                            iterator.remove();
                            continue;
                        }
                        Long timeout = Long.valueOf(entry.getValue().toString());
                        if (timeout == null) {
                            logger.error("timeout is null by class:[{}] future:[{}]", clazz.getName(), JSON.toJSONString(future));
                            continue;
                        }
                        Long start = Long.valueOf(String.valueOf(getFiledValue(clazz, future, "start")));
                        if (start == null) {
                            logger.error("start is null by class:[{}] future:[{}]", clazz.getName(), JSON.toJSONString(future));
                            continue;
                        }
                        if (System.currentTimeMillis() - start >= timeout) {
                            Long id = Long.valueOf(String.valueOf(getFiledValue(clazz, future, "id")));
                            if (id == null) {
                                logger.error("id is null by class:[{}] future:[{}]", clazz.getName(), JSON.toJSONString(future));
                                continue;
                            }
                            Long sent = Long.valueOf(String.valueOf(getFiledValue(clazz, future, "sent").toString()));
                            String msg = String.valueOf(invokeMethod(clazz, future, "getTimeoutMessage"));
                            Channel channel = (Channel) getFiledValue(clazz, future, "channel");
                            if (channel == null) {
                                logger.error("channel is null by class:[{}] future:[{}]", clazz.getName(), JSON.toJSONString(future));
                                continue;
                            }
                            Response timeoutResponse = new Response(id);
                            timeoutResponse.setStatus(sent != null && sent > 0 ? Response.SERVER_TIMEOUT : Response.CLIENT_TIMEOUT);
                            timeoutResponse.setErrorMessage(msg);
                            if (!future.isDone()) {
                                iterator.remove();
                                DefaultFuture.received(channel, timeoutResponse);
                            }
                        }
                    }
                    Thread.sleep(DURATION);
                } catch (Throwable e) {
                    logger.error("Exception when scan the timeout invocation of remoting.", e);
                }
            }
        }

        private Object invokeMethod(Class clazz, DefaultFuture future, String property) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
            Method method = clazz.getDeclaredMethod(property, boolean.class);
            method.setAccessible(true);
            return method.invoke(future, true);
        }
    }

第一种解决方案,构造一个异步线程适配器继承dubbo的FutureAdapter,并构建一个futureMap用于存放当前通过Rpc异步调用的future,同时在该类中开辟一个dubbo扫描线程,循环扫描futureMap,判断是否超时,如果超时且当前状态为未完成则将该dubbo线程移除

踩坑篇:记一次dubbo异步调用超时引发的血案_第1张图片

第二种解决方案,升级dubbo版本(经评估,影响面太大,放弃…)

总结

这生产事故的背后,整个开发流程中存在三个问题,1.未深入理解dubbo,肆意开发 2.开发过程中,沟通不够,导致重复踩坑 3.测试同学在测试过程中未将超时问题纳入用例

后续考虑将请求超时问题接入钉钉报警,主动发现,即时解决。

忘词
忘词
专注Java。分享日常学习开发经验。
公众号
踩坑篇:记一次dubbo异步调用超时引发的血案_第2张图片

你可能感兴趣的:(JAVA,dubbo,java,分布式,dubbo,微服务)