如何設計一個牛逼的文件搬運工?

  • 2019 年 11 月 10 日
  • 筆記
  • 前言
  • 理念
  • 設計
  • RPC 協議
  • 總結

本文系「莫那·魯道」投稿 原文地址:thinkinjava.cn/2019/10/29/2019/1029-SF/ 項目倉庫:https://github.com/stateIs0/send_file 歡迎大家 star, pr,issue。我來改進。 ? 歡迎胖友給艿艿,投稿、投稿、投稿。

前言

之前討論零拷貝的時候,我們知道,兩台機器之間傳輸文件,最快的方式就是 send file,眾所周知,在 Java 中,該技術對應的則是 FileChannel 類的 transferTo 和 transferFrom 方法。

在平時使用伺服器的時候,比如 nginx ,tomcat ,都有 send file 的選項,利用此技術,可大大提高文件傳輸效能。

另外,可能也有人談論 send file 的缺點,例如不能利用 gzip 壓縮,不能加密。這裡本文不做探討。

紙上得來終覺淺,絕知此事要躬行。

那麼,如何使用這兩個 api 實現一個 send file 伺服器和客戶端呢?

想像一下,你寫的 send file 伺服器利用 send file 技術,利用萬兆網卡,從各個 client 端 copy 海量文件,瞬間打爆你那 1TB 的磁碟和 48核的 CPU。並且,注意:只需很小的 JVM 記憶體就可以實現這樣一台強悍的伺服器。為什麼?如果你知道 send file 的原理,就會知道,使用 send file 技術時, 在用戶態中,是不需要多少記憶體的,數據都在內核態。

是不是很有成就感?什麼?沒有?那打擾了 ?。

另外,關於 send file,我們都知道,由於是直接從內核緩衝區進入到網卡驅動,我們幾乎可以稱之為 「零拷貝」,他的性能十分強勁。

但是。

除了這個,還有其他的嗎?答案是有的,send file 利用 DMA 的方式 copy 數據,而不是利用 CPU。注意,不利用 CPU 意味著什麼?意味著數據不會進入「快取行」,進一步,不會進入快取行,代表著快取行不會因為這個被污染,再進一步,就是不需要維護快取一致性。

還記得我們因為這個特性搞的那些關於 「偽共享」 的各種黑科技嗎?是不是又學到了一點呢??

理念

作為一個純粹的,高尚的,有趣的 sendFile 伺服器或者客戶端,使用場景是嵌入到某個服務中,或者某個中間件中,不需要搞成誇張的容器。我們可以借鑒一下,客戶端可以做成 Jedis 那樣的,如果你想搞個連接池也不是不可以,但 client 自身實例,還是單連接的。服務端可以做成 sun 的 httpServer 那種輕量的,隨時啟動,隨時關閉。

同時, 支援 oneway 的高性能發送,因為,只要機器不宕機,發送到網卡就意味著發送成功,這樣能大幅提高發送速度,減少客戶端阻塞時間。

另外,也支援帶有 ack 的穩定發送,即只有返回 ack 了,才能確認數據已經寫到目標伺服器磁碟了。

server 端支援海量連接,必須得是 reactor 網路模型,但我們不想在這麼小的組件里用 netty,太重了,還容易和使用方有 jar 衝突。所以,我們可以利用 Java 的 selector + nio 自己實現 Reactor 模型。

設計

IO 模型設計

設計圖:

如上圖,Server 端支援海量客戶端連接。

server 端含有 多個處理器,其中包括 accept 處理器,read 處理器 group, write 處理器 group。

accept 處理器將 serverSocketChannel 作為 key 註冊到一個單獨的 selector 上。專門用於監聽 accept 事件。類似 netty 的 boss 執行緒。

當 accept 處理器成功連接了一個 socket 時,會隨機將其交給一個 readProcessor(netty worker 執行緒?) 處理器,readProcessor 又會將其註冊到 readSelector 上,當發生 read 事件時,readProcessor 將接受數據。

可以看到,readProcessor 可以認為是一個多路復用的執行緒,利用 selector 的能力,他高效的管理著多個 socket。

readProcessor 在讀到數據後,會將其寫入到磁碟中(DMA 的方式,性能炸裂)。

然後,如果 client 在 RPC 協議中聲明「需要回復(id 不為 -1)」 時,那就將結果發送到 Reply Queue 中,反之不必。

當結果發送到 Reply Queue 後,writer 組中的 寫執行緒,則會從 Queue 中拉取回復包,然後將結果按照 RPC 協議,寫回到 client socket 中。

client socket 也會監聽著 read 事件,注意:client 是不需要 select 的,因為沒必要,selector 只是性能優化的一種方式——即一個執行緒管理海量連接,如果沒有 select, 應用層無法用較低的成本處理海量連接,注意,不是不能處理,只是不能高效處理。

回過來,當 client socket 得到 server 的數據包,會進行解碼反序列化,並喚醒阻塞在客戶端的執行緒。從而完成一次調用。

執行緒模型

設計圖:

如上圖所示。

在 client 端:

每個 Client 實例,維護一個 TCP 連接。該 Client 的寫入方法是執行緒安全的。

當用戶並發寫入時,可並發寫的同時並發回復,因為寫和回復是非同步的(此時可能會出現,執行緒 A 先 send ,執行緒 B 後 send,但由於網路延遲,B 先返回)。

在 server 端:

server 端維護著一個 ServerSocketChannel 實例,該實例的作用就是接收 accep 事件,且由一個執行緒維護這個 accept selector 。

當有新的 client 連接事件時,accept selector 就將這個連接「交給「 read 執行緒(默認 server 有 4 個 read 執行緒)。

什麼是「交給」?

注意:每個 read 執行緒都維護著一個單獨的 selector。4 個 read 執行緒,就維護了 4 個 selector。

當 accept 得到新的客戶端連接時,先從 4 個read 執行緒組裡 get 一個執行緒,然後將這個 客戶端連接 作為 key 註冊到這個執行緒所對應的 read selector 上。從而將這個 Socket 「交給」 read 執行緒。

而這個 read 執行緒則使用這個 selector 輪詢事件,如果 socket 可讀,那麼就進行讀,讀完之後,利用 DMA 寫進磁碟。

RPC 協議

Server RPC 回復包協議

欄位名稱

欄位長度(byte)

欄位作用

magic_num

4

魔數校驗,fast fail

version

1

rpc 協議版本

id

8

Request id, TCP 多路復用 id

length

8

rpc 實際消息內容的長度

Content

length

rpc 實際消息內容(JSON 序列化協議)

Client RPC 發送包協議

欄位名稱

欄位長度(byte)

欄位作用

magic_num

4

魔數校驗,fast fail

id

8

Request id, TCP 多路復用 id, 默認 -1,表示不回復

nameContent

2

Request id, TCP 多路復用 id

bodyLength

8

rpc 實際消息內容的長度

nameContent

bodyLength

文件名 UTF-8 數組

為什麼 發送包和返回包協議不同?為了高效。

總結

注意:這是一個能用的,性能不錯的,輕量的 SendFile 伺服器實現,本地測試時, IO寫盤達到 824MB/S,4c 4.2g inter i7 CPU 滿載。

程式碼地址:https://github.com/stateIs0/send_file

同時,歡迎大家 star, pr,issue。我來改進。