> 文章列表 > 关于jvm-sandbox-repeater dubbo回放异常的问题处理

关于jvm-sandbox-repeater dubbo回放异常的问题处理

关于jvm-sandbox-repeater dubbo回放异常的问题处理

还是引流回放的问题,今天测试的同学反馈说他做了流量回放,但是回放的好几个接口报错了,都是抛出来的服务器错误,请联系管理员,与预期的结果不符,但是实际这块逻辑是没有改动的,所以也只能是dubbo回放的问题了。

分析

我们先看下问题的现象是怎么样子的, 如下图:

关于jvm-sandbox-repeater dubbo回放异常的问题处理
预期是返回不存在的工单的结果才是对的,但是确实服务器异常的结果,

关于jvm-sandbox-repeater dubbo回放异常的问题处理
从这个结果来看就是dubbo的调用返回了null, 所以才会导致了这个问题。那dubbo的调用其实就是被我们mock的结果了,而且看代码我们看录制好的子调用数据

关于jvm-sandbox-repeater dubbo回放异常的问题处理

业务其实就只有一个dubbo的子调用,所以预期结果应该就是这个 getWorkOrderDetailByUid 返回了我们需要的不存在的工单才对,可是我们看到这个结果的 response 以及 throwable 都为空,这个就是为什么会返回null的原因了。

那我们又要去思考,为什么response与throwable都为null呢。 这个就要去看下录制的逻辑了,

/*** 处理return事件** @param event return事件*/protected void doReturn(ReturnEvent event) {if (RepeatCache.isRepeatFlow(Tracer.getTraceId())) {return;}Invocation invocation = RecordCache.getInvocation(event.invokeId);if (invocation == null) {log.debug("no valid invocation found in return,type={},traceId={}", invokeType, Tracer.getTraceId());return;}invocation.setResponse(processor.assembleResponse(event));invocation.setEnd(System.currentTimeMillis());listener.onInvocation(invocation);}

以上的内容 我们会发现在 doReturn 方法的时候会去获取processor.assembleResponse(event) 的结果,我们看看针对dubbo这块的逻辑是怎么获取结果的。

@Overridepublic Object assembleResponse(Event event) {// 在onResponse的before事件中组装responseif (event.type == Event.Type.RETURN) {Object appResponse = ((ReturnEvent) event).object;try {return MethodUtils.invokeMethod(appResponse, "getValue");} catch (Exception e) {// ignoreLogUtil.error("error occurred when assemble dubbo response", e);}}return null;}

这里我们需要知道appResonse 这里是个什么类型,其实他是dubbo调用的结果也就是RpcResult这个类。

public class RpcResult implements Result, Serializable {private static final long serialVersionUID = -6925924956850004727L;private Object result;private Throwable exception;private Map<String, String> attachments = new HashMap();public RpcResult(Object result) {this.result = result;}public RpcResult(Throwable exception) {this.exception = exception;}/** @deprecated */@Deprecatedpublic Object getResult() {return this.getValue();}/** @deprecated */@Deprecatedpublic void setResult(Object result) {this.setValue(result);}public Object getValue() {return this.result;}public void setValue(Object value) {this.result = value;}public Throwable getException() {return this.exception;}public void setException(Throwable e) {this.exception = e;}...
}

我们发现这里有一个问题,如果出现dubbo的调用的服务抛出异常的时候,实际上result这个结果就是null了, 而在exception这个值才能够知道具体是什么异常了。所以上述的代码逻辑有问题就是result为null的情况下,有可能是exception有值的。所以需要做如下的改动:

@Overridepublic Object assembleResponse(Event event) {// 在onResponse的before事件中组装responseif (event.type == Event.Type.RETURN) {Object appResponse = ((ReturnEvent) event).object;try {Object result =  MethodUtils.invokeMethod(appResponse, "getValue");if (result == null) {return MethodUtils.invokeMethod(appResponse, "getException");}else {return result;}} catch (Exception e) {// ignoreLogUtil.error("error occurred when assemble dubbo response", e);}}return null;}

先尝试获取value,如果为null的情况下 再去获取到exception的值。 并且还没完 因为刚才获取到的值其实是直接赋值给到了 invocation的response了即 invocation.setResponse(processor.assembleResponse(event));,这里我们需要如果有异常的时候将值赋值给到throwable才是对的。

@Override
protected void doReturn(ReturnEvent event) {if (RepeatCache.isRepeatFlow(Tracer.getTraceId())) {return;}Invocation invocation = RecordCache.getInvocation(event.invokeId);if (invocation == null) {log.debug("no valid invocation found in return,type={},traceId={}", invokeType, Tracer.getTraceId());return;}Boolean hasException = ((DubboConsumerInvocationProcessor)processor).isHasException(event);if (hasException) {invocation.setThrowable((Throwable) processor.assembleResponse(event));invocation.setResponse(buildExceptionResponse(invocation.getThrowable()));}   else {invocation.setResponse(processor.assembleResponse(event));}invocation.setEnd(System.currentTimeMillis());listener.onInvocation(invocation);
}

这里我们还做了一个小优化,因为一旦dubbo抛出异常的时候reponse就没有值了,这就导致了结果对比的地方是null, 完全没有办法看出来是异常,所以我们将异常做了一个转换再赋值给resone.即

private Map<String, String> buildExceptionResponse(Throwable e) {Map<String, String> map = new HashMap<String, String>();String clzName = e.getClass().getName();int index = clzName.lastIndexOf(".");map.put("exception", clzName.substring(index + 1));map.put("message", e.getMessage());return map;
}

将异常内容做个简化,存放到map中再进行返回。

那问题就解决了吗? 并不是的,我们这里的所有逻辑都是解决的是录制流量的问题,那回放呢,这块的逻辑是否正确呢

我们看下dubbo回放的逻辑,

@Overridepublic Object assembleMockResponse(BeforeEvent event, Invocation invocation) {try {Object dubboInvocation = event.argumentArray[1];Object response = invocation.getResponse();Class<?> aClass = event.javaClassLoader.loadClass("com.alibaba.dubbo.rpc.RpcResult");// 调用AsyncRpcResult#newDefaultAsyncResult返回Constructor constructor=aClass.getDeclaredConstructor(Object.class);return constructor.newInstance(response);} catch (Exception e) {LogUtil.error("error occurred when assemble dubbo mock response", e);return null;}}

我以上的代码就是在dubbo mock返回值的逻辑了,但是我们根据刚才前面的解释,就会发现这里有一个问题,那就是RpcResult的构造函数可以有 object的参数,也有throwable的参数的,而这个地方的mock一直都是在模拟正常的结果返回,而没有业务抛出异常的逻辑,所以这个地方也是必须要修改的。

修改的代码如下:

@Overridepublic Object assembleMockResponse(BeforeEvent event, Invocation invocation) {try {Object dubboInvocation = event.argumentArray[1];Class<?> aClass = event.javaClassLoader.loadClass("com.alibaba.dubbo.rpc.RpcResult");// 调用AsyncRpcResult#newDefaultAsyncResult返回Constructor constructor = null;if (invocation.getThrowable() != null) {constructor = aClass.getDeclaredConstructor(Throwable.class);return constructor.newInstance(invocation.getThrowable());}else {constructor = aClass.getDeclaredConstructor(Object.class);Object response = invocation.getResponse();return constructor.newInstance(response);}} catch (Exception e) {LogUtil.error("error occurred when assemble dubbo mock response", e);return null;}}

如果invocation 存在有throwable 的话,那就需要构造带有exception的RpcResult对象出来即可。

PS: 其实这里还有一个比较重要的”小问题”,就是我们dubbo调用过程中出现的一些业务的异常,并不能去走sanbod定义的throw的异常去,也就是以下这个代码的地方。

public void doMock(BeforeEvent event, Boolean entrance, InvokeType type) throws ProcessControlException {/** 获取回放上下文*/RepeatContext context = RepeatCache.getRepeatContext(Tracer.getTraceId());/** mock执行条件*/if (!skipMock(event, entrance, context) && context != null && context.getMeta().isMock()) {try {/** 构建mock请求*/final MockRequest request = MockRequest.builder().argumentArray(this.assembleRequest(event)).event(event).identity(this.assembleIdentity(event)).meta(context.getMeta()).recordModel(context.getRecordModel()).traceId(context.getTraceId()).type(type).repeatId(context.getMeta().getRepeatId()).index(SequenceGenerator.generate(context.getTraceId())).build();/** 执行mock动作*/final MockResponse mr = StrategyProvider.instance().provide(context.getMeta().getStrategyType()).execute(request);/** 处理策略推荐结果*/switch (mr.action) {case SKIP_IMMEDIATELY:break;case THROWS_IMMEDIATELY:ProcessControlException.throwThrowsImmediately(mr.throwable);break;case RETURN_IMMEDIATELY:ProcessControlException.throwReturnImmediately(assembleMockResponse(event, mr.invocation));break;default:ProcessControlException.throwThrowsImmediately(new RepeatException("invalid action"));break;}} catch (ProcessControlException pce) {throw pce;} catch (Throwable throwable) {ProcessControlException.throwThrowsImmediately(new RepeatException("unexpected code snippet here.", throwable));}}
}

我们可以发现 这里的mock策略会根据mr.action进行走具体哪个策略逻辑,(不过我们要注意一点,dubbo的子调用的一些异常我们仍然需要走的是 RETURN_IMMEDIATELY 这个是比较关键的)那mr的action又是怎么来的呢 我们就需要看下 execute的逻辑了

response = MockResponse.builder().action(invocation.getThrowable() == null ? Action.RETURN_IMMEDIATELY : Action.THROWS_IMMEDIATELY).throwable(invocation.getThrowable()).invocation(invocation).build();

这里我们没有截取比较多的代码,我们可以看到这里的action是根据是否有存在throwable来进行处理的, 所以一旦我们的逻辑这么走的话,就会进到 THROWS_IMMEDIATELY 就其实mock失败了,所以这里的代码我们还需要再改动一下。

response = MockResponse.builder().action(invocation.getThrowable() == null || invocation.getType().name().equals(InvokeType.ALIBB_DUBBO.name()) ? Action.RETURN_IMMEDIATELY : Action.THROWS_IMMEDIATELY).throwable(invocation.getThrowable()).invocation(invocation).build();

如果是dubbo的调用也不用抛出异常了。

总结

dubbo这块的mock与http等的差异还是比较大的,所以这块的mock其实还是有很多待解决的问题的。