乐趣区

关于java:用-Dubbo-传输文件被老板一顿揍

送大家以下 java 学习材料,文末有支付形式




公司之前有一个 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 的数据即可,一样不会有内存占用问题(更具体的文件报文解决形式能够参考我的另一篇文章《Tomcat 中是怎么解决文件上传的?》)。

那既然 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)。

所以如果有文件上传的场景,尽可能的用客户端直传的形式吧,敌对又节俭资源!

退出移动版