测试环境:
服务器 1台
OS: Red Hat Enterprise Linux Server release 5.4
CPU: 4xIntel(R) Xeon(R) CPU E5450 @ 3.00GHz
MEMORY:4G
客户端5台,配置同服务器
上述的测试环境都是采用的虚拟机,而且虚拟机性能不是很好,所以我主要是测连接数,其中有涉及到性能的地方不具可参考性。另外Comet需要关注的是它能支撑的连接数个数,而并非qps,当然qps也是我们需要考虑的性能点之一。
服务端环境配置
JDK版本1.6.0_18
Netty版本3.2.2
sudo vi /etc/sysctl.conf
加入如下配置:
net.core.somaxconn = 2048 net.core.rmem_default = 262144 net.core.wmem_default = 262144 net.core.rmem_max = 16777216 net.core.wmem_max = 16777216 net.core.somaxconn = 10000 net.core.netdev_max_backlog = 20000 net.ipv4.tcp_rmem = 7168 11264 16777216 net.ipv4.tcp_wmem = 7168 11264 16777216 net.ipv4.tcp_mem = 786432 2097152 3145728 net.ipv4.tcp_max_syn_backlog = 16384 net.ipv4.tcp_fin_timeout = 15 net.ipv4.tcp_max_syn_backlog = 16384 net.ipv4.tcp_tw_reuse = 1 net.ipv4.tcp_tw_recycle = 1 net.ipv4.tcp_max_orphans = 131072 net.ipv4.tcp_max_tw_buckets=180000 fs.file-max = 1000000
编辑完成后执行如下命令让配置生效
sudo /sbin/sysctl -p
sudo vi /etc/security/limits.conf
找到hard nofile和soft nofile配置信息,修改为如下:
* hard nofile 1000000 * soft nofile 1000000
客户端环境配置
由于系统默认参数,自动分配的端口数有限,是从32768到61000,所以我们需要更改客户端/etc/sysctl.conf的参数:
net.ipv4.ip_local_port_range = 1024 65535
编辑完成后执行如下命令让配置生效
sudo /sbin/sysctl -p
客户端程序是基于libevent写的一个测试程序,不断的建立新的连接请求。客户端与服务端需要建立大量的socket,所以我们需要调速一下最大文件描述符。客户端,需要创建六万多个socket,我设置最大为十万,在 /etc/security/limits.conf 中添加
* hard nofile 1000000 * soft nofile 1000000
java的内存调优
对于java的内存管理,肯定离不开GC的调优。长连接是一个长期占用内存的一种应用,直到连接中断才会被释放。如果按照传统的方式来分配Eden、S0、S1是存在问题的,如果按照默认的配置Eden空间大于Survivor,那么在进行GC的时候由于长连接所占用的内存并不释放,导致Survivor空间无法容纳,收集器会将无法容纳的数据丢入到Old区。
为了验证这个情况作了一个测试,java的配置参数如下:
-server -Xms1G -Xmx1G -XX:PermSize=64m -XX:+UseParallelGC -XX:+UseParallelGC -verbose:gc -XX:+PrintGCDetails
运行之后jstat日志信息如下,关注其中的红色部分,进行minor GC的时候由于S1的空间不够容纳,收集器将无法容纳的数据丢入到Old区,如果持续下去,会频繁进行Full GC,这是我们不想看到的。
S0 S1 E O P YGC YGCT FGC FGCT GCT
0.00 0.00 98.00 0.00 8.78 0 0.000 0 0.000 0.000
0.00 99.93 9.94 9.20 8.78 1 0.195 0 0.000 0.195
0.00 99.93 9.94 9.20 8.78 1 0.195 0 0.000 0.195
0.00 99.93 9.94 9.20 8.78 1 0.195 0 0.000 0.195
0.00 99.93 9.94 9.20 8.78 1 0.195 0 0.000 0.195
0.00 99.93 9.94 9.20 8.78 1 0.195 0 0.000 0.195
0.00 99.93 9.94 9.20 8.78 1 0.195 0 0.000 0.195
0.00 99.93 13.84 9.20 8.78 1 0.195 0 0.000 0.195
0.00 99.93 25.08 9.20 8.78 1 0.195 0 0.000 0.195
0.00 99.93 37.62 9.20 8.78 1 0.195 0 0.000 0.195
0.00 99.93 48.86 9.20 8.78 1 0.195 0 0.000 0.195
0.00 99.93 51.46 9.20 8.78 1 0.195 0 0.000 0.195
0.00 99.93 62.70 9.20 8.78 1 0.195 0 0.000 0.195
0.00 99.93 66.60 9.20 8.78 1 0.195 0 0.000 0.195
0.00 99.93 77.84 9.20 8.78 1 0.195 0 0.000 0.195
0.00 99.93 79.14 9.20 8.78 1 0.195 0 0.000 0.195
0.00 99.93 90.39 9.20 8.78 1 0.195 0 0.000 0.195
99.93 0.00 7.17 26.40 8.78 2 0.511 0 0.000 0.511
实际在线上的应用场景,连接每隔一段时间就会关闭。那么仍到Old区的内存几乎每次都能够完全回收,但是交给Old区来回收实在是太重了,如何避免频繁的Full GC是我们需要关注的。
接下来另外的一种配置方式,淘宝韩彰分享过这种做法。加大Survivor区,让Survivor区的空间和Eden空间一样大,这样可以保证在minor GC的时候数据不会放入到Old区。下一次执行minor GC的时候,先前的Survivor区的数据可以完全回收掉。
PS:前提是minor GC周期必须大于一个连接的生命周期,比如一个连接每隔30s会关闭,那么两次minor GC的时间间隔必须大于30s。
测试
最后,为了测试Netty支持最大的连接数,我们配置如下:
-server -Xms4G -Xmx4G -XX:NewSize=3584m -XX:PermSize=64m -XX:SurvivorRatio=1 -XX:+UseParallelGC -XX:-UseAdaptiveSizePolicy
Eden、S0、S1各1G,Old512M,UseParallelGC回收机制,加入UseAdaptiveSizePolicy不允许回收器自动调整Eden和Survivor区大小。
启动服务端的comet进程后,初始的对象占用 167.26MB。然后启动客户端连接到服务端,每个客户端建立6w个连接。当客户端的连接数达到 6w左右的时候就不能再连接了。
执行server端的dmesg命令,发现存在如下信息:
ip_conntrack: table full, dropping packet.
再执行如下命令
sudo cat /proc/sys/net/ipv4/ip_conntrack_max
是65322,看来是最大连接跟踪值开得较小了。
接下来执行sudo vi /etc/sysctl.conf 进行编辑,加入如下配置:
net.ipv4.ip_conntrack_max = 1000000 //设置最大连接跟踪值 net.ipv4.netfilter.ip_conntrack_max=1000000 net.ipv4.netfilter.ip_conntrack_tcp_timeout_time_wait=120 net.ipv4.netfilter.ip_conntrack_tcp_timeout_close_wait=60 net.ipv4.netfilter.ip_conntrack_tcp_timeout_fin_wait=120
再从新进行测试,现在连接已经可以突破6w了。
当生成12万个连接后,内存占用1194.75MB。
也就是说每个连接占用8.8k = (1194.75-167.26)*1024/120000
这只是纯粹的连接,还没有涉及到数据的传输,一旦涉及到数据的传输,每个连接占用的内存肯定会超过8.8k
当超过12万之后java进程会进行一次minor GC,耗时1.11秒(用的是虚拟机,线上环境应该会好上几倍)
具体信息如下:
398.120: [GC [PSYoungGen: 1223424K->504705K(2446720K)] 1223424K->504705K(2971008K), 1.1087630 secs] [Times: user=2.45 sys=0.81, real=1.11 secs]
持续增加连接,当增加到26万个连接系统仍然表现稳定。
当连接数达到27万左右的时候java进程进行了第二次minor GC,由于是一直保持着的长连接,数据是不会被释放的,此时杯具的事情发生了:
下面是第二次minor GC的信息
S0 S1 E O P YGC YGCT FGC FGCT GCT
0.00 41.26 100.00 0.00 8.81 1 1.109 0 0.000 1.109
5.51 41.26 100.00 0.00 8.81 2 1.109 0 0.000 1.109
21.86 41.26 100.00 0.00 8.81 2 1.109 0 0.000 1.109
40.32 41.26 100.00 0.00 8.81 2 1.109 0 0.000 1.109
56.11 41.26 100.00 0.00 8.81 2 1.109 0 0.000 1.109
67.12 41.26 100.00 0.00 8.81 2 1.109 0 0.000 1.109
78.00 41.26 100.00 0.00 8.81 2 1.109 0 0.000 1.109
80.87 41.26 100.00 0.00 8.81 2 1.109 0 0.000 1.109
80.92 41.26 100.00 0.00 8.81 2 1.109 0 0.000 1.109
80.92 41.26 100.00 0.00 8.81 2 1.109 0 0.000 1.109
.....
86.05 0.00 29.31 0.00 8.90 2 588.199 0 0.000 588.199
最终耗时587s,不可想象啊!
1777.982: [GC [PSYoungGen: 1728129K->1052673K(2446720K)] 1728129K->1052673K(2971008K), 587.0906510 secs]
结论:
上述测试场景下最多能够支撑26万左右的长连接。超过26万就会出现上述第二次minor GC的情况,可用性无法得到保障。
在真实场景下,并不是真正的保持长连接状态,当连接了一定的时间,连接会关闭掉。假如设定每个连接每隔30s会关闭,那么你只需要保证minior GC的周期大于30s就可以避免上述第二次minor GC的情况。因为放入到S1中的数据一定能在下一个GC周期内全部释放掉。
目前的测试场景还不能完全反映和还原线上真实场景,真实场景下有着各种复杂的情况,数据传输对内存的占用也会不一样。线上的连接数肯定要打折扣。
如果 minor GC 的周期大于30s,保守估计一个netty进程大概能支持10万左右长连接(1G的Eden空间的能容纳12万个连接)。
另外,感谢淘宝 李子 在整个测试过程中予以的帮助。