用 Dubbo 傳輸文件?被老板一頓揍
點擊上方藍色字體,選擇“設為星標”
回復”學習資料“獲取學習寶典
公司之前有一個 Dubbo 服務,其內部封裝了騰訊云的對象存儲服務 SDK,目的是統(tǒng)一管理這種三方服務的SDK,其他系統(tǒng)直接調用這個對象存儲的 Dubbo 服務。這樣可以避免因平臺 SDK 出現不兼容的大版本更新,從而導致公司所有系統(tǒng)修改跟著升級的問題。
想法是好的,不過這種做法并不合適,因為 Dubbo 并不適合傳輸文件。好在這個系統(tǒng)在上線不久就沒人用廢棄了……
雖然系統(tǒng)廢棄了,不過就這個 Dubbo 上傳文件的主題還是可以詳細分析下,聊聊它到底為什么不適合傳文件。
Dubbo 怎么傳文件?
難道這樣直接傳 File 嗎?
void sendPhoto(File photo);
當然不行!Dubbo 只是將對象進行序列化然后傳輸,而 File 對象就算序列化也無法處理文件的數據,所以只能直接發(fā)送文件內容:
void sendPhoto(byte[] photo);
但這樣就會導致 consumer 端需要一次性讀取完整的文件內容至內存中,再大的內存也扛不住這樣玩。而且 provider 端在接受數據解析報文時,也需要一次性將 byte[] 讀取至內存中,也是一樣有內存占用過高問題。
單連接模型問題
除了內存占用問題之外,Dubbo(這里指 Dubbo 協(xié)議)的單連接模型也不適合文件傳輸。
Dubbo 協(xié)議默認是單連接的模型,即一個 provider 的所有請求都是用一個 TCP 連接。默認使用 Netty 來進行傳輸,而 Netty 中為了保證 Channel 線程安全,會將寫入事件進行排隊處理。那么在單連接下,多個請求都會使用同一個連接,也就是同一個 Channel 進行寫入數據;當多個請求同時寫入時,如果某個報文過大,會導致 Channel 一直在發(fā)送這個報文,其他請求的報文寫入事件會進行排隊,遲遲無法發(fā)送,數據都沒有發(fā)送過去,那么其他的 consumer 也自然會處于阻塞等待響應的狀態(tài)中,一直無法返回了。
所以在單連接下,如果報文過大,會導致 Netty 的寫入事件處理阻塞,無法及時的將數據發(fā)送至服務端,從而造成請求白白阻塞的問題。
那既然單連接模型有這么大的缺點,為什么 Dubbo 還要采用單連接呢?
因為省資源啊,TCP 連接這種資源可是很寶貴的,如果單連接可以滿足絕大多數場景,那么完全不需要為每個請求準備一個連接。
Dubbo 文檔中也提到了單連接設計的原因:
因為服務的現狀大都是服務提供者少,通常只有幾臺機器,而服務的消費者多,可能整個網站都在訪問該服務,比如 Morgan 的提供者只有 6 臺提供者,卻有上百臺消費者,每天有 1.5 億次調用,如果采用常規(guī)的 hessian 服務,服務提供者很容易就被壓跨,通過單一連接,保證單一消費者不會壓死提供者,長連接,減少連接握手驗證等,并使用異步 IO,復用線程池,防止 C10K 問題。
雖然 Dubbo 協(xié)議默認單連接模型,但還是可以設置多連接的:
<dubbo:service connections="1"/>
<dubbo:reference connections="1"/>
不過多連接下,連接和請求并不是一一對應的,而是一個輪詢的機制。如下圖所示,當配置了N個連接時,對于每一個 Provider 實例都會維護多個連接,在執(zhí)行請求時會通過輪詢的機制,為每次請求分配不同的連接

為什么 HTTP 協(xié)議“適合”傳文件?
其實這么說并不嚴謹,并不是 HTTP 協(xié)議適合傳文件,Dubbo 還支持 HTTP 協(xié)議呢(雖然是半殘品),一樣不適合傳文件。
Dubbo 這類 RPC 框架為了滿足“調用本地方法像調用遠程一樣”,必須將數據序列化成語言里的對象,但這樣一來就導致無法處理 File 這種形式的對象了。
如果跳出 Dubbo 這種 RPC 框架特性的限制,單獨看 HTTP 協(xié)議的話,是很適合傳輸文件的。因為對于 Client 來說,只需要將報文發(fā)送至 Server,比如要傳輸的文件在本地的話,那我完全可以每次只讀取文件的一個 Buffer 大小,然后將這個 Buffer 的數據使用 Socket 發(fā)送即可;在這種方式下,同時存在于內存中的數據,只會有一個 Buffer 大小,不會有 Dubbo 那樣將全部數據讀取至內存的問題。
如下圖所示,Client 每次只從1GB 文件中讀取 4K 大小的 Buffer 數據,然后用 Socket 發(fā)送,直至將文件完全讀取并發(fā)送成功。那么這種方式下對于單次傳輸來說,內存始終都是只有 4K buffer 大小的占用,并不會像 Dubbo 那樣一次性全部讀取為 byte[] 再發(fā)送。
對于 Server 端也是一樣,Server 端也并不用一次性將所有報文讀取至內存中,在解析 Header 中的 Content-Length 后,直接包裝一個 InputStream,在這個 InputStream 內部進行讀取 Socket Buffer 的數據即可,一樣不會有內存占用問題(更詳細的文件報文處理方式可以參考我的另一篇文章《Tomcat 中是怎么處理文件上傳的?》)。
那既然 HTTP 協(xié)議“適合”傳輸文件,Spring Cloud 的標配 RPC 客戶端 - Feign 在傳輸文件上又會有什么問題呢?
Feign 適合傳輸文件嗎
Feign 其實并不能算一套 RPC 框架,它只是一個 Http Client 而已。在使用 Feign 時,Server 可以是任意的 Http Server,比如實現 Servlet 的 Tomcat/Jetty/Undertow,或者是其他語言的 Apache Server 等等。
而一般用 Feign 時,都是在 Spring Cloud 全家桶環(huán)境下,服務端往往是默認的 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 協(xié)議的文件上傳也提供了一個 feign-form 模塊,該模塊中提供了一些 FormEncoder??蔁o論哪種 FormEncoder 最后都是通過 Feign 封裝的 Output 對象進行輸出,不過這個 Output 對象卻不是那種包裝 Socket InputStream 作為中轉發(fā)送,而是直接作為一個數據的載體,用一個 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 的設計更適合小業(yè)務報文的傳輸(默認報文大小只有8MB)。
所以如果有文件上傳的場景,盡可能的用客戶端直傳的方式吧,友好又節(jié)省資源!
后臺回復 學習資料 領取學習視頻
如有收獲,點個在看,誠摯感謝

