还是引流回放的问题,今天测试的同学反馈说他做了流量回放,但是回放的好几个接口报错了,都是抛出来的服务器错误,请联系管理员,与预期的结果不符,但是实际这块的逻辑是没有改动的,所以也只能是dubbo回放的问题了。
分析
我们先看下问题的现象是怎么样子的, 如下图:
预期是返回不存在的工单的结果才是对的,但是确实服务器异常的结果,
从这个结果来看就是dubbo的调用返回了null, 所以才会导致了这个问题。那dubbo的调用其实就是被我们mock的结果了,而且看代码我们看录制好的子调用数据
业务其实就只有一个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这块的逻辑是怎么获取结果的。
@Override
public Object assembleResponse(Event event) {
// 在onResponse的before事件中组装response
if (event.type == Event.Type.RETURN) {
Object appResponse = ((ReturnEvent) event).object;
try {
return MethodUtils.invokeMethod(appResponse, "getValue");
} catch (Exception e) {
// ignore
LogUtil.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 */
@Deprecated
public Object getResult() {
return this.getValue();
}
/** @deprecated */
@Deprecated
public 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有值的。所以需要做如下的改动:
@Override
public Object assembleResponse(Event event) {
// 在onResponse的before事件中组装response
if (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) {
// ignore
LogUtil.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回放的逻辑,
@Override
public 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一直都是在模拟正常的结果返回,而没有业务抛出异常的逻辑,所以这个地方也是必须要修改的。
修改的代码如下:
@Override
public 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其实还是有很多待解决的问题的。