天天看點

從Netty到EPollSelectorImpl學習Java NIO

終于可以在寫了幾篇雞湯文後,來篇技術文章了,:),題圖是trustin lee,mina/netty都是他搞的,對java程式員尤其是寫通訊類的都産生了巨大影響,向他緻敬!

帶着問題去看代碼會比較的快和容易,我這次帶着這幾個問題:

1. epollselector裡的fdtokey裡的一大堆資料是怎麼出現的;

2. netty以及java的epoll部分代碼是如何讓n多連接配接的處理做到高效的,當然這主要是因為epoll,不過java部分的相關實作也是重要的。

由于公衆号貼代碼不太友善,在這我就不貼大段的代碼了,隻摘取一些非常關鍵的代),代碼部分我看的是server部分,畢竟server尤其是長連接配接類型的,通常會需要處理大量的連接配接,而且會主要是貼近我所關注的兩個問題的相關代碼。

netty在初始化server過程中主要做的事:

1. 啟動用于處理連接配接事件的線程,線程數預設為1;

2. 建立一個epollselectorimpl對象;

在bind時主要做的事:

1. 開啟端口監聽;

2. 注冊op_accept事件;

處理連接配接的線程通過selector來觸發動作:

<code>int selected = select(selector);</code>

這個會對應到epollselectorimpl.doselect,最關鍵的幾行代碼:

<code>pollwrapper.poll(timeout); int numkeysupdated = updateselectedkeys(); // 更新有事件變化的selectedkeys,selectedkeys是個set結構</code>

當numkeysupdated&gt;0時,就開始處理其中發生了事件的channel,對連接配接事件而言,就是去完成連接配接的建立,連接配接建立的具體動作交給nioworker來實作,每個nioworker在初始化時會建立一個epollselectorimpl執行個體,意味着每個nioworker線程會管理很多的連接配接,當建連完成後,注冊op_read事件,注冊的這個過程會調用到epollselectorimpl的下面的方法:

<code>protected void implregister(selectionkeyimpl ski) { selchimpl ch = ski.channel; fdtokey.put(integer.valueof(ch.getfdval()), ski); pollwrapper.add(ch); keys.add(ski); }</code>

從這段代碼就明白了epollselectorimpl的fdtokey的資料是在連接配接建立後産生的。

那什麼時候會從fdtokey裡删掉資料呢,既然放資料是建連接配接的時候,那猜測删除就是關連接配接的時候,翻看關連接配接的代碼,最終會調到epollselectorimpl的下面的方法:

<code>protected void impldereg(selectionkeyimpl ski) throws ioexception { assert (ski.getindex() &gt;= 0); selchimpl ch = ski.channel; int fd = ch.getfdval(); fdtokey.remove(new integer(fd)); pollwrapper.release(ch); ski.setindex(-1); keys.remove(ski); selectedkeys.remove(ski); deregister((abstractselectionkey)ski); selectablechannel selch = ski.channel(); if (!selch.isopen() &amp;&amp; !selch.isregistered()) ((selchimpl)selch).kill(); }</code>

從上面代碼可以看到,在這個過程中會從fdtokey中删除相應的資料。

翻代碼後,基本明白了fdtokey這個部分,在netty的實作下,預設會建立一個nioserverboss的線程,cpu * 2的nioworker的線程,每個線程都會建立一個epollselectorimpl,例如如果cpu為4核,那麼總共會建立9個epollselectorimpl,每建立一個連接配接,就會在其中一個nioworker的epollselectorimpl的fdtokey中放入selectionkeyimpl,當連接配接斷開時,就會相應的從fdtokey中删除資料,是以對于長連接配接server的場景而言,fdtokey裡有很多的資料是正常的。

——————————-

第一個問題解決後,繼續看第二個問題,怎麼用比較少的資源高效的處理那麼多連接配接的各種事件。

根據上面翻看代碼的記錄,可以看到在netty預設的情況下,采用的是1個線程來處理連接配接事件,cpu * 2個nioworker線程來處理讀寫事件。

連接配接動作因為很輕量,是以1個線程處理通常足夠了,當然,用戶端在設計重連的時候就得有避讓機制,否則所有機器同一時間點重連,那就悲催了。

在分布式應用中,網絡的讀寫會非常頻繁,是以讀寫事件的高效處理就非常重要了,在netty之上怎麼做到高效也很重要,具體可以看看我之前寫的那篇優化的文章,這裡就隻講netty之下的部分了,nioworker線程通過

int selected = select(selector);

來看是否有需要處理的,selected&gt;0就說明有需要處理,epollselectorimpl.doselect中可以看到一堆的連接配接都放在了pollwrapper中,如果有讀寫的事件要處理,這裡會有,這塊的具體實作還得往下繼續翻代碼,這塊沒再繼續翻了,在pollwrapper之後,就會updateselectedkeys();,這裡會把有相應事件發生的selectionkeyimpl放到selectedkeys裡,在netty這層就會拿着這個selectedkeys進行周遊,一個一個處理,這裡用多個線程去處理沒意義的原因是:從網卡上讀寫的動作是串行的,是以再多線程也沒意義。

是以基本可以看到,網絡讀寫的高效主要還是epoll本身做到,因為到了上層其實每次要處理的已經是有相應事件發生的連接配接,netty部分通過較少的幾個線程來有效的處理了讀寫事件,之是以讀寫事件不能像連接配接事件一樣用一個線程去處理,是因為讀的處理過程其實是比較複雜的,從網卡cp出資料後,還得去看資料是否完整(對業務請求而言),把資料封裝扔給業務線程等等,另外也正因為netty是要用nioworker線程處理很多連接配接的事件,是以在高并發場景中保持nioworker整個處理過程的快速,簡單是非常重要的。

——————————

帶着這兩個問題比以前更往下的翻了一些代碼後,确實比以前更了解java nio了,但其實還是說不到深入和精通,因為要更細其實還得往下翻代碼,到os層,是以我一如既往的覺得,其實java程式員要做到精通,是比c程式員難不少的,因為技術棧更長,而如果不是從上往下全部打通技術棧的話,在查問題的時候很容易出現查到某層就卡殼,我就屬于這種,是以我從來不認為自己精通java的某部分。

最近碰到的另外一個問題也是由于自己技術棧不夠完整造成排查進展一直緩慢,這問題現在還沒結果,碰到的現象就是已經觸發了netty避免epoll bug的workaround,日志裡出現:

selector.select() returned prematurely 512 times in a row; rebuilding selector.

這個日志的意思是selector.select裡沒有資料,但被連續喚醒了512次,這樣的情況很容易導緻一個cpu core 100%,netty認為這種情況出現時epoll bug造成的,在這種情況下會采取一個workaround方法,就是rebuilding selector,這個操作會造成連接配接重建,對高并發場景來說,這個會造成逾時等現象,是以影響還挺大的。

由于這個問題已經要查到os層,我完全無能為力,找了公司的一個超級高手幫忙查,目前的進展是看到有一個domain socket被close了,但epoll_wait的時候還是會選出這個fd,但目前還不知道為什麼會出現這現象,是以暫時這問題還是存在着,有同學有想法的也歡迎給些建議。

來自:http://hellojava.info/?p=494

作者:阿裡畢玄