Netty使用案例 -服务启动退出
了解守护线程守护线程是运行在程序后台的线程。通常守护线程是由JVM创建,用于辅助用户活着JVM工作,GC就是一个典型的守护线程。用户也可以手动的创建守护线程。我们一般程序中使用的主线程不是守护线程,Daemon线程在java里边的定义是,如果虚拟机中只有Daemon线程运行,则虚拟机退出。看以下例子:public class JvmServer {public static voi...
基于研究的代码地址
https://github.com/ljz0721cx/netty-std
了解守护线程
守护线程是运行在程序后台的线程。通常守护线程是由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的优雅退出。
- 不接收新的处理消息,将线程设置为ST_SHUTING_DOWN。
- 退出前将发送队列中尚未发送或者正在发送的消息处理完毕,把已经到期或在退出超时之前到期的定时任务执行完成,把用户注册到NIO线程的退出Hook任务执行完成。
- 所有Channel的释放,多路复用器的注册和关闭,所欲队列和定时任务的清空取消,最后是EventLoop线程退出。
- jvm的shutdownHook被触发之后,调用所有EventLoopGroup实例的shutdownGracefully方法进行优雅退出。由于Netty自身对优雅退出有完美支持。
更多推荐
所有评论(0)