1 等待數據就緒
2 從內核緩沖區copy到進程緩沖區(從socket通過socketChannel復制到ByteBuffer)
non-direct ByteBuffer: HeapByteBuffer,創建開銷小
direct ByteBuffer:通過操作系統native代碼,創建開銷大
基于block的傳輸通常比基于流的傳輸更高效
使用NIO做網絡編程容易,但離散的事件驅動模型編程困難,而且陷阱重重
Reactor模式:經典的NIO網絡框架
核心組件:
1 Synchronous Event Demultiplexer : Event loop + 事件分離
2 Dispatcher:事件派發,可以多線程
3 Request Handler:事件處理,業務代碼
理想的NIO框架:
1 優雅地隔離IO代碼和業務代碼
2 易于擴展
3 易于配置,包括框架自身參數和協議參數
4 提供良好的codec框架,方便marshall/unmarshall
5 透明性,內置良好的日志記錄和數據統計
6 高性能
NIO框架性能的關鍵因素
1 數據的copy
2 上下文切換(context switch)
3 內存管理
4 TCP選項,高級IO函數
5 框架設計
減少數據copy:
ByteBuffer的選擇
View ByteBuffer
FileChannel.transferTo/transferFrom
FileChannel.map/MappedByteBuffer
ByteBuffer的選擇:
不知道用哪種buffer時,用Non-Direct
沒有參與IO操作,用Non-Direct
中小規模應用(<1K并發連接),用Non-Direct
長生命周期,較大的緩沖區,用Direct
測試證明Direct比Non-Direct更快,用Direct
進程間數據共享(JNI),用Direct
一個Buffer發給多個Client,考慮使用view ByteBuffer共享數據,buffer.slice()
HeapByteBuffer緩存
使用ByteBuffer.slice()創建view ByteBuffer:
ByteBuffer buffer2 = buffer1.slice();
則buffer2的內容和buffer1的從position到limit的數據內容完全共享
但是buffer2的position,limit是獨立于buffer1的
傳輸文件的傳統方式:
byte[] buf = new byte[8192];
while(in.read(buf)>0){
out.write(buf);
}
使用NIO后:
FileChannel in = ...
WriteableByteChannel out = ...
in.transferTo(0,fsize,out);
性能會有60%的提升
FileChannel.map
將文件映射為內存區域——MappedByteBuffer
提供快速的文件隨機讀寫能力
平臺相關
適合大文件,只讀型操作,如大文件的MD5校驗等
沒有unmap方法,什么時候被回收取決于GC
減少上下文切換
時間緩存
Selector.wakeup
提高IO讀寫效率
線程模型
時間緩存:
1網絡服務器通常需要頻繁獲取系統時間:定時器,協議時間戳,緩存過期等
2 System.currentTimeMillis
a linux調用gettimeofday需要切換到內核態
b 普通機器上,1000萬次調用需要12秒,平均一次1.3毫秒
c 大部分應用不需要特別高的精度
3 SystemTimer.currentTimeMillis(自己創建)
a 獨立線程定期更新時間緩存
b currentTimeMillis直接返回緩存值
c 精度取決于定期間隔
d 1000萬次調用降低到59毫秒
Selector.wakeup() 主要作用:
解除阻塞在Selector.select()上的線程,立即返回
兩次成功的select()之間多次調用wakeup等價于一次調用
如果當前沒有阻塞在select()上,則本次wakeup將作用在下次select()上
什么時候wakeup() ?
注冊了新的Channel或者事件
Channel關閉,取消注冊
優先級更高的事件觸發(如定時器事件),希望及時處理
wakeup的原理:
1 linux上利用pipe調用創建一個管道
2 windows上是一個loopback的tcp連接,因為win32的管道無法加入select的fd set
3 將管道或者tcp連接加入selected fd set
4 wakeup向管道或者連接寫入一個字節
5 阻塞的select()因為有IO時間就緒,立即返回
可見wakeup的調用開銷不可忽視
減少wakeup調用:
1 僅在有需要時才調用。如往連接發送數據,通常是緩存在一個消息隊列,當且僅當隊列為空時注冊write并wakeup
booleanneedsWakeup=false;
synchronized(queue){
if(queue.isEmpty()) needsWakeup=true;
queue.add(session);
}
if(needsWakeup){
registerOPWrite();
selector.wakeup();
}
2 記錄調用狀態,避免重復調用,例如Netty的優化
讀到或者寫入0個字節:
不代表連接關閉
高負載或者慢速網絡下很常見的情況
通常的處理方法是返回并繼續注冊read/write,等待下次處理,缺點是系統調用開銷和線程切換開銷
其他解決辦法:循環一定次數寫入(如Mina)或者yield一定次數
啟用臨時選擇器Temporary Selector在當前線程注冊并poll,例如Girzzy中
在當前線程寫入:
當發送緩沖隊列為空的時候,可以直接往channel寫數據,而不是放入緩沖隊列,interest了write等待IO線程寫入,可以提高發送效率
優點是可以減少系統調用和線程切換
缺點是當前線程中斷會引起channel關閉
線程模型
selector的三個主要事件:read,write,accept,都可以運行在不同的線程上
通常Reactor實現為一個線程,內部維護一個selector
1 Boss Thread + worker Thread
boss thread處理accept,connect
worker thread處理read,write
Reactor線程數目:
1 Netty 1 + 2 * cpu
2 Mina 1 + cpu + 1
3 Grizzly 1 + 1
常見線程模型:
1 read和accept都運行在reactor線程上
2 accept運行在reactor線程上,read運行在單獨線程
3 read和accept都運行在單獨線程
4 read運行在reactor線程上,accept運行在單獨線程
選擇適當的線程模型:
類echo應用,unmashall和業務處理的開銷非常低,選擇模型1
模型2,模型3,模型4的accept處理開銷很低
最佳選擇:模型2。unmashall一般是cpu-bound,而業務邏輯代碼一般比較耗時,不要在reactor線程處理
內存管理
1 java能做的事情非常有限
2 緩沖區的管理
a 池化。ThreadLocal cache,環形緩沖區
b 擴展。putString,getString等高級API,緩沖區自動擴展和伸縮,處理不定長度字節
c 字節順序。跨語言通訊需要注意,默認字節順序Big-Endian,java的IO庫和class文件
數據結構的選擇
1 使用簡單的數據結構:鏈表,隊列,數組,散列表
2 使用j.u.c框架引入的并發集合類,lock-free,spin lock
3 任何數據結構都要注意容量限制,OutOfMemoryError
4 適當選擇數據結構的初始容量,降低GC帶來的影響
定時器的實現
1 定時器在網絡程序中頻繁使用
a 周期事件的觸發
b 異步超時的通知和移除
c 延遲事件的觸發
2 三個時間復雜度
a 插入定時器
b 刪除定時器
c PerTickBookkeeping,一次tick內系統需要執行的操作
3 Tick的方式
Selector.select(timeout)
Thread.sleep(timeout)
定時器的實現:鏈表
將定時器組織成鏈表結構
插入定時器,加入鏈表尾部
刪除定時器
PerTickBookkeeping,遍歷鏈表查找expire事件
定時器的實現:排序鏈表
將定時器組織成有序鏈表結構,按照expire截止時間升序排序
插入定時器,找到合適的位置插入
刪除定時器
PerTickBookkeeping,直接從表頭找起
定時器的實現:優先隊列
將定時器組織成優先隊列,按照expire截止時間作為優先級,優先隊列一般采用最小堆實現
插入定時器
刪除定時器
PerTickBookkeeping,直接取root判斷
定時器的實現:Hash wheel timer
將定時器組織成時間輪
指針按照一定周期旋轉,一個tick跳動一個槽位
定時器根據延時時間和當前指針位置插入到特定槽位
插入定時器
刪除定時器
PerTickBookkeeping
槽位和tick決定了精度和延時
定時器的實現:Hierarchical Timing
Hours Wheel,Minutes Wheel,Seconds Wheel
連接IDLE的判斷
1 連接處于IDLE狀態:一段時間沒有IO讀寫事件發生
2 實現方式:
a 每次IO讀寫都記錄IO讀和寫的時間戳
b 定時掃描所有連接,判斷當前時間和上一次讀或寫的時間差是否超過設定閥值,超過即認為連接處于IDLE狀態,通知業務處理器
c 定時的方式:基于select(timeout)或者定時器。Mina:select(timeout);Netty:HashWheelTimer
合理設置TCP/IP選項,有時會起到顯著效果,需要根據應用類型、協議設計、網絡環境、OS平臺等因素做考量,以測試結果為準
Socket緩沖區設置選項:SO_RCVBUF 和 SO_SNDBUF
Socket.setReceiveBufferSize/setSendBufferSize 僅僅是對底層平臺的提示,是否有效取決于底層平臺。因此get返回的不是真實的結果。
設置原則:
1 以太網上,4k通常是不夠的,增加到16k,吞吐量增加了40%
2 Socket緩沖區大小至少應該是連接的MSS的三倍,MSS=MTU+40,一般以太網卡的MTU=1500字節。
MSS:最大分段大小
MTU:最大傳輸單元
3 send buffer最好與對端的receive buffer尺寸一致
4 對于一次性發送大量數據的應用,增加緩沖區到48k、64k可能是唯一最有效的提高性能的方式。
為了最大化性能,send buffer至少要跟BDP(帶寬延遲乘積)一樣大。
5 同樣,對于大量接收數據的應用,提高接收緩沖區,能減少發送端的阻塞
6 如果應用既發送大量數據,又接收大量數據,則send buffer和
receive buffer應該同時增加
7 如果設置的ServerSocket的
receive buffer超過RFC1323定義的64k,那么必須在綁定端口前設置,以后accept產生的socket將繼承這一設置
8 無論緩沖區大小多少,你都應該盡可能地幫助TCP至少以那樣大小的塊寫入
BDP(帶寬延遲乘積)
為了優化TCP吞吐量,發送端應該發送足夠的數據包以填滿發送端和接收端之間的邏輯通道
BDP = 帶寬 * RTT
Nagle算法:SO_TCPNODELAY
通過將緩沖區內的小包自動相連組成大包,阻止發送大量小包阻塞網絡,提高網絡應用效率對于實時性要求較高的應用(telnet、網游),需要關閉此算法
Socket.setTcpNoDelay(true) 關閉算法
Socket.setTcpNoDelay(false)
打開算法,默認
SO_LINGER選項,控制socket關閉后的行為
Socket.setSoLinger(boolean linger,int timeout)
1 linger=false,timeout=-1
當socket主動close,調用的線程會馬上返回,不會阻塞,然后進入CLOSING狀態,殘留在緩沖區中的數據將繼續發送給對端,并且與對端進行FIN-ACK協議交換,最后進入TIME_WAIT狀態
2 linger=true,timeout>0
調用close的線程將阻塞,發生兩種可能的情況:一是剩余的數據繼續發送,進行關閉協議交換,二是超時過期,剩余數據將被刪除,進行FIN-ACK協議交換
3 linger=true,timeout=0
進行所謂“hard-close”,任何剩余的數據將被丟棄,并且FIN-ACK交換也不會發生,替代產生RST,讓對端拋出“connection reset”的SocketException
4 慎重使用此選項,TIME_WAIT狀態的價值:
可靠實現TCP連接終止
允許老的分節在網絡中流失,防止發給新的連接
持續時間=2*MSL(MSL為最大分節生命周期,一般為30秒到2分鐘)
SO_REUSEADDR:重用端口
Socket.setReuseAddress(boolean) 默認false
適用場景:
1 當一個使用本地地址和端口的socket1處于TIME_WAIT狀態時,你啟動的socket2要占用該地址和端口,就要用到此選項
2 SO_REUSEADDR允許同一端口上啟動一個服務的多個實例(多個進程),但每個實例綁定的地址是不能相同的
3 SO_REUSEADDR允許完全相同的地址和端口的重復綁定。但這只用于UDP的多播,不適用TCP
SO_REUSEPORT
listen做四元組,多進程同一地址同一端口做accept,適合大量短連接的web server
Freebsd獨有
其他選項:
Socket.setPerformancePreferences(connectionTime, latency, bandwidth) 設置連接時間、延遲、帶寬的相對重要性
Socket.setKeepAlive(boolean) 這是TCP層的keep-alive概念,非HTTP協議的。用于TCP連接保活,默認間隔2小時,建議在應用層做心跳
Socket.sendUrgentData(data) 帶外數據
技巧:
1 讀寫公平
Mina限制一次寫入的字節數不超過最大的讀緩沖區的1.5倍
2 針對FileChannel.transferTo的bug
Mina判斷異常,如果是temporarily unavailable的IOException,則認為傳輸字節數為0
3 發送消息,通常是放入一個緩沖區隊列注冊write,等待IO線程去寫
線程切換,系統調用
如果隊列為空,直接在當前線程channel.write,隱患是當前線程的中斷會引起連接關閉
4 事件處理優先級
ACE框架推薦:accept > write > read (推薦)
Mina 和 Netty:read > write
5 處理事件注冊的順序
在select()之前
在select()之后,處理wakeup競爭條件
Java Socket實現在不同平臺上的差異
由于各種OS平臺的socket實現不盡相同,都會影響到socket的實現
需要考慮性能和健壯性