apache / dubbo

The java implementation of Apache Dubbo. An RPC and microservice framework.
https://dubbo.apache.org/
Apache License 2.0
40.34k stars 26.4k forks source link

Dubbo升级Netty4后,异步调用场景存在参数窜包的问题 #10688

Open stoneapple opened 1 year ago

stoneapple commented 1 year ago

问题发现在dubbo2.5.x,初步分析dubbo3应该同样存在。 问题场景: 消费方和提供方默认的通信SPI均切换到netty4,消费方侧reference配置async=true。在如下的代码场景: Pojo a = new Pojo(); a.setName("zhangsan"); aysncService1.serve(a); Future f1 = RpcContext.getContext().getFuture();

a.setName("lisi"); aysncService2.serve(a); Future f2 = RpcContext.getContext().getFuture();

f1.get()+"--"+f2.get();

其中aysncService1和aysncService2两个服务引用均采用异步的配置,在netty4场景下,aysncService1.serve(a),传递给提供方的a对象,属性也编程lisi了,在netty3作为通信组件时,不存在该问题。

初步分析:由于netty4和netty3在客户端的线程模型上有较大的改动。netty3会先将数据序列化后交给nioworker,netty4中直接将对象打包到给nioworker的任务队列,在数据发送时才触发序列化。

解决思路:异步场景发送前对报文深拷贝后丢给任务队列,或者 将序列化前移到业务线程。

AlbumenJ commented 1 year ago

这一块看起来在 Dubbo 3 是没问题的,你可以试下跑 3.1.1 版本看看,有问题的话再提交个 issue 吧

ByrsH commented 3 months ago

2.7.x 也有这样的现象,问题的关键是前后两次请求共用变量 a (注意:假如前后两次请求是两个不同的对象,但是对象的成员变量公用了,也会有同样的问题)。

该问题在 3.2.0 版本得到了解决(虽然此次调整是为了提升效率,并没有说明为了解决这个问题)。commit 记录:https://github.com/apache/dubbo/commit/5016f550be52f14a232399409a3c97fa6d6db321

关键代码在 dubbo-remoting/dubbo-remoting-netty4/src/main/java/org/apache/dubbo/remoting/transport/netty4/NettyChannel.java 中:

try {
            Object outputMessage = message;
            if (!encodeInIOThread) {
                ByteBuf buf = channel.alloc().buffer();
                ChannelBuffer buffer = new NettyBackedChannelBuffer(buf);
                codec.encode(this, buffer, message);
                outputMessage = buf;
            }
            ChannelFuture future = writeQueue.enqueue(outputMessage).addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture future) throws Exception {
                    if (!(message instanceof Request)) {
                        return;
                    }
                    ChannelHandler handler = getChannelHandler();
                    if (future.isSuccess()) {
                        handler.sent(NettyChannel.this, message);
                    } else {
                        Throwable t = future.cause();
                        if (t == null) {
                            return;
                        }
                        Response response = buildErrorResponse((Request) message, t);
                        handler.received(NettyChannel.this, response);
                    }
                }
            });

Dubbo 在切换到 Netty channel 前对 message 做了 encode,然后放到队列里就返回了。当第二个请求 a.setName("lisi"); 时,就不会影响已经 encode 的 message了。

之前的版本是直接通过 netty channel writeAndFlush message,在第二个请求 a.setName("lisi"); 时,第一个请求还未完成 encode ,就会造成参数覆盖的情况出现。 2.7.22 版本 NettyChannel:

try {
            ChannelFuture future = channel.writeAndFlush(message);
            if (sent) {
                // wait timeout ms
                timeout = getUrl().getPositiveParameter(TIMEOUT_KEY, DEFAULT_TIMEOUT);
                success = future.await(timeout);
            }
            Throwable cause = future.cause();
            if (cause != null) {
                throw cause;
            }
        } catch (Throwable e) {
            removeChannelIfDisconnected(channel);
            throw new RemotingException(this, "Failed to send message " + PayloadDropper.getRequestWithoutData(message) + " to " + getRemoteAddress() + ", cause: " + e.getMessage(), e);
        }

对于使用者来说,应该尽量避免多次异步请求参数共享的情形出现,第二次调用时,可以对参数做一次深拷贝得到新的对象,然后再进行异步请求。

wcy666103 commented 3 months ago

2.7.x 也有这样的现象,问题的关键是前后两次请求共用变量 a (注意:假如前后两次请求是两个不同的对象,但是对象的成员变量公用了,也会有同样的问题)。

该问题在 3.2.0 版本得到了解决(虽然此次调整是为了提升效率,并没有说明为了解决这个问题)。commit 记录:5016f55

关键代码在 dubbo-remoting/dubbo-remoting-netty4/src/main/java/org/apache/dubbo/remoting/transport/netty4/NettyChannel.java 中:

try {
            Object outputMessage = message;
            if (!encodeInIOThread) {
                ByteBuf buf = channel.alloc().buffer();
                ChannelBuffer buffer = new NettyBackedChannelBuffer(buf);
                codec.encode(this, buffer, message);
                outputMessage = buf;
            }
            ChannelFuture future = writeQueue.enqueue(outputMessage).addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture future) throws Exception {
                    if (!(message instanceof Request)) {
                        return;
                    }
                    ChannelHandler handler = getChannelHandler();
                    if (future.isSuccess()) {
                        handler.sent(NettyChannel.this, message);
                    } else {
                        Throwable t = future.cause();
                        if (t == null) {
                            return;
                        }
                        Response response = buildErrorResponse((Request) message, t);
                        handler.received(NettyChannel.this, response);
                    }
                }
            });

Dubbo 在切换到 Netty channel 前对 message 做了 encode,然后放到队列里就返回了。当第二个请求 a.setName("lisi"); 时,就不会影响已经 encode 的 message了。

之前的版本是直接通过 netty channel writeAndFlush message,在第二个请求 a.setName("lisi"); 时,第一个请求还未完成 encode ,就会造成参数覆盖的情况出现。 2.7.22 版本 NettyChannel:

try {
            ChannelFuture future = channel.writeAndFlush(message);
            if (sent) {
                // wait timeout ms
                timeout = getUrl().getPositiveParameter(TIMEOUT_KEY, DEFAULT_TIMEOUT);
                success = future.await(timeout);
            }
            Throwable cause = future.cause();
            if (cause != null) {
                throw cause;
            }
        } catch (Throwable e) {
            removeChannelIfDisconnected(channel);
            throw new RemotingException(this, "Failed to send message " + PayloadDropper.getRequestWithoutData(message) + " to " + getRemoteAddress() + ", cause: " + e.getMessage(), e);
        }

对于使用者来说,应该尽量避免多次异步请求参数共享的情形出现,第二次调用时,可以对参数做一次深拷贝得到新的对象,然后再进行异步请求。

你可以帮忙提交个pr解决这个问题吗