Netty(二) 從執行緒模型的角度看 Netty 為什麼是高效能的?

crossoverJie發表於2019-01-19

前言

在之前的 SpringBoot 整合長連線心跳機制 一文中認識了 Netty。

但其實只是能用,為什麼要用 Netty?它有哪些優勢?這些其實都不清楚。

本文就來從歷史源頭說道說道。

傳統 IO

在 Netty 以及 NIO 出現之前,我們寫 IO 應用其實用的都是用 java.io.* 下所提供的包。

比如下面的虛擬碼:

ServeSocket serverSocket = new ServeSocket(8080);
Socket socket = serverSocket.accept() ;
BufferReader in = .... ;

String request ;
 
while((request = in.readLine()) != null){
	new Thread(new Task()).start()
}
複製程式碼

大概是這樣,其實主要想表達的是:這樣一個執行緒只能處理一個連線

如果是 100 個客戶端連線那就得開 100 個執行緒,1000 那就得 1000 個執行緒。

要知道執行緒資源非常寶貴,每次的建立都會帶來消耗,而且每個執行緒還得為它分配對應的棧記憶體。

即便是我們給 JVM 足夠的記憶體,大量執行緒所帶來的上下文切換也是受不了的。

並且傳統 IO 是阻塞模式,每一次的響應必須的是發起 IO 請求,處理請求完成再同時返回,直接的結果就是效能差,吞吐量低。

Reactor 模型

因此業界常用的高效能 IO 模型是 Reactor

它是一種非同步、非阻塞的事件驅動模型。

通常也表現為以下三種方式:

單執行緒

Netty(二) 從執行緒模型的角度看 Netty 為什麼是高效能的?

從圖中可以看出:

它是由一個執行緒來接收客戶端的連線,並將該請求分發到對應的事件處理 handler 中,整個過程完全是非同步非阻塞的;並且完全不存在共享資源的問題。所以理論上來說吞吐量也還不錯。

但由於是一個執行緒,對多核 CPU 利用率不高,一旦有大量的客戶端連線上來效能必然下降,甚至會有大量請求無法響應。 最壞的情況是一旦這個執行緒哪裡沒有處理好進入了死迴圈那整個服務都將不可用!

多執行緒

Netty(二) 從執行緒模型的角度看 Netty 為什麼是高效能的?

因此產生了多執行緒模型。

其實最大的改進就是將原有的事件處理改為了多執行緒。

可以基於 Java 自身的執行緒池實現,這樣在大量請求的處理上效能提示是巨大的。

雖然如此,但理論上來說依然有一個地方是單點的;那就是處理客戶端連線的執行緒。

因為大多數服務端應用或多或少在連線時都會處理一些業務,如鑑權之類的,當連線的客戶端越來越多時這一個執行緒依然會存在效能問題。

於是又有了下面的執行緒模型。

主從多執行緒

Netty(二) 從執行緒模型的角度看 Netty 為什麼是高效能的?

該模型將客戶端連線那一塊的執行緒也改為多執行緒,稱為主執行緒。

同時也是多個子執行緒來處理事件響應,這樣無論是連線還是事件都是高效能的。

Netty 實現

以上談了這麼多其實 Netty 的執行緒模型與之的類似。

我們回到之前 SpringBoot 整合長連線心跳機制 中的服務端程式碼:

    private EventLoopGroup boss = new NioEventLoopGroup();
    private EventLoopGroup work = new NioEventLoopGroup();


    /**
     * 啟動 Netty
     *
     * @return
     * @throws InterruptedException
     */
    @PostConstruct
    public void start() throws InterruptedException {

        ServerBootstrap bootstrap = new ServerBootstrap()
                .group(boss, work)
                .channel(NioServerSocketChannel.class)
                .localAddress(new InetSocketAddress(nettyPort))
                //保持長連線
                .childOption(ChannelOption.SO_KEEPALIVE, true)
                .childHandler(new HeartbeatInitializer());

        ChannelFuture future = bootstrap.bind().sync();
        if (future.isSuccess()) {
            LOGGER.info("啟動 Netty 成功");
        }
    }
複製程式碼

其實這裡的 boss 就相當於 Reactor 模型中處理客戶端連線的執行緒池。

work 自然就是處理事件的執行緒池了。

那麼如何來實現上文的三種模式呢?其實也很簡單:

單執行緒模型:

private EventLoopGroup group = new NioEventLoopGroup();
ServerBootstrap bootstrap = new ServerBootstrap()
                .group(group)
                .childHandler(new HeartbeatInitializer());
複製程式碼

多執行緒模型:

private EventLoopGroup boss = new NioEventLoopGroup(1);
private EventLoopGroup work = new NioEventLoopGroup();
ServerBootstrap bootstrap = new ServerBootstrap()
                .group(boss,work)
                .childHandler(new HeartbeatInitializer());
複製程式碼

主從多執行緒:

private EventLoopGroup boss = new NioEventLoopGroup();
private EventLoopGroup work = new NioEventLoopGroup();
ServerBootstrap bootstrap = new ServerBootstrap()
                .group(boss,work)
                .childHandler(new HeartbeatInitializer());
複製程式碼

相信大家一看也明白。

總結

其實看過了 Netty 的執行緒模型之後能否對我們平時做高效能應用帶來點啟發呢?

我認為是可以的:

  • 介面同步轉非同步處理。
  • 回撥通知結果。
  • 多執行緒提高併發效率。

無非也就是這些,只是做了這些之後就會帶來其他問題:

  • 非同步之後事務如何保證?
  • 回撥失敗的情況?
  • 多執行緒所帶來的上下文切換、共享資源的問題。

這就是一個博弈的過程,想要做到一個儘量高效的應用是需要不斷磨合試錯的。

上文相關的程式碼:

github.com/crossoverJi…

歡迎關注公眾號一起交流:

Netty(二) 從執行緒模型的角度看 Netty 為什麼是高效能的?

相關文章