POSTS
生产环境内存泄露分析
项目基础架构: Java+gRPC+Rails
因生产环境频繁出现宕机的情况,平均一周需要重启下服务器,这谁顶得住啊。。这个问题必须解决。在开始分析问题之前,我们先重启一次服务器,然后每天记录cpu、内存使用情况。内存记录如下:
重启后第一次内存展示:
➜ ~ free -h
total used free shared buff/cache available
Mem: 7.8G 4.3G 2.7G 14M 837M 3.2G
Swap: 0B 0B 0B
第二天:
➜ ~ free -h
total used free shared buff/cache available
Mem: 7.8G 5.2G 1.1G 14M 1.5G 2.3G
Swap: 0B 0B 0B
第三天:
➜ ~ free -h
total used free shared buff/cache available
Mem: 7.8G 5.6G 691M 14M 1.6G 1.9G
Swap: 0B 0B 0B
第四天:
➜ ~ free -h
total used free shared buff/cache available
Mem: 7.8G 5.8G 330M 14M 1.7G 1.7G
Swap: 0B 0B 0B
第五天:
➜ ~ free -h
total used free shared buff/cache available
Mem: 7.8G 6.4G 181M 14M 1.2G 1.0G
Swap: 0B 0B 0B
第六天:
➜ ~ free -h
total used free shared buff/cache available
Mem: 7.8G 6.8G 211M 14M 819M 704M
Swap: 0B 0B 0B
从上面可以看到内存占用每天递增,并不会释放,基本可以断定是内存泄露了,当内存消耗光服务器就卡死了。接下来我们就开始具体分析哪里出了问题,通过top命令查看进程内存占用情况:
➜ ~ top (然后按M,按照内存占用倒序排序展示)
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
1492 uugm 20 0 5784920 1.767g 9444 S 0.0 22.7 28:10.18 java
28426 root 20 0 3069812 1.201g 24992 S 0.0 15.4 9:24.44 ruby
2501 uugm 20 0 5781664 1.083g 9564 S 0.0 13.9 12:35.30 java
2672 uugm 20 0 5064376 857924 9216 S 0.0 10.5 4:11.96 java
2628 uugm 20 0 1177596 321708 9728 S 0.0 3.9 1:15.28 node
1771 uugm 20 0 4452920 255852 9180 S 0.0 3.1 3:48.27 java
其实通过内存占用记录(这里top的数据记录就没有粘贴了),可以判断有两个java进程(1492、2501)内存一直递增,一个ruby进程(28426)一直递增。经过查看这两个java进程都使用到了grpc,其他没有使用grpc的java进程内存没有递增,很有可能是grpc的问题,当然现在只是我的猜测,我们需要进一步分析。
1.将pid为1492的java进程的整个heap堆数据dump出来,文件形式为二进制文件
jmap -dump:format=b,file=heap.bin 1492
# 会生成heap.bin文件,可能会比较大,这里是1.3g
-rw------- 1 user group 1.3G Nov 25 20:52 heap.bin
2.下载二进制文件到本地
scp user@xxx.xxx.com:/home/user/heap.bin ../mat/
3.下载Memory Analyzer (MAT)内存分析软件。MAT下载
4.然后通过MAT打开heap.bin文件
可以看到有两个可疑问题
正如我们的猜想一样,确实和grpc有关,并且是grpc中依赖的netty库有关。由上图可知netty中的worker的WeakHashMap数组中存在大量对象没有被回收,从而导致内存一直增加。
5.经过一番google,然后找到了是netty版本的问题,我们项目中grpc版本是1.0.0,其中它依赖的netty版本是4.1.3.Final,此版本存在内存泄露。具体可以看https://github.com/grpc/grpc-java/issues/2358。
上面提到说可以指定固定的线程池来解决它,也可以更新netty版本。
but the issue is fixed in Netty 4.1.6.Final.
在Netty 4.1.6.Final版本修复了这个bug,我们就更新grpc版本来解决它。grpc版本应该小幅度提升并保证能解决此bug,以免项目中使用的一些方法被新版本废弃。我们就提升grpc到1.0.3,此版本netty为4.1.6.Final
好了,目前java进程的内存泄露分析到此为止,我们还有个ruby进程,内存也是持续增加,接下来继续分析。
6.难道ruby进程也存在内存泄露?
我网上搜了一些ruby内存泄露的帖子,https://ruby-china.org/topics/27057, https://ruby-china.org/topics/35238
他们的结论差不多都是:
* 减少全局对象(在Ruby中一个对象一旦被全局对象引用,它就不会被垃圾回收)
* 减少创建大的对象
* 减少创建的对象
* 数据库查询尽量使用limit限制查询结果条数
* 避免n+1查询
其实我们Rails项目中确实用到了一些全局对象并且是大对象,会不会是这个问题?但这些都是必须的,很多接口都要用到这些大对象,如果不用全局变量,那么每次请求都要新创建一个大对象,内存占用一样的大,了解了下ruby的gc机制,这些大对象用了之后可能还不会完全释放,内存占用也许会更大。
这就僵住了呀,那该怎么解决呢?
我无奈之下重启了下puma,看看有什么不同。
touch tmp/restart.txt
我惊奇的发现服务器内存瞬间释放了2G
➜ ~ free -h
total used free shared buff/cache available
Mem: 7.8G 4.8G 1.9G 14M 1.1G 2.6G
Swap: 0B 0B 0B
难道是puma的问题?
我又开始了搜索,哈哈。找到puma内存占用相关的帖子,https://ruby-china.org/topics/35236
引用别人的结论:
puma 和 unicorn 都不会主动降低操作系统内存占用, 但会复用已经申请过的内存.
所以会随着每分钟请求量的增加导致内存不断上升. 如果超出某个极限, 则有可能导致
内存爆掉. 可以考虑使用puma_worker_killer进行控制.
似乎又有点思路了,就算没有真正解决内存泄露的问题,也可以用puma_worker_killer来控制内存占用来解决服务器宕机的问题。
puma_worker_killer github地址: https://github.com/schneems/puma_worker_killer
7.按照puma_worker_killer文档,稍作修改puma.rb
before_fork do
PumaWorkerKiller.config do |config|
config.ram = 1024*2 # mb
config.frequency = 20 # seconds
config.percent_usage = 0.98
config.rolling_restart_frequency = false
config.pre_term = -> (worker) { File.open("#{Rails.root}/log/workers-killed.log", 'w') { |file| file.write("Worker #{worker.inspect} being killed") } }
end
PumaWorkerKiller.start
end
threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
threads 0, threads_count
bind 'unix:/tmp/puma.uugm-tz.sock'
environment ENV.fetch("RAILS_ENV") { "production" }
daemonize true
workers ENV.fetch("WEB_CONCURRENCY") { 2 }
preload_app!
plugin :tmp_restart
8.经过上面更改之后的内存记录
第一天:
➜ ~ free -h
total used free shared buff/cache available
Mem: 7.8G 5.2G 806M 14M 1.8G 2.3G
Swap: 0B 0B 0B
➜ ~ top
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
3577 uugm 20 0 5750012 1.217g 19884 S 0.0 15.6 5:07.58 java (rpc)
7354 root 20 0 2716908 984140 11928 S 0.0 12.0 3:36.96 ruby (puma: worker 0)
2672 uugm 20 0 5064376 857924 9216 S 0.0 10.5 4:59.27 java (ext-jcb)
2532 uugm 20 0 5778256 691360 22564 S 0.0 8.5 2:23.33 java (play-rpc)
8558 root 20 0 2318568 591512 11860 S 0.0 7.2 0:57.80 ruby (puma: worker 1)
4015 uugm 20 0 4452948 379472 19300 S 0.0 4.6 0:49.91 java
2628 uugm 20 0 1181244 329372 13344 S 0.0 4.0 1:28.98 node
3705 root 20 0 1109064 131188 4808 S 0.0 1.6 0:08.20 ruby
第二天:
➜ ~ free -h
total used free shared buff/cache available
Mem: 7.8G 5.0G 1.0G 14M 1.8G 2.5G
Swap: 0B 0B 0B
➜ ~ top
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
3577 uugm 20 0 5755144 1.269g 19884 S 0.0 16.3 7:27.48 java (rpc)
2672 uugm 20 0 5064376 857924 9216 S 0.3 10.5 5:14.08 java (ext-jcb)
8558 root 20 0 2453736 733712 11896 S 0.0 9.0 3:17.42 ruby (puma: worker 1)
2532 uugm 20 0 5780304 672312 22564 S 0.0 8.2 3:25.27 java (play-rpc)
12938 root 20 0 2321756 576872 11844 S 0.3 7.1 0:34.98 ruby (puma: worker 0)
4015 uugm 20 0 4452948 358768 19300 S 0.3 4.4 1:06.05 java
2628 uugm 20 0 1183744 333516 13344 S 0.0 4.1 1:31.31 node
3705 root 20 0 1109064 131216 4808 S 0.0 1.6 0:12.56 ruby
这里可以发现puma worker-0 进程的pid由7354变为12938,是因为当此worker内存占用过高时,puma_worker_killer将其重启了,从worker kill日志信息也可以看到。(之后worker pid发生变化都是这个原因,不在赘述了)
第三天:
➜ ~ free -h
total used free shared buff/cache available
Mem: 7.8G 5.6G 763M 14M 1.4G 1.9G
Swap: 0B 0B 0B
第四天:
➜ ~ free -h
total used free shared buff/cache available
Mem: 7.8G 5.4G 965M 14M 1.5G 2.1G
Swap: 0B 0B 0B
第五天:
➜ ~ free -h
total used free shared buff/cache available
Mem: 7.8G 5.1G 1.0G 14M 1.6G 2.4G
Swap: 0B 0B 0B
第六天:
➜ ~ free -h
total used free shared buff/cache available
Mem: 7.8G 5.5G 1.0G 14M 1.2G 1.9G
Swap: 0B 0B 0B
第七天:
➜ ~ free -h
total used free shared buff/cache available
Mem: 7.8G 6.1G 134M 14M 1.5G 1.3G
Swap: 0B 0B 0B
经过一周的观察,并没有发现服务器宕机,是好事。但是我们看到rpc进程内存还是在递增,ruby的内存占用经过puma_worker_killer算是得到了缓解。那我们重新看下java进程的内存占用问题。
9.重新dump一份堆二进制文件,打开MAT软件。
可以看到跟之前的可疑问题不一样了,java.lang.ref.Finalizer和org.postgresql.jdbc.PgConnection这两个类的对象占用率比较高。结合上面的可疑点3、可疑点4,很有可能是数据库连接未关闭的问题。
打开数据库连接对象的具体信息:
上图可以看到我们项目com.ut.uugm.core.assessment.AssessmentBusiness类可能存在问题。我们就具体看下代码:
private static final SqlSessionFactory SESSION = MybatisConfigure.sqlSessionFactory();
public Optional<InternalBusiness> getInternalBusinessScore(long assessmentId, String packageName) {
try (SqlSession sqlSession = SESSION.openSession(true)) {
do something
}
}
在AssessmentBusiness类中可以看到很多用try-with-resources方式来自动关闭资源,首先我们需要了解为啥这种方式能自动关闭资源,是因为资源类实现了java.lang.AutoCloseable接口,那么mybatis的org.apache.ibatis.session.SqlSession接口是否继承了此接口呢
public interface SqlSession extends Closeable {
<T> T selectOne(String var1);
<T> T selectOne(String var1, Object var2);
something...
}
public interface Closeable extends AutoCloseable {
public void close() throws IOException;
}
可见SqlSession继承了此接口。通过测试,在try语句块后不能再次调用sqlSession,资源应该是被关闭了的。
既然问题定位到这里了,还是要看看代码的其他地方,我们看到这里定义了个静态常量:
private static final SqlSessionFactory SESSION = MybatisConfigure.sqlSessionFactory()
我们知道静态常量存在方法区(在Hotspot中,jdk1.8以下被称为永久代,jdk1.8以上被称为元空间),此区域存放类的基本信息,GC频率很低。如果常量没有被引用则可以被回收,因为很多业务类都使用此方式创建SqlSessionFactory,并且MybatisConfigure.sqlSessionFactory()也创建的是一个单例对象,所以多个类的SESSION应该指向是同一个SqlSessionFactory,通过MAT看SESSION哈希地址相同也可以验证。那么多个类都持有此SESSION,此常量不被回收。你可能会问:Mybatis的SqlSessionFactory不是一个应用程序就该产生一个吗,然后由它创建SqlSession,此SESSION符合要求。但是需要注意的是,这里不是SqlSessionFactory产生一个实例的问题,而是常量没被回收导致其引用的数据库连接类也没被释放的问题。
这里我们稍微改下代码,去掉静态常量,使用下面方式获取session:
try (SqlSession sqlSession = MybatisConfigure.sqlSessionFactory().openSession(true)) {
do something...
}
修改获取session代码后的内存记录:
第一天:
➜ ~ free -h
total used free shared buff/cache available
Mem: 7.8G 5.1G 1.4G 14M 1.3G 2.4G
Swap: 0B 0B 0B
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
22467 uugm 20 0 5776732 1.027g 19832 S 0.0 13.2 4:25.98 java (rpc)
27514 root 20 0 2645540 947872 10964 S 0.0 11.6 2:14.79 ruby
22641 uugm 20 0 5781796 861156 22760 S 0.0 10.5 2:49.04 java (play)
第二天:
➜ ~ free -h
total used free shared buff/cache available
Mem: 7.8G 5.7G 689M 14M 1.5G 1.8G
Swap: 0B 0B 0B
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
22467 uugm 20 0 5778780 1.108g 19832 S 0.3 14.2 6:58.27 java (rpc)
32118 root 20 0 2842148 1.001g 11024 S 0.0 12.8 4:45.64 ruby
22641 uugm 20 0 5783844 0.985g 22760 S 0.0 12.6 4:58.51 java (play)
32592 root 20 0 2583072 873044 11220 S 0.0 10.7 3:02.37 ruby
第三天:
➜ ~ free -h
total used free shared buff/cache available
Mem: 7.8G 5.7G 332M 14M 1.7G 1.7G
Swap: 0B 0B 0B
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
22467 uugm 20 0 5787984 1.197g 19828 S 0.0 15.4 14:40.34 java (rpc)
22641 uugm 20 0 5783844 1.161g 22740 S 0.0 14.9 11:20.52 java (play)
17140 root 20 0 2848292 1.016g 10992 S 0.0 13.0 2:34.20 ruby
2672 uugm 20 0 5064376 890256 7380 S 0.0 10.9 31:20.52 java
第四天:
➜ ~ free -h
total used free shared buff/cache available
Mem: 7.8G 6.0G 213M 14M 1.6G 1.5G
Swap: 0B 0B 0B
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
22467 uugm 20 0 5787984 1.223g 17488 S 0.3 15.7 18:37.58 java (rpc)
22641 uugm 20 0 5783844 1.197g 18024 S 5.3 15.4 13:44.44 java (play)
22907 root 20 0 2843176 1.012g 11208 S 0.0 13.0 2:31.84 ruby
2672 uugm 20 0 5064376 910584 7380 S 0.0 11.1 31:58.08 java
第五天:
➜ ~ free -h
total used free shared buff/cache available
Mem: 7.8G 6.0G 183M 14M 1.6G 1.4G
Swap: 0B 0B 0B
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
22641 uugm 20 0 5783844 1.357g 15940 S 0.0 17.4 16:08.37 java (play)
22467 uugm 20 0 5787984 1.232g 17480 S 0.0 15.8 23:28.00 java (rpc)
29272 root 20 0 2850588 987.8m 11248 S 0.0 12.4 3:48.20 ruby
2672 uugm 20 0 5064376 917720 7388 S 0.0 11.2 32:33.34 java
第六天:
➜ ~ free -h
total used free shared buff/cache available
Mem: 7.8G 6.1G 325M 14M 1.3G 1.3G
Swap: 0B 0B 0B
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
22641 uugm 20 0 5783844 1.390g 16064 S 0.3 17.8 18:08.42 java (play)
22467 uugm 20 0 5788248 1.276g 17520 S 0.0 16.4 26:52.01 java (rpc)
3577 root 20 0 2977596 1.104g 11056 S 0.0 14.2 3:24.49 ruby
2672 uugm 20 0 5064376 917720 7388 S 0.0 11.2 33:04.85 java
第七天:
➜ ~ free -h
total used free shared buff/cache available
Mem: 7.8G 6.1G 311M 14M 1.4G 1.4G
Swap: 0B 0B 0B
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
22641 uugm 20 0 5783844 1.350g 9932 S 0.3 17.3 20:14.24 java (play)
22467 uugm 20 0 5788248 1.277g 9712 S 0.3 16.4 29:37.44 java (rpc)
9375 root 20 0 2783884 0.986g 11060 S 0.0 12.6 2:59.86 ruby
2672 uugm 20 0 5064376 917720 7388 S 0.0 11.2 33:38.38 java
第八天:
➜ ~ free -h
total used free shared buff/cache available
Mem: 7.8G 5.5G 1.5G 14M 824M 1.9G
Swap: 0B 0B 0B
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
22467 uugm 20 0 5788248 1.272g 9712 S 0.0 16.3 30:33.06 java(rpc)
22641 uugm 20 0 5783844 1.250g 9932 S 0.0 16.0 22:23.04 java (play)
2672 uugm 20 0 5064376 917720 7388 S 0.0 11.2 34:14.03 java
由上面记录可知,应用rpc内存占用趋于稳定,不再增加,说明内存泄露问题得到了解决。