为什么说 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"/>

不过多连接下,连接和请求并不是一一对应的,而是一个轮询的机制。如下图所示,当配置了N个连接时,对于每一个 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 等等。

而一般用 Feign 时,都是在 Spring Cloud 全家桶环境下,服务端往往是默认的 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)。
所以如果有文件上传的场景,尽可能的用客户端直传的方式吧,友好又节省资源!

码字不易 请大家给我点赞+关注+评论!一条龙

如果想要获取学习资料,可在下方获取联系方式;
或者想要一起讨论问题的也加入技术交流Q群

添加助手小姐姐的 VX:  Mlzg5201314zz 
或者加入技术交流Q群:614478470
记得备注[简书]
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342

推荐阅读更多精彩内容