Netty原始碼解析一——執行緒池模型之執行緒池NioEventLoopGroup

為了生活,加油發表於2022-02-21

本文基礎是需要有Netty的使用經驗,如果沒有編碼經驗,可以參考官網給的例子:https://netty.io/wiki/user-guide-for-4.x.html。另外本文也是針對的是Netty 4.1.x版本的。

Reactor模式

本文主要介紹Netty執行緒模型及其實現,介紹Netty執行緒模型前,首先會介紹下經典的Reactor執行緒模型,目前大多數網路框架都是基於Reactor模式進行設計和開發,Reactor模式基於事件驅動,有一個或多個併發輸入源,有一個Service Handler,有多個Request Handlers。這個Service Handler會同步的將輸入的請求(Event)多路複用的分發給相應的Request Handler,非常適合處理海量的I/O事件。下面簡單介紹下Reactor模式及其執行緒模型。

單執行緒模型

如圖所示,由於Reactor模式使用的是非同步非阻塞IO,所有的IO操作都不會導致阻塞。通常Reactor執行緒中聚合了多路複用器負責監聽網路事件,當有新連線到來時,觸發連線事件,Disdatcher負責使用Acceptor接受客戶端連線,建立通訊鏈路。當I/O事件就緒後,Disdatcher負責將事件分發到對應的event handler上負責處理。

該模型的缺點很明顯,不適用於高負載、高併發的應用場景;由於只有一個Reactor執行緒,一旦故障,整個系統通訊模組將不可用。

多執行緒模型

該模型的特點:

  • 專門由一個Reactor執行緒-Acceptor執行緒用於監聽服務端,接收客戶端連線請求;
  • 網路I/O操作讀、寫等由Reactor執行緒池負責處理;
  • 一個Reactor執行緒可同時處理多條鏈路,但一條鏈路只能對應一個Reactor執行緒,這樣可避免併發操作問題。

絕大多數場景下,Reactor多執行緒模型都可以滿足效能需求,但是,在極個別特殊場景中,一個Reactor執行緒負責監聽和處理所有的客戶端連線可能會存在效能問題。例如併發百萬客戶端連線,或者服務端需要對客戶端握手進行安全認證,但是認證本身非常損耗效能。因此,誕生了第三種執行緒模型。

主從多執行緒模型

該模型的特點:

  • 服務端使用一個獨立的主Reactor執行緒池來處理客戶端連線,當服務端收到連線請求時,從主執行緒池中隨機選擇一個Reactor執行緒作為Acceptor執行緒處理連線;
  • 鏈路建立成功後,將新建立的SocketChannel註冊到sub reactor執行緒池的某個Reactor執行緒上,由它處理後續的I/O操作。

Netty執行緒模型

Netty同時支援Reactor單執行緒模型 、Reactor多執行緒模型和Reactor主從多執行緒模型,使用者可根據啟動引數配置在這三種模型之間切換。Netty執行緒模型原理圖如下:

服務端啟動時,通常會建立兩個NioEventLoopGroup例項,對應了兩個獨立的Reactor執行緒池。常見服務端啟動程式碼實現如下:

EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
    ServerBootstrap b = new ServerBootstrap();
    b.group(bossGroup, workerGroup)
     .channel(NioServerSocketChannel.class)
     .option(ChannelOption.SO_BACKLOG, 100)
     .handler(new LoggingHandler(LogLevel.INFO))
     .childHandler(new ChannelInitializer<SocketChannel>() {
         @Override
         public void initChannel(SocketChannel ch) throws Exception {
               ......

實際上比較重要的建立執行緒池建立程式碼:

EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();

服務端啟動時建立了兩個NioEventLoopGroup,他們實際上時兩個獨立的Reactor執行緒池,一個負責接收客戶端的TCP連線,另一個用於處理I/O操作,或執行系統Task、定時任務Task等,接下來做一下原始碼分析。

 NioEventLoopGroup

首先看下NioEventLoopGroup的繼承關係:

可以看出最終還是呼叫了java的執行緒池建立方式,接下來看一下它的構造方法。

檢視程式碼
public class NioEventLoopGroup extends MultithreadEventLoopGroup {

 
    public NioEventLoopGroup() {
        this(0);
    }

    public NioEventLoopGroup(int nThreads) {
        this(nThreads, (Executor) null);
    }

    public NioEventLoopGroup(ThreadFactory threadFactory) {
        this(0, threadFactory, SelectorProvider.provider());
    }

  
    public NioEventLoopGroup(int nThreads, ThreadFactory threadFactory) {
        this(nThreads, threadFactory, SelectorProvider.provider());
    }

    public NioEventLoopGroup(int nThreads, Executor executor) {
        this(nThreads, executor, SelectorProvider.provider());
    }

    public NioEventLoopGroup(
            int nThreads, ThreadFactory threadFactory, final SelectorProvider selectorProvider) {
        this(nThreads, threadFactory, selectorProvider, DefaultSelectStrategyFactory.INSTANCE);
    }

    public NioEventLoopGroup(int nThreads, ThreadFactory threadFactory,
        final SelectorProvider selectorProvider, final SelectStrategyFactory selectStrategyFactory) {
        super(nThreads, threadFactory, selectorProvider, selectStrategyFactory, RejectedExecutionHandlers.reject());
    }

    public NioEventLoopGroup(
            int nThreads, Executor executor, final SelectorProvider selectorProvider) {
        this(nThreads, executor, selectorProvider, DefaultSelectStrategyFactory.INSTANCE);
    }

    public NioEventLoopGroup(int nThreads, Executor executor, final SelectorProvider selectorProvider,
                             final SelectStrategyFactory selectStrategyFactory) {
        super(nThreads, executor, selectorProvider, selectStrategyFactory, RejectedExecutionHandlers.reject());
    }

    public NioEventLoopGroup(int nThreads, Executor executor, EventExecutorChooserFactory chooserFactory,
                             final SelectorProvider selectorProvider,
                             final SelectStrategyFactory selectStrategyFactory) {
        super(nThreads, executor, chooserFactory, selectorProvider, selectStrategyFactory,
                RejectedExecutionHandlers.reject());
    }

    public NioEventLoopGroup(int nThreads, Executor executor, EventExecutorChooserFactory chooserFactory,
                             final SelectorProvider selectorProvider,
                             final SelectStrategyFactory selectStrategyFactory,
                             final RejectedExecutionHandler rejectedExecutionHandler) {
        super(nThreads, executor, chooserFactory, selectorProvider, selectStrategyFactory, rejectedExecutionHandler);
    }

    public NioEventLoopGroup(int nThreads, Executor executor, EventExecutorChooserFactory chooserFactory,
                             final SelectorProvider selectorProvider,
                             final SelectStrategyFactory selectStrategyFactory,
                             final RejectedExecutionHandler rejectedExecutionHandler,
                             final EventLoopTaskQueueFactory taskQueueFactory) {
        super(nThreads, executor, chooserFactory, selectorProvider, selectStrategyFactory,
                rejectedExecutionHandler, taskQueueFactory);
    }

    public NioEventLoopGroup(int nThreads, Executor executor, EventExecutorChooserFactory chooserFactory,
                             SelectorProvider selectorProvider,
                             SelectStrategyFactory selectStrategyFactory,
                             RejectedExecutionHandler rejectedExecutionHandler,
                             EventLoopTaskQueueFactory taskQueueFactory,
                             EventLoopTaskQueueFactory tailTaskQueueFactory) {
        super(nThreads, executor, chooserFactory, selectorProvider, selectStrategyFactory,
                rejectedExecutionHandler, taskQueueFactory, tailTaskQueueFactory);
    }

}

可以看到最終是呼叫了父類MultithreadEventExecutorGroup的構造方法,繼續跟蹤:

檢視程式碼
protected MultithreadEventExecutorGroup(int nThreads, ThreadFactory threadFactory, Object... args) {
        this(nThreads, threadFactory == null ? null : new ThreadPerTaskExecutor(threadFactory), args);
    }

 protected MultithreadEventExecutorGroup(int nThreads, Executor executor, Object... args) {
        this(nThreads, executor, DefaultEventExecutorChooserFactory.INSTANCE, args);
    }

 
  protected MultithreadEventExecutorGroup(int nThreads, Executor executor,
                                            EventExecutorChooserFactory chooserFactory, Object... args) {
        checkPositive(nThreads, "nThreads");

        if (executor == null) {
            //建立執行緒執行器以及執行緒工廠
            executor = new ThreadPerTaskExecutor(newDefaultThreadFactory());
        }

        //根據執行緒數構建EventExecutor陣列
        children = new EventExecutor[nThreads];

        for (int i = 0; i < nThreads; i ++) {
            boolean success = false;
            try {
                //初始化陣列中的執行緒,由NioEventLoopGroup建立NioEventLoop例項
                children[i] = newChild(executor, args);
                success = true;
            } catch (Exception e) {
                // TODO: Think about if this is a good exception type
                throw new IllegalStateException("failed to create a child event loop", e);
            } finally {
                //當初始化失敗時,需要優雅關閉,清理資源
                if (!success) {
                    for (int j = 0; j < i; j ++) {
                        children[j].shutdownGracefully();
                    }

                    for (int j = 0; j < i; j ++) {
                        EventExecutor e = children[j];
                        try {
                            //當執行緒未終止時,等待終止
                            while (!e.isTerminated()) {
                                e.awaitTermination(Integer.MAX_VALUE, TimeUnit.SECONDS);
                            }
                        } catch (InterruptedException interrupted) {
                            // Let the caller handle the interruption.
                            Thread.currentThread().interrupt();
                            break;
                        }
                    }
                }
            }
        }

        //根據執行緒數建立選擇器,選擇器主要適用於next()方法
        chooser = chooserFactory.newChooser(children);

        final FutureListener<Object> terminationListener = new FutureListener<Object>() {
            @Override
            public void operationComplete(Future<Object> future) throws Exception {
                if (terminatedChildren.incrementAndGet() == children.length) {
                    terminationFuture.setSuccess(null);
                }
            }
        };

        //為每個EventLoop執行緒增加執行緒終止監聽器
        for (EventExecutor e: children) {
            e.terminationFuture().addListener(terminationListener);
        }

        Set<EventExecutor> childrenSet = new LinkedHashSet<EventExecutor>(children.length);
        Collections.addAll(childrenSet, children);
        //建立執行器陣列只讀副本,便於在迭代查詢時使用
        readonlyChildren = Collections.unmodifiableSet(childrenSet);
    }

從程式碼看出,使用EventLoopGroup workerGroup = new NioEventLoopGroup()來建立執行緒池,如果不指定執行緒個數,那麼預設用0,在預設為0的情況下系統會使用預設的執行緒個數來建立執行緒池,如果制定了n>0個執行緒個數的話,就建立有限個數執行緒的執行緒池。那麼預設建立的執行緒個數規則是啥呢?可以詳見如下程式碼:

 static {
        DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt(
                "io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2));

        if (logger.isDebugEnabled()) {
            logger.debug("-Dio.netty.eventLoopThreads: {}", DEFAULT_EVENT_LOOP_THREADS);
        }
    }

可以看出是在CPU核心數*2與“io.netty.eventLoopThreads”這個配置引數取或並且與1比較大小得到的結果,就說如果“io.netty.eventLoopThreads”這系統引數配置了就用系統引數與1比較的最大值返回,如果沒有配置使用cpu核心數*2與1比較的最大值返回。

關於NioEventLoopGroup我們從程式碼跟蹤中做如下的總結,該類主要是完成三件事:

  1. 建立一定數量的NioEventLoop執行緒組並初始化。
  2. 建立執行緒選擇器chooser,當獲取執行緒時,通過選擇器來獲取。
  3. 建立執行緒工廠並構建執行緒執行器。

執行緒組的生產分兩步:第一步,建立一定數量的EventExecutor陣列;第二步,通過呼叫子類的newChild()方法完成這些EventExecutor陣列的初始化。為了提高可擴充套件性,Netty的執行緒組除了NioEventLoopGroup,還有Netty通過JNI方式提供的一套由epoll模型實現的EpollEventLoop Group執行緒組,以及其他I/O多路複用模型執行緒組,因此newChild()方法由具體的執行緒組子類來實現。

children[i] = newChild(executor, args);
檢視程式碼
protected EventLoop newChild(Executor executor, Object... args) throws Exception {
        SelectorProvider selectorProvider = (SelectorProvider) args[0];
        SelectStrategyFactory selectStrategyFactory = (SelectStrategyFactory) args[1];
        RejectedExecutionHandler rejectedExecutionHandler = (RejectedExecutionHandler) args[2];
        EventLoopTaskQueueFactory taskQueueFactory = null;
        EventLoopTaskQueueFactory tailTaskQueueFactory = null;

        int argsLength = args.length;
        if (argsLength > 3) {
            taskQueueFactory = (EventLoopTaskQueueFactory) args[3];
        }
        if (argsLength > 4) {
            tailTaskQueueFactory = (EventLoopTaskQueueFactory) args[4];
        }
        return new NioEventLoop(this, executor, selectorProvider,
                selectStrategyFactory.newSelectStrategy(),
                rejectedExecutionHandler, taskQueueFactory, tailTaskQueueFactory);
    }

在newChild()方法中,NioEventLoop的初始化引數有6個:第1個引數為NioEventLoopGroup執行緒組本身;第2個引數為執行緒執行器,用於啟動執行緒,在SingleThreadEventExecutor的doStartThread()方法中被呼叫;第3個引數為NIO的Selector選擇器的提供者;第4個引數主要在NioEventLoop的run()方法中用於控制選擇迴圈;第5個引數為非I/O任務提交被拒絕時的處理Handler;第6個引數為佇列工廠,在NioEventLoop中,佇列讀是單執行緒操作,而佇列寫則可能是多執行緒操作,使用支援多生產者、單消費者的佇列比較合適,預設為MpscChunkedArrayQueue佇列。

NioEventLoopGroup通過next()方法獲取NioEventLoop執行緒,最終會呼叫其父類MultithreadEventExecutorGroup的next()方法,委託父類的選擇器EventExecutorChooser。具體使用哪種選擇器物件取決於MultithreadEventExecutorGroup的構造方法中使用的策略模式。

根據執行緒條數是否為2的冪次來選擇策略,若是,則選擇器為PowerOfTwoEventExecutorChooser,其選擇策略使用與運算計算下一個選擇的執行緒組的下標index;若不是,則選擇器為GenericEventExecutorChooser,其選擇策略為使用求餘的方法計算下一個執行緒線上程組中的下標index。其中,PowerOfTwoEventExecutorChooser選擇器的與運算效能會更好。

根據執行緒條數是否為2的冪次來選擇策略,若是,則選擇器為PowerOfTwoEventExecutorChooser,其選擇策略使用與運算計算下一個選擇的執行緒組的下標index,此計算方法在第7章中也有相似的應用;若不是,則選擇器為GenericEventExecutorChooser,其選擇策略為使用求餘的方法計算下一個執行緒線上程組中的下標index。其中,PowerOfTwoEventExecutorChooser選擇器的與運算效能會更好。

public EventExecutorChooser newChooser(EventExecutor[] executors) {
    if (isPowerOfTwo(executors.length)) {
        return new PowerOfTwoEventExecutorChooser(executors);
    } else {
        return new GenericEventExecutorChooser(executors);
    }
}
private static boolean isPowerOfTwo(int val) {
    return (val & -val) == val;
}

由於Netty的NioEventLoop執行緒被包裝成了FastThreadLocalThread執行緒,同時,NioEventLoop執行緒的狀態由它自身管理,因此每個NioEventLoop執行緒都需要有一個執行緒執行器,並且線上程執行前需要通過執行緒工廠io.netty.util.concurrent.DefaultThreadFactory將其包裝成FastThreadLocalThread執行緒。執行緒執行器ThreadPerTaskExecutor與DefaultThreadFactory的newThread()方法的程式碼解讀如下:

檢視程式碼

public void execute(Runnable command) {
        //呼叫執行緒工廠類的newThread包裝執行緒,並且啟動,等待執行緒排程。
        threadFactory.newThread(command).start();
    }


public Thread newThread(Runnable r) {
        //包裝FastThreadLocalThread執行緒,執行緒的字首名字為NioEventLoopGroup-
        //服務啟動後可以通過Arthas工具檢視
        Thread t = newThread(FastThreadLocalRunnable.wrap(r), prefix + nextId.incrementAndGet());
        try {
            if (t.isDaemon() != daemon) {
                t.setDaemon(daemon);
            }

            if (t.getPriority() != priority) {
                t.setPriority(priority);
            }
        } catch (Exception ignored) {
            // Doesn't matter even if failed to set.
        }
        return t;
    }

    //包裝為FastThreadLocalThread執行緒
    protected Thread newThread(Runnable r, String name) {
        return new FastThreadLocalThread(threadGroup, r, name);
    }

 

相關文章