背景
新接业务需求要求响应时效要求控制在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,肆意开发 2.开发过程中,沟通不够,导致重复踩坑 3.测试同学在测试过程中未将超时问题纳入用例
后续考虑将请求超时问题接入钉钉报警,主动发现,即时解决。