LXC Java調試命令無效與共享PID Namespace

我們在項目中使用LXCLinux Container)對系統進行資源控制,上線期間發現一個問題,使用LXC啓動Java進程後,java調試命令(如jps/jstat)無效。其實,java調試命令無效只是問題的表面現象,真正原因在於Container與宿主機沒有共享PID Namespace。本文將分析其中原因,並給出解決方案。


一、問題現象

我們發現,使用lxc啓動Java進程後,jvm的調試命令(如jps/jstat)無效。正常情況下,jps命令可以獲得java進程的pid,是其他許多java/jvm調試命令的基礎,因此我們以jps命令爲例分析此問題。我們知道,Jps命令查看進程信息實際依賴目錄/tmp/hsperfdata_xxx中的臨時文件,且該目錄下所有文件都以進程的pid爲文件名。舉例來說,我們在物理節點啓動一個Java進程(不使用lxc),可以通過jps命令查看pid,且發現在目錄/tmp/hsperfdata_xxx下存在一個同名的文件。具體情況如圖1所示。

wKioL1MgQyKT6RjvAACjr13MQ3Y273.jpg

1 物理節點上啓動Java進程

測試與調研發現,當我們使用LXC啓動java進程時,jps返回的是lxc內部pid,而不是在宿主機上的pid,目錄/tmp/hsperfdata_xxx下的文件也是以lxc內部pid命名。當我們使用lxc-ps或者ps命令時,我可以獲得該java進程在宿主機上真正的pid。由於pid號無法正確對應,導致jps和其他java調試命令無效。具體情況如圖2所示。

wKioL1MgQzqR99rVAAIwJo6553A136.jpg

2 Container內啓動Java進程

一位博友似乎遇到過類似問題,在其博客(http://leonmau.blog.51cto.com/2202260/1210708)上給出了巧妙的解決方法,給予我很多啓示。不過,我認爲該方法稍顯繁雜,也沒有從根本上解決問題,因此我提出了自己的分析思路與解決方法。


二、問題分析

上述現象表明,lxc內部與宿主機使用兩套獨立的PID Namespace,二者沒有真正共享PIDNamespace該問題與lxc源碼中的“lxc clone機制有關。

閱讀lxc-0.7.5源碼,當使用lxc-startlxc-execute命令創建一個新的Container時,lxc會調用函數lxc_spawn(存在與lxc源文件start.c中),而lxc_spwan又會調用函數lxc_clone(存在與lxc源文件namespace.c),去clone一個lxc。在調用lxc_clone之前,會先設置clone_flags,相應源碼如表1代碼所示。

1 lxc源碼設置clone_flags

clone_flags  = CLONE_NEWUTS|CLONE_NEWPID|CLONE_NEWIPC|CLONE_NEWNS;
// lxc 源代碼設置的clone_flags

lxc源碼設置的clone_flags包括CLONE_NEWPID標誌位,該標誌位表明新lxc使用一套新的獨立的PIDNamespace


三、解決方案

明白了問題所在和代碼實現,我們開始解決此問題。這裏特別強調一點,本項目需求是資源控制,對隔離性沒有要求,不要求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調試工具全部有效。

wKioL1MgQ5yjYi-zAAI9sgCG6bs986.jpg

3修改LXC代碼後Container內啓動Java進程


四、新問題

在解決上述問題(共享PIDnamespace)之後,產生了一個原來不存在的新問題。對於啓動多個進程的腳本,lxc-stop\lxc-kill命令只能殺掉父進程,而不能殺掉子進程。以圖4爲例,Container內包括三個進程sh myloop.sh7708)、java MyLoop(7709)java Myoop7710),前一個是後兩個的父進程。當時用lxc-stop命令時,OS只殺死了父進程,而兩個子進程的父進程變爲了initpid1),即子進程變爲了孤兒進程,被init“收養

wKiom1MgQ83yUZKRAAJwlXChPzg864.jpg

4 修改LXC代碼後無法一次性刪除所有進程

我們發現,新問題與共享PID namespace有關。在修改lxc源碼前,lxc內部使用單獨一套PIDNamespace,內部存在一個與init類似的初始化進程,當使用lxc-stop命令時,會殺掉初始化進程,進而殺掉lxc內所有進程。在修改lxc源碼後,lxc內部與宿主機共享一套PID Namespace。此時,除了資源控制因素外,lxc內的進程與直接運行在宿主機上的進程沒有本質區別。對於啓動多個進程的腳本,lxc-stop命令只能殺掉其中的父進程,而不能殺掉其子進程。當然,使用kill命令仍然可以殺死對應進程。


五、新問題解決方案

本文討論的兩個問題是共享PID Namespace”這枚硬幣的兩面,所以我認爲這兩個問題很難同時得到根本解決,需要進行權衡。在本項目中,我們認爲共享PID Namespace更爲重要。因此,我提出一個具有可行性的方案,當應用方需要重新部署時,可以按照以下步驟:

1)應用方刪除所有應用進程;

2)使用lxc-stop/lxc-kill命令刪除lxc;(正常情況下,當lxc內部所有應用進程被殺死後,lxc會自行退出。但爲了保險起見,在刪除所有應用進程後,仍需要顯式地刪除lxc);

3)使用lxc重新啓動應用。

根據以上步驟,我們可以完全刪除Container,具體情況如圖5所示。

wKiom1MgQ9aiKgDeAAKCOGExdIw847.jpg

5 修改LXC代碼後先刪除所有進程再刪除Container


參考:

http://leonmau.blog.51cto.com/2202260/1210708



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章