面試官:為什么 Dubbo 不適合文件傳輸?
作者:空無
來源:SegmentFault 思否社區(qū)
背景
公司之前有一個 Dubbo 服務,其內(nèi)部封裝了騰訊云的對象存儲服務 SDK,目的是統(tǒng)一管理這種三方服務的SDK,其他系統(tǒng)直接調(diào)用這個對象存儲的 Dubbo 服務。這樣可以避免因平臺 SDK 出現(xiàn)不兼容的大版本更新,從而導致公司所有系統(tǒng)修改跟著升級的問題。
想法是好的,不過這種做法并不合適,因為 Dubbo 并不適合傳輸文件。好在這個系統(tǒng)在上線不久就沒人用廢棄了……
雖然系統(tǒng)廢棄了,不過就這個 Dubbo 上傳文件的主題還是可以詳細分析下,聊聊它到底為什么不適合傳文件。
Dubbo 怎么傳文件?
難道這樣直接傳 File 嗎?
void sendPhoto(File photo);
當然不行!Dubbo 只是將對象進行序列化然后傳輸,而 File 對象就算序列化也無法處理文件的數(shù)據(jù),所以只能直接發(fā)送文件內(nèi)容:
void sendPhoto(byte[] photo);
但這樣就會導致 consumer 端需要一次性讀取完整的文件內(nèi)容至內(nèi)存中,再大的內(nèi)存也扛不住這樣玩。而且 provider 端在接受數(shù)據(jù)解析報文時,也需要一次性將 byte[] 讀取至內(nèi)存中,也是一樣有內(nèi)存占用過高問題。
單連接模型問題
除了內(nèi)存占用問題之外,Dubbo(這里指 Dubbo 協(xié)議)的單連接模型也不適合文件傳輸。
Dubbo 協(xié)議默認是單連接的模型,即一個 provider 的所有請求都是用一個 TCP 連接。默認使用 Netty 來進行傳輸,而 Netty 中為了保證 Channel 線程安全,會將寫入事件進行排隊處理。那么在單連接下,多個請求都會使用同一個連接,也就是同一個 Channel 進行寫入數(shù)據(jù);當多個請求同時寫入時,如果某個報文過大,會導致 Channel 一直在發(fā)送這個報文,其他請求的報文寫入事件會進行排隊,遲遲無法發(fā)送,數(shù)據(jù)都沒有發(fā)送過去,那么其他的 consumer 也自然會處于阻塞等待響應的狀態(tài)中,一直無法返回了。
所以在單連接下,如果報文過大,會導致 Netty 的寫入事件處理阻塞,無法及時的將數(shù)據(jù)發(fā)送至服務端,從而造成請求白白阻塞的問題。
那既然單連接模型有這么大的缺點,為什么 Dubbo 還要采用單連接呢?
因為省資源啊,TCP 連接這種資源可是很寶貴的,如果單連接可以滿足絕大多數(shù)場景,那么完全不需要為每個請求準備一個連接。
Dubbo 文檔中也提到了單連接設計的原因:
因為服務的現(xiàn)狀大都是服務提供者少,通常只有幾臺機器,而服務的消費者多,可能整個網(wǎng)站都在訪問該服務,比如 Morgan 的提供者只有 6 臺提供者,卻有上百臺消費者,每天有 1.5 億次調(diào)用,如果采用常規(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 框架為了滿足“調(diào)用本地方法像調(diào)用遠程一樣”,必須將數(shù)據(jù)序列化成語言里的對象,但這樣一來就導致無法處理 File 這種形式的對象了。
如果跳出 Dubbo 這種 RPC 框架特性的限制,單獨看 HTTP 協(xié)議的話,是很適合傳輸文件的。因為對于 Client 來說,只需要將報文發(fā)送至 Server,比如要傳輸?shù)奈募诒镜氐脑挘俏彝耆梢悦看沃蛔x取文件的一個 Buffer 大小,然后將這個 Buffer 的數(shù)據(jù)使用 Socket 發(fā)送即可;在這種方式下,同時存在于內(nèi)存中的數(shù)據(jù),只會有一個 Buffer 大小,不會有 Dubbo 那樣將全部數(shù)據(jù)讀取至內(nèi)存的問題。
如下圖所示,Client 每次只從1GB 文件中讀取 4K 大小的 Buffer 數(shù)據(jù),然后用 Socket 發(fā)送,直至將文件完全讀取并發(fā)送成功。那么這種方式下對于單次傳輸來說,內(nèi)存始終都是只有 4K buffer 大小的占用,并不會像 Dubbo 那樣一次性全部讀取為 byte[] 再發(fā)送。

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-form 模塊,該模塊中提供了一些 FormEncoder。可無論哪種 FormEncoder 最后都是通過 Feign 封裝的 Output 對象進行輸出,不過這個 Output 對象卻不是那種包裝 Socket InputStream 作為中轉發(fā)送,而是直接作為一個數(shù)據(jù)的載體,用一個 ByteArrayOutputStream 來存儲編碼完成的數(shù)據(jù)。@RequiredArgsConstructor
@FieldDefaults(level = PRIVATE, makeFinal = true)
public class Output implements Closeable {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
//所有的數(shù)據(jù)在“編碼”之后,仍然會寫入到 ByteArrayOutputStream 這個內(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();
}
}
總結

