如何編寫一個 SendFile 伺服器
如何編寫一個 SendFile 伺服器
前言
之前討論零拷貝的時候,我們知道,兩臺機器之間傳輸檔案,最快的方式就是 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 滿載。
程式碼地址:github.com/stateIs0/se…
同時,歡迎大家 star, pr,issue。我來改進。