目錄
問題報告
一日運維報告某應用JVM進程被OOM kill
[日期] 10-15-108-158 kernel: Out of memory: Kill process 23537 (java) score 846 or sacrifice child [日期] 10-15-108-158 kernel: Killed process 23537, UID 501, (java) total-vm:6458012kB, anon-rss:3364716kB, file-rss:112kB
可以看出進程是在虛擬內存用到6.46G左右,駐留內存用到3.36G左右被OOM kill掉的。
##問題分析
應用概況
該JVM是一個大數據接口程序,供後臺報表應用查詢一些預計算生成的各種數據,Linux虛擬機配置了6GB內存。應用使用了大量的第三方庫連接MySQL,Kylin,Druid和ES等。同時JVM啓動參數配置了XX:+HeapDumpOnOutOfMemoryError
選項。
初步分析
Java堆內存,MetaSpace等使用情況
從運維Zabbix系統中可以查出此JVM運行幾周的堆內存和GC情況相當正常,並無堆內存泄露的情況。也並無hprof後綴的heap dump生成。
同時Meta Space也無任何泄露跡象。
監控記錄顯示Java線程數目也正常,並未發生線程數量劇增導致過量使用Thread Stack內存導致的泄露。
Java堆外內存分析
接着懷疑Java應用中是不是有第三方庫使用了DirectByteBuffer操作堆外內存導致的泄露。但因爲並未監控JVM堆外內存的使用情況,所以需要重新採集。
於是在重啓的JVM啓動參數中加入-XX:NativeMemoryTracking=detail
,並用crontab每小時運行jcmd {PID} VM.native_memory detail > nmt.$PID.`date '+%Y-%m-%d.%H:%M:%S'`.log
來記錄JVM管轄內存的變化。也運行
pmap -x $PID > pmapx.$PID.`date '+%Y-%m-%d.%H:%M:%S'`.log
來記錄操作系統進程內存段分佈情況。
經記錄,發現Native Memory Tracking(NMT)記錄的JVM管理的內存情況均正常,包括Internal部分反映的堆外內存使用情況均正常,如下所示:
- Internal (reserved=13043KB, committed=13043KB) (malloc=13011KB #19159) (mmap: reserved=32KB, committed=32KB)
既然JVM管理的內存均正常,那就只能懷疑是進程的原生內存發生了泄露。
進程原生內存分析(Native Memory)
通過上一個步驟每小時記錄的pmap內存使用和分段情況可以看出,進程使用的虛擬內存和駐留(RSS)內存不斷地增加,一週虛擬內存增加了2GB多。
total kB 3174032 657612 637292
total kB 3174032 681068 660744
total kB 3174032 707084 686760
total kB 3175060 732216 711884
total kB 3175060 766008 745676
.......省略幾萬字
total kB 5006796 3231676 3218728
total kB 5072332 3214132 3201180
total kB 5137868 3217816 3204860
total kB 5203404 3220000 3207032
在最新的pmap輸出中,可以發現大概有70多個加起來是65536kB(64MB)的內存塊,隨着時間推移越來越多,最後導致內存不夠。
00007f756c000000 1636 1540 1540 rw--- [ anon ]
00007f756c199000 63900 0 0 ----- [ anon ]
00007f7574000000 2552 1720 1720 rw--- [ anon ]
00007f757427e000 62984 0 0 ----- [ anon ]
00007f7578000000 2024 1900 1900 rw--- [ anon ]
00007f75781fa000 63512 0 0 ----- [ anon ]
00007f757c000000 14468 14436 14436 rw--- [ anon ]
00007f757ce21000 51068 0 0 ----- [ anon ]
00007f7580000000 4452 4420 4420 rw--- [ anon ]
00007f7580459000 61084 0 0 ----- [ anon ]
00007f7584000000 22908 22852 22852 rw--- [ anon ]
00007f758565f000 42628 0 0 ----- [ anon ]
00007f7588000000 51776 51584 51584 rw--- [ anon ]
00007f758b290000 13760 0 0 ----- [ anon ]
00007f758c000000 54680 54620 54620 rw--- [ anon ]
00007f758f566000 10856 0 0 ----- [ anon ]
00007f7590000000 44616 44544 44544 rw--- [ anon ]
00007f7592b92000 20920 0 0 ----- [ anon ]
00007f7594000000 65512 65484 65484 rw--- [ anon ]
而且這些64M的內存空間都不在Native Memory Tracking輸出的地址段內,不屬於JVM管理的內存。
原生內存詳細分析
內存內容分析
使用gdb 打印出這些內存區域的內容,起始地址使用pmap輸出的內存塊地址起始值,結束地址將起始地址加上想輸出的內存區大小。
gdb --batch --pid {PID} -ex "dump memory native_memory.dump 0x00007f7588000000 0x00007f7588001A40" 注意起始和終止地址要加上0x表示16進制
查看native_memory.dump文件,發現是
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
driver.name=Kylin JDBC Driver
driver.version=0.1
product.name=Kylin
product.version=0.1
jdbc.compliant=true
driver.version.major=0
driver.version.minor=8
database.version.major=0
database.version.minor=8
build.timestamp=20140918-2017ication Authority - G21:08U
1(c) 1998 VeriSign, Inc. - For authorized use only10U
280801235959Z01 VeriSign Trust Network0
0 UUS10U
VeriSign, Inc.1<0:U
3Class 1 Public Primary Certification Authority - G21:08U
1(c) 1998 VeriSign, Inc. - For authorized use only10U
使用腳本統計多個64M內存塊,發現90%都是類似的內容,那基本上可以定位是所使用的Kylin第三方庫引入的內存泄露。由於原生內存一般是通過malloc來申請內存,所以想通過malloc的profile工具來定位具體的內存分配模塊。
引入jemalloc
jemalloc的jeprof功能可以定位到原生內存的分配代碼棧。通過以下步驟引入jeprof。
- wget https://github.com/jemalloc/jemalloc/releases/download/5.1.0/jemalloc-5.1.0.tar.bz2
- tar xvf jemalloc-5.1.0.tar.bz2
- cd jemalloc-5.1.0
- ./configure --enable-prof --prefix=/usr/local
- make
- sudo make install
- 在啓動腳本中加入export MALLOC_CONF=“prof:true,lg_prof_sample:1,lg_prof_interval:26,prof_prefix:/{YOUR INTENDED FOLDER}/jeprof.out” to run.sh
- 同時在java命令前加上 LD_PRELOAD="/usr/local/lib/libjemalloc.so", 變成LD_PRELOAD="/usr/local/lib/libjemalloc.so" java -Xmx… -Xms…
- 啓動JVM
- 在Linux上,cd /{YOUR INTENDED FOLDER}/,
- 執行 jeprof --show_bytes --dot
which java
jeprof.out.xxxx.heap > leak_test.dot ,
jeprof.out.xxxx.heap是最新的jemalloc生成的內存統計分析. - 下載leak_test.dot 到本地folder {somepath},
- 下載graphviz(https://www.graphviz.org/download/)
- 執行dot -Tpng {somepath}/leak_test.dot -o {somepath}/leak_test.png
可以看到大部分泄露是來自於java.util.zip.Inflater.inflateBytes
java.util.zip.Inflater.inflateBytes調用棧跟蹤
使用阿里巴巴的https://alibaba.github.io/arthas/ 工具,attach到JVM,然後使用stack命令,可以看到以下調用棧。
$ options unsafe true
NAME BEFORE-VALUE AFTER-VALUE
-----------------------------------
unsafe false true
$ stack java.util.zip.Inflater inflate
Press Q or Ctrl+C to abort.
Affect(class-cnt:1 , method-cnt:2) cost in 254 ms.
ts=2019-03-24 10:20:58;thread_name=main;id=1;is_daemon=false;priority=5;TCCL=sun.misc.Launcher$AppClassLoader@18b4aac2
@java.util.zip.InflaterInputStream.read()
at java.io.FilterInputStream.read(FilterInputStream.java:133)
at java.io.FilterInputStream.read(FilterInputStream.java:107)
at java.util.Properties$LineReader.readLine(Properties.java:435)
at java.util.Properties.load0(Properties.java:353)
at java.util.Properties.load(Properties.java:341)
at org.apache.kylin.jdbc.shaded.org.apache.calcite.avatica.DriverVersion.load(DriverVersion.java:104)
at org.apache.kylin.jdbc.Driver.createDriverVersion(Driver.java:88)
at org.apache.kylin.jdbc.shaded.org.apache.calcite.avatica.UnregisteredDriver.<init>(UnregisteredDriver.java:56)
at org.apache.kylin.jdbc.Driver.<init>(Driver.java:70)
at sun.reflect.GeneratedConstructorAccessor14.newInstance(null:-1)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at java.lang.Class.newInstance(Class.java:442)
at xxxx.util.KylinUtil.getConnection(KylinUtil.java:18)
最後發現是寫業務代碼的同事非常錯誤地重複地在獲取連接時新建Driver對象所導致的。
Driver driver = (Driver) Class.forName("org.apache.kylin.jdbc.Driver").newInstance();
但進一步分析,就算是重複調用也不應該有內存泄露,接着分析Kylin JDBC Driver的代碼。
Kylin JDBC Driver內存泄露分析
通過查看源代碼,Kylin使用了https://github.com/apache/calcite-avatica 來實現JDBC Driver,Avatica中的代碼塊引入了內存泄露。
try {
final InputStream inStream =
driverClass.getClassLoader().getResourceAsStream(resourceName);
// inStream沒有close
重複調用這段代碼放大了內存泄露的規模。已經給開源項目提了issue和pull request,下個版本應該會修復這個問題。
結論
這次內存泄露使用多種工具,最後定位到了JVM原生內存的泄露根源。 技術人員碰到每一次問題,都是一次學習的機會,要沉下心來,剝絲去繭,一步步嘗試去觸及問題的本質,去享受雲開見月明那最後一刻的喜悅。