Netty使用案例 -服務啓動退出

瞭解守護線程

守護線程是運行在程序後臺的線程。通常守護線程是由JVM創建,用於輔助用戶活着JVM工作,GC就是一個典型的守護線程。用戶也可以手動的創建守護線程。我們一般程序中使用的主線程不是守護線程,Daemon線程在java裏邊的定義是,如果虛擬機中只有Daemon線程運行,則虛擬機退出。
看以下例子:

public class JvmServer {
    public static void main(String[] args) throws InterruptedException {
        long starttime = System.nanoTime();
        Thread t = new Thread(new Runnable() {
            public void run() {
                try {
                    TimeUnit.DAYS.sleep(Long.MAX_VALUE);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "Daemon-T");
        //這裏設置爲守護線程
        t.setDaemon(true);
        t.start();
        TimeUnit.SECONDS.sleep(15);
        System.out.println("系統退出,程序執行" + (System.nanoTime() - starttime) / 1000000000 + "s");
    }
}

//main方法執行完成後jvm退出,
系統退出,程序執行15s

這裏設置爲守護線程,所以在執行完main方法後,進程中只留下守護線程,所以虛擬機會退出

public class JvmServer {
    public static void main(String[] args) throws InterruptedException {
        long starttime = System.nanoTime();
        Thread t = new Thread(new Runnable() {
            public void run() {
                try {
                    TimeUnit.DAYS.sleep(Long.MAX_VALUE);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "Daemon-T");
        //這裏設置爲非守護線程
        t.setDaemon(false);
        t.start();
        TimeUnit.SECONDS.sleep(15);
        System.out.println("系統退出,程序執行" + (System.nanoTime() - starttime) / 1000000000 + "s");
    }
}

//main方法執行完成,但是進程並沒有退出
系統退出,程序執行15s

以上兩個例子可以說明:

  • 虛擬機中只有當所有的非守護線程都結束時候,虛擬機纔會結束
  • main線程運行結束,如果此時運行的其他線程全部是Daemon線程,JVM會使這些線程停止,同時退出。

Netty的NioEventLoop瞭解

public class ExitServer {
    static Logger logger= Logger.getLogger(ExitServer.class);
    public static void main(String[] args) throws InterruptedException {
        NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
        NioEventLoopGroup 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>() {
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            ChannelPipeline c = socketChannel.pipeline();
                            c.addLast(new LoggingHandler((LogLevel.INFO)));
                        }
                    });
            ChannelFuture ch=b.bind(8080).sync(); //使用同步的方式綁定服務監聽端口
        } finally {
            /*這裏註釋關閉線程組
             bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();*/
        }
    }
}

執行後沒有任何異常,程序退出了
通過b.bind(8080).sync().channel()方法綁定服務端口,並不是在調用方的線程中執行,而是通過NioEventLoop線程執行。最終的執行結果其實是調用了java NIO Socket的端口綁定操作,端口綁定執行完成,main函數就不會阻塞,如果後續沒有同步代碼,main線程就會退出,JVM進啓動堆棧信息如下:
在這裏插入圖片描述
我們期望的情況並沒有發生。可以看到線程中還有nio的相關線程。
在這裏插入圖片描述
將上邊的代碼中finally中的代碼註釋去掉再運行,發現main方法執行完後,一會jvm進程就會退出。
調用b.bind(8080).sync().channel()之後,儘管它會同步阻塞,等待端口綁定結果,但是端口綁定執行非常快,完成後傳給你續就繼續執行,程序在finally裏執行了bossGroup.shutdownGracefully();
和workerGroup.shutdownGracefully();它同時會關閉服務端的TCP連接接入線程池(bossGroup)和處理客戶端網絡I/O讀寫工作線程池(workerGroup),關閉之後,NioEventLoop線程退出,整個非守護線程就全部執行完畢了。此時main函數住線程已經執行完成,jvm就會退出,但是退出爲什麼沒有出現異常呢?是由於調用了優雅退出接口shutdownGracefully,所以整個退出過程沒有發生異常。
通過上邊的案例我們可得出以下總結:

  • NioEventLoop是非守護線程。
  • NioEventLoop運行之後,不會主動退出。
  • 只有調用shutdown系列方法,NioEventLoop纔會退出。
  • Netty是一個異步非阻塞框架

Netty同步調用

Netty是一個異步非阻塞的通信框架,所有的I/O操作都是異步的,Netty的ChannelFuture機制。但是爲了更方便使用滿足一些場景下使用同步阻塞等待I/O操作結果,所以提供了ChannelFuture,主要提供以下兩種:

  • 通過註冊監聽器GenericFutureListener,可以異步等待I/O執行結果。
  • 通過sync或者await,主動阻塞當前調用方的線程,等待操作結果,也就是通常說的異步轉同步。

註冊監聽器GenericFutureListener方式

public class ExitServer {
    static Logger logger= Logger.getLogger(ExitServer.class);
    public static void main(String[] args) throws InterruptedException {
        NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
        NioEventLoopGroup 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>() {
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            ChannelPipeline c = socketChannel.pipeline();
                            c.addLast(new LoggingHandler((LogLevel.INFO)));
                        }
                    });
            ChannelFuture ch=b.bind(8080);
            //這裏使用註冊監聽器方式
            ch.channel().closeFuture().addListener(new ChannelFutureListener() {
                public void operationComplete(ChannelFuture channelFuture) throws Exception {
                    //業務邏輯處理代碼,此處省略,如果這裏阻塞就會一直阻塞
                    //如果不實現就會退出main
                    logger.info(channelFuture.channel().toString()+"鏈路關閉");
                }
            });
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

通過sync的方式阻塞當前調用方的線程

/**
 * 程序意外退出現象
 * Created by lijianzhen1 on 2018/12/25.
 */
public class ExitServer {
    static Logger logger= Logger.getLogger(ExitServer.class);
    public static void main(String[] args) throws InterruptedException {

        NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
        NioEventLoopGroup 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>() {
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            ChannelPipeline c = socketChannel.pipeline();
                            c.addLast(new LoggingHandler((LogLevel.INFO)));
                        }
                    });

            ChannelFuture ch=b.bind(8080);
           //關閉同步調用,不會執行到finally
            ch.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

以上兩種方式都可以看到main函數處於阻塞狀態,這樣shutdownGracefully就不會執行了,程序也不再會退出。
在這裏插入圖片描述
打印main線程堆棧,發現main函數阻塞在CloseFuture中,等待Channel關閉。
在這裏插入圖片描述

實際項目中優化使用

在實際項目中我們不會使用main函數直接調用Netty服務端,業務往往是通過某種容器(Tomcat,SpringBoot等)拉起進程,然後通過容器來啓動初始化各種業務資源,因此不需要擔心Netty服務端會意外的退出,啓動netty服務端比較容易犯的錯誤是採用同步調用netty,導致初始化netty服務端的業務線程被阻塞,這種方式會導致調用方線程一直被阻塞,直到服務端監聽句柄關閉。舉例如下:
避免使用下邊的方式。

  • 初始化netty服務端
  • 同步阻塞等待服務端口關閉
  • 釋放I/O資源和句柄等
  • 調用方線程被釋放

以上沒有發揮異步的優勢,正確使用如下:

  • 初始化Netty服務
  • 綁定監聽端口
  • 向CloseFuture註冊監聽器,在監聽器中釋放資源
  • 調用方線程返回

具體代碼案例

public class ExitServer {
    static Logger logger = Logger.getLogger(ExitServer.class);

    public static void main(String[] args) throws InterruptedException {

        final NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
        final NioEventLoopGroup 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>() {
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            ChannelPipeline c = socketChannel.pipeline();
                            c.addLast(new LoggingHandler((LogLevel.INFO)));
                        }
                    });

            ChannelFuture ch = b.bind(8080).sync();
            ch.channel().closeFuture().addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture future) throws Exception {
                    //業務邏輯處理代碼,此處省略...
                    logger.info(future.channel().toString() + " 鏈路關閉");
                    bossGroup.shutdownGracefully();
                    workerGroup.shutdownGracefully();
                }
            });
            //鏈路關閉觸發closeFuture的監聽,等待服務關閉之後異步調用優雅釋放資源,這樣線程就不會阻塞
            ch.channel().close();
        } finally {
        //這裏將操作放在監聽器中
            //bossGroup.shutdownGracefully();
            //workerGroup.shutdownGracefully();
        }
    }
}

系統退出時,建議通過調用EventloopGroup的shutdownGracefully來完成內存隊列中積壓消息的處理,鏈路關閉和EventLoop線程的退出,實現停機不中斷業務。也就是所謂的優雅停機。

Netty優雅退出機制

強制退出對軟件來說就好比服務器突然斷電,會導致一系列不確定的問題。

  • 比如緩存中的數據還沒有持久化到硬盤中,導致數據丟失了。 正在進行文件寫操作,沒有更新完成,突然停止,導致文件損壞。
  • 線程的消息隊列中收到的請求消息還沒來得及處理,導致請求消息丟失。
  • 數據庫操作已經完成,例如賬戶餘額更新,準備返回應答消息給客戶端時,消息在發生隊列突然停止沒有返回。
  • 句柄資源沒有及時釋放等其他問題。

Java優雅退出通常是通過註冊Jdk的ShutsownHook來實現,當系統接收到退出指令時,首先標記系統處於退出狀態,不再接收新的消息,然後將積壓的消息處理完,最後調用資源回收將接口資源收回。
通過JDK的ShutdownHook實現優雅退出代碼如下:

public class JdkShutDown {
    public static void main(String[] args) throws InterruptedException {
        Runtime.getRuntime().addShutdownHook(new Thread(()->  {
            System.out.println("開始執行shutdown");
            System.out.println("jdk通過 ShutdownHook 優雅關閉");
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("結束執行shutdown");
        }));
        TimeUnit.SECONDS.sleep(7);
        System.exit(0);
    }
}

//執行調用結果
開始執行shutdown
jdk通過 ShutdownHook 優雅關閉
結束執行shutdown
Process finished with exit code 0

除了註冊ShutdownHook,還可以通過監聽信號量並註冊SingalHandler的方式實現優雅退出。

public class JvmSignalHandler {
    public static void main(String[] args) {
    //這裏我使用的mac,如果使用的linux系統使用跟進TERM,相當於執行kill pid。不是強制剎死,如果你使用的eclipse和idea時候點一下紅按鈕一樣的效果,記住不要點兩下。
        Signal sig = new Signal("INT"); 
        Signal.handle(sig,(s)->{
            System.out.println("signal handle start ... ");
            try {
                TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        new Thread(()->{
            try {
                System.out.println("main中的線程執行");
                TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"Daemon-T").start();
    }
}

啓動後並沒有發現SIGINT handler的線程
點擊關閉後,看到出現了SIGINT handler的線程,關閉停止時候觸發。
在這裏插入圖片描述
具體阻塞線程的堆棧信息如下。
在這裏插入圖片描述
放入ShutdownHook看看執行的效果。

public class JvmSignalHandler {
    public static void main(String[] args) {
        Signal sig = new Signal("INT");
        Signal.handle(sig,(s)->{
            System.out.println("signal handle start ... ");
            try {
                TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        Runtime.getRuntime().addShutdownHook(new Thread(()->{
            System.out.println("shutdownHook excute start ...");
            System.out.println("Netty NioEventLoopGroup shutdownGracefully ...");
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },""));

        new Thread(()->{
            try {
                System.out.println("main中的線程執行");
                TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"Daemon-T").start();
    }
}

重複上邊的操作,看到執行的結果並沒有什麼變化,由於SignalHander發生了阻塞,導致ShutdownHook無法執行,因此沒有打印ShutdownHook執行相關日誌。如果SignalHander執行操作比較耗時,建議異步或者放到ShutdownHook中。

Netty優雅退出

Netty優雅退出的接口和總的入口是EventLoopGroup,調用它的shutdownGracefully方法,代碼就是之前使用過的,主要有以下幾個方面來保證Netty的優雅退出。

  1. 不接收新的處理消息,將線程設置爲ST_SHUTING_DOWN。
  2. 退出前將發送隊列中尚未發送或者正在發送的消息處理完畢,把已經到期或在退出超時之前到期的定時任務執行完成,把用戶註冊到NIO線程的退出Hook任務執行完成。
  3. 所有Channel的釋放,多路複用器的註冊和關閉,所欲隊列和定時任務的清空取消,最後是EventLoop線程退出。
  4. jvm的shutdownHook被觸發之後,調用所有EventLoopGroup實例的shutdownGracefully方法進行優雅退出。由於Netty自身對優雅退出有完美支持。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章