dubbox升级至apache-dubbo-2.7.2的兼容方案

一、Dubbo 的前世今生

 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官网
 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)

异常信息总结

  1. 都是由提供方抛出的解码异常。

  2. 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
    
  3. 提供方所发布的服务并未被调用。

四、改造探索

 Dubbo 的远程调用执行步骤(与本文无关动作不列出)可大致分解为:

消费方编码请求信息 -> 信息传输(netty、mina) -> 提供方解码请求信息 -> 处理请求后编码响应结果 -> netty发送 -> 消费方解码响应结果

 此执行过程则完全能证实前一节的异常信息总结的结果,得出结论:apache-dubbo-2.7.2 与 dubbox-2.8.4 的编解码方式不完全一致。

4.1 比对 Hessian

 既然编解码方式不一致,那么就找出其不同之处。上述两个异常信息都指向 Hessian 序列化,那么就是用专业的比对软件 Beyond Compare 把 dubbo 所用到了 hessian 类进行两两比对。
 比对结果却不如人意。虽然 apache-dubbo-2.7.2 将 dubbo 作为独立依赖到项目中,而 dubbox-2.8.4 是内置在项目中,但是两者所使用到的类完全一致,并无差异(因无实质性进展,则不贴出比对结果占用版面)。

4.2 比对序列化前置动作

 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 来作为跨版本的兼容方案。那么问题又来了,流程分支的条件是什么?

五、源码调整

 流程分支的条件是作为区分两个版本的关键。什么属性能够最直观地区分版本呢?当然是版本号了。那么在编解码时能够拿到版本号?如果能拿到版本号,付出的性能损耗有多大?

  • 解码的流程分支条件
     通过分析 dubbox 编码的代码,发现是将 dubbo 协议的版本号硬编码成“2.0.0”(详见dubbox-2.8.4 的 DubboCodec 类中的第121行),序列化后发送出去;而 apache-dubbo 则是获取 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 解码时调整的代码。

  • 编码的流程分支条件
     消费方在编码时,可以通过 channel 变量获取到提供者的 URL 信息,从 URL 也能提取出 dubbo 协议的版本号,那么流程控制的条件就有了:
    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 相同,还能实现兼容吗?

5.1 解码时调整的代码

 直接将下述代码在 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;
}

5.2 编码时调整的代码

 直接将下述代码在 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);
}

你可能感兴趣的:(dubbo)