<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          用 Dubbo 傳輸文件?被老板一頓揍!

          共 4894字,需瀏覽 10分鐘

           ·

          2022-04-14 11:12

          往期熱門(mén)文章:

          1、45 個(gè) Git 經(jīng)典操作場(chǎng)景,專治不會(huì)合代碼!

          2、當(dāng) Transactional 碰到鎖,有個(gè)大坑!

          3、為了甩鍋,我寫(xiě)了個(gè)牛逼的日志切面!

          4、擼了一個(gè)高仿 B站!

          5、Spring Boot 生產(chǎn)中 16 條最佳實(shí)踐

          來(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 怎么傳文件?

          難道這樣直接傳 File 嗎?
          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)題。
          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 中將參數(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é)省資源!
          往期熱門(mén)文章:

          1、我滴個(gè)乖乖,我復(fù)現(xiàn)了Spring的漏洞,害怕!
          2、分布式鎖用 Redis 還是 Zookeeper?
          3、最適合晚上睡不著看的 8 個(gè)網(wǎng)站,建議收藏哦
          4、String長(zhǎng)度有限制嗎?
          5、14家互聯(lián)網(wǎng)公司裁員(1-2月裁員清單)
          6、Redis實(shí)現(xiàn)分布式鎖的8大坑!切記!
          7、請(qǐng)立即卸載這款 IDEA 插件!
          8、Thread.sleep(0) 到底有什么用?
          9、為什么不建議用try catch處理異常?
          10、MySQL 為啥不能用 UUID 做主鍵?

          瀏覽 22
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产亚洲精品久久久久久青梅 | 夜夜嗨视频 | 91成人视频 | 嫩草黄片 | 亚洲人成电影网 |