问题描述

这是之前做异地多活改造出现的一个问题,这篇文章简单做个记录。
描述一下问题的现象,我们线上一个应用调用大数据团队Dubbo接口的地方,突然开始报错,错误如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java.io.IOException: Unknown result flag, expect '0' '1' '2', get 4
at com.alibaba.dubbo.rpc.protocol.dubbo.DecodeableRpcResult.decode(DecodeableRpcResult.java:101)
at com.alibaba.dubbo.rpc.protocol.dubbo.DecodeableRpcResult.decode(DecodeableRpcResult.java:109)
at com.alibaba.dubbo.rpc.protocol.dubbo.DubboCodec.decodeBody(DubboCodec.java:90)
at com.alibaba.dubbo.remoting.exchange.codec.ExchangeCodec.decode(ExchangeCodec.java:119)
at com.alibaba.dubbo.remoting.exchange.codec.ExchangeCodec.decode(ExchangeCodec.java:80)
at com.alibaba.dubbo.rpc.protocol.dubbo.DubboCountCodec.decode(DubboCountCodec.java:46)
at com.alibaba.dubbo.remoting.transport.netty.NettyCodecAdapter$InternalDecoder.messageReceived(NettyCodecAdapter.java:134)
at org.jboss.netty.channel.SimpleChannelUpstreamHandler.handleUpstream(SimpleChannelUpstreamHandler.java:80)
at org.jboss.netty.channel.DefaultChannelPipeline.sendUpstream(DefaultChannelPipeline.java:564)
at org.jboss.netty.channel.DefaultChannelPipeline.sendUpstream(DefaultChannelPipeline.java:559)
at org.jboss.netty.channel.Channels.fireMessageReceived(Channels.java:274)
at org.jboss.netty.channel.Channels.fireMessageReceived(Channels.java:261)
at org.jboss.netty.channel.socket.nio.NioWorker.read(NioWorker.java:349)
at org.jboss.netty.channel.socket.nio.NioWorker.processSelectedKeys(NioWorker.java:280)
at org.jboss.netty.channel.socket.nio.NioWorker.run(NioWorker.java:200)
at org.jboss.netty.util.ThreadRenamingRunnable.run(ThreadRenamingRunnable.java:108)
at org.jboss.netty.util.internal.DeadLockProofWorker$1.run(DeadLockProofWorker.java:44)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)

看到这个异常堆栈,很容易看出是decode阶段出现了问题。
由于我们的应用最近没有发布,于是去问大数据的同学是否做了什么改动,大数据的同学表示升级了Dubbo版本至2.6.6,而我们这边使用的版本是公司内部为异地多活改造而封装的一个版本叫2.5.6.2-D-RELEASE
那么问题很可能就是因为版本不兼容导致的。但是对于Dubbo这样体量的开源项目,不至于不做向前的兼容,所以也有可能是我们内部的版本改动了一些东西导致了这个问题的发生。

问题定位及分析

定位到报错的这行代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public Object decode(Channel channel, InputStream input) throws IOException {
ObjectInput in = CodecSupport.getSerialization(channel.getUrl(), serializationType)
.deserialize(channel.getUrl(), input);

byte flag = in.readByte();
switch (flag) {
case DubboCodec.RESPONSE_NULL_VALUE:
break;
case DubboCodec.RESPONSE_VALUE:
try {
Type[] returnType = RpcUtils.getReturnTypes(invocation);
setValue(returnType == null || returnType.length == 0 ? in.readObject() :
(returnType.length == 1 ? in.readObject((Class<?>) returnType[0])
: in.readObject((Class<?>) returnType[0], returnType[1])));
} catch (ClassNotFoundException e) {
throw new IOException(StringUtils.toString("Read response data failed.", e));
}
break;
case DubboCodec.RESPONSE_WITH_EXCEPTION:
try {
Object obj = in.readObject();
if (obj instanceof Throwable == false)
throw new IOException("Response data error, expect Throwable, but get " + obj);
setException((Throwable) obj);
} catch (ClassNotFoundException e) {
throw new IOException(StringUtils.toString("Read response data failed.", e));
}
break;
default:
throw new IOException("Unknown result flag, expect '0' '1' '2', get " + flag);
}
return this;
}

