netty原始碼分析之揭開reactor執行緒的面紗(二)

閃電俠發表於2018-10-23

如果你對netty的reactor執行緒不瞭解,建議先看下上一篇文章netty原始碼分析之揭開reactor執行緒的面紗(一),這裡再把reactor中的三個步驟的圖貼一下

reactor執行緒

我們已經瞭解到netty reactor執行緒的第一步是輪詢出注冊在selector上面的IO事件(select),那麼接下來就要處理這些IO事件(process selected keys),本篇文章我們將一起來探討netty處理IO事件的細節

我們進入到reactor執行緒的 run 方法,找到處理IO事件的程式碼,如下

processSelectedKeys();
複製程式碼

跟進去

private void processSelectedKeys() {
    if (selectedKeys != null) {
        processSelectedKeysOptimized(selectedKeys.flip());
    } else {
        processSelectedKeysPlain(selector.selectedKeys());
    }
}
複製程式碼

我們發現處理IO事件,netty有兩種選擇,從名字上看,一種是處理優化過的selectedKeys,一種是正常的處理

我們對優化過的selectedKeys的處理稍微展開一下,看看netty是如何優化的,我們檢視 selectedKeys 被引用過的地方,有如下程式碼

private SelectedSelectionKeySet selectedKeys;

private Selector NioEventLoop.openSelector() {
    //...
    final SelectedSelectionKeySet selectedKeySet = new SelectedSelectionKeySet();
    // selectorImplClass -> sun.nio.ch.SelectorImpl
    Field selectedKeysField = selectorImplClass.getDeclaredField("selectedKeys");
    Field publicSelectedKeysField = selectorImplClass.getDeclaredField("publicSelectedKeys");
    selectedKeysField.setAccessible(true);
    publicSelectedKeysField.setAccessible(true);
    selectedKeysField.set(selector, selectedKeySet);
    publicSelectedKeysField.set(selector, selectedKeySet);
    //...
    selectedKeys = selectedKeySet;
}
複製程式碼

首先,selectedKeys是一個 SelectedSelectionKeySet 類物件,在NioEventLoopopenSelector 方法中建立,之後就通過反射將selectedKeys與 sun.nio.ch.SelectorImpl 中的兩個field繫結

sun.nio.ch.SelectorImpl 中我們可以看到,這兩個field其實是兩個HashSet

// Public views of the key sets
private Set<SelectionKey> publicKeys;             // Immutable
private Set<SelectionKey> publicSelectedKeys;     // Removal allowed, but not addition
protected SelectorImpl(SelectorProvider sp) {
    super(sp);
    keys = new HashSet<SelectionKey>();
    selectedKeys = new HashSet<SelectionKey>();
    if (Util.atBugLevel("1.4")) {
        publicKeys = keys;
        publicSelectedKeys = selectedKeys;
    } else {
        publicKeys = Collections.unmodifiableSet(keys);
        publicSelectedKeys = Util.ungrowableSet(selectedKeys);
    }
}
複製程式碼

selector在呼叫select()族方法的時候,如果有IO事件發生,就會往裡面的兩個field中塞相應的selectionKey(具體怎麼塞有待研究),即相當於往一個hashSet中add元素,既然netty通過反射將jdk中的兩個field替換掉,那我們就應該意識到是不是netty自定義的SelectedSelectionKeySetadd方法做了某些優化呢?

帶著這個疑問,我們進入到 SelectedSelectionKeySet 類中探個究竟

final class SelectedSelectionKeySet extends AbstractSet<SelectionKey> {

    private SelectionKey[] keysA;
    private int keysASize;
    private SelectionKey[] keysB;
    private int keysBSize;
    private boolean isA = true;

    SelectedSelectionKeySet() {
        keysA = new SelectionKey[1024];
        keysB = keysA.clone();
    }

    @Override
    public boolean add(SelectionKey o) {
        if (o == null) {
            return false;
        }

        if (isA) {
            int size = keysASize;
            keysA[size ++] = o;
            keysASize = size;
            if (size == keysA.length) {
                doubleCapacityA();
            }
        } else {
            int size = keysBSize;
            keysB[size ++] = o;
            keysBSize = size;
            if (size == keysB.length) {
                doubleCapacityB();
            }
        }

        return true;
    }

    private void doubleCapacityA() {
        SelectionKey[] newKeysA = new SelectionKey[keysA.length << 1];
        System.arraycopy(keysA, 0, newKeysA, 0, keysASize);
        keysA = newKeysA;
    }

    private void doubleCapacityB() {
        SelectionKey[] newKeysB = new SelectionKey[keysB.length << 1];
        System.arraycopy(keysB, 0, newKeysB, 0, keysBSize);
        keysB = newKeysB;
    }

    SelectionKey[] flip() {
        if (isA) {
            isA = false;
            keysA[keysASize] = null;
            keysBSize = 0;
            return keysA;
        } else {
            isA = true;
            keysB[keysBSize] = null;
            keysASize = 0;
            return keysB;
        }
    }

    @Override
    public int size() {
        if (isA) {
            return keysASize;
        } else {
            return keysBSize;
        }
    }

