先點贊再看,養成好習慣
公司以前有一個 Dubbo 服務,其內部封裝了騰訊雲的對象存儲服務 SDK,目的是統一管理這種三方服務的SDK,其餘系統直接調用這個對象存儲的 Dubbo 服務。這樣能夠避免因平臺 SDK 出現不兼容的大版本更新,從而致使公司全部系統修改跟着升級的問題。
java
想法是好的,不過這種作法並不合適,由於 Dubbo 並不適合傳輸文件。好在這個系統在上線不久就沒人用廢棄了……
segmentfault
雖然系統廢棄了,不過就這個 Dubbo 上傳文件的主題仍是能夠詳細分析下,聊聊它到底爲何不適合傳文件。安全
難道這樣直接傳 File 嗎?併發
void sendPhoto(File photo);
固然不行!Dubbo 只是將對象進行序列化而後傳輸,而 File 對象就算序列化也沒法處理文件的數據,因此只能直接發送文件內容:框架
void sendPhoto(byte[] photo);
但這樣就會致使 consumer 端須要一次性讀取完整的文件內容至內存中,再大的內存也扛不住這樣玩。並且 provider 端在接受數據解析報文時,也須要一次性將 byte[] 讀取至內存中,也是同樣有內存佔用太高問題。異步
除了內存佔用問題以外,Dubbo(這裏指 Dubbo 協議)的單鏈接模型也不適合文件傳輸。
ide
Dubbo 協議默認是單鏈接的模型,即一個 provider 的全部請求都是用一個 TCP 鏈接。默認使用 Netty 來進行傳輸,而 Netty 中爲了保證 Channel 線程安全,會將寫入事件進行排隊處理。那麼在單鏈接下,多個請求都會使用同一個鏈接,也就是同一個 Channel 進行寫入數據;當多個請求同時寫入時,若是某個報文過大,會致使 Channel 一直在發送這個報文,其餘請求的報文寫入事件會進行排隊,遲遲沒法發送,數據都沒有發送過去,那麼其餘的 consumer 也天然會處於阻塞等待響應的狀態中,一直沒法返回了。
post
因此在單鏈接下,若是報文過大,會致使 Netty 的寫入事件處理阻塞,沒法及時的將數據發送至服務端,從而形成請求白白阻塞的問題。
網站
那既然單鏈接模型有這麼大的缺點,爲何 Dubbo 還要採用單鏈接呢?
ui
由於省資源啊,TCP 鏈接這種資源但是很寶貴的,若是單鏈接能夠知足絕大多數場景,那麼徹底不須要爲每一個請求準備一個鏈接。
Dubbo 文檔中也提到了單鏈接設計的緣由:
由於服務的現狀大都是服務提供者少,一般只有幾臺機器,而服務的消費者多,可能整個網站都在訪問該服務,好比 Morgan 的提供者只有 6 臺提供者,卻有上百臺消費者,天天有 1.5 億次調用,若是採用常規的 hessian 服務,服務提供者很容易就被壓跨,經過單一鏈接,保證單一消費者不會壓死提供者,長鏈接,減小鏈接握手驗證等,並使用異步 IO,複用線程池,防止 C10K 問題。
雖然 Dubbo 協議默認單鏈接模型,但仍是能夠設置多鏈接的:
<dubbo:service connections="1"/> <dubbo:reference connections="1"/>
不過多鏈接下,鏈接和請求並非一一對應的,而是一個輪詢的機制。以下圖所示,當配置了N個鏈接時,對於每個 Provider 實例都會維護多個鏈接,在執行請求時會經過輪詢的機制,爲每次請求分配不一樣的鏈接
其實這麼說並不嚴謹,並非 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 其實並不能算一套 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)。
因此若是有文件上傳的場景,儘量的用客戶端直傳的方式吧,友好又節省資源!
原創不易,禁止未受權的轉載。若是個人文章對您有幫助,就請點贊/收藏/關注鼓勵支持一下吧❤❤❤❤❤❤