记一次海外大型SLG游戏服务器进程被OOM的修复经历

 

事情经过

最近刚接手一个多次获得海外GooglePlay推荐的SLG的游戏项目,服务器是java的netty框架写的,客户端是cocos lua。

好吧既然服务器进程运行在jvm之上,吃内存倒是挺厉害的,我一个16G内存的服务器被吃的满满的,这个时候为了解决内存不足,我开启了4G的虚拟内存,方法如下:

sudo dd if=/dev/zero of=/swapfile bs=256M count=16 
sudo mkswap /swapfile 
sudo swapon /swapfile 
#开机自动启动
echo "/mnt/swapfile swap swap defaults 0 0 " >> /etc/fstab

#After compiling, you may wish to 
#Code: 
sudo swapoff /swapfile 
sudo rm /swapfile

 开启虚拟内存之后就在线上稳定运行了半年多,突然有一天一个游戏服务进程被oom杀死了,首先我从/var/log/message里看到oom的记录,再到进程运行目录下查看了宕机前的堆栈信息,日志如下:

 

但是看内存够够的:

空闲内存还有148516KB+虚拟内存4G,此时怀疑是java的GC垃圾回收在搞鬼?首先我们知道Xms 是 GC 算法进行垃圾收集评判标准中一个必不可少的元素。另外-Xms和-Xmx设置相同时可避免Java堆自动扩展。

Xmx的内存是在Java进程启动的时候直接分配(预留)的,而不是不断增加的。因为大部分 GC 算法依赖于被分配为连续的内存块的堆,因此不能在堆需要扩大时再分配更多本机内存。所有堆内存必须预先保留。

在这里说明下Xmx指定内存并不是真正的分配,而是一种保留,内存保留 != 内存分配。当本机内存被保留时,无法使用物理内存或其他存储器作为备用内存。尽管保留地址空间块不会耗尽物理资源,但会阻止内存被用于其他用途。由保留从未使用的内存导致的泄漏与泄漏分配的内存一样严重。

那Xmx和Xmn如何设置呢?

依据的原则是根据Java Performance里面的推荐公式来进行设置。

 

具体来讲:
Java整个堆大小设置,Xmx 和 Xms设置为老年代存活对象的3-4倍,即FullGC之后的老年代内存占用的3-4倍
永久代 PermSize和MaxPermSize设置为老年代存活对象的1.2-1.5倍。
年轻代Xmn的设置为老年代存活对象的1-1.5倍。
老年代的内存大小设置为老年代存活对象的2-3倍。

BTW:
1、Sun官方建议年轻代的大小为整个堆的3/8左右, 所以按照上述设置的方式,基本符合Sun的建议。
2、堆大小=年轻代大小+年老代大小, 即xmx=xmn+老年代大小 。 Permsize不影响堆大小。
3、为什么要按照上面的来进行设置呢? 没有具体的说明,但应该是根据多种调优之后得出的一个结论。

如何确认老年代存活对象大小?
方式1(推荐/比较稳妥):
JVM参数中添加GC日志,GC日志中会记录每次FullGC之后各代的内存大小,观察老年代GC之后的空间大小。可观察一段时间内(比如2天)的FullGC之后的内存情况,根据多次的FullGC之后的老年代的空间大小数据来预估FullGC之后老年代的存活对象大小(可根据多次FullGC之后的内存大小取平均值)

方式2:(强制触发FullGC, 会影响线上服务,慎用)
方式1的方式比较可行,但需要更改JVM参数,并分析日志。同时,在使用CMS回收器的时候,有可能不能触发FullGC(只发生CMS GC),所以日志中并没有记录FullGC的日志。在分析的时候就比较难处理。
BTW:使用jstat -gcutil工具来看FullGC的时候, CMS GC是会造成2次的FullGC次数增加。 具体可参见之前写的一篇关于jstat使用的文章
所以,有时候需要强制触发一次FullGC,来观察FullGC之后的老年代存活对象大小。
注:强制触发FullGC,会造成线上服务停顿(STW),要谨慎,建议的操作方式为,在强制FullGC前先把服务节点摘除,FullGC之后再将服务挂回可用节点,对外提供服务
在不同时间段触发FullGC,根据多次FullGC之后的老年代内存情况来预估FullGC之后的老年代存活对象大小

