咱們在項目中使用LXC(Linux Container)對系統進行資源控制,上線期間發現一個問題,使用LXC啓動Java進程後,java調試命令(如jps/jstat)無效。其實,java調試命令無效只是問題的表面現象,真正緣由在於Container與宿主機沒有共享PID Namespace。本文將分析其中緣由,並給出解決方案。java
1、問題現象jvm
咱們發現,使用lxc啓動Java進程後,jvm的調試命令(如jps/jstat)無效。正常狀況下,jps命令能夠得到java進程的pid,是其餘許多java/jvm調試命令的基礎,所以咱們以jps命令爲例分析此問題。咱們知道,Jps命令查看進程信息實際依賴目錄/tmp/hsperfdata_xxx中的臨時文件,且該目錄下全部文件都以進程的pid爲文件名。舉例來講,咱們在物理節點啓動一個Java進程(不使用lxc),能夠經過jps命令查看pid,且發如今目錄/tmp/hsperfdata_xxx下存在一個同名的文件。具體狀況如圖1所示。ide
圖1 物理節點上啓動Java進程函數
測試與調研發現,當咱們使用LXC啓動java進程時,jps返回的是lxc內部pid,而不是在宿主機上的pid,目錄/tmp/hsperfdata_xxx下的文件也是以lxc內部pid命名。當咱們使用lxc-ps或者ps命令時,我能夠得到該java進程在宿主機上真正的pid。因爲pid號沒法正確對應,致使jps和其餘java調試命令無效。具體狀況如圖2所示。工具
圖2 Container內啓動Java進程oop
一位博友彷佛遇到過相似問題,在其博客(http://leonmau.blog.51cto.com/2202260/1210708)上給出了巧妙的解決方法,給予我不少啓示。不過,我認爲該方法稍顯繁雜,也沒有從根本上解決問題,所以我提出了本身的分析思路與解決方法。測試
2、問題分析spa
上述現象代表,lxc內部與宿主機使用兩套獨立的PID Namespace,兩者沒有真正共享PIDNamespace。該問題與lxc源碼中的「lxc clone機制」有關。pwa
閱讀lxc-0.7.5源碼,當使用lxc-start或lxc-execute命令建立一個新的Container時,lxc會調用函數lxc_spawn(存在與lxc源文件start.c中),而lxc_spwan又會調用函數lxc_clone(存在與lxc源文件namespace.c),去clone一個lxc。在調用lxc_clone以前,會先設置clone_flags,相應源碼如表1代碼所示。3d
表1 lxc源碼設置clone_flags
clone_flags = CLONE_NEWUTS|CLONE_NEWPID|CLONE_NEWIPC|CLONE_NEWNS; // lxc 源代碼設置的clone_flags
lxc源碼設置的clone_flags包括CLONE_NEWPID標誌位,該標誌位代表新lxc使用一套新的獨立的PIDNamespace。
3、解決方案
明白了問題所在和代碼實現,咱們開始解決此問題。這裏特別強調一點,本項目需求是資源控制,對隔離性沒有要求,不要求Container之間、Contianer與宿主機之間使用獨立的Namespace。只有符合上述前提,纔可使用本解決方案。
具體來講,我修改lxc源碼。在源碼lxc-0.7.5-test/src/lxc/start.c中,找到函數lxc_spawn,修改clone_flags,去除CLONE_NEWPID標誌位,使得container與宿主機共享同一個PIDNamespace。修改後代碼如表2代碼所示。而後編譯安裝(./configure, make, sudo make install)。
表2 修改後的lxc源碼設置clone_flags
clone_flags = CLONE_NEWUTS| CLONE_NEWIPC|CLONE_NEWNS; // 設置的clone_flags,去除CLONE_NEWPID
咱們實驗一下,這樣修改是否有效。當咱們使用修改後的LXC啓動java進程時,jps直接返回宿主機上的pid,目錄/tmp/hsperfdata_xxx下的文件也是如此。jps的返回結果與lxc-ps(或者ps)命令的返回結果一致,具體狀況如圖3所示。因爲jps能夠返回正確的pid,其餘以jps爲基礎的調試命令也能夠正常使用。經實際測試,Java調試工具所有有效。
圖3修改LXC代碼後Container內啓動Java進程
4、新問題
在解決上述問題(共享PIDnamespace)以後,產生了一個原來不存在的新問題。對於啓動多個進程的腳本,lxc-stop\lxc-kill命令只能殺掉父進程,而不能殺掉子進程。以圖4爲例,Container內包括三個進程sh myloop.sh(7708)、java MyLoop(7709)、java Myoop(7710),前一個是後兩個的父進程。當時用lxc-stop命令時,OS只殺死了父進程,而兩個子進程的父進程變爲了init(pid:1),即子進程變爲了孤兒進程,被init「收養」。
圖4 修改LXC代碼後沒法一次性刪除全部進程
咱們發現,新問題與共享PID namespace有關。在修改lxc源碼前,lxc內部使用單獨一套PIDNamespace,內部存在一個與init相似的初始化進程,當使用lxc-stop命令時,會殺掉初始化進程,進而殺掉lxc內全部進程。在修改lxc源碼後,lxc內部與宿主機共享一套PID Namespace。此時,除了資源控制因素外,lxc內的進程與直接運行在宿主機上的進程沒有本質區別。對於啓動多個進程的腳本,lxc-stop命令只能殺掉其中的父進程,而不能殺掉其子進程。固然,使用kill命令仍然能夠殺死對應進程。
5、新問題解決方案
本文討論的兩個問題是「共享PID Namespace」這枚硬幣的兩面,因此我認爲這兩個問題很難同時獲得根本解決,須要進行權衡。在本項目中,咱們認爲共享PID Namespace更爲重要。所以,我提出一個具備可行性的方案,當應用方須要從新部署時,能夠按照如下步驟:
1)應用方刪除全部應用進程;
2)使用lxc-stop/lxc-kill命令刪除lxc;(正常狀況下,當lxc內部全部應用進程被殺死後,lxc會自行退出。但爲了保險起見,在刪除全部應用進程後,仍須要顯式地刪除lxc);
3)使用lxc從新啓動應用。
根據以上步驟,咱們能夠徹底刪除Container,具體狀況如圖5所示。
圖5 修改LXC代碼後先刪除全部進程再刪除Container
參考: