1. 程式人生 > 程式設計 >如何編寫一個 SendFile 伺服器

如何編寫一個 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 的資料包,會進行解碼反序列化,並喚醒阻塞在客戶端的執行緒。從而完成一次呼叫。

執行緒模型

設計圖:

image-20191029093524267

如上圖所示。

在 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 滿載。

image-20191029120446781

程式碼地址:github.com/stateIs0/se…

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