为什么都说Dubbo不适合传输大文件?苦思许久的我终于有了答案

Posted 守夜人爱吃兔子

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了为什么都说Dubbo不适合传输大文件?苦思许久的我终于有了答案相关的知识,希望对你有一定的参考价值。

背景

之前公司有一个 Dubbo 服务,内部封装了腾讯云的对象存储服务 SDK,是为了统一管理这种三方服务的SDK,其他系统直接调用这个对象存储的 Dubbo 服务。用来避免因平台 SDK 出现不兼容的大版本更新,导致公司所有系统修改跟着升级的问题。

然而因为 Dubbo 并不适合传输大包,所以虽然想法不错,但这种做法还是并不合适,于是这个系统在上线不久就遭废弃没人用了……

不过系统虽然废弃了,但是我们可以顺着 Dubbo 上传文件的主题来详细分析下,说说看它究竟是因为什么不适合传文件。

 

Dubbo 怎么传文件?

难道直接这样传 File 吗?

void sendPhoto(File photo);

自然是不可以的!Dubbo 只是将对象进行序列化然后传输,可 File 对象就算序列化也无法处理文件的数据,所以只能直接发送文件内容:

void sendPhoto(byte[] photo);

但这样会导致 consumer 端需要一次性读取完整的文件内容至内存中,这样玩的话,再大的内存也都遭不住。

并且 provider 端在接受数据解析报文时, byte[] 也需要一次性读取至内存中,内存占用过高的问题同样存在。

 

单连接模型问题

除了内存占用问题之外,Dubbo(这里指 Dubbo 协议)的单连接模型也不适合文件传输。 ​

Dubbo 协议默认是单连接的模型,也就是一个 provider 的所有请求都是用一个 TCP 连接。默认使用 Netty 来进行传输,而 Netty 中为了保证 Channel 线程安全,会将写入事件进行排队处理。那么在单连接下,多个请求都会使用同一个连接,也就是同一个 Channel 进行写入数据;当多个请求同时写入时,如果某个报文过大,会导致 Channel 一直在发送这个报文,其他请求的报文写入事件会进行排队,迟迟无法发送,连数据都没有发送过去,那么其他的 consumer 也自然会处于阻塞等待响应的状态中,一直无法返回了。 ​

所以在单连接下,如果报文过大,将会导致 Netty 地写入事件处理阻塞,数据将无法及时发送至服务端,从而造成请求白白阻塞的问题。 ​

那有的朋友可能会问,单连接模型缺点都这么大了, Dubbo 为什么还要采用单连接呢? ​

很简单,因为省资源,TCP 连接这样的资源可是很宝贵的,如果单连接可以满足绝大多数场景,那么也就完全没必要为每个请求准备一个连接。 ​

在Dubbo 文档中也提到了单连接设计的原因:

因为服务的现状大都是服务提供者少,通常只有几台机器,而服务的消费者多,可能整个网站都在访问该服务,比如 Morgan 的提供者只有 6 台提供者,却有上百台消费者,每天有 1.5 亿次调用,如果采用常规的 hessian 服务,服务提供者很容易就被压垮,通过单一连接,保证单一消费者不会压死提供者,长连接,减少连接握手验证等,并使用异步 IO,复用线程池,防止 C10K 问题。

虽然 Dubbo 协议默认单连接模型,但还是可以设置多连接的:

<dubbo:service connections="1"/>
<dubbo:reference connections="1"/>

不过多连接下,连接和请求是一个轮询的机制,并不是一一对应的。

如下图,当配置了数个连接时,对于每一个 Provider 实例都会维护多个连接,在执行请求时会通过轮询的机制,为每次请求分配不同的连接

 

为什么 HTTP 协议“适合”传文件?

这么说其实不严谨,并不是 HTTP 协议适合传文件,Dubbo 还支持 HTTP 协议呢(虽然是半残品),一样不适合传文件。 ​

Dubbo 这类 RPC 框架为了满足“调用本地方法像调用远程一样”,必须将数据序列化成语言里的对象,但这样一来就导致无法处理 File 这种形式的对象了。 ​

如果跳出 Dubbo 这种 RPC 框架特性的限制,单独看 HTTP 协议的话,是很适合传输文件的。因为对于 Client 来说,只需要将报文发送至 Server,比如要传输的文件在本地的话,那我完全可以每次只读取文件的一个 Buffer 大小,然后将这个 Buffer 的数据使用 Socket 发送即可;在这种方式下,同时存在于内存中的数据,只会有一个 Buffer 大小,不会有 Dubbo 那样将全部数据读取至内存的问题。 ​

如下图所示,Client 每次只从1GB 文件中读取 4K 大小的 Buffer 数据,然后用 Socket 发送,直至将文件完全读取并发送成功。那么这种方式下对于单次传输来说,内存始终都是只有 4K buffer 大小的占用,并不会像 Dubbo 那样一次性全部读取为 byte[] 再发送。

