線上故障主要會包括 CPU、磁盤、內存以及網絡問題,而大多數故障可能會包含不止一個層面的問題,因此進行排查時候儘可能四個方面依次排查一遍。同時例如 jstack、jmap 等工具也是不囿於一個方面的問題的,基本上出問題就是 df、free、top 三連,而後依次 jstack、jmap 伺候,具體問題具體分析便可。java
CPUios
通常來說咱們首先會排查 CPU 方面的問題。CPU 異常每每仍是比較好定位的。緣由包括業務邏輯問題(死循環)、頻繁 gc 以及上下文切換過多。而最多見的每每是業務邏輯(或者框架邏輯)致使的,可使用 jstack 來分析對應的堆棧狀況。程序員
使用 jstack 分析 CPU 問題面試
咱們先用 ps 命令找到對應進程的 pid(若是你有好幾個目標進程,能夠先用 top 看一下哪一個佔用比較高)。segmentfault
接着用top -H -p pid來找到 CPU 使用率比較高的一些線程緩存
而後將佔用最高的 pid 轉換爲 16 進制printf '%xn' pid獲得 nid網絡
接着直接在 jstack 中找到相應的堆棧信息jstack pid |grep 'nid' -C5 –color多線程
能夠看到咱們已經找到了 nid 爲 0x42 的堆棧信息,接着只要仔細分析一番便可。架構
固然更常見的是咱們對整個 jstack 文件進行分析,一般咱們會比較關注 WAITING 和 TIMED_WAITING 的部分,BLOCKED 就不用說了。咱們可使用命令cat jstack.log | grep "java.lang.Thread.State" | sort -nr | uniq -c來對 jstack 的狀態有一個總體的把握,若是 WAITING 之類的特別多,那麼多半是有問題啦。併發
頻繁 gc
固然咱們仍是會使用 jstack 來分析問題,但有時候咱們能夠先肯定下 gc 是否是太頻繁,使用jstat -gc pid 1000命令來對 gc 分代變化狀況進行觀察,1000 表示採樣間隔(ms),S0C/S1C、S0U/S1U、EC/EU、OC/OU、MC/MU 分別表明兩個 Survivor 區、Eden 區、老年代、元數據區的容量和使用量。YGC/YGT、FGC/FGCT、GCT 則表明 YoungGc、FullGc 的耗時和次數以及總耗時。若是看到 gc 比較頻繁,再針對 gc 方面作進一步分析,具體能夠參考一下 gc 章節的描述。
上下文切換
針對頻繁上下文問題,咱們可使用vmstat命令來進行查看
cs(context switch)一列則表明了上下文切換的次數。
若是咱們但願對特定的 pid 進行監控那麼可使用 pidstat -w pid命令,cswch 和 nvcswch 表示自願及非自願切換。
磁盤
磁盤問題和 CPU 同樣是屬於比較基礎的。首先是磁盤空間方面,咱們直接使用df -hl來查看文件系統狀態
更多時候,磁盤問題仍是性能上的問題。咱們能夠經過 iostatiostat -d -k -x來進行分析
最後一列%util能夠看到每塊磁盤寫入的程度,而rrqpm/s以及wrqm/s分別表示讀寫速度,通常就能幫助定位到具體哪塊磁盤出現問題了。
另外咱們還須要知道是哪一個進程在進行讀寫,通常來講開發本身內心有數,或者用 iotop 命令來進行定位文件讀寫的來源。
不過這邊拿到的是 tid,咱們要轉換成 pid,能夠經過 readlink 來找到 pidreadlink -f /proc/*/task/tid/../..。
找到 pid 以後就能夠看這個進程具體的讀寫狀況cat /proc/pid/io
咱們還能夠經過 lsof 命令來肯定具體的文件讀寫狀況lsof -p pid
內存
內存問題排查起來相對比 CPU 麻煩一些,場景也比較多。主要包括 OOM、GC 問題和堆外內存。通常來說,咱們會先用free命令先來檢查一發內存的各類狀況。
堆內內存
內存問題大多還都是堆內內存問題。表象上主要分爲 OOM 和 Stack Overflo。
OOM
JMV 中的內存不足,OOM 大體能夠分爲如下幾種:
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
這個意思是沒有足夠的內存空間給線程分配 Java 棧,基本上仍是線程池代碼寫的有問題,好比說忘記 shutdown,因此說應該首先從代碼層面來尋找問題,使用 jstack 或者 jmap。若是一切都正常,JVM 方面能夠經過指定Xss來減小單個 thread stack 的大小。另外也能夠在系統層面,能夠經過修改/etc/security/limits.confnofile 和 nproc 來增大 os 對線程的限制
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
這個意思是堆的內存佔用已經達到-Xmx 設置的最大值,應該是最多見的 OOM 錯誤了。解決思路仍然是先應該在代碼中找,懷疑存在內存泄漏,經過 jstack 和 jmap 去定位問題。若是說一切都正常,才須要經過調整Xmx的值來擴大內存。
Caused by: java.lang.OutOfMemoryError: Meta space
這個意思是元數據區的內存佔用已經達到XX:MaxMetaspaceSize設置的最大值,排查思路和上面的一致,參數方面能夠經過XX:MaxPermSize來進行調整(這裏就不說 1.8 之前的永久代了)。
Stack Overflow
棧內存溢出,這個你們見到也比較多。
Exception in thread "main" java.lang.StackOverflowError
表示線程棧須要的內存大於 Xss 值,一樣也是先進行排查,參數方面經過Xss來調整,但調整的太大可能又會引發 OOM。
使用 JMAP 定位代碼內存泄漏
上述關於 OOM 和 Stack Overflo 的代碼排查方面,咱們通常使用 JMAPjmap -dump:format=b,file=filename pid來導出 dump 文件
經過 mat(Eclipse Memory Analysis Tools)導入 dump 文件進行分析,內存泄漏問題通常咱們直接選 Leak Suspects 便可,mat 給出了內存泄漏的建議。另外也能夠選擇 Top Consumers 來查看最大對象報告。和線程相關的問題能夠選擇 thread overview 進行分析。除此以外就是選擇 Histogram 類概覽來本身慢慢分析,你們能夠搜搜 mat 的相關教程。
平常開發中,代碼產生內存泄漏是比較常見的事,而且比較隱蔽,須要開發者更加關注細節。好比說每次請求都 new 對象,致使大量重複建立對象;進行文件流操做但未正確關閉;手動不當觸發 gc;ByteBuffer 緩存分配不合理等都會形成代碼 OOM。
另外一方面,咱們能夠在啓動參數中指定-XX:+HeapDumpOnOutOfMemoryError來保存 OOM 時的 dump 文件。
gc 問題和線程
gc 問題除了影響 CPU 也會影響內存,排查思路也是一致的。通常先使用 jstat 來查看分代變化狀況,好比 youngGC 或者 fullGC 次數是否是太多呀;EU、OU 等指標增加是否是異常呀等。
線程的話太多並且不被及時 gc 也會引起 oom,大部分就是以前說的unable to create new native thread。除了 jstack 細細分析 dump 文件外,咱們通常先會看下整體線程,經過pstreee -p pid |wc -l。
或者直接經過查看/proc/pid/task的數量即爲線程數量。
堆外內存
若是碰到堆外內存溢出,那可真是太不幸了。首先堆外內存溢出表現就是物理常駐內存增加快,報錯的話視使用方式都不肯定,若是因爲使用 Netty 致使的,那錯誤日誌裏可能會出現OutOfDirectMemoryError錯誤,若是直接是 DirectByteBuffer,那會報OutOfMemoryError: Direct buffer memory。
堆外內存溢出每每是和 NIO 的使用相關,通常咱們先經過 pmap 來查看下進程佔用的內存狀況pmap -x pid | sort -rn -k3 | head -30,這段意思是查看對應 pid 倒序前 30 大的內存段。這邊能夠再一段時間後再跑一次命令看看內存增加狀況,或者和正常機器比較可疑的內存段在哪裏。
咱們若是肯定有可疑的內存端,須要經過 gdb 來分析gdb --batch --pid {pid} -ex "dump memory filename.dump {內存起始地址} {內存起始地址+內存塊大小}"
獲取 dump 文件後可用 heaxdump 進行查看hexdump -C filename | less,不過大多數看到的都是二進制亂碼。
NMT 是 Java7U40 引入的 HotSpot 新特性,配合 jcmd 命令咱們就能夠看到具體內存組成了。須要在啓動參數中加入 -XX:NativeMemoryTracking=summary 或者 -XX:NativeMemoryTracking=detail,會有略微性能損耗。
通常對於堆外內存緩慢增加直到爆炸的狀況來講,能夠先設一個基線jcmd pid VM.native_memory baseline。
而後等放一段時間後再去看看內存增加的狀況,經過jcmd pid VM.native_memory detail.diff(summary.diff)作一下 summary 或者 detail 級別的 diff。
能夠看到 jcmd 分析出來的內存十分詳細,包括堆內、線程以及 gc(因此上述其餘內存異常其實均可以用 nmt 來分析),這邊堆外內存咱們重點關注 Internal 的內存增加,若是增加十分明顯的話那就是有問題了。
detail 級別的話還會有具體內存段的增加狀況,以下圖。
此外在系統層面,咱們還可使用 strace 命令來監控內存分配 strace -f -e "brk,mmap,munmap" -p pid
這邊內存分配信息主要包括了 pid 和內存地址。
不過其實上面那些操做也很難定位到具體的問題點,關鍵仍是要看錯誤日誌棧,找到可疑的對象,搞清楚它的回收機制,而後去分析對應的對象。好比 DirectByteBuffer 分配內存的話,是須要 full GC 或者手動 system.gc 來進行回收的(因此最好不要使用-XX:+DisableExplicitGC)。那麼其實咱們能夠跟蹤一下 DirectByteBuffer 對象的內存狀況,經過jmap -histo:live pid手動觸發 fullGC 來看看堆外內存有沒有被回收。若是被回收了,那麼大機率是堆外內存自己分配的過小了,經過-XX:MaxDirectMemorySize進行調整。若是沒有什麼變化,那就要使用 jmap 去分析那些不能被 gc 的對象,以及和 DirectByteBuffer 之間的引用關係了。
GC 問題
堆內內存泄漏老是和 GC 異常相伴。不過 GC 問題不僅是和內存問題相關,還有可能引發 CPU 負載、網絡問題等系列併發症,只是相對來講和內存聯繫緊密些,因此咱們在此單獨總結一下 GC 相關問題。
咱們在 CPU 章介紹了使用 jstat 來獲取當前 GC 分代變化信息。而更多時候,咱們是經過 GC 日誌來排查問題的,在啓動參數中加上-verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps來開啓 GC 日誌。
常見的 Young GC、Full GC 日誌含義在此就不作贅述了。
針對 gc 日誌,咱們就能大體推斷出 youngGC 與 fullGC 是否過於頻繁或者耗時過長,從而對症下藥。咱們下面將對 G1 垃圾收集器來作分析,這邊也建議你們使用 G1-XX:+UseG1GC。
youngGC 過頻繁
youngGC 頻繁通常是短週期小對象較多,先考慮是否是 Eden 區/新生代設置的過小了,看可否經過調整-Xmn、-XX:SurvivorRatio 等參數設置來解決問題。若是參數正常,可是 young gc 頻率仍是過高,就須要使用 Jmap 和 MAT 對 dump 文件進行進一步排查了。
youngGC 耗時過長
耗時過長問題就要看 GC 日誌裏耗時耗在哪一塊了。以 G1 日誌爲例,能夠關注 Root Scanning、Object Copy、Ref Proc 等階段。Ref Proc 耗時長,就要注意引用相關的對象。Root Scanning 耗時長,就要注意線程數、跨代引用。Object Copy 則須要關注對象生存週期。並且耗時分析它須要橫向比較,就是和其餘項目或者正常時間段的耗時比較。好比說圖中的 Root Scanning 和正常時間段比增加較多,那就是起的線程太多了。
觸發 fullGC
G1 中更多的仍是 mixedGC,但 mixedGC 能夠和 youngGC 思路同樣去排查。觸發 fullGC 了通常都會有問題,G1 會退化使用 Serial 收集器來完成垃圾的清理工做,暫停時長達到秒級別,能夠說是半跪了。
fullGC 的緣由可能包括如下這些,以及參數調整方面的一些思路:
另外,咱們能夠在啓動參數中配置-XX:HeapDumpPath=/xxx/dump.hprof來 dump fullGC 相關的文件,並經過 jinfo 來進行 gc 先後的 dump
jinfo -flag +HeapDumpBeforeFullGC pid
jinfo -flag +HeapDumpAfterFullGC pid
jinfo -flag +HeapDumpBeforeFullGC pid
jinfo -flag +HeapDumpAfterFullGC pid
這樣獲得 2 份 dump 文件,對比後主要關注被 gc 掉的問題對象來定位問題。
網絡
涉及到網絡層面的問題通常都比較複雜,場景多,定位難,成爲了大多數開發的噩夢,應該是最複雜的了。這裏會舉一些例子,並從 tcp 層、應用層以及工具的使用等方面進行闡述。
超時
超時錯誤大部分處在應用層面,因此這塊着重理解概念。超時大致能夠分爲鏈接超時和讀寫超時,某些使用鏈接池的客戶端框架還會存在獲取鏈接超時和空閒鏈接清理超時。
咱們在設置各類超時時間中,須要確認的是儘可能保持客戶端的超時小於服務端的超時,以保證鏈接正常結束。
在實際開發中,咱們關心最多的應該是接口的讀寫超時了。
如何設置合理的接口超時是一個問題。若是接口超時設置的過長,那麼有可能會過多地佔用服務端的 tcp 鏈接。而若是接口設置的太短,那麼接口超時就會很是頻繁。
服務端接口明明 rt 下降,但客戶端仍然一直超時又是另外一個問題。這個問題其實很簡單,客戶端到服務端的鏈路包括網絡傳輸、排隊以及服務處理等,每個環節均可能是耗時的緣由。
TCP 隊列溢出
tcp 隊列溢出是個相對底層的錯誤,它可能會形成超時、rst 等更表層的錯誤。所以錯誤也更隱蔽,因此咱們單獨說一說。
如上圖所示,這裏有兩個隊列:syns queue(半鏈接隊列)、accept queue(全鏈接隊列)。三次握手,在 server 收到 client 的 syn 後,把消息放到 syns queue,回覆 syn+ack 給 client,server 收到 client 的 ack,若是這時 accept queue 沒滿,那就從 syns queue 拿出暫存的信息放入 accept queue 中,不然按 tcp_abort_on_overflow 指示的執行。
tcp_abort_on_overflow 0 表示若是三次握手第三步的時候 accept queue 滿了那麼 server 扔掉 client 發過來的 ack。tcp_abort_on_overflow 1 則表示第三步的時候若是全鏈接隊列滿了,server 發送一個 rst 包給 client,表示廢掉這個握手過程和這個鏈接,意味着日誌裏可能會有不少connection reset / connection reset by peer。
那麼在實際開發中,咱們怎麼能快速定位到 tcp 隊列溢出呢?
netstat 命令,執行 netstat -s | egrep "listen|LISTEN"
如上圖所示,overflowed 表示全鏈接隊列溢出的次數,sockets dropped 表示半鏈接隊列溢出的次數。
ss 命令,執行 ss -lnt
上面看到 Send-Q 表示第三列的 listen 端口上的全鏈接隊列最大爲 5,第一列 Recv-Q 爲全鏈接隊列當前使用了多少。
接着咱們看看怎麼設置全鏈接、半鏈接隊列大小吧:
全鏈接隊列的大小取決於 min(backlog, somaxconn)。backlog 是在 socket 建立的時候傳入的,somaxconn 是一個 os 級別的系統參數。而半鏈接隊列的大小取決於 max(64, /proc/sys/net/ipv4/tcp_max_syn_backlog)。
在平常開發中,咱們每每使用 servlet 容器做爲服務端,因此咱們有時候也須要關注容器的鏈接隊列大小。在 Tomcat 中 backlog 叫作acceptCount,在 Jetty 裏面則是acceptQueueSize。
RST 異常
RST 包表示鏈接重置,用於關閉一些無用的鏈接,一般表示異常關閉,區別於四次揮手。
在實際開發中,咱們每每會看到connection reset / connection reset by peer錯誤,這種狀況就是 RST 包致使的。
端口不存在
若是像不存在的端口發出創建鏈接 SYN 請求,那麼服務端發現本身並無這個端口則會直接返回一個 RST 報文,用於中斷鏈接。
主動代替 FIN 終止鏈接
通常來講,正常的鏈接關閉都是須要經過 FIN 報文實現,然而咱們也能夠用 RST 報文來代替 FIN,表示直接終止鏈接。實際開發中,可設置 SO_LINGER 數值來控制,這種每每是故意的,來跳過 TIMED_WAIT,提供交互效率,不閒就慎用。
客戶端或服務端有一邊發生了異常,該方向對端發送 RST 以告知關閉鏈接
咱們上面講的 tcp 隊列溢出發送 RST 包其實也是屬於這一種。這種每每是因爲某些緣由,一方沒法再能正常處理請求鏈接了(好比程序崩了,隊列滿了),從而告知另外一方關閉鏈接。
接收到的 TCP 報文不在已知的 TCP 鏈接內
好比,一方機器因爲網絡實在太差 TCP 報文失蹤了,另外一方關閉了該鏈接,而後過了許久收到了以前失蹤的 TCP 報文,但因爲對應的 TCP 鏈接已不存在,那麼會直接發一個 RST 包以便開啓新的鏈接。
一方長期未收到另外一方的確認報文,在必定時間或重傳次數後發出 RST 報文
這種大多也和網絡環境相關了,網絡環境差可能會致使更多的 RST 報文。
以前說過 RST 報文多會致使程序報錯,在一個已關閉的鏈接上讀操做會報connection reset,而在一個已關閉的鏈接上寫操做則會報connection reset by peer。一般咱們可能還會看到broken pipe錯誤,這是管道層面的錯誤,表示對已關閉的管道進行讀寫,每每是在收到 RST,報出connection reset錯後繼續讀寫數據報的錯,這個在 glibc 源碼註釋中也有介紹。
咱們在排查故障時候怎麼肯定有 RST 包的存在呢?固然是使用 tcpdump 命令進行抓包,並使用 wireshark 進行簡單分析了。tcpdump -i en0 tcp -w xxx.cap,en0 表示監聽的網卡。
接下來咱們經過 wireshark 打開抓到的包,可能就能看到以下圖所示,紅色的就表示 RST 包了。
TIME_WAIT 和 CLOSE_WAIT
TIME_WAIT 和 CLOSE_WAIT 是啥意思相信你們都知道。
在線上時,咱們能夠直接用命令netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'來查看 time-wait 和 close_wait 的數量
用 ss 命令會更快ss -ant | awk '{++S[$1]} END {for(a in S) print a, S[a]}'
TIME_WAIT
time_wait 的存在一是爲了丟失的數據包被後面鏈接複用,二是爲了在 2MSL 的時間範圍內正常關閉鏈接。它的存在其實會大大減小 RST 包的出現。
過多的 time_wait 在短鏈接頻繁的場景比較容易出現。這種狀況能夠在服務端作一些內核參數調優:
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 1
固然咱們不要忘記在 NAT 環境下由於時間戳錯亂致使數據包被拒絕的坑了,另外的辦法就是改小tcp_max_tw_buckets,超過這個數的 time_wait 都會被幹掉,不過這也會致使報time wait bucket table overflow的錯。
CLOSE_WAIT
close_wait 每每都是由於應用程序寫的有問題,沒有在 ACK 後再次發起 FIN 報文。close_wait 出現的機率甚至比 time_wait 要更高,後果也更嚴重。每每是因爲某個地方阻塞住了,沒有正常關閉鏈接,從而漸漸地消耗完全部的線程。
想要定位這類問題,最好是經過 jstack 來分析線程堆棧來排查問題,具體可參考上述章節。這裏僅舉一個例子。
開發同窗說應用上線後 CLOSE_WAIT 就一直增多,直到掛掉爲止,jstack 後找到比較可疑的堆棧是大部分線程都卡在了countdownlatch.await方法,找開發同窗瞭解後得知使用了多線程可是確沒有 catch 異常,修改後發現異常僅僅是最簡單的升級 sdk 後常出現的class not found。
今天就分享這麼多,歡迎各位朋友在留言區評論,對於有價值的留言,我都會一一回復的。若是以爲文章對你有一丟丟幫助,請給我點個贊吧,讓更多人看到該文章。另外,小編最近將收集的Java程序員進階架構師和麪試的資料作了一些整理,免費分享給每一位學習Java的朋友,須要的能夠進羣:751827870,歡迎你們進羣和我一塊兒交流。