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