对于 Server 端也是一样,Server 端也并不用一次性将所有报文读取至内存中,在解析 Header 中的 Content-Length 后,直接包装一个 InputStream,在这个 InputStream 内部进行读取 Socket Buffer 的数据即可,一样不会有内存占用问题

那既然 HTTP 协议“适合”传输文件,Spring Cloud 的标配 RPC 客户端 - Feign 在传输文件上又会有什么问题呢?

 

Feign 适合传输文件吗

Feign 其实并不能算一套 RPC 框架,它只是一个 Http Client 而已。在使用 Feign 时,Server 可以是任意的 Http Server,比如实现 Servlet 的 Tomcat/Jetty/Undertow,或者是其他语言的 Apache Server 等等。 ​

而一般都是在 Spring Cloud 全家桶环境下用 Feign,服务端往往是默认的 Tomcat。而 Tomcat 在读取文件报文(form-data)时,会先将报文暂存至磁盘,然后通过 FileItem 读取磁盘中的报文内容。所以在对于 Server 端来说,不会一次性将完整的报文数据读取至内存中,也就不会有内存占用过高的问题。 ​

Feign 中上传文件有以下几种方式:

interface SomeApi {

  // File parameter
  @RequestLine("POST /send_photo")
  @Headers("Content-Type: multipart/form-data")
  void sendPhoto (@Param("is_public") Boolean isPublic, @Param("photo") File photo);

  // byte[] parameter
  @RequestLine("POST /send_photo")
  @Headers("Content-Type: multipart/form-data")
  void sendPhoto (@Param("is_public") Boolean isPublic, @Param("photo") byte[] photo);

  // FormData parameter
  @RequestLine("POST /send_photo")
  @Headers("Content-Type: multipart/form-data")
  void sendPhoto (@Param("is_public") Boolean isPublic, @Param("photo") FormData photo);
    
  // MultipartFile parameter
  @RequestLine("POST /send_photo")
  @Headers("Content-Type: multipart/form-data")
  void sendPhoto(@RequestPart(value = "photo") MultipartFile photo);
    
  // Group all parameters within a POJO
  @RequestLine("POST /send_photo")
  @Headers("Content-Type: multipart/form-data")
  void sendPhoto (MyPojo pojo);

  class MyPojo {

    @FormProperty("is_public")
    Boolean isPublic;

    File photo;
  }
}

Feign 中将参数的编码/序列化抽象为一个 Encoder,对于 HTTP 协议的文件上传也提供了一个 feign-form 模块,该模块中提供了一些 FormEncoder。可无论哪种 FormEncoder 最后都是通过 Feign 封装的 Output 对象进行输出,不过这个 Output 对象却不是那种包装 Socket InputStream 作为中转发送,而是直接作为一个数据的载体,用一个 ByteArrayOutputStream 来存储编码完成的数据。 ​

所以无论怎么定义 FormEncoder,最后数据都会写入到这个 Output 的 ByteArrayOutputStream 中,仍然会将所有数据完整的读取至内存中,一样会有内存占用高的问题。

@RequiredArgsConstructor
@FieldDefaults(level = PRIVATE, makeFinal = true)
public class Output implements Closeable {

  ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

  //所有的数据在“编码”之后,仍然会写入到 ByteArrayOutputStream 这个内存 OutputStream 中
  public Output write (byte[] bytes) {
    outputStream.write(bytes);
    return this;
  }

 
  public Output write (byte[] bytes, int offset, int length) {
    outputStream.write(bytes, offset, length);
    return this;
  }

  public byte[] toByteArray () {
    return outputStream.toByteArray();
  }

}

但好在 Feign 只是个 HTTP Client,Server 端还是“增量”读取的,对于 Server 端来说不会有这个内存问题。

 

总结

其实 Dubbo 不光是不适合传输文件,大报文场景下都不太合适,Dubbo 的设计更适合小业务报文的传输(默认报文大小只有8MB)。

所以如果有文件上传的场景,尽可能地用客户端直传的方式吧,节省资源又友好!

 

最后

都看到这里了,不如点个一键三连再划走吧,你的支持就是我最大的动力,还有更多java干货文章每天持续更新中。祝大家万事胜意!

 

以上是关于为什么都说Dubbo不适合传输大文件?苦思许久的我终于有了答案的主要内容,如果未能解决你的问题,请参考以下文章

为什么说 Dubbo 不适合传输文件?

我用 Dubbo 传输文件,差点被开除

Dubbo支持的9种通讯协议你知道吗?

Dubbo面试踩坑

干货|Dubbo面试题及答案

dubbo 各通讯协议比较