無論是技術開發人員還是架構設計人員都是在實踐中成長起來的,他們通過實踐進行總結,總結後把經驗昇華並再次應用到實踐中去,進一步提供生產效率。
本文介紹筆者在互聯網公司裏線上應急和技術攻關過程中積累的應用層腳本和Java虛擬機命令,這些腳本和命令在發現問題和定位問題的過程中起到關鍵作用,在特定的問題環境下,堪稱快速定位問題的小倚天劍以及快速解決問題的微屠龍刀。
本文在介紹腳本和命令之前,先給大家介紹筆者的Linux環境以及在Linux環境下搭建的一個原創Java發號器服務,用來向大家演示腳本和命令的使用方法,力爭做到讓大家拿來即用的效果。另外,在介紹完所有的腳本和命令之後,我會把所有的命令和腳本收集在一個表格中,便於大家隨時參考和使用,並推薦大家把這個表格打印出來放在自己的辦公桌上,需要的時候看一眼,便可快速發現和解決問題的工具。
腳本和命令系列主題中計劃提供兩篇文章,這篇文章是腳本和命令系列主題中的其中一篇,本文聚焦在那些“神奇的”應用層腳本和Java虛擬機命令,曾經在不同程度上幫助筆者在線上應急和技術攻關的過程中解決過不小的問題,通過這篇文章把這些腳本和命令推廣給讀者,讓讀者也能夠應用在實踐中,切實有效的幫助讀者解決實際問題。
環境搭建和示例服務啓動
首先,使用的Linux版本爲:
OS:Ubuntu 14.04.2 LTS
內核:3.16.0-30-generic
硬件架構:x86_64
使用的JDK版本爲:
jdk1.8.0_20
爲了讓大家瞭解使用命令的真實環境,我們在Linux系統中搭建了原創發號器服務Vesta,在介紹下面的每個命令的同時,都會以監控和維護原創發號器服務爲例來說明,如果大家對發號器服務的設計、實現和使用方式感興趣,或者大家想了解如何開發一款專業的開源軟件,請參考文章如何設計一款多場景分佈式發號器,本文聚焦在如何使用這些高效的腳本和Linux命令來維護一款線上服務。
爲了搭建Vesta發號器服務,我們需要從Github地址https://github.com/robertleepeak/vesta-id-generator.git下載源代碼:
robert@robert-ubuntu1410:~/working/workspace$ git clone https://github.com/robertleepeak/vesta-id-generator.git 正克隆到 'vesta-id-generator'... remote: Counting objects: 944, done. remote: Compressing objects: 100% (53/53), done. 接收對象中: 100% (944/944), 474.66 KiB | 337.00 KiB/s, done. remote: Total 944 (delta 19), reused 0 (delta 0), pack-reused 881 處理 delta 中: 100% (221/221), done. 檢查連接... 完成。
下載後工作目錄爲:
robert@robert-ubuntu1410:~/working/workspace$ ll 總用量 12 drwxrwxr-x 3 robert robert 4096 4月 8 18:31 ./ drwxr-xr-x 6 robert robert 4096 4月 8 17:33 ../ drwxrwxr-x 12 robert robert 4096 4月 8 18:31 vesta-id-generator/
進入Vesta發號器項目根目錄並列出目錄結構:
robert@robert-ubuntu1410:~/working/workspace/vesta-id-generator$ ll 總用量 108 drwxrwxr-x 12 robert robert 4096 4月 8 18:31 ./ drwxrwxr-x 3 robert robert 4096 4月 8 18:31 ../ -rwxrwxr-x 1 robert robert 3843 4月 8 18:31 assembly.xml* -rwxrwxr-x 1 robert robert 494 4月 8 18:31 brief.txt* -rwxrwxr-x 1 robert robert 65 4月 8 18:31 deploy-maven.sh* -rw-rw-r-- 1 robert robert 12292 4月 8 18:31 .DS_Store drwxrwxr-x 8 robert robert 4096 4月 8 18:31 .git/ -rwxrwxr-x 1 robert robert 231 4月 8 18:31 .gitignore* -rwxrwxr-x 1 robert robert 11358 4月 8 18:31 LICENSE* -rwxrwxr-x 1 robert robert 902 4月 8 18:31 make-release.sh* -rwxrwxr-x 1 robert robert 1985 4月 8 18:31 pom.xml* -rwxrwxr-x 1 robert robert 2515 4月 8 18:31 README.md* -rwxrwxr-x 1 robert robert 1429 4月 8 18:31 todo.txt* drwxrwxr-x 3 robert robert 4096 4月 8 18:31 vesta-client/ drwxrwxr-x 2 robert robert 4096 4月 8 18:31 vesta-doc/ drwxrwxr-x 3 robert robert 4096 4月 8 18:31 vesta-intf/ drwxrwxr-x 3 robert robert 4096 4月 8 18:31 vesta-rest/ drwxrwxr-x 3 robert robert 4096 4月 8 18:31 vesta-rest-netty/ drwxrwxr-x 4 robert robert 4096 4月 8 18:31 vesta-sample/ drwxrwxr-x 3 robert robert 4096 4月 8 18:31 vesta-server/ drwxrwxr-x 4 robert robert 4096 4月 8 18:31 vesta-service/ drwxrwxr-x 3 robert robert 4096 4月 8 18:31 vesta-theme/
然後,直接運行發佈腳本 make-release.sh
進行打包發佈:
robert@robert-ubuntu1410:~/working/workspace/vesta-id-generator$ ./make-release.sh [INFO] Scanning for projects... [INFO] ------------------------------------------------------------------------ [INFO] Reactor Build Order: [INFO] [INFO] vesta-id-generator [INFO] vesta-intf [INFO] vesta-service [INFO] vesta-rest [INFO] vesta-rest-netty [INFO] vesta-client [INFO] vesta-server [INFO] vesta-sample [INFO] vesta-sample-embed [INFO] vesta-sample-client ...... [INFO] ------------------------------------------------------------------------ [INFO] Reactor Summary: [INFO] [INFO] vesta-id-generator ................................. SUCCESS [ 11.630 s] [INFO] vesta-intf ......................................... SUCCESS [ 3.489 s] [INFO] vesta-service ...................................... SUCCESS [ 1.140 s] [INFO] vesta-rest ......................................... SUCCESS [ 4.749 s] [INFO] vesta-rest-netty ................................... SUCCESS [ 2.482 s] [INFO] vesta-client ....................................... SUCCESS [ 0.111 s] [INFO] vesta-server ....................................... SUCCESS [ 1.879 s] [INFO] vesta-sample ....................................... SUCCESS [ 0.005 s] [INFO] vesta-sample-embed ................................. SUCCESS [ 0.475 s] [INFO] vesta-sample-client ................................ SUCCESS [ 0.200 s] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 27.026 s [INFO] Finished at: 2017-04-08T21:31:34+08:00 [INFO] Final Memory: 42M/616M [INFO] ------------------------------------------------------------------------
最後,啓動發號器REST服務:
robert@robert-ubuntu1410:~/working/workspace/vesta-id-generator/releases/vesta-id-generator-0.0.1-release/bin$ ll 總用量 110968 drwxrwxr-x 2 robert robert 4096 4月 8 21:34 ./ drwxrwxr-x 6 robert robert 4096 4月 8 21:34 ../ -rw-rw-r-- 1 robert robert 47117221 4月 8 21:34 vesta-all-src-0.0.1.tar.gz -rw-rw-r-- 1 robert robert 34524462 4月 8 21:34 vesta-lib-0.0.1.tar.gz -rw-rw-r-- 1 robert robert 14745549 4月 8 21:33 vesta-rest-0.0.1-bin.tar.gz -rw-rw-r-- 1 robert robert 8656473 4月 8 21:33 vesta-rest-netty-0.0.1-bin.tar.gz -rw-rw-r-- 1 robert robert 8539966 4月 8 21:33 vesta-server-0.0.1-bin.tar.gz -rw-rw-r-- 1 robert robert 27079 4月 8 21:34 vesta-src-0.0.1.tar.gz robert@robert-ubuntu1410:~/working/workspace/vesta-id-generator/releases/vesta-id-generator-0.0.1-release/bin$ tar xzvf vesta-rest-0.0.1-bin.tar.gz vesta-rest-0.0.1/bin/ vesta-rest-0.0.1/bin/server.sh vesta-rest-0.0.1/bin/start.sh vesta-rest-0.0.1/bin/check.sh vesta-rest-0.0.1/bin/stop.sh vesta-rest-0.0.1/lib/vesta-rest-0.0.1.jar vesta-rest-0.0.1/lib/vesta-rest-0.0.1-sources.jar robert@robert-ubuntu1410:~/working/workspace/vesta-id-generator/releases/vesta-id-generator-0.0.1-release/bin/vesta-rest-0.0.1/bin$ ./start.sh apppath: /home/robert/working/workspace/vesta-id-generator/releases/vesta-id-generator-0.0.1-release/bin/vesta-rest-0.0.1 Vesta Rest Server is started.
然後,使用curl語句測試發號器服務是否正常運行:
robert@robert-ubuntu1410:~/working/workspace/vesta-id-generator/releases/vesta-id-generator-0.0.1-release/bin/vesta-rest-0.0.1/bin$ curl "http://localhost:8080/genid" 2382742310220727293 robert@robert-ubuntu1410:~/working/workspace/vesta-id-generator/releases/vesta-id-generator-0.0.1-release/bin/vesta-rest-0.0.1/bin$ curl "http://localhost:8080/expid?id=2382742310220727293" {"machine":1021,"seq":0,"time":71618055,"genMethod":2,"type":0,"version":0}
看見發號器正確產生ID=2382742310220727293,並能反解成JSON字符串:
{"machine":1021,"seq":0,"time":71618055,"genMethod":2,"type":0,"version":0}
這說明發號器服務啓動正常,然後使用Linux命令查看服務進程,確保服務進程運行正常:
robert@robert-ubuntu1410:~$ ps -elf | grep java 0 S robert 8244 1847 2 80 0 - 231991 futex_ 21:54 pts/7 00:00:23 java -server -Xms512m -Xmx512m -Xmn128m -XX:PermSize=128m -Xss256k -XX:+DisableExplicitGC -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:+UseCMSCompactAtFullCollection -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=60 -verbose:gc -XX:+PrintGCDateStamps -XX:+PrintTenuringDistribution -XX:+PrintGCDetails -Xloggc:./logs/gc.log -cp /home/robert/working/workspace/vesta-id-generator/releases/vesta-id-generator-0.0.1-release/bin/vesta-rest-0.0.1/extlib -jar ./lib/vesta-rest-0.0.1.jar
接下來的章節我們圍繞這個服務來介紹後續的每個腳本和命令。
神奇的腳本解決不可思議的問題
本章介紹那些“神奇的”腳本,這些“神奇的”腳本伴我度過了多少個日日夜夜已經不記得了,凡是線上有緊急問題的時候,或者做一個疑難問題的技術攻關的時候,我都會與他們並肩作戰,同時也爲多個同事解決了不少“不可思議的”問題,因此,這裏我希望能夠把這些腳本分享給每一個奮鬥在一線的技術人員,幫助他們解決手頭上的棘手問題。
如果大家對腳本開發感興趣,可以加入我的開源項目wizard-scripts,我們把實踐過程中使用的腳本逐漸的彙總,最後形成一個專業的開源項目,來幫助更多的同行們,歡迎大家加入開源隊伍。
01
show-busiest-java-threads
此命令通過結合Linux操作系統的ps命令和jvm自帶的jstack命令,查找Java進程內CPU利用率最高的線程,一般適用於服務器負載較高的場景,並需要快速定位導致負載高的原因。
本腳本來自一個叫候鳥樹(筆者在互聯網行業入行前的引路人)的網友,原作者不詳,這裏保留原作者名爲了表示對技術人的尊重。
命令格式:
./show-busiest-java-threads -p 進程號 -c 顯示條數
./show-busiest-java-threads -h
使用示例:
./show-busiest-java-threads -p 8244 -c 3
示例輸出:
robert@robert-ubuntu1410:~/working/scripts$ ./show-busiest-java-threads -p 8244 -c 3 The stack of busy(0.3%) thread(8257/0x2041) of java process(8244) of user(robert): "C2 CompilerThread1" #7 daemon prio=9 os_prio=0 tid=0xc545d400 nid=0x2041 waiting on condition [0x00000000] java.lang.Thread.State: RUNNABLE The stack of busy(0.3%) thread(8256/0x2040) of java process(8244) of user(robert): "C2 CompilerThread0" #6 daemon prio=9 os_prio=0 tid=0xc545bc00 nid=0x2040 waiting on condition [0x00000000] java.lang.Thread.State: RUNNABLE The stack of busy(0.1%) thread(8260/0x2044) of java process(8244) of user(robert): "VM Periodic Task Thread" os_prio=0 tid=0xc5463c00 nid=0x2044 waiting on condition
腳本源碼:
#!/bin/bash # @Function # Find out the highest cpu consumed threads of java, and print the stack of these threads. # # @Usage # $ ./show-busy-java-threads # # @author Jerry Lee PROG=`basename $0` usage() { cat < /dev/null; then [ -n "$JAVA_HOME" ] && [ -f "$JAVA_HOME/bin/jstack" ] && [ -x "$JAVA_HOME/bin/jstack" ] && { export PATH="$JAVA_HOME/bin:$PATH" } || { redEcho "Error: jstack not found on PATH and JAVA_HOME!" exit 1 } fi uuid=`date +%s`_${RANDOM}_$$ cleanupWhenExit() { rm /tmp/${uuid}_* &> /dev/null } trap "cleanupWhenExit" EXIT printStackOfThread() { while read threadLine ; do pid=`echo ${threadLine} | awk '{print $1}'` threadId=`echo ${threadLine} | awk '{print $2}'` threadId0x=`printf %x ${threadId}` user=`echo ${threadLine} | awk '{print $3}'` pcpu=`echo ${threadLine} | awk '{print $5}'` jstackFile=/tmp/${uuid}_${pid} [ ! -f "${jstackFile}" ] && { jstack ${pid} > ${jstackFile} || { redEcho "Fail to jstack java process ${pid}!" rm ${jstackFile} continue } } redEcho "The stack of busy(${pcpu}%) thread(${threadId}/0x${threadId0x}) of java process(${pid}) of user(${user}):" sed "/nid=0x${threadId0x}/,/^$/p" -n ${jstackFile} done } [ -z "${pid}" ] && { ps -Leo pid,lwp,user,comm,pcpu --no-headers | awk '$4=="java"{print $0}' | sort -k5 -r -n | head --lines "${count}" | printStackOfThread } || { ps -Leo pid,lwp,user,comm,pcpu --no-headers | awk -v "pid=${pid}" '$1==pid,$4=="java"{print $0}' | sort -k5 -r -n | head --lines "${count}" | printStackOfThread }
02
find-in-jar
此腳本在Jar包中的包名和類名中查找某一關鍵字,並高亮顯示匹配的Jar包名稱和路徑,多用於定位 java.lang.NoClassDefFoundError
和 java.lang.ClassNotFoundException
的問題,以及類版本重複或者衝突的問題等。
此腳本是筆者的原創,在不同的公司裏幫助很多同事解決了大項目中類版本重複和衝突的問題。
命令格式:
find-in-jar 關鍵字或者類名 路徑
使用示例:
find-in-jar ByteBufferHolder .
示例輸出:
robert@robert-ubuntu1410:~/working/workspace/vesta-id-generator$ find-in-jar ByteBufferHolder . ./releases/vesta-id-generator-0.0.1-release/lib/tomcat-embed-core-8.0.20.jar 1165 2015-02-15 18:11 org/apache/coyote/ByteBufferHolder.class ./target/vesta-id-generator-0.0.1-release/lib/tomcat-embed-core-8.0.20.jar 1165 2015-02-15 18:11 org/apache/coyote/ByteBufferHolder.class
腳本源碼:
#!/bin/bash find . -name "*.jar" > /tmp/find_in_jar_temp while read line do if unzip -l $line | grep $1 &> /tmp/find_in_jar_temp_second then echo $line | sed 's#\(.*\)#\x1b[1;31m\1\x1b[00m#' cat /tmp/find_in_jar_temp_second fi done < /tmp/find_in_jar_temp
03
grep-in-jar
次腳本在Jar包中進行二進制內容查找,通常會解決一些線上出現的“不可思議”的問題,例如:某些功能上線沒有生效、某些日誌沒有打印等,通常是上線工具或者上線過程出現了問題,把線上的二進制包拉下來並查找特定的關鍵字來定位問題。
此腳本最初來自於微博同事Axb,後來進行了些許修改,在很多棘手的問題上表現了強大的戰鬥力。
命令格式:
grep-in-jar 關鍵字 路徑
使用示例:
grep-in-jar "vesta" .
示例輸出:
robert@robert-ubuntu1410:~/working/workspace/vesta-id-generator$ grep-in-jar "vesta" . find 'vesta' in . ==> Found "vesta" in ./vesta-sample/vesta-sample-embed/target/vesta-sample-embed-0.0.1.jar ==> Found "vesta" in ./vesta-sample/vesta-sample-embed/target/vesta-sample-embed-0.0.1-sources.jar ==> Found "vesta" in ./vesta-sample/vesta-sample-client/target/vesta-sample-client-0.0.1.jar ==> Found "vesta" in ./vesta-sample/vesta-sample-client/target/vesta-sample-client-0.0.1-sources.jar ==> Found "vesta" in ./vesta-rest-netty/target/vesta-rest-netty-0.0.1-sources.jar ==> Found "vesta" in ./vesta-rest-netty/target/vesta-rest-netty-0.0.1.jar
腳本源碼:
#!/bin/bash ### grep text in jars if [ $# -lt 2 ];then echo 'Usage : jargrep text path' exit 1; fi LOOK_FOR=$1 LOOK_FOR=`echo ${LOOK_FOR//\./\/}` folder=$2 echo "find '$LOOK_FOR' in $folder " for i in `find $2 -name "*jar"` do unzip -p $i | grep "$LOOK_FOR" > /dev/null if [ $? = 0 ] then echo "==> Found \"$LOOK_FOR\" in $i" fi done
04
jar-conflict-detect
此腳本用於識別衝突的Jar包,可以在一個根目錄下找到所有包含相同類的Jar包,並且根據相同類的多少來判斷Jar包的相似度,常常用於某些功能上線不可用或者沒有按照預期起到作用,使用此腳本分析是否存在兩個版本的類,而老版本的類被Java虛擬機加載,其實,JVM規範並沒有規定類路徑下相同類的加載順序,實現JVM規範的虛擬機的實現機制也各不相同,因此無法判斷相同的類中哪個版本的類會被先加載,因此Jar包衝突是個非常討厭的問題。
此腳本最初來自於作者hongjiang,爲了能夠在我的Linux版本上正常運行,後來進行了些許修改,它在很多棘手的問題上表現了強大的戰鬥力。
命令格式:
jar-conflict-detect 路徑
使用示例:
jar-conflict-detect .
示例輸出:
robert@robert-ubuntu1410:~/working/workspace/vesta-id-generator/target/vesta-id-generator-0.0.1-release/lib$ jar-conflict-detect . Similarity DuplicateClasses File1 File2 %21 6 commons-logging-1.1.3.jar jcl-over-slf4j-1.7.11.jar %02 10 commons-beanutils-1.8.0.jar commons-collections-3.2.1.jar See /tmp/cp-verbose.log for more details.
腳本源碼:
#!/bin/bash if [ $# -eq 0 ];then echo "please enter classpath dir" exit -1 fi if [ ! -d "$1" ]; then echo "not a directory" exit -2 fi tmpfile="/tmp/.cp$(date +%s)" tmphash="/tmp/.hash$(date +%s)" verbose="/tmp/cp-verbose.log" declare -a files=(`find "$1" -name "*.jar"`) for ((i=0; i < ${#files[@]}; i++)); do jarName=`basename ${files[$i]}` list=`unzip -l ${files[$i]} | awk -v fn=$jarName '/\.class$/{print $NF,fn}'` size=`echo "$list" | wc -l` echo $jarName $size >> $tmphash echo "$list" done | sort | awk 'NF{ a[$1]++;m[$1]=m[$1]","$2}END{for(i in a) if(a[i] > 1) print i,substr(m[i],2) }' > $tmpfile awk '{print $2}' $tmpfile | awk -F',' '{i=1;for(;i<=NF;i++) for(j=i+1;j<=NF;j++) print $i,$j}' | sort | uniq -c | sort -nrk1 | while read line; do dup=${line%% *} jars=${line#* } jar1=${jars% *} jar2=${jars#* } len_jar1=`grep -F "$jar1" $tmphash | grep ^"$jar1" | awk '{print $2}'` len_jar2=`grep -F "$jar2" $tmphash | grep ^"$jar2" | awk '{print $2}'` # Modified by Robert 2017.4.9 #len=$(($len_jar1 > $len_jar2 ? $len_jar1 : $len_jar2)) len_jar1=`echo $len_jar1 | awk -F' ' '{print $1}'` len_jar2=`echo $len_jar2 | awk -F' ' '{print $1}'` if [ $len_jar1 -gt $len_jar2 ] then len=$len_jar1 else len=$len_jar2 fi per=$(echo "scale=2; $dup/$len" | bc -l) echo ${per/./} $dup $jar1 $jar2 done | sort -nr -k1 -k2 | awk 'NR==1{print "Similarity DuplicateClasses File1 File2"}{print "%"$0}'| column -t sort $tmpfile | awk '{print $1,"\n\t\t",$2}' > $verbose echo "See $verbose for more details." rm -f $tmpfile rm -f $tmphash
05
http-spy
此腳本利用Linux命令nc檢查HTTP請求參數、請求頭和請求體等信息,常常用於調試基於HTTP協議的服務調用,如果一次HTTP調用沒有的達到預期的效果,首先檢查傳遞的參數是否正確,這包括請求參數、請求頭、請求體等信息,這種場景正式此腳本的用武之地。
命令格式:
http-spy
使用示例:
http-spy
示例輸出:
robert@robert-ubuntu1410:~/working/workspace/vesta-id-generator/target/vesta-id-generator-0.0.1-release/lib$ curl "http://localhost:8888?abc=def"
robert@robert-ubuntu1410:~/working/scripts$ ./http-spy GET /?abc=def HTTP/1.1 User-Agent: curl/7.35.0 Host: localhost:8888 Accept: */*
腳本源碼:
#!/bin/bash while true; do nc -l 8888; done
06
show-mysql-qps
此腳本可以用於快速查看Mysql實例的負載情況,包括QPS、TPS、提交數、回滾數、併發線程、執行線程等。
命令格式:
show-mysql-qps 用戶名 密碼
使用示例:
show-mysql-qps root ****
示例輸出:
robert@robert-ubuntu1410:~/working/scripts$ show-mysql-qps.sh root ****** QPS Commit Rollback TPS Threads_con Threads_run ------------------------------------------------------- 1 0 0 0 1 1 1 0 0 0 1 1 1 0 0 0 1 1 1 0 0 0 1 1
腳本源碼:
#!/bin/bash mysqladmin -uroot extended-status -i1 | awk 'BEGIN{local_switch=0;print "QPS Commit Rollback TPS Threads_con Threads_run \n------------------------------------------------------- "} $2 ~ /Queries$/ {q=$4-lq;lq=$4;} $2 ~ /Com_commit$/ {c=$4-lc;lc=$4;} $2 ~ /Com_rollback$/ {r=$4-lr;lr=$4; $2 ~ /Threads_connected$/ {tc=$4;} $2 ~ /Threads_running$/ {tr=$4; if(local_switch==0){ local_switch=1; count=0 } else { if(count>10) { count=0; print "------------------------------------------------------- \nQPS Commit Rollback TPS Threads_con Threads_run \n------------------------------------------------------- "; } else { count+=1; printf "%-6d %-8d %-7d %-8d %-10d %d \n", q,c,r,c+r,tc,tr; } } }'
Java虛擬機相關的命令
本節介紹Java世界裏那些有用的虛擬機命令,有了他們的伴隨,你會感覺到解決Java世界裏面的問題事半功倍。
本章並不會介紹所有的JDK自帶的工具命令,而是結合實踐給讀者介紹那些最常用最重要的Java虛擬機相關的命令,以及一些開源的Java虛擬機工具,目的是幫助大家在現實中解決問題。
如果想全面瞭解JDK各種命令,請參考JDK官方文檔。
01
jad
jad反編譯工具可以將字節碼的二進制類反編譯回Java源代碼,常常用於遇到問題但是無法在源代碼中定位的場景,通過反編譯字節碼可以分析程序事實上執行的流程,從而定位深層次的問題。
由於歷史原因或者其他原因,有些時候項目依賴了沒有源代碼的jar包,如果出現問題需要定位,那麼jad命令可是個小倚天劍。
jad下載地址:JAD
如果不習慣使用命令行,可以下載界面版本jd-gui,可以一次反編譯一個JAR包,並在類之間有簡單的導航操作。
界面版本jd-gui下載地址:JD-GUI
如果開發者對字節碼進行了混淆,反編譯的源代碼將很難讀懂,混淆的代碼只能通過混淆後給出的摘要文件進行分析,這種場景下使用反編譯工具作用不大。
使用示例:
jad AbstractIdServiceImpl.class
示例輸出:
robert@robert-ubuntu1410:~/working/workspace/vesta-id-generator/target/vesta-id-generator-0.0.1-release/lib/com/robert/vesta/service/impl$ jad AbstractIdServiceImpl.class Parsing AbstractIdServiceImpl.class...The class file version is 49.0 (only 45.3, 46.0 and 47.0 are supported) Generating AbstractIdServiceImpl.jad
robert@robert-ubuntu1410:~/working/workspace/vesta-id-generator/target/vesta-id-generator-0.0.1-release/lib/com/robert/vesta/service/impl$ ll 總用量 40 -rw-rw-r-- 1 robert robert 5375 4月 8 21:33 AbstractIdServiceImpl.class -rw-rw-r-- 1 robert robert 5197 4月 9 10:01 AbstractIdServiceImpl.jad -rw-rw-r-- 1 robert robert 2965 4月 8 21:33 IdServiceImpl.class
02
btrace
java應用服務在生產環境中可能會出現各種各樣的問題,有些問題在找到根源原因之前看似很“不可思議”,有些時候並沒有產生異常或者錯誤消息,這時我們無法根據已有的日誌來定位問題,那麼我們需要更多的信息,例如:參數、返回值、程序邏輯判斷、循環次數等信息,來追蹤問題,如果臨時增加日誌,需要重新上線,成本較高,使用遠程調試(後續會在專用文章中介紹gdb等遠程調試工具)又會影響線上流量甚至導致客戶程序超時,這個時候btrace應運而生,btrace可以動態地跟蹤java運行時程序,將定製化的跟蹤字節碼切面注入到運行類中,對運行代碼無侵入,根據官方信息判斷對性能上的影響也可以忽略不計。
瞭解和下載btrace:BTRACE下載主頁
btrace源代碼:BTRACE Github主頁
下面介紹btrace的使用方法:
命令格式:
btrace [-p port] [-cp classpath] pid btrace-script
參數解析:
-
port指定btrace agent的服務端監聽端口號,供客戶端連接
-
classpath用來指定依賴的類加載路徑
-
pid表示進程號,可通過jps或者ps命令獲取
-
btrace-script即爲btrace跟蹤切面腳本
在運行命令之前,我們需要編寫btrace的跟蹤腳本:
import java.util.Date; import com.sun.btrace.BTraceUtils; import static com.sun.btrace.BTraceUtils.*; import com.sun.btrace.annotations.*; @BTrace public class Btrace { @OnMethod( clazz = "com.cloudate.controller.AdminController", method = "sayHello", location = @Location(Kind.RETURN) ) public static void sayHello(@Duration long duration) {//單位是納秒,要轉爲毫秒 println(strcat("duration(ms): ", str(duration / 1000000))); } }
這個跟蹤腳本對業務代碼的方法進行攔截,並打印方法執行時間:
package com.cloudate.controller; public class AdminController { public String sayHello(String name, int age) { return "hello everyone"; } }
*** 使用示例:***
btrace -p 2020 -cp ~/servlet-api.jar 1507 ~/BTrace.java
當AdminController類的sayHello被調用的時候,控制檯就會打印方法執行時間,這對定位線上的棘手問題有非常大的幫助。
03
jmap
生產中java應用服務發生OutOfMemoryError那是家常便飯,發生了OutOfMemoryError或者發現內存不足報警,我們經常需要找到是什麼原因造成的,要找到內存都去哪兒了,jmap在這個場景是用來定位問題的主要工具,它主要用來查看Java進程內存的使用情況。
jmap是JDK自帶的監控工具,在JDK的根目錄裏可以找到。
使用示例1:
按照佔用空間大小打印程序中類的列表,從這個列表中可以分析哪些類佔用了比較多的內存,再結合代碼找到問題的所在。
jmap -histo:live 2743
示例輸出:
num #instances #bytes class name ---------------------------------------------- 1: 44398 7206296 [C 2: 23166 2789688 [B 3: 14511 1276968 java.lang.reflect.Method 4: 43256 692096 java.lang.String 5: 22102 530448 org.springframework.boot.loader.util.AsciiBytes 6: 11047 530256 org.springframework.boot.loader.jar.JarEntryData 7: 5800 510592 java.lang.Class 8: 18088 434112 java.util.HashMap$Node 9: 6465 356336 [Ljava.lang.Object; 10: 14582 349968 java.util.concurrent.ConcurrentHashMap$Node 11: 2914 319928 [Ljava.util.HashMap$Node; 12: 6920 221440 java.util.LinkedHashMap$Entry 13: 2008 192768 org.springframework.boot.loader.jar.JarEntry 14: 5691 182112 java.lang.ref.SoftReference 15: 7467 179208 java.lang.ref.WeakReference 16: 9410 166712 [Ljava.lang.Class; 17: 2786 156016 java.util.LinkedHashMap 18: 101 136256 [Ljava.util.concurrent.ConcurrentHashMap$Node; 19: 1761 112704 java.lang.reflect.Field 20: 3081 105056 [Ljava.lang.String; 21: 4357 104568 java.beans.MethodRef ......
使用示例2:
按照佔用空間大小打印程序中加載的動態鏈接庫的列表,其實,Java進程在操作系統中會加載多個動態鏈接庫,Java進程本身和動態鏈接庫都會在其佔用的虛擬地址空間上分配內存,Java堆和棧等內存空間分配在Java進程本身,Java直接內存會分配在Java進程堆內存外或者依賴的動態鏈接庫上,因此,此命令幫助大家定位是Java進程本身佔用內存較大,還是哪個動態鏈接庫佔用內存較多,在定位直接內導致的內存泄露的場景有很大的作用。
jmap 2743
示例輸出:
robert@robert-ubuntu1410:~$ jmap 2743 Attaching to process ID 2743, please wait... Debugger attached successfully. Server compiler detected. JVM version is 25.20-b23 0x08048000 5K /home/robert/working/softwares/jdk1.8.0_20/bin/java 0xe6b2e000 78K /home/robert/working/softwares/jdk1.8.0_20/jre/lib/i386/libnio.so 0xe6d06000 100K /home/robert/working/softwares/jdk1.8.0_20/jre/lib/i386/libnet.so 0xf65b5000 113K /home/robert/working/softwares/jdk1.8.0_20/jre/lib/i386/libzip.so 0xf65cf000 41K /lib32/libnss_files-2.19.so 0xf65db000 41K /lib32/libnss_nis-2.19.so 0xf65e7000 89K /lib32/libnsl-2.19.so 0xf6705000 42K /home/robert/working/softwares/jdk1.8.0_20/jre/lib/i386/libmanagement.so 0xf671a000 183K /home/robert/working/softwares/jdk1.8.0_20/jre/lib/i386/libjava.so 0xf673f000 29K /lib32/librt-2.19.so 0xf6789000 273K /lib32/libm-2.19.so 0xf67cf000 12213K /home/robert/working/softwares/jdk1.8.0_20/jre/lib/i386/server/libjvm.so 0xf759b000 1709K /lib32/libc-2.19.so 0xf7748000 13K /lib32/libdl-2.19.so 0xf774d000 91K /home/robert/working/softwares/jdk1.8.0_20/lib/i386/jli/libjli.so 0xf7762000 687K /lib32/libpthread-2.19.so 0xf777e000 29K /lib32/libnss_compat-2.19.so 0xf7789000 54K /home/robert/working/softwares/jdk1.8.0_20/jre/lib/i386/libverify.so 0xf779a000 131K /lib32/ld-2.19.so
使用示例3:
java堆的內存結構很複雜,包括新生代、老年代、持久代、直接內存等,jmap命令可以查看堆的概要信息。
jmap -heap 38574
示例輸出:
robert@robert-ubuntu1410:~$ jmap -heap 38574 Attaching to process ID 38574, please wait... Debugger attached successfully. Server compiler detected. JVM version is 25.65-b01 using parallel threads in the new generation. using thread-local object allocation. Concurrent Mark-Sweep GC Heap Configuration: MinHeapFreeRatio = 40 MaxHeapFreeRatio = 70 MaxHeapSize = 536870912 (512.0MB) NewSize = 134217728 (128.0MB) MaxNewSize = 134217728 (128.0MB) OldSize = 402653184 (384.0MB) NewRatio = 2 SurvivorRatio = 8 MetaspaceSize = 21807104 (20.796875MB) CompressedClassSpaceSize = 1073741824 (1024.0MB) MaxMetaspaceSize = 17592186044415 MB G1HeapRegionSize = 0 (0.0MB) Heap Usage: New Generation (Eden + 1 Survivor Space): capacity = 120848384 (115.25MB) used = 111727304 (106.55146026611328MB) free = 9121080 (8.698539733886719MB) 92.45246010074905% used Eden Space: capacity = 107479040 (102.5MB) used = 104015920 (99.19731140136719MB) free = 3463120 (3.3026885986328125MB) 96.77786478182165% used From Space: capacity = 13369344 (12.75MB) used = 7711384 (7.354148864746094MB) free = 5657960 (5.395851135253906MB) 57.67959893918505% used To Space: capacity = 13369344 (12.75MB) used = 0 (0.0MB) free = 13369344 (12.75MB) 0.0% used concurrent mark-sweep generation: capacity = 402653184 (384.0MB) used = 15998072 (15.256950378417969MB) free = 386655112 (368.74304962158203MB) 3.973164161046346% used 15700 interned Strings occupying 2047808 bytes.
使用示例4:
有些Java內存問題不是顯而易見的,從類、動態鏈接庫、堆的概要信息的角度上,無法定位具體產生的原因,我們需要對Java堆的內部結構進行剖析才能進一步分析問題的根源原因,這通常通過jmap命令導出Java堆的快照,然後通過其他工具或者甚至可視化內存分析工具(例如:JHAT、JMAT、JProfiler、Jconsole、JVisualVM)等進行詳細分析。
jmap -dump:format=b,file=./heap.hprof 2743
示例輸出:
robert@robert-ubuntu1410:~$ jmap -dump:format=b,file=./heap.hprof 2743 Dumping heap to /home/robert/heap.hprof ... Heap dump file created
robert@robert-ubuntu1410:~$ ll 總用量 27184 ...... -rw------- 1 robert robert 27632924 4月 9 11:15 heap.hprof
04
jstat
jstat利用了JVM內建的指令對Java應用程序的資源和性能進行實時的命令行的監控,包括了對堆大小和垃圾回收狀況的監控等等,與jmap對比,jstat更傾向於輸出積累的信息與打印GC等的統計信息等。
jstat是JDK自帶的監控工具,在JDK的根目錄裏可以找到。
使用示例:
jstat -gcutil 2743 5000 10
示例輸出:
robert@robert-ubuntu1410:~$ jstat -gcutil 2743 5000 10 S0 S1 E O M CCS YGC YGCT FGC FGCT GCT 0.00 0.00 0.75 4.82 98.92 - 8 0.174 6 0.332 0.506 0.00 0.00 0.75 4.82 98.92 - 8 0.174 6 0.332 0.506
名詞解析:
S0C:年輕代中第一個survivor(倖存區)的容量 (字節)
S1C:年輕代中第二個survivor(倖存區)的容量 (字節)
S0U:年輕代中第一個survivor(倖存區)目前已使用空間 (字節)
S1U:年輕代中第二個survivor(倖存區)目前已使用空間 (字節)
EC:年輕代中Eden(伊甸園)的容量 (字節)
EU:年輕代中Eden(伊甸園)目前已使用空間 (字節)
OC:Old代的容量 (字節)
OU:Old代目前已使用空間 (字節)
PC:Perm(持久代)的容量 (字節)
PU:Perm(持久代)目前已使用空間 (字節)
YGC:從應用程序啓動到採樣時年輕代中gc次數
YGCT:從應用程序啓動到採樣時年輕代中gc所用時間(s)
FGC:從應用程序啓動到採樣時old代(全gc)gc次數
FGCT:從應用程序啓動到採樣時old代(全gc)gc所用時間(s)
GCT:從應用程序啓動到採樣時gc用的總時間(s)
NGCMN:年輕代(young)中初始化(最小)的大小 (字節)
NGCMX:年輕代(young)的最大容量 (字節)
NGC:年輕代(young)中當前的容量 (字節)
OGCMN:old代中初始化(最小)的大小 (字節)
OGCMX:old代的最大容量 (字節)
OGC:old代當前新生成的容量 (字節)
PGCMN:perm代中初始化(最小)的大小 (字節)
PGCMX:perm代的最大容量 (字節)
PGC:perm代當前新生成的容量 (字節)
S0:年輕代中第一個survivor(倖存區)已使用的佔當前容量百分比
S1:年輕代中第二個survivor(倖存區)已使用的佔當前容量百分比
E:年輕代中Eden(伊甸園)已使用的佔當前容量百分比
O:old代已使用的佔當前容量百分比
P:perm代已使用的佔當前容量百分比
S0CMX:年輕代中第一個survivor(倖存區)的最大容量 (字節)
S1CMX :年輕代中第二個survivor(倖存區)的最大容量 (字節)
ECMX:年輕代中Eden(伊甸園)的最大容量 (字節)
DSS:當前需要survivor(倖存區)的容量 (字節)(Eden區已滿)
TT: 持有次數限制
MTT : 最大持有次數限制
05
jstack
jstack命令用於打印出給定的java進程ID的線程堆棧快照信息,從而可以看到Java進程內線程的執行狀態、這個你在執行的任務等等,可以據此分析線程等待、死鎖等問題。
jstack也是JDK自帶的命令,在JDK的根目錄裏可以找到。本文第二章“神奇的”腳本中的show-busiest-java-threads腳本也是基於此命令實現的。
使用示例:
jstack 2743
示例輸出:
robert@robert-ubuntu1410:~$ jstack 2743 2017-04-09 12:06:51 Full thread dump Java HotSpot(TM) Server VM (25.20-b23 mixed mode): "Attach Listener" #23 daemon prio=9 os_prio=0 tid=0xc09adc00 nid=0xb4c waiting on condition [0x00000000] java.lang.Thread.State: RUNNABLE "http-nio-8080-Acceptor-0" #22 daemon prio=5 os_prio=0 tid=0xc3341000 nid=0xb02 runnable [0xbf1bd000] java.lang.Thread.State: RUNNABLE at sun.nio.ch.ServerSocketChannelImpl.accept0(Native Method) at sun.nio.ch.ServerSocketChannelImpl.accept(ServerSocketChannelImpl.java:241) - locked <0xcf8938d8> (a java.lang.Object) at org.apache.tomcat.util.net.NioEndpoint$Acceptor.run(NioEndpoint.java:688) at java.lang.Thread.run(Thread.java:745) "http-nio-8080-ClientPoller-1" #21 daemon prio=5 os_prio=0 tid=0xc35bc400 nid=0xb01 runnable [0xbf1fe000] java.lang.Thread.State: RUNNABLE at sun.nio.ch.EPollArrayWrapper.epollWait(Native Method) at sun.nio.ch.EPollArrayWrapper.poll(EPollArrayWrapper.java:269) at sun.nio.ch.EPollSelectorImpl.doSelect(EPollSelectorImpl.java:79) at sun.nio.ch.SelectorImpl.lockAndDoSelect(SelectorImpl.java:86) - locked <0xcf99b100> (a sun.nio.ch.Util$2) - locked <0xcf99b0f0> (a java.util.Collections$UnmodifiableSet) - locked <0xcf99aff8> (a sun.nio.ch.EPollSelectorImpl) at sun.nio.ch.SelectorImpl.select(SelectorImpl.java:97) at org.apache.tomcat.util.net.NioEndpoint$Poller.run(NioEndpoint.java:1052) at java.lang.Thread.run(Thread.java:745) ......
06
jinfo
jinfo可以輸出並修改運行時的java進程的環境變量和虛擬機參數。
使用示例:
jinfo 38574
示例輸出:
$ jinfo 38574 Attaching to process ID 38574, please wait... Debugger attached successfully. Server compiler detected. JVM version is 25.65-b01 Java System Properties: java.runtime.name = Java(TM) SE Runtime Environment java.vm.version = 25.65-b01 sun.boot.library.path = /Library/Java/JavaVirtualMachines/jdk1.8.0_65.jdk/Contents/Home/jre/lib java.protocol.handler.pkgs = null|org.springframework.boot.loader user.country.format = CN gopherProxySet = false java.vendor.url = http://java.oracle.com/ ...... VM Flags: Non-default VM flags: -XX:CICompilerCount=3 -XX:CMSInitiatingOccupancyFraction=60 -XX:+CMSParallelRemarkEnabled -XX:+DisableExplicitGC -XX:InitialHeapSize=536870912 -XX:MaxHeapSize=536870912 -XX:MaxNewSize=134217728 -XX:MaxTenuringThreshold=6 -XX:MinHeapDeltaBytes=196608 -XX:NewSize=134217728 -XX:OldPLABSize=16 -XX:OldSize=402653184 -XX:+PrintGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintTenuringDistribution -XX:ThreadStackSize=256 -XX:+UseCMSCompactAtFullCollection -XX:+UseCMSInitiatingOccupancyOnly -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseConcMarkSweepGC -XX:+UseFastUnorderedTimeStamps -XX:+UseParNewGC Command line: -Xms512m -Xmx512m -Xmn128m -XX:PermSize=128m -Xss256k -XX:+DisableExplicitGC -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:+UseCMSCompactAtFullCollection -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=60 -verbose:gc -XX:+PrintGCDateStamps -XX:+PrintTenuringDistribution -XX:+PrintGCDetails -Xloggc:./logs/gc.lo
07
其他命令
除了上面介紹的常用Java虛擬機相關命令以外,我們還有兩類工具,這節只會做功能介紹,不做詳細的展開。
-
基本命令:
-
javah:生成java類的C頭文件,一般用於開發JNI庫
-
jps:用來查找Java進程,通常使用ps命令代替
-
jhat:用來分析內存堆快照文件
-
jdb:遠程調試,用於線上定位問題
-
jstatd:jstat的服務器版本
-
-
Java虛擬機圖形界面分析工具:
-
JConsole:JDK自帶的可以查看Java內存和線程堆棧的工具,已經過時
-
JVisualVM:JDK自帶的可以查看Java內存和線程堆棧的工具,是JConsole的替代版本
-
JMAT:Eclipse組織開發的全功能的開源Java性能跟蹤、分析和定位工具
-
JProfiler:全功能的商業化Java性能跟蹤、分析和定位工具
-
一次OOM定位與修復過程中與監控同事現場編寫的腳本
本節提供一個筆者在實踐過程中解決OOM問題的一個簡單腳本,這個腳本是爲了解決OOM(unable to create native thread)的問題而在問題機器上臨時編寫,並臨時使用的,腳本並沒有寫的很專業,只是爲了抓取需要的信息並解決問題,但是在線上問題十分火急的情況下,這個腳本會有大用處。
#!/bin/bash ps -Leo pid,lwp,user,pcpu,pmem,cmd >> /tmp/pthreads.log echo "ps -Leo pid,lwp,user,pcpu,pmem,cmd >> /tmp/pthreads.log" >> /tmp/pthreads.log echo `date` >> /tmp/pthreads.log echo 1 pid=`ps aux|grep tomcat|grep cwh|awk -F ' ' '{print $2}'` echo 2 echo "pstack $pid >> /tmp/pstack.log" >> /tmp/pstack.log pstack $pid >> /tmp/pstack.log echo `date` >> /tmp/pstack.log echo 3 echo "lsof >> /tmp/sys-o-files.log" >> /tmp/sys-o-files.log lsof >> /tmp/sys-o-files.log echo `date` >> /tmp/sys-o-files.log echo 4 echo "lsof -p $pid >> /tmp/service-o-files.log" >> /tmp/service-o-files.log lsof -p $pid >> /tmp/service-o-files.log echo `date` >> /tmp/service-o-files.log echo 5 echo "jstack -l $pid >> /tmp/js.log" >> /tmp/js.log jstack -l -F $pid >> /tmp/js.log echo `date` >> /tmp/js.log echo 6 echo "free -m >> /tmp/free.log" >> /tmp/free.log free -m >> /tmp/free.log echo `date` >> /tmp/free.log echo 7 echo "vmstat 2 1 >> /tmp/vm.log" >> /tmp/vm.log vmstat 2 1 >> /tmp/vm.log echo `date` >> /tmp/vm.log echo 8 echo "jmap -dump:format=b,file=/tmp/heap.hprof 2743" >> /tmp/jmap.log jmap -dump:format=b,file=/tmp/heap.hprof >> /tmp/jmap.log echo `date` >> /tmp/jmap.log echo 9 echo end
對於這次線上產生的OOM問題,與運維和監控部們的同事奮鬥了幾天幾夜,終於通過在線上抓取信息、分析問題、在性能壓測部門同事的幫助下,最小化重現問題並找到問題的根源原因,最後,針對問題產生的根源提供了有效的方案,隨後,會在新的一篇文章中專門討論如何解決線上的OutOfMemeoryError的問題,這篇新文章將會包括OutOfMemoryError的種類,對於每種OutOfMemoryError產生的原因,以及解決的辦法等。
而本節只給讀者介紹這個看似簡陋而又信息滿滿的Java服務的監控腳本,如果讀者在線上已經遇到了OOM的問題,可以順着這個腳本的思路,利用本文提供的各種腳本和命令來深挖問題的根本原因。
場景與命令彙總表
正如文章開始提到,我會把所有的命令和腳本收集在一個表格中,稱爲“場景與命令彙總表”,便於大家隨時參考和使用,並推薦大家把這個表格打印出來放在自己的辦公桌上,需要的時候看一眼,便可快速發現和解決問題的工具。
序號 | 場景 | 腳本 |
---|---|---|
1 | 服務器負載高、服務超時、CPU利用率高 | show-busiest-java-threads |
2 | java.lang.NoClassDefFoundError、java.lang.ClassNotFoundException、程序未按照預期運行 | find-in-jar |
3 | 程序未按照預期運行、上線後未執行新邏輯、查找某些關鍵字 | grep-in-jar |
4 | Jar包版本衝突、程序未按照預期運行 | jar-conflict-detect |
5 | HTTP調用後發現未按照預期輸出結果 | http-spy |
6 | 數據庫負載高、SQL超時 | show-mysql-qps |
7 | 沒有源碼的Jar包出了問題、破解別人的代碼 | jad |
8 | 線上出問題還無法上線打點日誌、線上調試、做切面 | btrace |
9 | 內存不足、OutOfMemoryError | jmap |
10 | 內存不足、OutOfMemoryError、GC頻繁、服務超時、響應長尾 | jstat |
11 | 服務超時、線程死鎖、服務器負載高 | jstack |
12 | 查看或者修改Java進程環境變量和Java虛擬機變量 | jinfo |
13 | 使用JNI開發Java本地程序庫 | javah |
14 | 查找java進程ID | jps |
15 | 分析jmap產生的java堆的快照 | jhat |
16 | QA環境無法重現,需要在準生產線上遠程調試 | jdb |
17 | 與jstat相同,但是可以在線下用客戶端連接,可線下操作 | jstatd |
18 | 簡單的有界面的內存分析工具,JDK自帶 | JConsole |
19 | 全面的有界面的內存分析工具,JDK自帶 | JVisualVM |
20 | 專業的Java進程性能分析和跟蹤工具 | JMAT |
21 | 商業化的Java進程性能分析和跟蹤工具 | JProfiler |
總結經驗
本文開始介紹了學習那些高效應用層腳本和Java虛擬機命令的示例服務Vesta的配置和上線,然後,在以Vesta服務運行爲背景下重點介紹了筆者積累的高效的應用層腳本,可以幫助讀者解決服務負載高、JAR包衝突、驗證線上服務代碼、動態添加線上日誌的問題,然後給讀者介紹了關鍵的幾個Java虛擬機命令,幫助大家查看Java虛擬機運行狀態、線程堆棧、內存使用情況、GC頻率等,能夠幫助大家對自己的服務保駕護航。
正如文章開始提到,另外一篇文章聚焦在線上應急和技術攻關過程中,你必須學會使用的那些Linux基礎命令,將會盡快在後續的文章中與讀者見面,敬請期待。