全網講解最透徹:高性能網絡應用框架Netty,僅此一篇 網絡編程性能的瓶頸 Reactor 模式 Netty 中的線程模型 用 Netty 實現 Echo 程序服務端 總結

Netty 是一個高性能網絡應用框架,應用非常普遍,目前在 Java 領域裏,Netty 基本上成爲網絡程序的標配了。Netty 框架功能豐富,也非常複雜,今天我們主要分析 Netty 框架中的線程模型,而線程模型直接影響着網絡程序的性能

在介紹 Netty 的線程模型之前,我們首先需要把問題搞清楚,瞭解網絡編程性能的瓶頸在哪裏,然後再看 Netty 的線程模型是如何解決這個問題的。

網絡編程性能的瓶頸

之前,我們寫過一個簡單的網絡程序echo,採用的是阻塞式 I/O(BIO)。BIO 模型裏,所有 read() 操作和 write() 操作都會阻塞當前線程的,如果客戶端已經和服務端建立了一個連接,而遲遲不發送數據,那麼服務端的 read() 操作會一直阻塞,所以使用 BIO 模型,一般都會爲每個 socket 分配一個獨立的線程,這樣就不會因爲線程阻塞在一個 socket 上而影響對其他 socket 的讀寫。BIO 的線程模型如下圖所示,每一個 socket 都對應一個獨立的線程;爲了避免頻繁創建、消耗線程,可以採用線程池,但是 socket 和線程之間的對應關係並不會變化。

BIO 這種線程模型適用於 socket 連接不是很多的場景;但是現在的互聯網場景,往往需要服務器能夠支撐十萬甚至百萬連接,而創建十萬甚至上百萬個線程顯然並不現實,所以BIO 線程模型無法解決百萬連接的問題。如果仔細觀察,你會發現互聯網場景中,雖然連接多,但是每個連接上的請求並不頻繁,所以線程大部分時間都在等待 I/O 就緒。也就是說線程大部分時間都阻塞在那裏,這完全是浪費,如果我們能夠解決這個問題,那就不需要這麼多線程了。

順着這個思路,我們可以將線程模型優化爲下圖這個樣子,可以用一個線程來處理多個連接,這樣線程的利用率就上來了,同時所需的線程數量也跟着降下來了。這個思路很好,可是使用 BIO 相關的 API 是無法實現的,這是爲什麼呢?因爲 BIO 相關的 socket 讀寫操作都是阻塞式的,而一旦調用了阻塞式 API,在 I/O 就緒前,調用線程會一直阻塞,也就無法處理其他的 socket 連接了。

好在 Java 裏還提供了非阻塞式(NIO)API,利用非阻塞式 API 就能夠實現一個線程處理多個連接了。那具體如何實現呢?現在普遍都是採用 Reactor 模式,包括 Netty 的實現。所以,要想理解 Netty 的實現,接下來我們就需要先了解一下 Reactor 模式。

Reactor 模式

下面是 Reactor 模式的類結構圖,其中 Handle 指的是 I/O 句柄,在 Java 網絡編程裏,它本質上就是一個網絡連接。Event Handler 很容易理解,就是一個事件處理器,其中handle_event() 方法處理 I/O 事件,也就是每個 Event Handler 處理一個 I/O Handle;get_handle() 方法可以返回這個 I/O 的 Handle。Synchronous Event Demultiplexer 可以理解爲操作系統提供的 I/O 多路複用 API,例如 POSIX 標準裏的 select() 以及 Linux 裏面的 epoll()。

Reactor 模式的核心自然是Reactor 這個類,其中 register_handler() 和remove_handler() 這兩個方法可以註冊和刪除一個事件處理器;handle_events() 方式是核心,也是 Reactor 模式的發動機,這個方法的核心邏輯如下:首先通過同步事件多路選擇器提供的 select() 方法監聽網絡事件,當有網絡事件就緒後,就遍歷事件處理器來處理該網絡事件。由於網絡事件是源源不斷的,所以在主程序中啓動 Reactor 模式,需要以while(true){} 的方式調用 handle_events() 方法。

