來(lái)自:https://juejin.cn/post/6963642641506369566公司之前有一個(gè) Dubbo 服務(wù),其內(nèi)部封裝了騰訊云的對(duì)象存儲(chǔ)服務(wù) SDK,目的是統(tǒng)一管理這種三方服務(wù)的SDK,其他系統(tǒng)直接調(diào)用這個(gè)對(duì)象存儲(chǔ)的 Dubbo 服務(wù)。這樣可以避免因平臺(tái) SDK 出現(xiàn)不兼容的大版本更新,從而導(dǎo)致公司所有系統(tǒng)修改跟著升級(jí)的問(wèn)題。
想法是好的,不過(guò)這種做法并不合適,因?yàn)?Dubbo 并不適合傳輸文件。好在這個(gè)系統(tǒng)在上線不久就沒(méi)人用廢棄了……雖然系統(tǒng)廢棄了,不過(guò)就這個(gè) Dubbo 上傳文件的主題還是可以詳細(xì)分析下,聊聊它到底為什么不適合傳文件。Dubbo 怎么傳文件?
void?sendPhoto(File?photo);
當(dāng)然不行!Dubbo 只是將對(duì)象進(jìn)行序列化然后傳輸,而 File 對(duì)象就算序列化也無(wú)法處理文件的數(shù)據(jù),所以只能直接發(fā)送文件內(nèi)容:void?sendPhoto(byte[]?photo);
但這樣就會(huì)導(dǎo)致 consumer 端需要一次性讀取完整的文件內(nèi)容至內(nèi)存中,再大的內(nèi)存也扛不住這樣玩。而且 provider 端在接受數(shù)據(jù)解析報(bào)文時(shí),也需要一次性將 byte[] 讀取至內(nèi)存中,也是一樣有內(nèi)存占用過(guò)高問(wèn)題。單連接模型問(wèn)題
除了內(nèi)存占用問(wèn)題之外,Dubbo(這里指 Dubbo 協(xié)議)的單連接模型也不適合文件傳輸。Dubbo 協(xié)議默認(rèn)是單連接的模型,即一個(gè) provider 的所有請(qǐng)求都是用一個(gè) TCP 連接。默認(rèn)使用 Netty 來(lái)進(jìn)行傳輸,而 Netty 中為了保證 Channel 線程安全,會(huì)將寫(xiě)入事件進(jìn)行排隊(duì)處理。那么在單連接下,多個(gè)請(qǐng)求都會(huì)使用同一個(gè)連接,也就是同一個(gè) Channel 進(jìn)行寫(xiě)入數(shù)據(jù);當(dāng)多個(gè)請(qǐng)求同時(shí)寫(xiě)入時(shí),如果某個(gè)報(bào)文過(guò)大,會(huì)導(dǎo)致 Channel 一直在發(fā)送這個(gè)報(bào)文,其他請(qǐng)求的報(bào)文寫(xiě)入事件會(huì)進(jìn)行排隊(duì),遲遲無(wú)法發(fā)送,數(shù)據(jù)都沒(méi)有發(fā)送過(guò)去,那么其他的 consumer 也自然會(huì)處于阻塞等待響應(yīng)的狀態(tài)中,一直無(wú)法返回了。所以在單連接下,如果報(bào)文過(guò)大,會(huì)導(dǎo)致 Netty 的寫(xiě)入事件處理阻塞,無(wú)法及時(shí)的將數(shù)據(jù)發(fā)送至服務(wù)端,從而造成請(qǐng)求白白阻塞的問(wèn)題。那既然單連接模型有這么大的缺點(diǎn),為什么 Dubbo 還要采用單連接呢?因?yàn)槭≠Y源啊,TCP 連接這種資源可是很寶貴的,如果單連接可以滿足絕大多數(shù)場(chǎng)景,那么完全不需要為每個(gè)請(qǐng)求準(zhǔn)備一個(gè)連接。Dubbo 文檔中也提到了單連接設(shè)計(jì)的原因:因?yàn)榉?wù)的現(xiàn)狀大都是服務(wù)提供者少,通常只有幾臺(tái)機(jī)器,而服務(wù)的消費(fèi)者多,可能整個(gè)網(wǎng)站都在訪問(wèn)該服務(wù),比如 Morgan 的提供者只有 6 臺(tái)提供者,卻有上百臺(tái)消費(fèi)者,每天有 1.5 億次調(diào)用,如果采用常規(guī)的 hessian 服務(wù),服務(wù)提供者很容易就被壓跨,通過(guò)單一連接,保證單一消費(fèi)者不會(huì)壓死提供者,長(zhǎng)連接,減少連接握手驗(yàn)證等,并使用異步 IO,復(fù)用線程池,防止 C10K 問(wèn)題。
雖然 Dubbo 協(xié)議默認(rèn)單連接模型,但還是可以設(shè)置多連接的:<dubbo:service?connections="1"/>
<dubbo:reference?connections="1"/>
不過(guò)多連接下,連接和請(qǐng)求并不是一一對(duì)應(yīng)的,而是一個(gè)輪詢的機(jī)制。如下圖所示,當(dāng)配置了N個(gè)連接時(shí),對(duì)于每一個(gè) Provider 實(shí)例都會(huì)維護(hù)多個(gè)連接,在執(zhí)行請(qǐng)求時(shí)會(huì)通過(guò)輪詢的機(jī)制,為每次請(qǐng)求分配不同的連接為什么 HTTP 協(xié)議“適合”傳文件?
其實(shí)這么說(shuō)并不嚴(yán)謹(jǐn),并不是 HTTP 協(xié)議適合傳文件,Dubbo 還支持 HTTP 協(xié)議呢(雖然是半殘品),一樣不適合傳文件。Dubbo 這類 RPC 框架為了滿足“調(diào)用本地方法像調(diào)用遠(yuǎn)程一樣”,必須將數(shù)據(jù)序列化成語(yǔ)言里的對(duì)象,但這樣一來(lái)就導(dǎo)致無(wú)法處理 File 這種形式的對(duì)象了。如果跳出 Dubbo 這種 RPC 框架特性的限制,單獨(dú)看 HTTP 協(xié)議的話,是很適合傳輸文件的。因?yàn)閷?duì)于 Client 來(lái)說(shuō),只需要將報(bào)文發(fā)送至 Server,比如要傳輸?shù)奈募诒镜氐脑?,那我完全可以每次只讀取文件的一個(gè) Buffer 大小,然后將這個(gè) Buffer 的數(shù)據(jù)使用 Socket 發(fā)送即可;在這種方式下,同時(shí)存在于內(nèi)存中的數(shù)據(jù),只會(huì)有一個(gè) Buffer 大小,不會(huì)有 Dubbo 那樣將全部數(shù)據(jù)讀取至內(nèi)存的問(wèn)題。如下圖所示,Client 每次只從1GB 文件中讀取 4K 大小的 Buffer 數(shù)據(jù),然后用 Socket 發(fā)送,直至將文件完全讀取并發(fā)送成功。那么這種方式下對(duì)于單次傳輸來(lái)說(shuō),內(nèi)存始終都是只有 4K buffer 大小的占用,并不會(huì)像 Dubbo 那樣一次性全部讀取為 byte[] 再發(fā)送。
對(duì)于 Server 端也是一樣,Server 端也并不用一次性將所有報(bào)文讀取至內(nèi)存中,在解析 Header 中的 Content-Length 后,直接包裝一個(gè) InputStream,在這個(gè) InputStream 內(nèi)部進(jìn)行讀取 Socket Buffer 的數(shù)據(jù)即可,一樣不會(huì)有內(nèi)存占用問(wèn)題(更詳細(xì)的文件報(bào)文處理方式可以參考我的另一篇文章《Tomcat 中是怎么處理文件上傳的?》)。那既然 HTTP 協(xié)議“適合”傳輸文件,Spring Cloud 的標(biāo)配 RPC 客戶端 - Feign 在傳輸文件上又會(huì)有什么問(wèn)題呢?Feign 適合傳輸文件嗎
Feign 其實(shí)并不能算一套 RPC 框架,它只是一個(gè) Http Client 而已。在使用 Feign 時(shí),Server 可以是任意的 Http Server,比如實(shí)現(xiàn) Servlet 的 Tomcat/Jetty/Undertow,或者是其他語(yǔ)言的 Apache Server 等等。而一般用 Feign 時(shí),都是在 Spring Cloud 全家桶環(huán)境下,服務(wù)端往往是默認(rèn)的 Tomcat。而 Tomcat 在讀取文件報(bào)文(form-data)時(shí),會(huì)先將報(bào)文暫存至磁盤(pán),然后通過(guò) FileItem 讀取磁盤(pán)中的報(bào)文內(nèi)容。所以在對(duì)于 Server 端來(lái)說(shuō),不會(huì)一次性將完整的報(bào)文數(shù)據(jù)讀取至內(nèi)存中,也就不會(huì)有內(nèi)存占用過(guò)高的問(wèn)題。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 中將參數(shù)的編碼/序列化抽象為一個(gè) Encoder,對(duì)于 HTTP 協(xié)議的文件上傳也提供了一個(gè)?feign-form?模塊,該模塊中提供了一些 FormEncoder。可無(wú)論哪種 FormEncoder 最后都是通過(guò) Feign 封裝的 Output 對(duì)象進(jìn)行輸出,不過(guò)這個(gè) Output 對(duì)象卻不是那種包裝 Socket InputStream 作為中轉(zhuǎn)發(fā)送,而是直接作為一個(gè)數(shù)據(jù)的載體,用一個(gè) ByteArrayOutputStream 來(lái)存儲(chǔ)編碼完成的數(shù)據(jù)。所以無(wú)論怎么定義 FormEncoder,最后數(shù)據(jù)都會(huì)寫(xiě)入到這個(gè) Output 的 ByteArrayOutputStream 中,仍然會(huì)將所有數(shù)據(jù)完整的讀取至內(nèi)存中,一樣會(huì)有內(nèi)存占用高的問(wèn)題。@RequiredArgsConstructor
@FieldDefaults(level?=?PRIVATE,?makeFinal?=?true)
public?class?Output?implements?Closeable?{
??ByteArrayOutputStream?outputStream?=?new?ByteArrayOutputStream();
??//所有的數(shù)據(jù)在“編碼”之后,仍然會(huì)寫(xiě)入到?ByteArrayOutputStream?這個(gè)內(nèi)存?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 只是個(gè) HTTP Client,Server 端還是“增量”讀取的,對(duì)于 Server 端來(lái)說(shuō)不會(huì)有這個(gè)內(nèi)存問(wèn)題。總結(jié)
其實(shí) Dubbo 不光是不適合傳輸文件,大報(bào)文場(chǎng)景下都不太合適,Dubbo 的設(shè)計(jì)更適合小業(yè)務(wù)報(bào)文的傳輸(默認(rèn)報(bào)文大小只有8MB)。所以如果有文件上傳的場(chǎng)景,盡可能的用客戶端直傳的方式吧,友好又節(jié)省資源!