100万并发连接服务器笔记之Java Netty处理1M连接会怎么样

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了100万并发连接服务器笔记之Java Netty处理1M连接会怎么样相关的知识,希望对你有一定的参考价值。

每一种该语言在某些极限情况下的表现一般都不太一样,那么我常用的Java语言,在达到100万个并发连接情况下,会怎么样呢,有些好奇,更有些期盼。
这次使用经常使用的顺手的netty NIO框架(netty-3.6.5.Final),封装的很好,接口很全面,就像它现在的域名 netty.io,专注于网络IO。
整个过程没有什么技术含量,浅显分析过就更显得有些枯燥无聊,准备好,硬着头皮吧。
测试服务器配置
运行在VMWare Workstation 9中,64位Centos 6.2系统,分配14.9G内存左右,4核。
已安装有Java7版本:
java version "1.7.0_21"
Java(TM) SE Runtime Environment (build 1.7.0_21-b11)
Java HotSpot(TM) 64-Bit Server VM (build 23.21-b01, mixed mode)

在/etc/sysctl.conf中添加如下配置:
fs.file-max = 1048576
net.ipv4.ip_local_port_range = 1024 65535
net.ipv4.tcp_mem = 786432 2097152 3145728
net.ipv4.tcp_rmem = 4096 4096 16777216
net.ipv4.tcp_wmem = 4096 4096 16777216

net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 1

在/etc/security/limits.conf中添加如下配置:
* soft nofile 1048576
* hard nofile 1048576

测试端
测试端无论是配置还是程序和以前一样,翻看前几篇博客就可以看到client5.c的源码,以及相关的配置信息等。
服务器程序
这次也是很简单呐,没有业务功能,客户端HTTP请求,服务端输出chunked编码内容。
入口HttpChunkedServer.java:
唯一的自定义处理器HttpChunkedServerHandler.java:
启动脚本start.sh
达到100万并发连接时的一些信息
每次服务器端达到一百万个并发持久连接之后,然后关掉测试端程序,断开所有的连接,等到服务器端日志输出在线用户为0时,再次重复以上步骤。在这反反复复的情况下,观察内存等信息的一些情况。以某次断开所有测试端为例后,当前系统占用为(设置为list_free_1):
total used free shared buffers cached
Mem: 15189 7736 7453 0 18 120
-/+ buffers/cache: 7597 7592
Swap: 4095 948 3147

通过top观察,其进程相关信息
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
4925 root 20 0 8206m 4.3g 2776 S 0.3 28.8 50:18.66 java

在启动脚本start.sh中,我们设置堆内存为6G。
ps aux|grep java命令获得信息:
root 4925 38.0 28.8 8403444 4484764 ? Sl 15:26 50:18 java -server...HttpChunkedServer 8000

RSS占用内存为4484764K/1024K=4379M
然后再次启动测试端,在服务器接收到online user 1023749时,ps aux|grep java内容为:
root 4925 43.6 28.4 8403444 4422824 ? Sl 15:26 62:53 java -server...

查看当前网络信息统计
ss -s
Total: 1024050 (kernel 1024084)
TCP: 1023769 (estab 1023754, closed 2, orphaned 0, synrecv 0, timewait 0/0), ports 12

Transport Total IP IPv6
* 1024084 - -
RAW 0 0 0
UDP 7 6 1
TCP 1023767 12 1023755
INET 1023774 18 1023756
FRAG 0 0 0

