"K8S爲咱們提供自動部署調度應用的能力,並經過健康檢查接口自動重啓失敗的應用,確保服務的可用性,但這種自動運維在某些特殊狀況下會形成咱們的應用陷入持續的調度過程致使業務受損,本文就生產線上一個核心的平臺應用被K8S頻繁重啓調度問題展開剖解,抽絲剝繭一步步從系統到應用的展開分析,最後定位到代碼層面解決問題"html
在搭建devops基礎設施後,業務已經全盤容器化部署,並基於k8s實現自動調度,但個別業務運行一段時間後會被k8s自動重啓,且重啓的無規律性,有時候發生在下午,有時發生在凌晨,從k8s界面看,有的被重啓了上百次:java
k8s是根據pod yaml裏定義的重啓策略執行重啓,這個策略經過: .spec.restartPolicy 進行設置,支持如下三種策略:node
出問題的應用是走CICD自動打包發佈,Yaml也是CD環節自動生成,並無顯示指定重啓策略,因此默認採用Always策略,那麼k8s在哪些狀況會觸發重啓呢,主要有如下場景:docker
出問題的應用正常運行一段時間纔出現的重啓,而且POD自己的Yaml文件以及所在的namespace並沒設置CPU上限,那麼能夠排除:1 3 4 6, 業務是採用Springboot開發的,若是無端退出,JVM自己會產生dump文件,但由重啓行爲是K8s本身觸發的,即便POD裏產生裏dump文件,由於運行時沒有把dump文件目錄映射到容器外面,因此無法去查看上次被重啓時是否產生裏dump文件,因此2 5都有可能致使k8s重啓該業務,不過k8s提供命令能夠查看POD上一次推出緣由,具體命令以下:windows
NAMESPACE=prod SERVER=dts POD_ID=$(kubectl get pods -n ${NAMESPACE} |grep ${SERVER}|awk '{print $1}') kubectl describe pod $POD_ID -n ${NAMESPACE}
命令運行結果顯示POD是由於memory使用超限,被kubelet組件自動kill重啓(若是reason爲空或者unknown,多是上述的緣由2或者是不限制內存和CPU可是該POD在極端狀況下被OS kill,這時能夠查看/var/log/message進一步分析緣由),CICD在建立業務時默認爲每一個業務POD設置最大的內存爲2G,但在基礎鏡像的run腳本中,JVM的最大最小都設置爲2G:後端
exec java -Xmx2g -Xms2g -jar ${WORK_DIR}/*.jar
在分析應用運行的環境和,咱們進一步分析應用使用的JVM自己的狀態,首先看下JVM內存使用狀況命令: jmap -heap {PID}
JVM申請的內存: (eden)675.5+(from)3.5+(to)3.5+(OldGeneration)1365.5=2048mb理論上JVM一啓動就會OOMKill,但事實是業務運行一段時間後才被kill,雖然JVM聲明須要2G內存,可是沒有當即消耗2G內存,經過top命令查看:PS: top和free命令在docker裏看到的內存都是宿主機的,要看容器內部的內存大小和使用,可使用下列命令:api
cat /sys/fs/cgroup/memory/memory.limit_in_bytes
當配-Xmx2g -Xms2g時,虛擬機會申請2G內存,但提交的頁面在首次訪問以前不會消耗任何物理存儲,該業務進程當時實際使用的內存爲1.1g,隨着業務運行,到必定時間後JVM的使用內存會逐步增長,直到達到2G被kill。內存管理相關文章推薦:Reserving and Committing MemoryJvmMemoryUsageoracle
執行命令:運維
jmap -dump:format=b,file=./dump.hprof [pid]
導入JvisualVM分析,發現裏面有大量的Span對象未被回收,未被回收的緣由是被隊列裏item對象引用:隔斷時間執行:異步
jmap -histo pid |grep Span
發現span對象個數一直在增長,span屬於業務工程依賴的分佈式調用鏈追蹤系統DTS裏的對象,DTS是一個透明化無侵入的基礎系統,而該業務也沒有顯示持有Span的引用,在DTS的設計裏,Span是在業務線程產生,而後放入阻塞隊列,等待序列化線程異步消費,生產和消費代碼以下:從以上代碼看,Span在持續增長,應該就是消費者線程自己的消費速度小於了生產者的速度,消費線程執行的消費邏輯是順序IO寫盤,按照ECS普通盤30-40m的IOPS算,每一個Span經過dump看到,平均大小在150byte,理論上每秒能夠寫:3010241024/150=209715,因此不該該是消費邏輯致使消費率降緩,再看代碼裏有個sleep(50)也就是每秒最多能夠寫20個Span,該業務有個定時任務在運行,每次會產生較多的Span對象,且若是此時有其餘業務代碼在運行,也會產生大量的Span,遠大於消費速度,因此出現了對象的積壓,隨着時間推移,內存消耗逐步增大,致使OOMKill。dump該業務的線程棧:
jstack pid >stack.txt
卻發現有兩個寫線程,一個狀態始終是waiting on condition,另外一個dump屢次爲sleep:可是代碼裏是經過Executors.newSingleThreadExecutor(thf);起的單線程池,怎麼會出現兩個消費者呢? 進一步查看代碼記錄,原來始終11月份一次修改時把發送後端的邏輯集成到核心代碼裏,該功能在以前的版本里採用外部jar依賴注入的方式自動裝配的,這樣在如今的版本中會出現兩個Sender對象,其中自動建立的Sender對象沒有被DTS系統引用,他裏面的隊列始終未empty,致使旗下的消費者線程始終阻塞,而內置的Sender對象由於Sleep(50)致使消費速度降低從而出現堆積,Dump時是沒法明確捕獲到他的running狀態,看上去一直在sleep,經過觀察消費線程系列化寫入的文件,發現數據一直在寫入,說明消費線程確實是在運行的.
經過代碼提交記錄瞭解到,上上個版本業務在某些狀況會產生大量的Span,Span的消費速度很是快,會致使該線程CPU飆升的比較厲害,爲了緩解這種狀況,因此加了sleep,實際上發現問題後業務代碼已經進行優化,DTS系統是不須要修改的,DTS應是發現問題,推進業務修復和優化,基礎系統的修改應該很是慎重,由於影響面很是廣。 針對POD的最大內存等於虛擬機最大內存的問題,經過修改CD代碼,默認會在業務配置的內存大小里加200M,爲何是200M不是更多呢?由於k8s會計算當前運行的POD的最大內存來評估當前節點能夠容量多少個POD,若是配置爲+500m或者更多,會致使K8S認爲該節點資源不足致使浪費,但也不能過少過少,由於應用除了自己的代碼外,還會依賴部分第三方共享庫等,也可能致使Pod頻繁重啓.
上述問題的根因是人爲下降了異步線程的消費速度,致使消息積壓引發內存消耗持續增加致使OOM,但筆者更想強調的是,當咱們把應用部署到K8S或者Docker時,**POD和Docker分配的內存須要比應用使用的最大內存適當大一些**,不然就會出現能正常啓動運行,但跑着跑着就頻繁重啓的場景,如問題中的場景,POD指定裏最大內存2G,理論上JVM啓動若是當即使用裏2G確定當即OOM,開發或者運維能當即分析緣由,代價會小不少,可是由於現代操做系統內存管理都是VMM(虛擬內存管理)機制,當JVM參數配置爲: -Xmx2g -Xms2g時,**虛擬機會申請2G內存,但提交的頁面在首次訪問以前不會消耗任何物理存儲,**因此就出現理論上啓動就該OOM的問題延遲到應用慢慢運行直到內存達到2G時被kill,致使定位分析成本很是高。另外,對於JVM dump這種對問題分析很是重要的日誌,必定要映射存儲到主機目錄且保證不被覆蓋,否則容器銷燬時很難去找到這種日誌。