之前的一篇文章从TCP协议层面讲述了产生大量CLOSE_WAIT的原因,但是并没有深入到线上的服务器以及所使用的框架。所以这篇文章是上一篇文章的延伸,侧重于从netty框架层面解释产生大量CLOSE_WAIT的原因。

下面的内容主要分为以下几个部分:

  • netty的基本原理

  • 线上的基本架构

  • 产生大量CLOSE_WAIT的原因

  • 如何避免这种情况

netty的基本原理

大家的都知道Netty采用的是NIO,网上也有很多资料讲解NIO的工作原理,所以这里就不再赘述了,如果你感兴趣的话可以自己找一些资料看一看。Netty的整体架构偏向于事件驱动,内部有两个主要的线程组: boss和worker。boss线程组用于接收客户端的accept请求,然后将初始化好的channel丢给worker线程组,worker线程组又可以叫NIO线程组,其主要负责:

  • channel的pipeline初始化

  • pipeline中handler的调用

下面这张图简单的描述了二者之间是如何交互工作的,

http://ww2.sinaimg.cn/large/87f5e2f6jw1fa3g8n536lj20vl124agi.jpg

其中handler的执行依赖于你设置的线程池,在默认情况下所有的handler的执行都由Worker线程来完成。比较好的方法是将业务处理放置到其他线程池中,不至于堵塞Worker线程。

线上的基本架构

除了netty默认启动的两个线程池,我们还额外添加了一个大的业务线程池,用于进行业务处理,这是因为业务处理依赖后端的资源,且耗时较长。使用业务线程池可以使得Worker线程可以专注于IO方面的操作。耗时较长的handler是通过addLast(EventExecutorGroup executor, String name, ChannelHandler handler)在pipeline初始化的时候注册到对应的业务线程池上的。这里使用的业务线程池是netty提供的DefaultEventExecutorGroup,它由N个线程组成,每个线程都有自己单独的队列。业务handler对应的任务会进入到线程池中某个线程的队列中,由线程按序进行处理。

产生大量CLOSE_WAIT的原因

到这里你可能会产生疑惑,似乎上述的使用方法没有什么太大的问题,那么大量CLOSE_WAIT是如何产生的呢?上面的架构在大多数情况下都可以非常正常的工作,但是当后端资源变慢的时候,就会产生问题。

在pipeline进行初始化的时候,会将handler绑定到对应的线程上,而绑定使用的就是addLast方法。当使用了额外的业务线程池处理耗时的handler的时候,Worker线程会提交一个任务(任务A)到对应的业务线程的队列中,这个任务主要就负责将handler绑定到这个业务线程上。如果当前业务处理遇到问题(比如后端数据库变慢等),就会有很多的业务task堆积在队列中,那么这个绑定线程的任务就无法得到及时的处理。而Worker线程在进行pipeline初始化的时候是同步的,也就是如果某个handler绑定线程耗时较长,就会导致Worker线程一直处于等待状态。当出现这种情况的时候,新进来的请求就会被堆积在IO线程中,得不到及时的处理。如果这时候出现大量的断连请求,而IO线程无法及时的进行处理,CLOSE_WAIT就会突增。

http://ww4.sinaimg.cn/large/87f5e2f6jw1fa3g8muqb4j20j60ommzj.jpg

上图简单的描述了Worker线程和业务线程的交互。

如何避免这种情况

既然知道了产生问题的根本原因,那么有什么好的办法能够解决这个问题呢?我们主要需要解决的是Worker线程卡住的问题,使用业务线程池是没有问题的,问题在于我们初始化pipeline的时候依赖了业务线程池,我们需要保证IO线程每一次处理任务都尽可能的快。于是有了以下的解决方法:

在Handler内部添加业务线程池,并且将所有Handler都注册在Worker线程上。这样可以保证Handler的注册不会阻塞Worker线程,当Handler需要处理任务的时候将任务提交到内部线程池,并且快速返回。业务线程池完成任务后回调对应的Handler进行下一步的处理。这样就可以保证整个过程不会因为资源慢等问题而导致的Worker线程卡住从而导致的CLOSE_WAIT突增等问题。。。

http://ww4.sinaimg.cn/large/87f5e2f6jw1fa3g8mj9vtj20md0dhjsw.jpg