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

          NIO trick and trap NIO的技巧與陷阱

          Posted on 2011-12-20 20:41 cooperzh 閱讀(2400) 評(píng)論(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ū)動(dòng)模型編程困難,而且陷阱重重

          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)計(jì)
          6 高性能

          NIO框架性能的關(guān)鍵因素
          1 數(shù)據(jù)的copy
          2 上下文切換(context switch)
          3 內(nèi)存管理
          4 TCP選項(xiàng),高級(jí)IO函數(shù)
          5 框架設(shè)計(jì)

          減少數(shù)據(jù)copy:
          ByteBuffer的選擇
          View ByteBuffer
          FileChannel.transferTo/transferFrom
          FileChannel.map/MappedByteBuffer

          ByteBuffer的選擇:
          不知道用哪種buffer時(shí),用Non-Direct
          沒有參與IO操作,用Non-Direct
          中小規(guī)模應(yīng)用(<1K并發(fā)連接),用Non-Direct
          長生命周期,較大的緩沖區(qū),用Direct
          測試證明Direct比Non-Direct更快,用Direct
          進(jìn)程間數(shù)據(jù)共享(JNI),用Direct
          一個(gè)Buffer發(fā)給多個(gè)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是獨(dú)立于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);
          性能會(huì)有60%的提升

          FileChannel.map
          將文件映射為內(nèi)存區(qū)域——MappedByteBuffer
          提供快速的文件隨機(jī)讀寫能力
          平臺(tái)相關(guān)
          適合大文件,只讀型操作,如大文件的MD5校驗(yàn)等
          沒有unmap方法,什么時(shí)候被回收取決于GC

          減少上下文切換
          時(shí)間緩存
          Selector.wakeup
          提高IO讀寫效率
          線程模型

          時(shí)間緩存:
          1網(wǎng)絡(luò)服務(wù)器通常需要頻繁獲取系統(tǒng)時(shí)間:定時(shí)器,協(xié)議時(shí)間戳,緩存過期等
          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 獨(dú)立線程定期更新時(shí)間緩存
             b currentTimeMillis直接返回緩存值
             c 精度取決于定期間隔
             d 1000萬次調(diào)用降低到59毫秒


          Selector.wakeup() 主要作用:
          解除阻塞在Selector.select()上的線程,立即返回
          兩次成功的select()之間多次調(diào)用wakeup等價(jià)于一次調(diào)用
          如果當(dāng)前沒有阻塞在select()上,則本次wakeup將作用在下次select()上
          什么時(shí)候wakeup() ?
          注冊了新的Channel或者事件
          Channel關(guān)閉,取消注冊
          優(yōu)先級(jí)更高的事件觸發(fā)(如定時(shí)器事件),希望及時(shí)處理

          wakeup的原理:
          1 linux上利用pipe調(diào)用創(chuàng)建一個(gè)管道
          2 windows上是一個(gè)loopback的tcp連接,因?yàn)閣in32的管道無法加入select的fd set
          3 將管道或者tcp連接加入selected fd set
          4 wakeup向管道或者連接寫入一個(gè)字節(jié)
          5 阻塞的select()因?yàn)橛蠭O時(shí)間就緒,立即返回
          可見wakeup的調(diào)用開銷不可忽視

          減少wakeup調(diào)用:
          1 僅在有需要時(shí)才調(diào)用。如往連接發(fā)送數(shù)據(jù),通常是緩存在一個(gè)消息隊(duì)列,當(dāng)且僅當(dāng)隊(duì)列為空時(shí)注冊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個(gè)字節(jié):
          不代表連接關(guān)閉
          高負(fù)載或者慢速網(wǎng)絡(luò)下很常見的情況
          通常的處理方法是返回并繼續(xù)注冊read/write,等待下次處理,缺點(diǎn)是系統(tǒng)調(diào)用開銷和線程切換開銷
          其他解決辦法:循環(huán)一定次數(shù)寫入(如Mina)或者yield一定次數(shù)
          啟用臨時(shí)選擇器Temporary Selector在當(dāng)前線程注冊并poll,例如Girzzy中

          在當(dāng)前線程寫入:
          當(dāng)發(fā)送緩沖隊(duì)列為空的時(shí)候,可以直接往channel寫數(shù)據(jù),而不是放入緩沖隊(duì)列,interest了write等待IO線程寫入,可以提高發(fā)送效率
          優(yōu)點(diǎn)是可以減少系統(tǒng)調(diào)用和線程切換
          缺點(diǎn)是當(dāng)前線程中斷會(huì)引起channel關(guān)閉

          線程模型
          selector的三個(gè)主要事件:read,write,accept,都可以運(yùn)行在不同的線程上

          通常Reactor實(shí)現(xiàn)為一個(gè)線程,內(nèi)部維護(hù)一個(gè)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)行在單獨(dú)線程
          3 read和accept都運(yùn)行在單獨(dú)線程
          4 read運(yùn)行在reactor線程上,accept運(yùn)行在單獨(dú)線程

          選擇適當(dāng)?shù)木€程模型:
          類echo應(yīng)用,unmashall和業(yè)務(wù)處理的開銷非常低,選擇模型1
          模型2,模型3,模型4的accept處理開銷很低
          最佳選擇:模型2。unmashall一般是cpu-bound,而業(yè)務(wù)邏輯代碼一般比較耗時(shí),不要在reactor線程處理

          內(nèi)存管理
          1 java能做的事情非常有限
          2 緩沖區(qū)的管理
             a 池化。ThreadLocal cache,環(huán)形緩沖區(qū)
             b 擴(kuò)展。putString,getString等高級(jí)API,緩沖區(qū)自動(dòng)擴(kuò)展和伸縮,處理不定長度字節(jié)
             c 字節(jié)順序。跨語言通訊需要注意,默認(rèn)字節(jié)順序Big-Endian,java的IO庫和class文件

          數(shù)據(jù)結(jié)構(gòu)的選擇
          1 使用簡單的數(shù)據(jù)結(jié)構(gòu):鏈表,隊(duì)列,數(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帶來的影響

          定時(shí)器的實(shí)現(xiàn)
          1 定時(shí)器在網(wǎng)絡(luò)程序中頻繁使用
              a 周期事件的觸發(fā)
              b 異步超時(shí)的通知和移除
              c 延遲事件的觸發(fā)
          2 三個(gè)時(shí)間復(fù)雜度
              a 插入定時(shí)器
              b 刪除定時(shí)器
              c PerTickBookkeeping,一次tick內(nèi)系統(tǒng)需要執(zhí)行的操作
          3 Tick的方式
              Selector.select(timeout)
              Thread.sleep(timeout)

          定時(shí)器的實(shí)現(xiàn):鏈表
          將定時(shí)器組織成鏈表結(jié)構(gòu)
          插入定時(shí)器,加入鏈表尾部
          刪除定時(shí)器
          PerTickBookkeeping,遍歷鏈表查找expire事件

          定時(shí)器的實(shí)現(xiàn):排序鏈表
          將定時(shí)器組織成有序鏈表結(jié)構(gòu),按照expire截止時(shí)間升序排序
          插入定時(shí)器,找到合適的位置插入
          刪除定時(shí)器
          PerTickBookkeeping,直接從表頭找起

          定時(shí)器的實(shí)現(xiàn):優(yōu)先隊(duì)列
          將定時(shí)器組織成優(yōu)先隊(duì)列,按照expire截止時(shí)間作為優(yōu)先級(jí),優(yōu)先隊(duì)列一般采用最小堆實(shí)現(xiàn)
          插入定時(shí)器
          刪除定時(shí)器
          PerTickBookkeeping,直接取root判斷

          定時(shí)器的實(shí)現(xiàn):Hash wheel timer
          將定時(shí)器組織成時(shí)間輪
          指針按照一定周期旋轉(zhuǎn),一個(gè)tick跳動(dòng)一個(gè)槽位
          定時(shí)器根據(jù)延時(shí)時(shí)間和當(dāng)前指針位置插入到特定槽位
          插入定時(shí)器
          刪除定時(shí)器
          PerTickBookkeeping
          槽位和tick決定了精度和延時(shí)

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

          連接IDLE的判斷
          1 連接處于IDLE狀態(tài):一段時(shí)間沒有IO讀寫事件發(fā)生
          2 實(shí)現(xiàn)方式:
              a 每次IO讀寫都記錄IO讀和寫的時(shí)間戳
              b 定時(shí)掃描所有連接,判斷當(dāng)前時(shí)間和上一次讀或?qū)懙臅r(shí)間差是否超過設(shè)定閥值,超過即認(rèn)為連接處于IDLE狀態(tài),通知業(yè)務(wù)處理器
             c 定時(shí)的方式:基于select(timeout)或者定時(shí)器。Mina:select(timeout);Netty:HashWheelTimer

          合理設(shè)置TCP/IP選項(xiàng),有時(shí)會(huì)起到顯著效果,需要根據(jù)應(yīng)用類型、協(xié)議設(shè)計(jì)、網(wǎng)絡(luò)環(huán)境、OS平臺(tái)等因素做考量,以測試結(jié)果為準(zhǔn)

          Socket緩沖區(qū)設(shè)置選項(xiàng):SO_RCVBUF 和 SO_SNDBUF
          Socket.setReceiveBufferSize/setSendBufferSize 僅僅是對底層平臺(tái)的提示,是否有效取決于底層平臺(tái)。因此get返回的不是真實(shí)的結(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)該同時(shí)增加
          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)的小包自動(dòng)相連組成大包,阻止發(fā)送大量小包阻塞網(wǎng)絡(luò),提高網(wǎng)絡(luò)應(yīng)用效率對于實(shí)時(shí)性要求較高的應(yīng)用(telnet、網(wǎng)游),需要關(guān)閉此算法
          Socket.setTcpNoDelay(true) 關(guān)閉算法
          Socket.setTcpNoDelay(false) 
          打開算法,默認(rèn)

          SO_LINGER選項(xiàng),控制socket關(guān)閉后的行為
          Socket.setSoLinger(boolean linger,int timeout)
          linger=false,timeout=-1
          當(dāng)socket主動(dòng)close,調(diào)用的線程會(huì)馬上返回,不會(huì)阻塞,然后進(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í)過期,剩余數(shù)據(jù)將被刪除,進(jìn)行FIN-ACK協(xié)議交換
          linger=true,timeout=0
          進(jìn)行所謂“hard-close”,任何剩余的數(shù)據(jù)將被丟棄,并且FIN-ACK交換也不會(huì)發(fā)生,替代產(chǎn)生RST,讓對端拋出“connection reset”的SocketException
          4 慎重使用此選項(xiàng),TIME_WAIT狀態(tài)的價(jià)值:
              可靠實(shí)現(xiàn)TCP連接終止
              允許
          老的分節(jié)在網(wǎng)絡(luò)中流失,防止發(fā)給新的連接
             持續(xù)時(shí)間=2*MSL(MSL為最大分節(jié)生命周期,一般為30秒到2分鐘)

          SO_REUSEADDR:重用端口
          Socket.setReuseAddress(boolean) 默認(rèn)false
          適用場景:
          1 當(dāng)一個(gè)使用本地地址和端口的socket1處于TIME_WAIT狀態(tài)時(shí),你啟動(dòng)的socket2要占用該地址和端口,就要用到此選項(xiàng)
          SO_REUSEADDR允許同一端口上啟動(dòng)一個(gè)服務(wù)的多個(gè)實(shí)例(多個(gè)進(jìn)程),但每個(gè)實(shí)例綁定的地址是不能相同的
          3 SO_REUSEADDR允許完全相同的地址和端口的重復(fù)綁定。但這只用于UDP的多播,不適用TCP

          SO_REUSEPORT
          listen做四元組,多進(jìn)程同一地址同一端口做accept,適合大量短連接的web server
          Freebsd獨(dú)有

          其他選項(xiàng):
          Socket.setPerformancePreferences(connectionTime, latency, bandwidth) 設(shè)置連接時(shí)間、延遲、帶寬的相對重要性
          Socket.setKeepAlive(boolean) 這是TCP層的keep-alive概念,非HTTP協(xié)議的。用于TCP連接保活,默認(rèn)間隔2小時(shí),建議在應(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ā)送消息,通常是放入一個(gè)緩沖區(qū)隊(duì)列注冊write,等待IO線程去寫
              線程切換,系統(tǒng)調(diào)用
              如果隊(duì)列為空,直接在當(dāng)前線程channel.write,隱患是當(dāng)前線程的中斷會(huì)引起連接關(guān)閉
          4 事件處理優(yōu)先級(jí)
              ACE框架推薦:accept > write > read (推薦)
               Mina 和 Netty:read > write
          5 處理事件注冊的順序
              在select()之前
              在select()之后,處理wakeup競爭條件

          Java Socket實(shí)現(xiàn)在不同平臺(tái)上的差異
          由于各種OS平臺(tái)的socket實(shí)現(xiàn)不盡相同,都會(huì)影響到socket的實(shí)現(xiàn)
          需要考慮性能和健壯性
              
           












          主站蜘蛛池模板: 富蕴县| 五指山市| 太和县| 惠安县| 昌图县| 平安县| 濮阳县| 双桥区| 思茅市| 盐池县| 富蕴县| 榕江县| 聂拉木县| 游戏| 大同县| 苏尼特左旗| 巫山县| 永福县| 来安县| 迁西县| 防城港市| 平顶山市| 略阳县| 福州市| 积石山| 民县| 和政县| 阜南县| 民勤县| 双牌县| 财经| 醴陵市| 曲水县| 平乡县| 钟祥市| 富顺县| 泾阳县| 达尔| 游戏| 井陉县| 台北县|