1
2
3
public static final byte RESPONSE_WITH_EXCEPTION = 0;
public static final byte RESPONSE_VALUE = 1;
public static final byte RESPONSE_NULL_VALUE = 2;

我们看到我们使用的2.5.6.2-D-RELEASE版本的DubboResponseValue只支持三个值,分别是0, 1, 2
2.6.6版本的增加了3, 4, 5

1
2
3
public static final byte RESPONSE_WITH_EXCEPTION_WITH_ATTACHMENTS = 3;
public static final byte RESPONSE_VALUE_WITH_ATTACHMENTS = 4;
public static final byte RESPONSE_NULL_VALUE_WITH_ATTACHMENTS = 5;

那么,Dubbo本身的兼容性处理是怎么做的呢?容易想到兼容处理应该是在服务提供方的Dubboencode阶段做的,于是找到2.6.6版本的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Override
protected void encodeResponseData(Channel channel, ObjectOutput out, Object data, String version) throws IOException {
Result result = (Result) data;
// currently, the version value in Response records the version of Request
boolean attach = Version.isSupportResponseAttatchment(version);
Throwable th = result.getException();
if (th == null) {
Object ret = result.getValue();
if (ret == null) {
out.writeByte(attach ? RESPONSE_NULL_VALUE_WITH_ATTACHMENTS : RESPONSE_NULL_VALUE);
} else {
out.writeByte(attach ? RESPONSE_VALUE_WITH_ATTACHMENTS : RESPONSE_VALUE);
out.writeObject(ret);
}
} else {
out.writeByte(attach ? RESPONSE_WITH_EXCEPTION_WITH_ATTACHMENTS : RESPONSE_WITH_EXCEPTION);
out.writeObject(th);
}

if (attach) {
// returns current version of Response to consumer side.
result.getAttachments().put(Constants.DUBBO_VERSION_KEY, Version.getProtocolVersion());
out.writeObject(result.getAttachments());
}
}

我们看到,下面这行代码是兼容性处理的关键,用来表示当前Dubbo版本是否支持带attachmentsResponse

1
boolean attach = Version.isSupportResponseAttatchment(version);

利用这个attach标志,来设置不同的响应类型。在attachfalse的时候,都设置为低版本的相应码(0, 1, 2)

1
2
3
4
5
6
7
8
9
10
11
12
public static boolean isSupportResponseAttatchment(String version) {
if (version == null || version.length() == 0) {
return false;
}
// for previous dubbo version(2.0.10/020010~2.6.2/020602), this version is the jar's version, so they need to be ignore
int iVersion = getIntVersion(version);
if (iVersion >= 20010 && iVersion <= 20602) {
return false;
}

return iVersion >= LOWEST_VERSION_FOR_RESPONSE_ATTATCHMENT;
}

看了一下getIntVersion的逻辑,大致就是把版本转换成只含数字和.的字符串,再将.替换成0,得到一个整型的版本号,
那么2.5.6.2-D-RELEASE转换后的结果,就是2050602,而这个值会让isSupportResponseAttatchment返回true
很容易看出,代码的本意是让2.0.10-2.6.2版本不支持新的响应类型,而实际上2.5.6.2-D-RELEASE是基于2.5.x改造而来,理应是不支持的。

问题的结论

到这里,我们暂且不去关注3、4、5这三个响应类型会带来什么新的特性,但是问题的原因我们已经找到,就是2.6.2版本之后的DubboResponseencodedecode时有不一样的处理。这个处理本身可能没有问题,但是Dubbo所使用的解析和判断版本号的方式在我看来本身不够严谨,或者说我们内部封装的版本号在命名上没有遵循Dubbo官方的规范,从而导致了上述的兼容性的问题。

那么解决这个问题的方法可能有下面两个方向:

  1. Dubbo的版本号解析和判断的方式是否可以改进
  2. Dubbo版本号的命名方式是否有一个规范让开发者去遵循,从而去适应Dubbo源码中的类似上面的Magic Code以免发生一些不可预料的问题。