void Reactor : : handle_events()
{
    /*
     * 通過同步事件多路選擇器提供的
     * select() 方法監聽網絡事件
     */
    select( handlers );
    /* 處理網絡事件 */
    for ( h in handlers )
    {
        h.handle_event();
    }
}
/* 在主程序中啓動事件循環 */
while ( true )
{
    handle_events();

Netty 中的線程模型

Netty 的實現雖然參考了 Reactor 模式,但是並沒有完全照搬,Netty 中最核心的概念是事件循環(EventLoop),其實也就是 Reactor 模式中的 Reactor,負責監聽網絡事件並調用事件處理器進行處理。在 4.x 版本的 Netty 中,網絡連接和 EventLoop 是穩定的多對1 關係,而 EventLoop 和 Java 線程是 1 對 1 關係,這裏的穩定指的是關係一旦確定就不再發生變化。也就是說一個網絡連接只會對應唯一的一個 EventLoop,而一個 EventLoop也只會對應到一個 Java 線程,所以一個網絡連接只會對應到一個 Java 線程

一個網絡連接對應到一個 Java 線程上,有什麼好處呢?最大的好處就是對於一個網絡連接的事件處理是單線程的,這樣就避免了各種併發問題

Netty 中的線程模型可以參考下圖,這個圖和前面我們提到的理想的線程模型圖非常相似,核心目標都是用一個線程處理多個網絡連接。

Netty 中還有一個核心概念是EventLoopGroup,顧名思義,一個 EventLoopGroup 由一組 EventLoop 組成。實際使用中,一般都會創建兩個 EventLoopGroup,一個稱爲bossGroup,一個稱爲 workerGroup。爲什麼會有兩個 EventLoopGroup 呢?

這個和 socket 處理網絡請求的機制有關,socket 處理 TCP 網絡連接請求,是在一個獨立的 socket 中,每當有一個 TCP 連接成功建立,都會創建一個新的 socket,之後對 TCP 連接的讀寫都是由新創建處理的 socket 完成的。也就是說處理 TCP 連接請求和讀寫請求是通過兩個不同的 socket 完成的。上面我們在討論網絡請求的時候,爲了簡化模型,只是討論了讀寫請求,而沒有討論連接請求。

在 Netty 中,bossGroup 就用來處理連接請求的,而 workerGroup 是用來處理讀寫請求的。bossGroup 處理完連接請求後,會將這個連接提交給 workerGroup 來處理,workerGroup 裏面有多個 EventLoop,那新的連接會交給哪個 EventLoop 來處理呢?這就需要一個負載均衡算法,Netty 中目前使用的是輪詢算法

下面我們用 Netty 重新實現以下 echo 程序的服務端,近距離感受一下 Netty。

用 Netty 實現 Echo 程序服務端

下面的示例代碼基於 Netty 實現了 echo 程序服務端:首先創建了一個事件處理器(等同於 Reactor 模式中的事件處理器),然後創建了 bossGroup 和 workerGroup,再之後創建並初始化了 ServerBootstrap,代碼還是很簡單的,不過有兩個地方需要注意一下。

第一個,如果 NettybossGroup 只監聽一個端口,那 bossGroup 只需要 1 個 EventLoop就可以了,多了純屬浪費。

第二個,默認情況下,Netty 會創建“2*CPU 核數”個 EventLoop,由於網絡連接與EventLoop 有穩定的關係,所以事件處理器在處理網絡事件的時候是不能有阻塞操作的,否則很容易導致請求大面積超時。如果實在無法避免使用阻塞操作,那可以通過線程池來異步處理。

/* 事件處理器 */
final EchoServerHandler serverHandler
    = new EchoServerHandler();
/* boss 線程組 */
EventLoopGroup bossGroup
    = new NioEventLoopGroup( 1 );
/* worker 線程組 */
EventLoopGroup workerGroup
    = new NioEventLoopGroup();
try {
    ServerBootstrap b = new ServerBootstrap();
    b.group( bossGroup, workerGroup )
    .channel( NioServerSocketChannel.class )
    .childHandler( new ChannelInitializer<SocketChannel>()
               {
                   @Override
                   public void initChannel( SocketChannel ch )
                   {
                       ch.pipeline().addLast( serverHandler );
                   }
               } );
    /* bind 服務端端口 */
    ChannelFuture f = b.bind( 9090 ).sync();
    f.channel().closeFuture().sync();
} finally {
    /* 終止工作線程組 */
    workerGroup.shutdownGracefully();
    /* 終止 boss 線程組 */
    bossGroup.shutdownGracefully();
}

/* socket 連接處理器 */
class EchoServerHandler extends
ChannelInboundHandlerAdapter {
    /* 處理讀事件 */
    @Override
    public void channelRead(
        ChannelHandlerContext ctx, Object msg )
    {
        ctx.write( msg );
    }

    /* 處理讀完成事件 */
    @Override
    public void channelReadComplete(
        ChannelHandlerContext ctx )
    {
        ctx.flush();
    }

    /* 處理異常事件 */
    @Override
    public void exceptionCaught(
        ChannelHandlerContext ctx, Throwable cause )
    {
        cause.printStackTrace();
        ctx.close();
    }
}

總結

Netty 是一個款優秀的網絡編程框架,性能非常好,爲了實現高性能的目標,Netty 做了很多優化,例如優化了 ByteBuffer、支持零拷貝等等,和併發編程相關的就是它的線程模型了。Netty 的線程模型設計得很精巧,每個網絡連接都關聯到了一個線程上,這樣做的好處是:對於一個網絡連接,讀寫操作都是單線程執行的,從而避免了併發程序的各種問題。

如果想深入瞭解併發編程原理可閱讀:《不愧是阿里P7私傳“併發編程核心講義”,實戰案例,個個是經典

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