netty源碼分析之揭開reactor線程的面紗(二)

如果你對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 類對象,在NioEventLoop 的 openSelector 方法中創建,之後就通過反射將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鏈接:https://github.com/netty/netty/issues/6058#,目前在4.1.9.Final 版本中,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中異步執行任務機制的細節,盡請期待

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章