咱们在项目中使用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
参考: