Netty 移动开发注意事项

1. 最大句柄数修改

百万长连接接入,首先需要优化的就是Linux内核参数,其中Linux最大文件句柄数是最重要的调优参数之一,默认单进程打开的最大句柄数是1024,通过ulimit -a可以查看相关参数,示例如下:

[root@lilinfeng ~]# ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 256324
max locked memory       (kbytes, -l) 64
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024

......后续输出省略

当单个推送服务接收到的链接超过上限后,就会报“too many open files”,所有新的客户端接入将失败。

通过vi /etc/security/limits.conf 添加如下配置参数:修改之后保存,注销当前用户,重新登录,通过ulimit -a 查看修改的状态是否生效。

*  soft  nofile  1000000
*  hard  nofile  1000000

需要指出的是,尽管我们可以将单个进程打开的最大句柄数修改的非常大,但是当句柄数达到一定数量级之后,处理效率将出现明显下降,因此,需要根据服务器的硬件配置和处理能力进行合理设置。如果单个服务器性能不行也可以通过集群的方式实现。


2. 当心CLOSE_WAIT

从事移动推送服务开发的同学可能都有体会,移动无线网络可靠性非常差,经常存在客户端重置连接,网络闪断等。

在百万长连接的推送系统中,服务端需要能够正确处理这些网络异常,设计要点如下:

    客户端的重连间隔需要合理设置,防止连接过于频繁导致的连接失败(例如端口还没有被释放);
    客户端重复登陆拒绝机制;
    服务端正确处理I/O异常和解码异常等,防止句柄泄露。

最后特别需要注意的一点就是close_wait 过多问题,由于网络不稳定经常会导致客户端断连,如果服务端没有能够及时关闭socket,就会导致处于close_wait状态的链路过多。close_wait状态的链路并不释放句柄和内存等资源,如果积压过多可能会导致系统句柄耗尽,发生“Too many open files”异常,新的客户端无法接入,涉及创建或者打开句柄的操作都将失败。

下面对close_wait状态进行下简单介绍,被动关闭TCP连接状态迁移图如下所示:
点击查看原图
图3-1 被动关闭TCP连接状态迁移图

close_wait是被动关闭连接是形成的,根据TCP状态机,服务器端收到客户端发送的FIN,TCP协议栈会自动发送ACK,链接进入close_wait状态。但如果服务器端不执行socket的close()操作,状态就不能由close_wait迁移到last_ack,则系统中会存在很多close_wait状态的连接。通常来说,一个close_wait会维持至少2个小时的时间(系统默认超时时间的是7200秒,也就是2小时)。如果服务端程序因某个原因导致系统造成一堆close_wait消耗资源,那么通常是等不到释放那一刻,系统就已崩溃。

导致close_wait过多的可能原因如下:

    程序处理Bug,导致接收到对方的fin之后没有及时关闭socket,这可能是Netty的Bug,也可能是业务层Bug,需要具体问题具体分析;
    关闭socket不及时:例如I/O线程被意外阻塞,或者I/O线程执行的用户自定义Task比例过高,导致I/O操作处理不及时,链路不能被及时释放。

下面我们结合Netty的原理,对潜在的故障点进行分析。

设计要点1:不要在Netty的I/O线程上处理业务(心跳发送和检测除外)。Why? 对于Java进程,线程不能无限增长,这就意味着Netty的Reactor线程数必须收敛。Netty的默认值是CPU核数 * 2,通常情况下,I/O密集型应用建议线程数尽量设置大些,但这主要是针对传统同步I/O而言,对于非阻塞I/O,线程数并不建议设置太大,尽管没有最优值,但是I/O线程数经验值是[CPU核数 + 1,CPU核数*2 ]之间。

假如单个服务器支撑100万个长连接,服务器内核数为32,则单个I/O线程处理的链接数L = 100/(32 * 2) = 15625。 假如每5S有一次消息交互(新消息推送、心跳消息和其它管理消息),则平均CAPS = 15625 / 5 = 3125条/秒。这个数值相比于Netty的处理性能而言压力并不大,但是在实际业务处理中,经常会有一些额外的复杂逻辑处理,例如性能统计、记录接口日志等,这些业务操作性能开销也比较大,如果在I/O线程上直接做业务逻辑处理,可能会阻塞I/O线程,影响对其它链路的读写操作,这就会导致被动关闭的链路不能及时关闭,造成close_wait堆积。

设计要点2:在I/O线程上执行自定义Task要当心。Netty的I/O处理线程NioEventLoop支持两种自定义Task的执行:

    普通的Runnable: 通过调用NioEventLoop的execute(Runnable task)方法执行;
    定时任务ScheduledFutureTask:通过调用NioEventLoop的schedule(Runnable command, long delay, TimeUnit unit)系列接口执行。

为什么NioEventLoop要支持用户自定义Runnable和ScheduledFutureTask的执行,并不是本文要讨论的重点,后续会有专题文章进行介绍。本文重点对它们的影响进行分析。

在NioEventLoop中执行Runnable和ScheduledFutureTask,意味着允许用户在NioEventLoop中执行非I/O操作类的业务逻辑,这些业务逻辑通常用消息报文的处理和协议管理相关。它们的执行会抢占NioEventLoop I/O读写的CPU时间,如果用户自定义Task过多,或者单个Task执行周期过长,会导致I/O读写操作被阻塞,这样也间接导致close_wait堆积。

所以,如果用户在代码中使用到了Runnable和ScheduledFutureTask,请合理设置ioRatio的比例,通过NioEventLoop的setIoRatio(int ioRatio)方法可以设置该值,默认值为50,即I/O操作和用户自定义任务的执行时间比为1:1。

我的建议是当服务端处理海量客户端长连接的时候,不要在NioEventLoop中执行自定义Task,或者非心跳类的定时任务。

设计要点3:IdleStateHandler使用要当心。很多用户会使用IdleStateHandler做心跳发送和检测,这种用法值得提倡。相比于自己启定时任务发送心跳,这种方式更高效。但是在实际开发中需要注意的是,在心跳的业务逻辑处理中,无论是正常还是异常场景,处理时延要可控,防止时延不可控导致的NioEventLoop被意外阻塞。例如,心跳超时或者发生I/O异常时,业务调用Email发送接口告警,由于Email服务端处理超时,导致邮件发送客户端被阻塞,级联引起IdleStateHandler的AllIdleTimeoutTask任务被阻塞,最终NioEventLoop多路复用器上其它的链路读写被阻塞。

对于ReadTimeoutHandler和WriteTimeoutHandler,约束同样存在

此条目发表在network分类目录,贴了标签。将固定链接加入收藏夹。