如何触发FullGC ?
使用jmap工具可触发FullGC
jmap -dump:live,format=b,file=heap.bin <pid> 将当前的存活对象dump到文件,此时会触发FullGC
jmap -histo:live <pid> 打印每个class的实例数目,内存占用,类全名信息.live子参数加上后,只统计活的对象数量. 此时会触发FullGC。

 

这里提供三个计算公式:

年老代大小=-Xmx-Xmn

整个堆大小=年轻代大小 + 年老代大小 + 持久代大小。

持久代一般固定大小为64m,所以增大年轻代(-Xmn)后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8

 JVM其他参数

-XX:SurvivorRatio

用于设置Eden和其中一个Survivor的比值,默认比例为8(Eden):1(一个survivor),这个值也比较重要。

例如:-XX:SurvivorRatio=4:设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6。
-Xss128k:设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。
-Xloggc:file
与-verbose:gc功能类似,只是将每次GC事件的相关情况记录到一个文件中,文件的位置最好在本地,以避免网络的潜在问题。
若与verbose命令同时出现在命令行中,则以-Xloggc为准。
-Xprof
跟踪正运行的程序,并将跟踪数据在标准输出输出;适合于开发环境调试。

-Xrunhprof

-Xdebug:JVM调试参数,用于远程调试

例如在tomcat中的远程调试设置方法为-Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8000

-Xbootclasspath:

-Xbootclasspath用来指定你需要加载,但不想通过校验的类路径。JVM 会对所有的类在加载前进行校验并为每个类通过一个int数值来应用。这个是保证 JVM稳定的必要过程,但比较耗时,如果你希望跳过这个过程,就把你的类通过这个参数来指定。

 -Xnoclassgc:

 -Xnoclassgc 表示不对方法区进行垃圾回收。请谨慎使用。

-XX:MaxMetaspaceSize

java8中-XX:MaxMetaspaceSize=10M设置MetaSpace的最大值为10m。默认是Java的Metaspace空间:不受限制

 

言归正传

解决问题要紧,这里我们注意到

V  [libjvm.so+0x4e0537]  report_vm_out_of_memory(char const*, int, unsigned long, VMErrorType, char const*)+0x67

内存不足导致分配失败这就是JVM发生致命的原因所在

接下来就是所有线程信息,看到其中有许多线程处于阻塞状态:

我们先查看当前JVM的堆栈参数:

java -XX:+PrintFlagsFinal -version | grep -iE 'HeapSize|PermSize|ThreadStackSize'

[root@bin]# ./java -XX:+PrintFlagsFinal -version | grep -iE 'HeapSize|PermSize|ThreadStackSize'
     intx CompilerThreadStackSize                   = 0                                   {pd product}
    uintx ErgoHeapSizeLimit                         = 0                                   {product}
    uintx HeapSizePerGCThread                       = 87241520                            {product}
    uintx InitialHeapSize                          := 260046848                           {product}
    uintx LargePageHeapSizeThreshold                = 134217728                           {product}
    uintx MaxHeapSize                              := 1073741824‬                          {product}
     intx ThreadStackSize                           = 1024                                {pd product}
     intx VMThreadStackSize                         = 1024                                {pd product}
java version "1.8.0_212"
Java(TM) SE Runtime Environment (build 1.8.0_212-b10)
Java HotSpot(TM) 64-Bit Server VM (build 25.212-b10, mixed mode)

 显然这个是配小了,我修改了其中一个服务器的启动脚本参数,将VMOPT的Xms和Xmx参数放大:

@TITLE [gameServer1:6131]
@SET JRE_PATH=../runtime/jre/bin
@SET LIBS_PATH=../runtime/libs
@set VMOPT=-server -XX:+TieredCompilation -Xms512m -Xmx4096m -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:./log/gs.gc
@SET CLASSPATH=%LIBS_PATH%/*.jar;./gameServer.jar
@PATH %JRE_PATH%
@java %VMOPT% GameMain

现在运行下来快两周了,暂时再没有这种oom问题了,java的JVM坑不少,内存倒是吃的挺厉害的,作为多年的C++技术人再次鄙视了java一次。

之前做过的游戏(比如摩尔庄园,赛尔号,忍者Q传,超神王者,hitmang go系列,unkilled,Knights of ages)有很多踩坑的地方,没有时间和精力整理,接下来我会将后边项目遇到的线上游戏问题汇总到该专栏。

 

已标记关键词 清除标记
实付 9.90元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值