    @Override
    public boolean remove(Object o) {
        return false;
    }

    @Override
    public boolean contains(Object o) {
        return false;
    }

    @Override
    public Iterator<SelectionKey> iterator() {
        throw new UnsupportedOperationException();
    }
}
複製程式碼

該類其實很簡單,繼承了 AbstractSet,說明該類可以當作一個set來用,但是底層使用兩個陣列來交替使用,在add方法中,判斷當前使用哪個陣列,找到對應的陣列,然後經歷下面三個步驟 1.將SelectionKey塞到該陣列的邏輯尾部 2.更新該陣列的邏輯長度+1 3.如果該陣列的邏輯長度等於陣列的物理長度,就將該陣列擴容

我們可以看到,待程式跑過一段時間,等陣列的長度足夠長,每次在輪詢到nio事件的時候,netty只需要O(1)的時間複雜度就能將 SelectionKey 塞到 set中去,而jdk底層使用的hashSet需要O(lgn)的時間複雜度

這裡關於為何使用兩個陣列迴圈交替使用,其實我也是很費解,思考了很久,查詢SelectedSelectionKeySet 所有使用的地方,我覺得使用一個陣列就能夠達到優化目的,並且不用每次都判斷使用哪個陣列,所以對於該問題,我提了一個issue給netty官方,官方也給出了答覆說會跟進,issue連結:github.com/netty/netty… 版本中,netty已經將SelectedSelectionKeySet.java 底層使用一個陣列了,連結

關於netty對SelectionKeySet的優化我們暫時就跟這麼多,下面我們繼續跟netty對IO事件的處理,轉到processSelectedKeysOptimized

 private void processSelectedKeysOptimized(SelectionKey[] selectedKeys) {
     for (int i = 0;; i ++) {
         // 1.取出IO事件以及對應的channel
         final SelectionKey k = selectedKeys[i];
         if (k == null) {
             break;
         }
         selectedKeys[i] = null;
         final Object a = k.attachment();
         // 2.處理該channel
         if (a instanceof AbstractNioChannel) {
             processSelectedKey(k, (AbstractNioChannel) a);
         } else {
             NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
             processSelectedKey(k, task);
         }
         // 3.判斷是否該再來次輪詢
         if (needsToSelectAgain) {
             for (;;) {
                 i++;
                 if (selectedKeys[i] == null) {
                     break;
                 }
                 selectedKeys[i] = null;
             }
             selectAgain();
             selectedKeys = this.selectedKeys.flip();
             i = -1;
         }
     }
 }
複製程式碼

我們可以將該過程分為以下三個步驟

1.取出IO事件以及對應的netty channel類

這裡其實也能體會到優化過的 SelectedSelectionKeySet 的好處,遍歷的時候遍歷的是陣列,相對jdk原生的HashSet效率有所提高

拿到當前SelectionKey之後,將selectedKeys[i]置為null,這裡簡單解釋一下這麼做的理由:想象一下這種場景,假設一個NioEventLoop平均每次輪詢出N個IO事件,高峰期輪詢出3N個事件,那麼selectedKeys的物理長度要大於等於3N,如果每次處理這些key,不置selectedKeys[i]為空,那麼高峰期一過,這些儲存在陣列尾部的selectedKeys[i]對應的SelectionKey將一直無法被回收,SelectionKey對應的物件可能不大,但是要知道,它可是有attachment的,這裡的attachment具體是什麼下面會講到,但是有一點我們必須清楚,attachment可能很大,這樣一來,這些元素是GC root可達的,很容易造成gc不掉,記憶體洩漏就發生了

這個bug在 4.0.19.Final版本中被修復,建議使用netty的專案升級到最新版本^^

2.處理該channel

拿到對應的attachment之後,netty做了如下判斷

if (a instanceof AbstractNioChannel) {
    processSelectedKey(k, (AbstractNioChannel) a);
} 
複製程式碼

原始碼讀到這,我們需要思考為啥會有這麼一條判斷,憑什麼說attachment可能會是 AbstractNioChannel物件?

我們的思路應該是找到底層selector, 然後在selector呼叫register方法的時候,看一下注冊到selector上的物件到底是什麼鬼,我們使用intellij的全域性搜尋引用功能,最終在 AbstractNioChannel中搜尋到如下方法

protected void doRegister() throws Exception {
    // ...
    selectionKey = javaChannel().register(eventLoop().selector, 0, this);
    // ...
}
複製程式碼

javaChannel() 返回netty類AbstractChannel對應的jdk底層channel物件

protected SelectableChannel javaChannel() {
    return ch;
}
複製程式碼

我們檢視到SelectableChannel方法,結合netty的 doRegister() 方法,我們不難推論出,netty的輪詢序號產生器制其實是將AbstractNioChannel內部的jdk類SelectableChannel物件註冊到jdk類Selctor物件上去,並且將AbstractNioChannel作為SelectableChannel物件的一個attachment附屬上,這樣再jdk輪詢出某條SelectableChannel有IO事件發生時,就可以直接取出AbstractNioChannel進行後續操作

