我们在项目中使用LXC(Linux 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所示。
图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进程
一位博友似乎遇到过类似问题,在其博客(http://leonmau.blog.51cto.com/2202260/1210708)上给出了巧妙的解决方法,给予我很多启示。不过,我认为该方法稍显繁杂,也没有从根本上解决问题,因此我提出了自己的分析思路与解决方法。
二、问题分析
上述现象表明,lxc内部与宿主机使用两套独立的PID Namespace,二者没有真正共享PIDNamespace。该问题与lxc源码中的“lxc clone机制”有关。
阅读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代码所示。
表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调试工具全部有效。
图3修改LXC代码后Container内启动Java进程
四、新问题
在解决上述问题(共享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命令仍然可以杀死对应进程。
五、新问题解决方案
本文讨论的两个问题是“共享PID Namespace”这枚硬币的两面,所以我认为这两个问题很难同时得到根本解决,需要进行权衡。在本项目中,我们认为共享PID Namespace更为重要。因此,我提出一个具有可行性的方案,当应用方需要重新部署时,可以按照以下步骤:
1)应用方删除所有应用进程;
2)使用lxc-stop/lxc-kill命令删除lxc;(正常情况下,当lxc内部所有应用进程被杀死后,lxc会自行退出。但为了保险起见,在删除所有应用进程后,仍需要显式地删除lxc);
3)使用lxc重新启动应用。
根据以上步骤,我们可以完全删除Container,具体情况如图5所示。
图5 修改LXC代码后先删除所有进程再删除Container
参考:
http://leonmau.blog.51cto.com/2202260/1210708
本文出自 “说话的白菜” 博客,转载请与作者联系!