如何編寫一個 SendFile 服務器

如何編寫一個 SendFile 服務器

前言

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

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

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

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

那么,如何使用這兩個 api 實現(xiàn)一個 send file 服務器和客戶端呢?

想象一下,你寫的 send file 服務器利用 send file 技術,利用萬兆網(wǎng)卡,從各個 client 端 copy 海量文件,瞬間打爆你那 1TB 的磁盤和 48核的 CPU。并且,注意:只需很小的 JVM 內存就可以實現(xiàn)這樣一臺強悍的服務器。為什么?如果你知道 send file 的原理,就會知道,使用 send file 技術時, 在用戶態(tài)中,是不需要多少內存的,數(shù)據(jù)都在內核態(tài)。

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

另外,關于 send file,我們都知道,由于是直接從內核緩沖區(qū)進入到網(wǎng)卡驅動,我們幾乎可以稱之為 “零拷貝”,他的性能十分強勁。

但是。

除了這個,還有其他的嗎?答案是有的,send file 利用 DMA 的方式 copy 數(shù)據(jù),而不是利用 CPU。注意,不利用 CPU 意味著什么?意味著數(shù)據(jù)不會進入“緩存行”,進一步,不會進入緩存行,代表著緩存行不會因為這個被污染,再進一步,就是不需要維護緩存一致性。

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

理念

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

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

另外,也支持帶有 ack 的穩(wěn)定發(fā)送,即只有返回 ack 了,才能確認數(shù)據(jù)已經(jīng)寫到目標服務器磁盤了。

server 端支持海量連接,必須得是 reactor 網(wǎng)絡模型,但我們不想在這么小的組件里用 netty,太重了,還容易和使用方有 jar 沖突。所以,我們可以利用 Java 的 selector + nio 自己實現(xiàn) Reactor 模型。

設計

IO 模型設計

設計圖:

image

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

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

accept 處理器將 serverSocketChannel 作為 key 注冊到一個單獨的 selector 上。專門用于監(jiān)聽 accept 事件。類似 netty 的 boss 線程。

當 accept 處理器成功連接了一個 socket 時,會隨機將其交給一個 readProcessor(netty worker 線程?) 處理器,readProcessor 又會將其注冊到 readSelector 上,當發(fā)生 read 事件時,readProcessor 將接受數(shù)據(jù)。

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

readProcessor 在讀到數(shù)據(jù)后,會將其寫入到磁盤中(DMA 的方式,性能炸裂)。

然后,如果 client 在 RPC 協(xié)議中聲明“需要回復(id 不為 -1)” 時,那就將結果發(fā)送到 Reply Queue 中,反之不必。

當結果發(fā)送到 Reply Queue 后,writer 組中的 寫線程,則會從 Queue 中拉取回復包,然后將結果按照 RPC 協(xié)議,寫回到 client socket 中。

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

回過來,當 client socket 得到 server 的數(shù)據(jù)包,會進行解碼反序列化,并喚醒阻塞在客戶端的線程。從而完成一次調用。

線程模型

設計圖:

image-20191029093524267

如上圖所示。

在 client 端:

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

當用戶并發(fā)寫入時,可并發(fā)寫的同時并發(fā)回復,因為寫和回復是異步的(此時可能會出現(xiàn),線程 A 先 send ,線程 B 后 send,但由于網(wǎng)絡延遲,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 協(xié)議

Server RPC 回復包協(xié)議

字段名稱 字段長度(byte) 字段作用
magic_num 4 魔數(shù)校驗,fast fail
version 1 rpc 協(xié)議版本
id 8 Request id, TCP 多路復用 id
length 8 rpc 實際消息內容的長度
Content length rpc 實際消息內容(JSON 序列化協(xié)議)

Client RPC 發(fā)送包協(xié)議

字段名稱 字段長度(byte) 字段作用
magic_num 4 魔數(shù)校驗,fast fail
id 8 Request id, TCP 多路復用 id, 默認 -1,表示不回復
nameContent 2 Request id, TCP 多路復用 id
bodyLength 8 rpc 實際消息內容的長度
nameContent bodyLength 文件名 UTF-8 數(shù)組

為什么 發(fā)送包和返回包協(xié)議不同?為了高效。

總結

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

image-20191029120446781

代碼地址:https://github.com/stateIs0/send_file

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

EOF

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容