下面是jdk中的register方法

     //*
     //* @param  sel
     //*         The selector with which this channel is to be registered
     //*
     //* @param  ops
     //*         The interest set for the resulting key
     //*
     //* @param  att
     //*         The attachment for the resulting key; may be <tt>null</tt>
public abstract SelectionKey register(Selector sel, int ops, Object att)
        throws ClosedChannelException;
複製程式碼

由於篇幅原因,詳細的 processSelectedKey(SelectionKey k, AbstractNioChannel ch) 過程我們單獨寫一篇文章來詳細展開,這裡就簡單說一下 1.對於boss NioEventLoop來說,輪詢到的是基本上就是連線事件,後續的事情就通過他的pipeline將連線扔給一個worker NioEventLoop處理 2.對於worker NioEventLoop來說,輪詢到的基本上都是io讀寫事件,後續的事情就是通過他的pipeline將讀取到的位元組流傳遞給每個channelHandler來處理

上面處理attachment的時候,還有個else分支,我們也來分析一下 else部分的程式碼如下

NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
processSelectedKey(k, task);
複製程式碼

說明註冊到selctor上的attachment還有另外一中型別,就是 NioTask,NioTask主要是用於當一個 SelectableChannel 註冊到selector的時候,執行一些任務

NioTask的定義

public interface NioTask<C extends SelectableChannel> {
    void channelReady(C ch, SelectionKey key) throws Exception;
    void channelUnregistered(C ch, Throwable cause) throws Exception;
}
複製程式碼

由於NioTask 在netty內部沒有使用的地方,這裡不過多展開

3.判斷是否該再來次輪詢

if (needsToSelectAgain) {
    for (;;) {
        i++;
        if (selectedKeys[i] == null) {
            break;
        }
        selectedKeys[i] = null;
    }
    selectAgain();
    selectedKeys = this.selectedKeys.flip();
    i = -1;
}
複製程式碼

我們回憶一下netty的reactor執行緒經歷前兩個步驟,分別是抓取產生過的IO事件以及處理IO事件,每次在抓到IO事件之後,都會將 needsToSelectAgain 重置為false,那麼什麼時候needsToSelectAgain會重新被設定成true呢?

還是和前面一樣的思路,我們使用intellij來幫助我們檢視needsToSelectAgain被使用的地方,在NioEventLoop類中,只有下面一處將needsToSelectAgain設定為true

NioEventLoop.java

void cancel(SelectionKey key) {
    key.cancel();
    cancelledKeys ++;
    if (cancelledKeys >= CLEANUP_INTERVAL) {
        cancelledKeys = 0;
        needsToSelectAgain = true;
    }
}
複製程式碼

繼續檢視 cancel 函式被呼叫的地方

AbstractChannel.java

@Override
protected void doDeregister() throws Exception {
    eventLoop().cancel(selectionKey());
}
複製程式碼

不難看出,在channel從selector上移除的時候,呼叫cancel函式將key取消,並且當被去掉的key到達 CLEANUP_INTERVAL 的時候,設定needsToSelectAgain為true,CLEANUP_INTERVAL預設值為256

private static final int CLEANUP_INTERVAL = 256;
複製程式碼

也就是說,對於每個NioEventLoop而言,每隔256個channel從selector上移除的時候,就標記 needsToSelectAgain 為true,我們還是跳回到上面這段程式碼

if (needsToSelectAgain) {
    for (;;) {
        i++;
        if (selectedKeys[i] == null) {
            break;
        }
        selectedKeys[i] = null;
    }
    selectAgain();
    selectedKeys = this.selectedKeys.flip();
    i = -1;
}
複製程式碼

每滿256次,就會進入到if的程式碼塊,首先,將selectedKeys的內部陣列全部清空,方便被jvm垃圾回收,然後重新呼叫selectAgain重新填裝一下 selectionKey

private void selectAgain() {
    needsToSelectAgain = false;
    try {
        selector.selectNow();
    } catch (Throwable t) {
        logger.warn("Failed to update SelectionKeys.", t);
    }
}
複製程式碼

netty這麼做的目的我想應該是每隔256次channel斷線,重新清理一下selectionKey,保證現存的SelectionKey及時有效

到這裡,我們初次閱讀原始碼的時候對reactor的第二個步驟的瞭解已經足夠了。總結一下:netty的reactor執行緒第二步做的事情為處理IO事件,netty使用陣列替換掉jdk原生的HashSet來保證IO事件的高效處理,每個SelectionKey上繫結了netty類AbstractChannel物件作為attachment,在處理每個SelectionKey的時候,就可以找到AbstractChannel,然後通過pipeline的方式將處理序列到ChannelHandler,回撥到使用者方法

下一篇文章,我們將一起來看下netty中reactor執行緒中最後一步,runTasks,你將瞭解到netty中非同步執行任務機制的細節,盡請期待

如果你想系統地學Netty,我的小冊《Netty 入門與實戰:仿寫微信 IM 即時通訊系統》可以幫助你,如果你想系統學習Netty原理,那麼你一定不要錯過我的Netty原始碼分析系列視訊:coding.imooc.com/class/230.h…

相關文章