posts - 12, comments - 0, trackbacks - 0, articles - 7
            BlogJava :: 首頁 :: 新隨筆 :: 聯(lián)系 :: 聚合  :: 管理

          NIO trick and trap NIO的技巧與陷阱

          Posted on 2011-12-20 20:41 cooperzh 閱讀(2396) 評論(0)  編輯  收藏 所屬分類: NIO
          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)
          插入定時器,加入鏈表尾部
          刪除定時器
          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決定了精度和延時

          定時器的實現(xiàn):Hierarchical Timing
          Hours Wheel,Minutes Wheel,Seconds Wheel

          連接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) 
          打開算法,默認(rèn)

          SO_LINGER選項,控制socket關(guān)閉后的行為
          Socket.setSoLinger(boolean linger,int timeout)
          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)
          linger=true,timeout>0
          調(diào)用close的線程將阻塞,發(fā)生兩種可能的情況:一是剩余的數(shù)據(jù)繼續(xù)發(fā)送,進(jìn)行關(guān)閉協(xié)議交換,二是超時過期,剩余數(shù)據(jù)將被刪除,進(jìn)行FIN-ACK協(xié)議交換
          linger=true,timeout=0
          進(jìn)行所謂“hard-close”,任何剩余的數(shù)據(jù)將被丟棄,并且FIN-ACK交換也不會發(fā)生,替代產(chǎn)生RST,讓對端拋出“connection reset”的SocketException
          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要占用該地址和端口,就要用到此選項
          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)
          需要考慮性能和健壯性
              
           












          主站蜘蛛池模板: 岑溪市| 牡丹江市| 蓝山县| 高州市| 乃东县| 吕梁市| 平果县| 保山市| 利津县| 邻水| 长武县| 台南县| 怀仁县| 西城区| 天水市| 阿合奇县| 榆林市| 武威市| 绿春县| 汉寿县| 盘山县| 顺义区| 阜平县| 九寨沟县| 涟水县| 积石山| 贺州市| 苍南县| 晋宁县| 长乐市| 安龙县| 忻州市| 濮阳县| 黄大仙区| 北票市| 惠安县| 扶沟县| 贵南县| 五峰| 澳门| 安阳县|