今天完成了测试机器人,开始对服务器进行性能测试。
测试环境:
客户端:开始10000个连接,每个连接每秒发送一个包(业务逻辑只对内存操作)。
1、服务器内存1G,发现在连接达到1000个左右时,服务器接近假死状态。
查看gc: jstat -gcutil 21340 5000 999999
S0 S1 E O P YGC YGCT FGC FGCT GCT 0.00 100.00 100.00 100.00 38.29 908 12.490 927 1793.564 1806.054 0.00 100.00 100.00 100.00 38.29 908 12.490 928 1797.939 1810.429 0.00 100.00 100.00 100.00 38.29 908 12.490 930 1800.529 1813.019 0.00 100.00 100.00 100.00 38.29 908 12.490 933 1809.093 1821.583 0.00 100.00 100.00 100.00 38.29 908 12.490 935 1813.729 1826.219 0.00 100.00 100.00 100.00 38.29 908 12.490 938 1818.452 1830.942 0.00 100.00 100.00 100.00 38.29 908 12.490 939 1822.462 1834.952 0.00 100.00 100.00 100.00 38.29 908 12.490 941 1827.227 1839.717 0.00 100.00 100.00 100.00 38.29 908 12.490 944 1834.183 1846.673 0.00 100.00 100.00 100.00 38.29 908 12.490 946 1836.594 1849.085 0.00 100.00 100.00 100.00 38.29 908 12.490 948 1842.616 1855.106 0.00 99.99 100.00 100.00 38.29 908 12.490 951 1849.040 1861.531 0.00 100.00 100.00 100.00 38.29 908 12.490 953 1854.135 1866.625 0.00 100.00 100.00 100.00 38.29 908 12.490 956 1858.858 1871.348 0.00 100.00 100.00 100.00 38.29 908 12.490 957 1862.671 1875.161 0.00 100.00 100.00 100.00 38.29 908 12.490 959 1867.481 1879.971
看出full gc 次数频繁。
2、把内存改为2G再次测试,同时把log日志关闭(udp syslog发送,对性能影响应该不是很大,可以在最后再测试未关闭日志的情况)
这次效果明显,达到2000连接,也未产生full gc,连接数继续加大。
连接已经上升到5000,依旧无压力。
再继续往上升时,发现连续全断开了。
查看log,发现是超时断开了,设置的是2分钟超时。
3、继续解决超时的问题。
机器人连接主要操作是两步,一是登陆(相对来说比较消耗时间),二是发送指令(1秒发送1个,异步发送,速度非常快)。
我这里是创建了一个连接池来处理每个机器人。
在开始运行时,即生成10000个线程,然后依次执行登陆、发送指令,然后继续将自己放入连接池。
而问题则出现在了第一步,在开始登陆时,10000个线程全部放入了连接池,所以连接池需要全部分执行完这10000个线程的登陆,才会进入循环,不停发送指令。而这10000个线程的耗时超过了2分钟,导致大量连接被服务器主动断开。
找到问题后,就好解决了。
再创建一个连接池,所有完成登陆的连接都放入第二个连接池。这样,所有的登陆操作在第一个连接池中,完成登陆的连接在第二个连接池中。顺利解决问题。
4、在测试中发现,虽然使用了udp往rsyslog发送日志,但日志量如果过大,依旧会影响到性能。
5、OK,现在的结果比较理想。最大2G内存,通过probe查看实际消耗不到1G内存,连接数还可以继续往上提升。
在后台查看状态时,又出现问题了,每秒处理一条指令,1万个连接,每秒需要处理1万条指令,我开启了20个线程,发现根本处理不过来。指令队列中的指令越堆越多。
尝试把线程最大数量修改为50。
奇怪,修改为50线程后,没有再出现堆积指令队列的现象,再改为20看下。
果真,发现慢的原因了,游戏使用定时存储,在存储时,1万个玩家对象都要保存进去,才会导致指令队列变慢了。找到原因就好解决了,把定时存储放在另一个线程时,不要占用玩家指令的线程。
2G内存测试的1万个连接,最终稳定在7900个,流量5M左右。其中2千多个连接失败,不知道是因为我在是自己本本上开启1万个连接引起的,还是有其它bug,等下再测试。先试下3G内存开5万个连接。
找了两台PC,每台分别开启5000个连接,非常稳定,没有出现掉线现象。另一台连接提升至1万。有10来个连接断掉。看来连接断开不是服务器的原因,应该是PC上的连接数限制之类的有关。只是还没有查到具体限制。
gc也是很稳定。fullgc不是很频繁,对了,在测试中还发现异步存储原来和用户处理线程都放在同一个线程池,导致的在存储时阻塞了用户处理线程,发现后,专门为异步存储指令开始一个连接池,解决掉阻塞用户处理线程的问题。
2013.09.23 mina2改为netty4,继续进行优化。
3G内存,1万个连接,idle78%左右。网络流量发送74k/s,接收4.4M/S(未开启no_delay)
在压力测试中打开tcpnodelay,结果很多连接直接创建失败。统计了下大概有180个连接创建失败。
这次只是在客户端上打开了tcp_nodelay,查看流量没有什么变化,可能是因为1秒发送一个包,所以只是降低了延迟,对于流量并没有变化。还有可能是因为我在系统注册中已经把no_delay打开了,所以这个开关并没有生效。
再测试下服务器端打开nodelay.服务器打开nodelay后性能并无下降,返回感觉有稍微提升。
使用jprofiler进行性能分析(jprofiler官网上直接下,可以免费用10天,10天对于性能分析足够了)。
查看发现以下几种对象在内存中占用过多:
protobuf对象数量在百万以上。占用的内存达到1.5G左右。
现在开始频繁的出现fgc.
强制结束掉客户端,查看对象是否被回收。好吧,现在完全感觉是内存泄露了。
修改服务器处理程序,每次接收包后,设置autoread为false,处理完包后再设置为true.先跑2个小时看看情况再说。因为上次查看时,指令队列长度最多有400多了,这样可以限制指令队列不会太长,也可以避免以后恶意用户不间断发包把服务器卡住。
找到原因了,是因为数据包太多了,处理不过来,要发送的数据没有发送出去,造成数据不断积累,从而耗尽内存。
2013.09.26记:
1、decoder中的byte[]已经改为使用theadlocal缓存,大量减少了字节数组的创建。
2、去除了中间对象,直接在decoder中解析出ActionRunnable对象,同时此对象使用对象缓冲池回收,大量减少了创建对象的数量。
3、动手修改protobuf,能够缓存protobuf对象。
- 先从网上找到protoc的源码,动手修改、
- 添加reInit方法,这个方法和init不同,他对集合对象只执行clear,不重新创建一个list赋值。
- 添加parseFrom2,内部调用reinit方法并执行parseFrom(byte[])的功能。
- 修改完成后,gc情况大为改善。
4、动手修改protobuf中的toByteArray().
- 查看protobuf代码,发现一个protobuf中共用一个byte[],因此也可以使用threadlocal缓存进行优化。
public byte[] toByteArray() { try { final byte[] result = cn.joylab.game.net.cache.ThreadLocalCache.getBytesProtobuf(getSerializedSize()); final CodedOutputStream output = CodedOutputStream .newInstance(result); writeTo(output); output.checkNoSpaceLeft(); return result; } catch (IOException e) { throw new RuntimeException( "Serializing to a byte array threw an IOException " + "(should never happen).", e); } }
- 发现这几个方法都提供公开调用,这样不必去修改protoc直接从外部生成byte[].
/** * 使用了线程缓存,避免了byte[]的大量创建 * @param messageLite * @return * @throws IOException */ private byte[] toByteArray(MessageLite messageLite) throws IOException{ byte[] result = cn.joylab.game.net.cache.ThreadLocalCache .getBytesProtobuf(messageLite.getSerializedSize()); CodedOutputStream output = CodedOutputStream.newInstance(result); messageLite.writeTo(output); return result; }
5、动手修改protobuf中的Builder对象。
- 这里比较麻烦一些。因为分为builder对象及message对象。message对象由builder进行生成。为了减少对象产生。这里builder和message对象都需要进行缓存。
- builder对象比较方便,本身自带clear()方法,从缓存中取出直接clear()后就可以供后续使用
- message对象需要手动调用reInit清理
- builder在newBuilder中从缓存中取,在builder()后放入缓存,放入时调用clear()
- message对象在builder中取,在decoder完成后放入缓存。取出时调用reInit清理。
- 在toByteArray因为用的byte[]是缓存的,所以会遇到checkNoSpaceLeft无法通过。这里
2013.10.07 真奇怪,感觉文章丢失了几段内容,记不起来是丢失了哪几部分了。
这里先记录下国庆前了解的内容。
在每秒1万个数据包的压力下,1小时左右,jvm内存耗尽,无限开始fgc。
这里使用了java自带的内存对象分析工具
jmap -dump:format=b,file=dump1.dump 12377
将内存保存为文件。
然后使用:
jhat dump1.dump 可以使用小型http server来查看内存。
这里就是使用了这个工具来发现了netty自带的内存缓存分配造成的问题。jvm分配了3G内存,netty给自己的缓存分配了2G内存导致内存不足。
MAX_CHUNK_SIZE (I) : 1073741824 MIN_PAGE_SIZE (I) : 4096 DEFAULT_MAX_ORDER (I) : 11 DEFAULT_PAGE_SIZE (I) : 8192 DEFAULT_NUM_DIRECT_ARENA (I) : 8 DEFAULT_NUM_HEAP_ARENA (I) : 8
PoolChunk中分配了具体的内存:
16M的byte[].属性为:memoryMap
chunkSize (I) : 16777216
freeBytes (I) : 0
freeBytes为0,说明已经被分配了,为什么没有被释放掉。难道没有被使用,这样的话,需要先输出netty的statistics来查看原因。
可能找到内存未释放的原因了。我在decoder中写了两句测试代码,未被注释掉,导致内存不断被消耗而没有被释放掉。
对了,jetty有一个continuation技术,可以实现http短连接的io与业务逻辑处理线程分离,实现时,可以借签(同时和netty的http技术比较一下)。
0 条评论。