通过top查看一下
top -p 4925
top - 17:51:30 up 3:02, 4 users, load average: 1.03, 1.80, 1.19
Tasks: 1 total, 0 running, 1 sleeping, 0 stopped, 0 zombie
Cpu0 : 0.9%us, 2.6%sy, 0.0%ni, 52.9%id, 1.0%wa, 13.6%hi, 29.0%si, 0.0%st
Cpu1 : 1.4%us, 4.5%sy, 0.0%ni, 80.1%id, 1.9%wa, 0.0%hi, 12.0%si, 0.0%st
Cpu2 : 1.5%us, 4.4%sy, 0.0%ni, 80.5%id, 4.3%wa, 0.0%hi, 9.3%si, 0.0%st
Cpu3 : 1.9%us, 4.4%sy, 0.0%ni, 84.4%id, 3.2%wa, 0.0%hi, 6.2%si, 0.0%st
Mem: 15554336k total, 15268728k used, 285608k free, 3904k buffers
Swap: 4194296k total, 1082592k used, 3111704k free, 37968k cached

PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
4925 root 20 0 8206m 4.2g 2220 S 3.3 28.4 62:53.66 java

四核都被占用了,每一个核心不太平均。这是在虚拟机中得到结果,可能真实服务器会更好一些。 因为不是CPU密集型应用,CPU不是问题,无须多加关注。
系统内存状况
free -m
total used free shared buffers cached
Mem: 15189 14926 263 0 5 56
-/+ buffers/cache: 14864 324
Swap: 4095 1057 3038

物理内存已经无法满足要求了,占用了1057M虚拟内存。
查看一下堆内存情况
jmap -heap 4925
Attaching to process ID 4925, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 23.21-b01

using parallel threads in the new generation.
using thread-local object allocation.
Concurrent Mark-Sweep GC

Heap Configuration:
MinHeapFreeRatio = 40
MaxHeapFreeRatio = 70
MaxHeapSize = 6442450944 (6144.0MB)
NewSize = 629145600 (600.0MB)
MaxNewSize = 629145600 (600.0MB)
OldSize = 5439488 (5.1875MB)
NewRatio = 2
SurvivorRatio = 1
PermSize = 52428800 (50.0MB)
MaxPermSize = 52428800 (50.0MB)
G1HeapRegionSize = 0 (0.0MB)

Heap Usage:
New Generation (Eden + 1 Survivor Space):
capacity = 419430400 (400.0MB)
used = 308798864 (294.49354553222656MB)
free = 110631536 (105.50645446777344MB)
73.62338638305664% used
Eden Space:
capacity = 209715200 (200.0MB)
used = 103375232 (98.5863037109375MB)
free = 106339968 (101.4136962890625MB)
49.29315185546875% used
From Space:
capacity = 209715200 (200.0MB)
used = 205423632 (195.90724182128906MB)
free = 4291568 (4.0927581787109375MB)
97.95362091064453% used
To Space:
capacity = 209715200 (200.0MB)
used = 0 (0.0MB)
free = 209715200 (200.0MB)
0.0% used
concurrent mark-sweep generation:
capacity = 5813305344 (5544.0MB)
used = 4213515472 (4018.321487426758MB)
free = 1599789872 (1525.6785125732422MB)
72.48054631000646% used
Perm Generation:
capacity = 52428800 (50.0MB)
used = 5505696 (5.250640869140625MB)
free = 46923104 (44.749359130859375MB)
10.50128173828125% used

1439 interned Strings occupying 110936 bytes.

老生代占用内存为72%,较为合理,毕竟系统已经处理100万个连接。
再次断开所有测试端,看看系统内存(free -m)
total used free shared buffers cached
Mem: 15189 7723 7466 0 13 120
-/+ buffers/cache: 7589 7599
Swap: 4095 950 3145

记为list_free_2。
list_free_1和list_free_2两次都释放后的内存比较结果,系统可用物理已经内存已经降到7589M,先前可是7597M物理内存。
总之,我们的JAVA测试程序在内存占用方面已经,最低需要7589 + 950 = 8.6G内存为最低需求内存吧。
GC日志
我们在启动脚本处设置的一大串参数,到底是否达到目标,还得从gc日志处获得具体效果,推荐使用GCViewer。
GC事件概览:

其它:

总之:
只进行了一次Full GC,代价太高,停顿了12秒。
PartNew成为了停顿大户,导致整个系统停顿了41秒之久,不可接受。
当前JVM调优喜忧参半,还得继续努力等
小结
Java与与Erlang、C相比,比较麻烦的事情,需要在程序一开始就得准备好它的堆栈到底需要多大空间,换个说法就是JVM启动参数设置堆内存大小,设置合适的垃圾回收机制,若以后程序需要更多内存,需停止程序,编辑启动参数,然后再次启动。总之一句话,就是麻烦。单单JVM的调优,就得持续不断的根据检测、信息、日志等进行适当微调。
参考技术A 如果你指的是单机的话,不说Netty会怎么样,服务器都有可能直接崩溃掉,你的算一下,按平均每链接传输数据1K,100W链接大概数据量会在1G左右,G级服务器网卡也受不了的,我们在网络编程中对单机来讲,成功解决了C10K的问题,这种M级别的链接,可能暂时解决不了。对于如此大的并发,一般我们都是通过负载均衡的方式进行处理,如新浪微博,同时在线100W以上,通过约100多个节点处理,每个节点也就才10000并发左右。 参考技术B 如果你指的是单机的话,不说Netty会怎么样,服务器都有可能直接崩溃掉,你的算一下,按平均每链接传输数据1K,100W链接大概数据量会在1G左右,G级服务器网卡也受不了的,我们在网络编程中对单机来讲,成功解决了C10K的问题,这种M级别的链接,可能暂时解决不了。对于如此大的并发,一般我们都是通过负载均衡的方式进行处理,如新浪微博,同时在线100W以上,通过约100多个节点处理,每个节点也就才10000并发左右。本回答被提问者和网友采纳

Netty对读写空闲检测的支持

说起读写空闲检测机制,如果在之前的工作中没有接触过相关开发,可能会觉得这是一处难点。即便系统中涉及到了相关的机制,但大多数情况下这个功能点也会被封装成底层组件,不通过分析源码,不容易清晰理解。


根据我的了解,读写检测机制一般会应用在即时通讯领域和集群部署的环境当中。结合我的工作经验来讲,我之前参与的一个广电项目中,分布式缓存系统之间要进行数据同步,系统要间进行长链接通讯,涉及到心跳检测机制,比如消息发起方会每隔3秒钟向消息接收方发送一个数据(或心跳)包,如果指定时间内对方没有数据包返回,则再次发送,数次之后仍然没有数据包返回,那么认为接收方服务宕机或异常,此后同步不成功的数据写入到备份表,待链接重新建立或手动触发来进行再次同步。


机制大体清楚之后,下面我们写一个简单的例子来了解一下Netty对读写空闲检测支持,这个例子还是基于上一个聊天程序来进行扩展。


首先,定义ConnectionHandler类用于处理读写空闲事件,此类继承ChannelInboundHandlerAdapter(从命名可以看出此类是一个适配器类),需要重写它的userEventTriggered方法,代码如下:


public class ConnectionHandler extends ChannelInboundHandlerAdapter {

   //当读写空闲发生时,IdleStateHandler处理器被调用,会触发空闲状态事件,evt代表该空闲状态事件;

   @Override

   public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {

       if (evt instanceof IdleStateEvent) {

           IdleStateEvent event = (IdleStateEvent) evt;

           String eventType = null;

           switch (event.state()) {

               case READER_IDLE:

                   eventType = "读空闲";

                   break;

               case WRITER_IDLE:

                   eventType = "写空闲";

                   break;

               case ALL_IDLE:

                   eventType = "读写空闲";

                   break;

           }

           System.out.println(ctx.channel().remoteAddress() + "超时事件: " + eventType);

           ctx.channel().close();

       }

   }

}


此方法的第二个参数evt表示被触发的空闲事件,需要通过instanceof关键字来判断这个事件的类型是否是我们所关心的空闲状态事件——IdleStateEvent。

