Dubbo 是阿里巴巴内部使用的分布式业务框架,在2011年由阿里巴巴开源。由于 Dubbo 在阿里巴巴内部经过广泛的业务验证,在很短时间内,就迅速成为了国内该类开源项目的佼佼者,并产生了许多衍生版本,如网易、京东、新浪、当当、网易考拉等等。2014年10月30日发布 2.4.11 版本后,突然停止更新(其实在2012年10月之后就基本停止了重要升级,改为阶段性维护)。
而在官方停止更新 Dubbo 之后,部分互联网公司公开了自行维护的 Dubbo 版本,比较著名的如当当的 DubboX、新浪的 Motan、网易考拉的 DubboK 等。
经过三年的沉寂,在2017年9月,阿里宣布重启 Dubbo 项目,并决策在未来对开源进行长期的持续投入。随后 Dubbo 开始了密集的更新,并将停摆三年以来大量分支上的特性及缺陷修正快速整合。2018 年 2 月 15 日经过一系列的投票,阿里将 Dubbo 捐献给 Apache 基金会,正式进入 Apache 孵化器,于 2019年5月16日正式成为 Apache 的顶级项目。这也是阿里巴巴微服务继 Apache RocketMQ 后的又一个 Apache 顶级项目。
DubboX官网
DubboX 是当当基于 Dubbo 2.4.8 的衍生版本,待续。。。
经测试后发现跨版本调用存在异常。
提供方 | 消费方 | 测试结果 | |
---|---|---|---|
apache-dubbo-2.7.2 | → | apache-dubbo-2.7.2 | 成功 |
apache-dubbo-2.7.2 | → | dubbox-2.8.4 | 失败 |
dubbox-2.8.4 | → | dubbox-2.8.4 | 成功 |
dubbox-2.8.4 | → | apache-dubbo-2.7.2 | 失败 |
apache-dubbo-2.7.2 作为提供方,dubbox-2.8.4 作为消费方。
[26/07/19 10:12:35:018 CST] NettyServerWorker-5-1 WARN dubbo.DecodeableRpcInvocation: [DUBBO] Decode rpc invocation failed: expected map/object at java.lang.String (Ljava/lang/String;), dubbo version: , current host: 158.220.142.204
com.alibaba.com.caucho.hessian.io.HessianProtocolException: expected map/object at java.lang.String (Ljava/lang/String;)
at com.alibaba.com.caucho.hessian.io.AbstractDeserializer.error(AbstractDeserializer.java:131)
at com.alibaba.com.caucho.hessian.io.AbstractMapDeserializer.readObject(AbstractMapDeserializer.java:70)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2268)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2075)
at org.apache.dubbo.common.serialize.hessian2.Hessian2ObjectInput.readObject(Hessian2ObjectInput.java:92)
at org.apache.dubbo.rpc.protocol.dubbo.DecodeableRpcInvocation.decode(DecodeableRpcInvocation.java:124)
at org.apache.dubbo.rpc.protocol.dubbo.DecodeableRpcInvocation.decode(DecodeableRpcInvocation.java:70)
at org.apache.dubbo.rpc.protocol.dubbo.DubboCodec.decodeBody(DubboCodec.java:133)
at org.apache.dubbo.remoting.exchange.codec.ExchangeCodec.decode(ExchangeCodec.java:125)
at org.apache.dubbo.remoting.exchange.codec.ExchangeCodec.decode(ExchangeCodec.java:85)
at org.apache.dubbo.rpc.protocol.dubbo.DubboCountCodec.decode(DubboCountCodec.java:46)
at org.apache.dubbo.remoting.transport.netty4.NettyCodecAdapter$InternalDecoder.decode(NettyCodecAdapter.java:95)
at io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:489)
at io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:428)
at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:265)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340)
at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1434)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:965)
at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:163)
at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:647)
at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:582)
at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:499)
at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:461)
at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:884)
at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
at java.lang.Thread.run(Thread.java:745)
dubbox-2.8.4 作为提供方, apache-dubbo-2.7.2 作为消费方。
[26/07/19 10:17:14:014 CST] New I/O worker #1 WARN dubbo.DecodeableRpcInvocation: [DUBBO] Decode rpc invocation failed: expected integer at 0x12 java.lang.String (Ljava/lang/String;), dubbo version: 2.0.0, current host: 127.0.0.1
com.alibaba.com.caucho.hessian.io.HessianProtocolException: expected integer at 0x12 java.lang.String (Ljava/lang/String;)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.error(Hessian2Input.java:2720)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.expect(Hessian2Input.java:2691)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readInt(Hessian2Input.java:773)
at com.alibaba.dubbo.common.serialize.support.hessian.Hessian2ObjectInput.readInt(Hessian2ObjectInput.java:58)
at com.alibaba.dubbo.rpc.protocol.dubbo.DecodeableRpcInvocation.decode(DecodeableRpcInvocation.java:106)
at com.alibaba.dubbo.rpc.protocol.dubbo.DecodeableRpcInvocation.decode(DecodeableRpcInvocation.java:74)
at com.alibaba.dubbo.rpc.protocol.dubbo.DubboCodec.decodeBody(DubboCodec.java:138)
at com.alibaba.dubbo.remoting.exchange.codec.ExchangeCodec.decode(ExchangeCodec.java:134)
at com.alibaba.dubbo.remoting.exchange.codec.ExchangeCodec.decode(ExchangeCodec.java:95)
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:70)
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:268)
at org.jboss.netty.channel.Channels.fireMessageReceived(Channels.java:255)
at org.jboss.netty.channel.socket.nio.NioWorker.read(NioWorker.java:88)
at org.jboss.netty.channel.socket.nio.AbstractNioWorker.process(AbstractNioWorker.java:109)
at org.jboss.netty.channel.socket.nio.AbstractNioSelector.run(AbstractNioSelector.java:312)
at org.jboss.netty.channel.socket.nio.AbstractNioWorker.run(AbstractNioWorker.java:90)
at org.jboss.netty.channel.socket.nio.NioWorker.run(NioWorker.java:178)
at org.jboss.netty.util.ThreadRenamingRunnable.run(ThreadRenamingRunnable.java:108)
at org.jboss.netty.util.internal.DeadLockProofWorker$1.run(DeadLockProofWorker.java:42)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
at java.lang.Thread.run(Thread.java:745)
异常信息总结
都是由提供方抛出的解码异常。
apache-dubbo-2.7.2 作为消费者接收到的 dubbo 请求版本号为空;dubbox-2.8.4 作为消费者接收到的 dubbo 请求版本号为 2.0.0。
expected map/object at java.lang.String (Ljava/lang/String;), dubbo version: , current host: 158.220.142.204
expected integer at 0x12 java.lang.String (Ljava/lang/String;), dubbo version: 2.0.0, current host: 127.0.0.1
提供方所发布的服务并未被调用。
Dubbo 的远程调用执行步骤(与本文无关动作不列出)可大致分解为:
消费方编码请求信息 -> 信息传输(netty、mina) -> 提供方解码请求信息 -> 处理请求后编码响应结果 -> netty发送 -> 消费方解码响应结果
此执行过程则完全能证实前一节的异常信息总结的结果,得出结论:apache-dubbo-2.7.2 与 dubbox-2.8.4 的编解码方式不完全一致。
既然编解码方式不一致,那么就找出其不同之处。上述两个异常信息都指向 Hessian 序列化,那么就是用专业的比对软件 Beyond Compare 把 dubbo 所用到了 hessian 类进行两两比对。
比对结果却不如人意。虽然 apache-dubbo-2.7.2 将 dubbo 作为独立依赖到项目中,而 dubbox-2.8.4 是内置在项目中,但是两者所使用到的类完全一致,并无差异(因无实质性进展,则不贴出比对结果占用版面)。
dubbo 是消费方将所发送的请求拆解成一个个非常小的独立单位(接口名、方法名、版本号、参数类型、参数值等)交由 Hessian 序列化的以实现编码的目的,那么解码是编码的逆过程,此解码过程也恰好出现在异常日志中,即由 DecodeableRpcInvocation
类的 decode(Channel channel, InputStream input)
方法完成。
通过比对的 decode(Channel channel, InputStream input) 方法,发现两者却有不同之处,理论上来说将两者的不同之处通过 if
放在不同的流程分支中,就可正常解码。那么流程分支条件是什么呢?
使用流程分支可以处理解码问题,但是需要调整两个 dubbo 版本的源码。 dubbox 已经在本人的生产线上使用,apache-dubbo-2.7.2 是欲要使用的版本,不可能先把生产上所有的 dubbox 更换一个版本,再升级至 apache-dubbo-2.7.2,那样的代价还不如直接全部升至 dubbo-2.7.2,影响面太大。之所以探究跨版本调用,就是希望各个系统逐步完成升级,需要有一个过渡期,而这个过渡期则需要跨版本调用,所以只能在后者的版本基础上作调整,以实现向下兼容的目的。
在不调整 dubbox 源码的前提下要实现跨版本调用,也就是说 apache-dubbo-2.7.2 不但要能解析 dubbox 的发送来请求,响应给 dubbox 的请求也须按照 dubbox 的方式编码。
dubbo 的编码处理类具化到 DubboCodec
类的 encodeRequestData(Channel channel, ObjectOutput out, Object data, String version)
方法,通过比对两个版本,的确存在差异的部分。依旧使用流程分支 if
来作为跨版本的兼容方案。那么问题又来了,流程分支的条件是什么?
流程分支的条件是作为区分两个版本的关键。什么属性能够最直观地区分版本呢?当然是版本号了。那么在编解码时能够拿到版本号?如果能拿到版本号,付出的性能损耗有多大?
org.apache.dubbo.common.Version
类中的常量 DEFAULT_DUBBO_PROTOCOL_VERSION 的值 “2.0.2”。所以 apache-dubbo-2.7.2 解码时用于区分两个版本的流程分支条件就有了。但为了其他某个版本也出现出现与 dubbox 相同的 protocolVersion,最好再加上 dubbox 解码时独有的特性。if("2.0.0".equls(dubboVersion) && Integer.parseInt(desc) >= 0){
//......
}
注意:以上写法在 desc 不为数字时会抛异常,具体变更代码详见5.1 解码时调整的代码。
URL url = channel.getUrl();
String protocolVersion = url.getParameter(Constants.DUBBO_VERSION_KEY, Version.getProtocolVersion);
if("2.0.0".equals(protocolVersion)){
// ......
}
若存在第三个版本,其 dubbo 协议号也为 2.0.0,但编码方式与 apache-dubbo-2.7.2 相同,还能实现兼容吗?
直接将下述代码在 apache-dubbo-2.7.2 源码基础上替换掉 org.apache.dubbo.rpc.protocol.dubbo.DecodeableRpcInvocation
类的 decode(Channel channel,InputStream input) 整个方法。
public Object decode(Channel channel, InputStream input) throws IOException {
ObjectInput in = CodecSupport.getSerialization(channel.getUrl(), serializationType)
.deserialize(channel.getUrl(), input);
String dubboVersion = in.readUTF();
request.setVersion(dubboVersion);
setAttachment(Constants.DUBBO_VERSION_KEY, dubboVersion);
setAttachment(Constants.PATH_KEY, in.readUTF());
setAttachment(Constants.VERSION_KEY, in.readUTF());
setMethodName(in.readUTF());
try {
Object[] args;
Class<?>[] pts;
// NOTICE modified Ernest.Wu
String desc = in.readUTF();
String pattern = "-?[0-9]+.?[0-9]*";
Integer argNum = null;
// 判断是否为数字且不小于0
if ("2.0.0".equals(dubboVersion) && desc.matches(pattern) && (argNum = Integer.parseInt(desc)) >= 0) {
if (argNum == 0) {
pts = DubboCodec.EMPTY_CLASS_ARRAY;
args = DubboCodec.EMPTY_OBJECT_ARRAY;
} else {
args = new Object[argNum];
pts = new Class[argNum];
for (int i = 0; i < args.length; i++) {
try {
args[i] = in.readObject();
pts[i] = args[i].getClass();
} catch (Exception e) {
if (log.isWarnEnabled()) {
log.warn("Decode argument failed: " + e.getMessage(), e);
}
}
}
}
} else {
desc = argNum == null ? desc : in.readUTF();
if (desc.length() == 0) {
pts = DubboCodec.EMPTY_CLASS_ARRAY;
args = DubboCodec.EMPTY_OBJECT_ARRAY;
} else {
pts = ReflectUtils.desc2classArray(desc);
args = new Object[pts.length];
for (int i = 0; i < args.length; i++) {
try {
args[i] = in.readObject(pts[i]);
} catch (Exception e) {
if (log.isWarnEnabled()) {
log.warn("Decode argument failed: " + e.getMessage(), e);
}
}
}
}
}
setParameterTypes(pts);
Map<String, String> map = (Map<String, String>) in.readObject(Map.class);
if (map != null && map.size() > 0) {
Map<String, String> attachment = getAttachments();
if (attachment == null) {
attachment = new HashMap<>();
}
attachment.putAll(map);
setAttachments(attachment);
}
//decode argument ,may be callback
for (int i = 0; i < args.length; i++) {
args[i] = decodeInvocationArgument(channel, this, pts, i, args[i]);
}
setArguments(args);
} catch (ClassNotFoundException e) {
throw new IOException(StringUtils.toString("Read invocation data failed.", e));
} finally {
if (in instanceof Cleanable) {
((Cleanable) in).cleanup();
}
}
return this;
}
直接将下述代码在 apache-dubbo-2.7.2 源码基础上替换掉 org.apache.dubbo.rpc.protocol.dubbo.DubboCodec
类的 encodeRequestData(Channel channel, ObjectOutput out, Object data, String version) 整个方法。
protected void encodeRequestData(Channel channel, ObjectOutput out, Object data, String version) throws IOException {
RpcInvocation inv = (RpcInvocation) data;
out.writeUTF(version);
out.writeUTF(inv.getAttachment(Constants.PATH_KEY));
out.writeUTF(inv.getAttachment(Constants.VERSION_KEY));
out.writeUTF(inv.getMethodName());
// NOTICE modified by Ernest.Wu
URL url = channel.getUrl();
String protocolVersion = url.getParameter(Constants.DUBBO_VERSION_KEY, Version.getProtocolVersion());
if ("2.0.0".equals(protocolVersion)) {
if (serializationType(channel.getUrl()) && !containComplexArguments(inv)) {
out.writeInt(inv.getParameterTypes().length);
} else {
out.writeInt(-1);
out.writeUTF(ReflectUtils.getDesc(inv.getParameterTypes()));
}
} else {
out.writeUTF(ReflectUtils.getDesc(inv.getParameterTypes()));
}
Object[] args = inv.getArguments();
if (args != null) {
for (int i = 0; i < args.length; i++) {
out.writeObject(encodeInvocationArgument(channel, inv, i));
}
}
out.writeObject(RpcUtils.getNecessaryAttachments(inv));
}
再将如下两个方法新增到该类中。
/**
* 匹配kryo和fst序列方式
*
* @param invocation
* @return
*/
private boolean containComplexArguments(RpcInvocation invocation) {
for (int i = 0; i < invocation.getParameterTypes().length; i++) {
Object argument = invocation.getArguments()[i];
if (argument == null || invocation.getParameterTypes()[i] != argument.getClass()) {
return true;
}
}
return false;
}
private boolean serializationType(URL url) {
String serialization = url.getParameter(Constants.SERIALIZATION_KEY, Constants.DEFAULT_REMOTING_SERIALIZATION);
return "fst".equals(serialization) || "kryo".equals(serialization);
}