天天看点

单机百万连接调优和Netty应用级别调优

作者:Grey

原文地址:单机百万连接调优和Netty应用级别调优

本文为深度解析Netty源码的学习笔记。

准备两台Linux服务器,一个充当服务端,一个充当客户端。

服务端

操作系统:CentOS 7

配置:4核8G

IP:192.168.118.138

客户端

IP:192.168.118.139

服务端和客户端均要配置java环境,基于jdk1.8。

如果服务端只开一个端口,客户端连接的时候,端口号是有数量限制的(非root用户,从1024到65535,大约6w),所以服务端开启一个端口,客户端和服务端的连接最多6w个左右。

为了模拟单机百万连接,我们在服务端开启多个端口,例如<code>8000~8100</code>,一共100个端口,客户端还是6w的连接,但是可以连接服务端的不同端口,所以就可以模拟服务端百万连接的情况。

服务端程序的主要逻辑是:

绑定<code>8000</code>端口一直到<code>8099</code>端口,一共100个端口,每2s钟统计一下连接数。

<code>channelActive</code>触发的时候,连接<code>+1</code>, <code>channelInactive</code>触发的时候,连接<code>-1</code>。

代码见:Server.java

客户端程序的主要逻辑是:

循环连接服务端的端口(从<code>8000</code>一直到<code>8099</code>)。

代码见:Client.java

准备好客户端和服务端的代码后,打包成<code>Client.jar</code>和<code>Server.jar</code>并上传到客户端和服务端的<code>/data/app</code>目录下。打包配置参考pom.xml

服务端和客户端在<code>/data/app</code>下分别准备两个启动脚本,其中服务端准备的脚本为<code>startServer.sh</code>, 客户端准备的脚本为<code>startClient.sh</code>,内容如下:

startServer.sh

startClient.sh

脚本文件见:startServer.sh 和 startClient.sh

先启动服务端

查看日志,待服务端把100个端口都绑定好以后。

在启动客户端

然后查看服务端日志,服务端在支撑了3942个端口号以后,报了如下错误:

使用<code>ulimit -n</code>命令可以查看一个jvm进程最多可以打开的文件个数,这个是局部文件句柄限制,默认是1024,我们可以修改这个值

增加如下两行

以上配置表示每个进程可以打开的最大文件数是一百万。

除了突破局部文件句柄数限制,还需要突破全局文件句柄数限制,修改如下配置文件

将这个数量修改为一百万

通过这种方式修改的配置在重启后失效,如果要使重启也生效,需要修改如下配置

在文件末尾加上

服务端和客户端在调整完局部文件句柄限制和全局文件句柄限制后,再次启动服务端,待端口绑定完毕后,启动客户端。

查看服务端日志,可以看到,服务端单机连接数已经达到百万级别。

服务端接受到客户端的数据,进行一些相对耗时的操作(比如数据库查询,数据处理),然后把结果返回给客户端。

在服务端,模拟通过<code>sleep</code>方法来模拟耗时操作,规则如下:

在<code>90.0%</code>情况下,处理时间为<code>1ms</code>

在<code>95.0%</code>情况下,处理时间为<code>10ms</code>

在<code>99.0%</code>情况下,处理时间为<code>100ms</code>

在<code>99.9%</code>情况下,处理时间为<code>1000ms</code>

代码如下

获取当前时间戳,客户端在和服务端建立连接后,会每隔1s给服务端发送数据,发送的数据就是当前的时间戳,服务端获取到这个时间戳以后,会把这个时间戳再次返回给客户端,所以客户端会拿到发送时候的时间戳,然后客户端用当前时间减去收到的时间戳,就是这个数据包的处理时间,记录下这个时间,然后统计数据包发送的次数,根据这两个变量,可以求出QPS和AVG,其中:

QPS 等于 总的请求量 除以 持续到当前的时间

AVG 等于 总的响应时间除以请求总数

客户端源码参考:Client.java

服务端源码参考:Server.java

服务端在不做任何优化的情况下,关键代码如下

运行服务端和客户端,查看客户端日志

将服务端代码做如下调整

其中<code>ServerBusinessThreadPoolHandler</code>中,使用了自定义的线程池来处理耗时的<code>getResult</code>方法。关键代码如下:

再次运行服务端和客户端,可以查看客户端日志,QPS和AVG指标都有明显的改善

实际生产过程中,<code>Executors.newFixedThreadPool(1000);</code>中配置的数量需要通过压测来验证。

我们可以通过Netty提供的线程池来处理耗时的Handler,这样的话,无需调整Handler的逻辑(对原有Handler无代码侵入),关键代码:

其中<code>businessGroup</code>是Netty自带的线程池

<code>ServerBusinessHandler</code>中的所有方法,都会在<code>businessGroup</code>中执行。

再次启动服务端和客户端,查看客户端日志

参考Netty性能调优奇技淫巧还有其他的吗?

1.如果QPS过高,数据传输过快的情况下,调用writeAndFlush可以考虑拆分成多次write,然后单次flush,也就是批量flush操作

2.分配和释放内存尽量在reactor线程内部做,这样内存就都可以在reactor线程内部管理

3.尽量使用堆外内存,尽量减少内存的copy操作,使用CompositeByteBuf可以将多个ByteBuf组合到一起读写

4.外部线程连续调用eventLoop的异步调用方法的时候,可以考虑把这些操作封装成一个task,提交到eventLoop,这样就不用多次跨线程

5.尽量调用ChannelHandlerContext.writeXXX()方法而不是channel.writeXXX()方法,前者可以减少pipeline的遍历

6.如果一个ChannelHandler无数据共享,那么可以搞成单例模式,标注@Shareable,节省对象开销对象

7.如果要做网络代理类似的功能,尽量复用eventLoop,可以避免跨reactor线程

Github

深度解析Netty源码