如果是的话,调用这个对象的state方法来分析被触发的空闲状态事件的类型,state方法的返回类型为枚举类型IdelState,它分为三种状态READER_IDLE(读空闲),WRITER_IDLE(写空闲)和ALL_IDLE(读写空闲)。通过switch语句可以对不同的空闲状态进行条件判断,进而执行相应的处理逻辑,接下来我们通过上下文对象ctx获取channel链接信息,调用close方法将链接关闭。


然后,按照套路将我们编写的ConnectionHandler类添加到ChannelInitializer中,此处应用到了我们上一个聊天程序服务端的Initializer类,我们需要对之前编写的ChatServerInitializer类进行简单改造,具体的改造代码如下:


public class ChatServerInitializer extends ChannelInitializer<SocketChannel> {

   @Override

   protected void initChannel(SocketChannel ch) throws Exception {

       ChannelPipeline pipeline = ch.pipeline();

       pipeline.addLast(new IdleStateHandler(1, 1, 1, TimeUnit.MINUTES));//空闲状态检测的一个处理器;

       pipeline.addLast(new ConnectionHandler());//空闲状态事件发生时,执行userEventTriggered回调方法;

       pipeline.addLast(new DelimiterBasedFrameDecoder(4096, Delimiters.lineDelimiter()));

       pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));

       pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));

       pipeline.addLast(new ChatServerHandler());

   }

}


从代码中我们看到,我们新增了一个IdleStateHandler类,此类由Netty所提供,它是本文的重点,它表示当channel链接不执行读操作,写操作或读写操作时会触发空闲状态事件。结合我们上一个聊天程序的例子来讲,这里所说的读操作,实际上是指客户端没有向服务端发送数据,因此服务端无法从Socket中读取数据;写操作则与之相反,服务端不向Socket中写入数据,则会触发写空闲事件,因此客户端也就接收不到服务端发来的任何数据了;读写空闲就更好理解了,读或写空闲状态只要满足其一,空闲状态事件就会被触发。(这里IdleStateHandler类的构造方法有四个参数,前三个参数分别代表了读空闲,写空闲,读写空闲状态事件触发所需要等待的空闲时间,第四个参数为时间单位,我们这里指定的是分钟,此IdleStateHandler对象表示服务端如果在一分钟内没有从某一链接读取或没有向某一链接写入数据,就会触发空闲状态事件。


最后,当空闲状态事件被触发时,我们编写的ConnectionHandler类的userEventTriggered回调方法将会被调用,根据不同的空闲状态来执行特定的处理逻辑。聊天程序服务端的启动类不需要修改,至此我们的改造基本完成。如果一个客户端在一分钟内没有向服务端发送消息,服务端控制台会打印如下信息,并关闭与此客户端的链接:


/127.0.0.1:51985超时事件: 写空闲

/127.0.0.1:51985 下线


(客户端程序在接下来的文章中介绍。)


需要额外说一些的是,我们在实现ChannelInitializer类的initChannel方法时,往往需要在pipeline中加入多个Handler处理器,这些处理器有我们自己编写的,也有Netty内置的,细心观察会发现,Netty程序的编写还是有迹可循的。另外,我们之前曾讲过pipeline对象实际上是一个包含头尾节点的双向链表,任务从pipeline的第一个handler执行到最后一个,再从最后一个handler返回到第一个。这跟我们Servlet编程中的过滤器或拦截器的机制类似,本质上都是对责任链设计模式的应用。


感谢阅读,下一篇文章再见。

以上是关于100万并发连接服务器笔记之Java Netty处理1M连接会怎么样的主要内容,如果未能解决你的问题,请参考以下文章

java Netty NIO 如何突破 65536 个端口的限制?如何做到10万~50万的长连接?

打造百万连接服务器《新版架构师系列Netty4.X视频教程》限时7天免费领取!

Java并发——Netty线程模型

如何让单机下Netty支持百万长连接?

让Netty Linux 突破100万的连接量

微言Netty:百万并发基石上的epoll之剑