前言
在遇到實(shí)際性能問題時(shí),除了關(guān)注系統(tǒng)性能指標(biāo)。還要結(jié)合應(yīng)用程序的系統(tǒng)的日志、堆棧信息、GClog、threaddump等數(shù)據(jù)進(jìn)行問題分析和定位。關(guān)于性能指標(biāo)分析可以參考前一篇JVM性能調(diào)優(yōu)實(shí)踐——性能指標(biāo)分析。
JVM的調(diào)優(yōu)和故障處理可以使用JDK的幾個(gè)常用命令工具。因?yàn)楸疚氖腔贒ocker容器內(nèi)部的Springboot服務(wù)。需要調(diào)整一下docker容器的啟動參數(shù),才可以使用jmap等工具。jmap命令需要使用Linux的Capability的PTRACE_ATTACH權(quán)限。而Docker自1.10在默認(rèn)的seccomp配置文件中禁用了PTRACE_ATTACH。目前使用的Docker version是17.04.0-ce。支持的Capability列表可以詳看runtime-privilege-and-linux-capabilities。
調(diào)整Capability的方式也比較方便。可以如下直接在運(yùn)行參數(shù)后面加?cap_add,cap-drop
$docker run --cap-add=ALL --cap-drop=MKNOD ...1
也可以在compose中增加:
cap_add: - ALL cap_drop: - NET_ADMIN - SYS_ADMIN12345
Docker容器中的服務(wù)進(jìn)程
在排查問題時(shí),一般是先通過JVM性能調(diào)優(yōu)實(shí)踐——性能指標(biāo)分析中的幾個(gè)命令來分析基礎(chǔ)的服務(wù)器狀態(tài)和信息。在微服務(wù)架構(gòu)中,每臺服務(wù)器部署著若干運(yùn)行著服務(wù)的容器。在不能通過應(yīng)用日志或者問題現(xiàn)象定位問題服務(wù)時(shí),需要找到問題容器。
先通過TOP命令找到耗費(fèi)關(guān)鍵資源的進(jìn)程。
top - 11:45:13 up 318 days, 20:43, 2 users, load average: 0.15, 0.19, 0.18Tasks: 172 total, 1 running, 171 sleeping, 0 stopped, 0 zombie%Cpu(s): 3.1 us, 1.9 sy, 0.0 ni, 94.7 id, 0.0 wa, 0.0 hi, 0.3 si, 0.0 stKiB Mem: 8175392 total, 7868636 used, 306756 free, 204400 buffersKiB Swap: 0 total, 0 used, 0 free. 849564 cached Mem PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 31399 root 20 0 3585612 806804 12228 S 3.0 9.9 548:20.94 java 6331 root 20 0 3445612 925660 15784 S 2.7 11.3 41:40.29 java 31122 root 20 0 3460712 888776 11568 S 2.0 10.9 484:19.31 java 31147 root 20 0 3288180 811476 12748 S 1.3 9.9 263:44.73 java 8506 root 20 0 3254088 750880 6116 S 1.0 9.2 760:45.19 java 22940 root 20 0 1029012 70584 23396 S 0.7 0.9 0:10.68 node 24550 root 20 0 1229088 43096 8712 S 0.7 0.5 160:15.74 node 7 root 20 0 0 0 0 S 0.3 0.0 606:49.74 rcu_sched 454 sshd 20 0 32792 1924 188 S 0.3 0.0 29:15.40 nginx 13721 root 20 0 25396 1956 1324 S 0.3 0.0 56:29.17 AliYunDunUpdate 16225 root 20 0 3072752 429296 6848 S 0.3 5.3 42:51.01 java 20795 root 20 0 2408848 75344 3960 S 0.3 0.9 2361:22 java 23581 root 20 0 16736 2676 2196 R 0.3 0.0 0:00.01 top 31352 root 20 0 206920 1488 1024 S 0.3 0.0 1:20.48 docker-containe 32000 root 20 0 3061760 403708 6548 S 0.3 4.9 127:01.39 java ... 省略其他信息1234567891011121314151617181920212223
因?yàn)镈ocker容器中還有java進(jìn)程,所以需要找到具體的父子進(jìn)程id.用ps -ef命令如下所示。第二列是PID(進(jìn)程ID),第三列是PPID(父進(jìn)程ID)。
$ps -ef |grep java root 6310 6293 0 May21 ? 00:00:00 /bin/sh -c java -Dcontainer.host.ip=...root 6331 6310 2 May21 ? 00:41:51 java -Dcontainer.host.ip= -server ... root 8482 8465 0 Apr16 ? 00:00:00 /bin/sh -c java -Dcontainer.host.ip...root 8506 8482 1 Apr16 ? 12:40:53 java -Dcontainer.host.ip= -server...... 省略其他信息123456
可以使用docker inspect查看容器內(nèi)部信息,找到對應(yīng)的容器實(shí)例的進(jìn)程信息。如下即可打印當(dāng)前宿主機(jī)的所有運(yùn)行的容器實(shí)例的PID,為了方便映射,可以打印對應(yīng)容器名字,或者容器ID:
## 打印容器pid和容器id$docker ps -q | xargs docker inspect --format '{{.State.Pid}}, {{.ID}}' | grep "^${PID}"## 打印容器pid和容器name $ docker ps -q | xargs docker inspect --format '{{.State.Pid}}, {{.Name}}' | grep "^${PID}" 6310, /service-item31369, /gateway-api31094, /service-resource31025, /service-trade30916, /service-user16204, /service-analytics8482, /service-financial... 省略其他信息12345678910111213
如果要分析最消耗內(nèi)存的進(jìn)程,對應(yīng)的pid= 6331,其所在的docker進(jìn)程id也即父進(jìn)程id= 6310,可以定位出service-item服務(wù)最消耗內(nèi)存資源。定位到服務(wù)之后,即可使用docker exec -it service-item ‘/bin/sh’查看容器內(nèi)部信息。
JVM調(diào)優(yōu)基礎(chǔ)命令
在容器內(nèi)部,就可以進(jìn)一步使用jdk提供的jps、jstack、jstat、jmap等工具來進(jìn)行jvm問題排查和調(diào)優(yōu)。
jps[options] [hostid]
jps主要用來輸出JVM中運(yùn)行的進(jìn)程狀態(tài)信息。
-q 輸出類名、Jar名和傳入main方法的參數(shù)
-m 輸出傳入main方法的參數(shù)
-l 輸出main類或Jar的全限名
-v 輸出傳入JVM的參數(shù)
如下查看運(yùn)行的java進(jìn)程信息,打印jar名以及運(yùn)行main方法傳入的參數(shù):
/opt/app # jps -l -m6 /opt/app/app.jar --server.port=8080327 sun.tools.jps.Jps -l -m1234
jstat
jstat - [-t] [-h] [ [] 1
jstat命令可以用于持續(xù)觀察虛擬機(jī)內(nèi)存中各個(gè)分區(qū)的使用率以及GC的統(tǒng)計(jì)數(shù)據(jù)。vmid是Java虛擬機(jī)ID,在Linux/Unix系統(tǒng)取進(jìn)程ID。
如下面輸出的信息,采樣時(shí)間間隔為1000ms,采樣5次:
/opt/app # jstat -gc 6 1000 5 S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT 1536.0 1536.0 1233.7 0.0 171520.0 169769.2 249344.0 57018.6 93912.0 91906.8 11264.0 10853.7 6224 47.439 5 3.423 50.8631536.0 1536.0 1233.7 0.0 171520.0 169805.4 249344.0 57018.6 93912.0 91906.8 11264.0 10853.7 6224 47.439 5 3.423 50.8631536.0 1536.0 0.0 1536.0 171520.0 3527.9 249344.0 60347.4 96728.0 94808.1 11520.0 11174.7 6225 47.453 5 3.423 50.8761536.0 1536.0 0.0 1536.0 171520.0 4742.1 249344.0 60347.4 96728.0 94808.1 11520.0 11174.7 6225 47.453 5 3.423 50.8761536.0 1536.0 0.0 1536.0 171520.0 7589.3 249344.0 60347.4 96728.0 94808.1 11520.0 11174.7 6225 47.453 5 3.423 50.87612345678
上述各個(gè)列的含義:
S0C、S1C、S0U、S1U:young代的Survivor 0/1區(qū)容量(Capacity)和使用量(Used)。0是FromSurvivor,1是ToSurvivor。
EC、EU:Eden區(qū)容量和使用量
OC、OU:年老代容量和使用量
MC、MU:元數(shù)據(jù)區(qū)(Metaspace)已經(jīng)committed的內(nèi)存空間和使用量
CCSC、CCSU:壓縮Class(Compressed class space)committed的內(nèi)存空間和使用量。
YGC、YGT:young代GC次數(shù)和GC耗時(shí)
FGC、FGCT:Full GC次數(shù)和Full GC耗時(shí)
GCT:GC總耗時(shí)
可以通過分區(qū)占用量上看到,在第2-3秒之間發(fā)生了一次YGC。YGC次數(shù)+1,并且Survivor from區(qū)的內(nèi)存空間從1233.7->0,Survivor from從0->1536。Eden區(qū)也釋放了很多內(nèi)存空間。其他變化的空間占用也有元數(shù)據(jù)區(qū)以及元數(shù)據(jù)區(qū)的壓縮Class區(qū)。Compressed class space也是元數(shù)據(jù)區(qū)的一部分,默認(rèn)是1G,也可以關(guān)閉。具體的jvm8內(nèi)存分布不再詳述。下一篇GC優(yōu)化會再展開整理下。
如果只看gc的總統(tǒng)計(jì)信息,也可以用jstat -gcutil vmid查詢:
/opt/app # jstat -gcutil 6 S0 S1 E O M CCS YGC YGCT FGC FGCT GCT 0.00 100.00 73.76 24.20 98.02 97.00 6225 47.453 5 3.423 50.876 123
jmap [option] pid
jmap可以用來查看堆內(nèi)存的使用詳情。內(nèi)存各個(gè)分區(qū)可以通過jmap -heap pid來查看。得到的輸出如下:
$jmap -heap 6Attaching to process ID 6, please wait...Debugger attached successfully.Server compiler detected.JVM version is 25.121-b13using thread-local object allocation.Parallel GC with 2 thread(s)Heap Configuration: MinHeapFreeRatio = 0 MaxHeapFreeRatio = 100 MaxHeapSize = 536870912 (512.0MB) NewSize = 44564480 (42.5MB) MaxNewSize = 178782208 (170.5MB) OldSize = 89653248 (85.5MB) NewRatio = 2 SurvivorRatio = 8 MetaspaceSize = 21807104 (20.796875MB) CompressedClassSpaceSize = 1073741824 (1024.0MB) MaxMetaspaceSize = 17592186044415 MB G1HeapRegionSize = 0 (0.0MB)Heap Usage:PS Young GenerationEden Space: capacity = 170393600 (162.5MB) used = 99020080 (94.43290710449219MB) free = 71373520 (68.06709289550781MB) 58.11255821814904% usedFrom Space: capacity = 4194304 (4.0MB) used = 786432 (0.75MB) free = 3407872 (3.25MB) 18.75% usedTo Space: capacity = 4194304 (4.0MB) used = 0 (0.0MB) free = 4194304 (4.0MB) 0.0% usedPS Old Generation capacity = 255328256 (243.5MB) used = 65264912 (62.24147033691406MB) free = 190063344 (181.25852966308594MB) 25.561178783126927% used39531 interned Strings occupying 4599760 bytes.1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
Heap Configuration是堆內(nèi)存的配置信息。可以通過運(yùn)行參數(shù)改變。一般通過分析內(nèi)存分布和使用情況以及GC信息,可以針對不同的應(yīng)用不斷調(diào)整到合適的堆內(nèi)存分區(qū)配置。
Heap Usage可以看堆內(nèi)存實(shí)時(shí)的占用情況。
使用jmap -histo[:live] pid查看堆內(nèi)存中的對象的數(shù)目,占用內(nèi)存(單位是byte),如果帶上live則只統(tǒng)計(jì)活對象,如下:
/opt/app/logs # jmap -histo:live 6 | more num #instances #bytes class name---------------------------------------------- 1: 127610 19132008 [C 2: 6460 4074512 [B 3: 37041 3259608 java.lang.reflect.Method 4: 125182 3004368 java.lang.String 5: 86616 2771712 java.util.concurrent.ConcurrentHashMap$Node 6: 70783 2265056 java.util.HashMap$Node 7: 17686 1967496 java.lang.Class 8: 15834 1448440 [Ljava.util.HashMap$Node; 9: 35360 1414400 java.util.LinkedHashMap$Entry 10: 21948 1231624 [Ljava.lang.Object; 11: 9940 1165728 [I 12: 986 1064480 [Ljava.util.concurrent.ConcurrentHashMap$Node; 13: 18685 1046360 java.util.LinkedHashMap 14: 30351 971232 java.lang.ref.WeakReference 15: 50340 805440 java.lang.Object 16: 13490 539600 java.lang.ref.SoftReference 17: 17705 513768 [Ljava.lang.String; 18: 18781 450744 org.springframework.security.access.method.DelegatingMethodSecurityMetadataSource$DefaultCacheKey 19: 20272 434456 [Ljava.lang.Class; 20: 17270 414480 java.beans.MethodRef 21: 23616 377856 java.lang.Integer 22: 11192 358144 java.util.LinkedList 23: 14911 357864 java.util.ArrayList 24: 5700 319200 java.beans.MethodDescriptor12345678910111213141516171819202122232425262728293031
以上示例的排序是按照占用內(nèi)存字節(jié)數(shù)倒序的。class name列中”[C,[B,[I “是代表char,byte,int.”[L+類名”代表其他實(shí)例。這種寫法跟Class文件的Java的類型表述含義是一致的。
在進(jìn)行問題排查時(shí),可以使用jmap把進(jìn)程內(nèi)存使用情況dump到文件中,或者dump**.hprof**文件,在本地使用MAT(Eclipse Memory Analyzer)進(jìn)行分析。也可以直接用jhat分析查看。
/opt/app# jmap -dump:format=b,file=heapdump 6Dumping heap to /opt/app/logs/heapdump ...Heap dump file created /opt/app# jmap -dump:live,format=b,file=heapLive.hprof 6 123456
jstack [option] pid
jstack可以用來查看Java進(jìn)程內(nèi)的線程堆棧信息。
-l long listings,會打印出額外的鎖信息,在發(fā)生死鎖時(shí)可以用jstack -l pid來觀察鎖持有情況
-m mixed mode,不僅會輸出Java堆棧信息,還會輸出C/C++堆棧信息(比如Native方法)
輸出信息如下:
/opt/app # jstack -l 6Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.121-b13 mixed mode):"elasticsearch[Oneg the Prober][listener][T#1]" #221 daemon prio=5 os_prio=0 tid=0x00007fc2a418a800 nid=0x195 waiting on condition [0x00007fc28318d000] java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method) - parking to wait for <0x00000000e29f88d0> (a java.util.concurrent.LinkedTransferQueue) at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) at java.util.concurrent.LinkedTransferQueue.awaitMatch(LinkedTransferQueue.java:737) at java.util.concurrent.LinkedTransferQueue.xfer(LinkedTransferQueue.java:647) at java.util.concurrent.LinkedTransferQueue.take(LinkedTransferQueue.java:1269) at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1067) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1127) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) at java.lang.Thread.run(Thread.java:745) Locked ownable synchronizers: - None12345678910111213141516171819
在線上問題排查線程鎖信息時(shí),jstack是個(gè)非常好用的工具,結(jié)合應(yīng)用日志可以迅速定位到問題線程。
Java性能分析工具
對于Java性能調(diào)優(yōu),以前一直比較好用的工具是JRockit,JProfile(商業(yè))等工具,但隨著JDK7 up40版本之后,jdk會自帶JMC(JavaMissionControl)工具。可以分析本地應(yīng)用以及連接遠(yuǎn)程ip使用。提供了實(shí)時(shí)分析線程、內(nèi)存,CPU、GC等信息的可視化界面。從jdk8 up40開始,JMC還提供了在運(yùn)行時(shí)創(chuàng)建JFR記錄(飛行記錄器)。如果是全面分析heap dump,再綜合使用MAT(Eclipse Memory Analyzer)。基本就可以做很多日常的性能調(diào)優(yōu)以及線上問題排查了。下文簡單介紹一些JMC,基于java version “1.8.0_60”。
Java Mission Control
在Mac上使用的話,需要先找到j(luò)dk中的jmc路徑。
$find /Library/Java -name missioncontrol 12
在我本地的目錄是/Library/Java/JavaVirtualMachines/jdk1.8.0_60.jdk/Contents/Home/lib/missioncontrol/。打開jmc之后,常用的話可以留在dock下面。
啟動需要觀察的應(yīng)用,然后即可在JMC的MBean服務(wù)器中觀察到綜合信息如下:
進(jìn)一步觀察內(nèi)存以及GC的情況,視圖如下,可以觀察到運(yùn)行時(shí)內(nèi)存各個(gè)分區(qū)的占用率。對于“堆直方圖”默認(rèn)是不開啟的,可以通過右上角的刷新值來啟用,會影響性能。一般用于排查內(nèi)存中的大對象的回收問題以及OOM問題時(shí)可以開啟觀察。
對于線程死鎖、線程池資源方面的分析,可以到線程視圖中觀察活動線程。
Java飛行記錄器(Java Flight Recorder)
Java 8 up40開始,可以使用JMC創(chuàng)建JFR記錄。JFR可以采樣分析收集Java應(yīng)用程序以及JVM的信息, 它的最小開銷小于2%,不會影響其他JVM優(yōu)化。JFR不會記錄所有方法調(diào)用,只會探測熱點(diǎn)方法,但不包含Native方法的線程采樣。如果要開啟JFR,需要應(yīng)用啟動參數(shù)中添加:
-XX:+UnlockCommertialFeatures -XX:+FlightRecorder 1
一般還是建議本地調(diào)優(yōu)和分析時(shí)使用。JFR可以提供固定時(shí)間的采樣(默認(rèn)是1min),以及持續(xù)時(shí)間的記錄。它們都會dump到一個(gè)“.jfr”的文件中。
分析內(nèi)存信息如下,可以看到內(nèi)存使用量,以及基礎(chǔ)的GC配置和統(tǒng)計(jì)信息:
詳細(xì)分析內(nèi)存情況時(shí),需要進(jìn)一步查看“內(nèi)存分配”以及“對象統(tǒng)計(jì)信息”。其中“對象統(tǒng)計(jì)信息”也是默認(rèn)不開啟的,需要在創(chuàng)建jfr時(shí)選擇“啟用”如下:
然后即可看到對象統(tǒng)計(jì)信息:
對于熱點(diǎn)方法以及熱點(diǎn)線程的采樣分析圖表也很直觀,在分析一些循環(huán)調(diào)用時(shí)可以重點(diǎn)關(guān)注熱點(diǎn)方法,對于有問題的熱點(diǎn)方法可以進(jìn)一步查看“堆棧跟蹤”下的調(diào)用鏈:
總結(jié)
本文主要介紹了java常用的性能優(yōu)化和排查問題的工具,以及JavaMissionControl工具的一些功能。JMC是官方提供的免費(fèi)工具,結(jié)合MAT,基本可以處理性能優(yōu)化的80%場景。JMC還可以鏈接遠(yuǎn)程ip進(jìn)行分析。但對于線上問題排查,還是建議使用jstat,jstack,jmap工具等,結(jié)合top、vmstat等快速排查和定位問題。
性能排查一般問題都集中在cpu、內(nèi)存。前者分析線程,后者分析具體出現(xiàn)問題的內(nèi)存分區(qū)。對于磁盤、IO等資源瓶頸需要綜合很多業(yè)務(wù)場景進(jìn)行具體定位。
還有一部分比較大的話題,就是GC,會在下一篇進(jìn)行總結(jié)和整理。如果想學(xué)習(xí)Java工程化、高性能及分布式、深入淺出。微服務(wù)、Spring,MyBatis,Netty源碼分析的朋友可以加我的Java進(jìn)階群:617434785,群里有阿里大牛直播講解技術(shù),以及Java大型互聯(lián)網(wǎng)技術(shù)的視頻免費(fèi)分享給大家。