最近容器部署标准化,本来很简单,也没什么特殊的要求,但是在容器里,因为临时需要启动进程,结果停止后出现了僵尸进程。
以Java应用为例,这里以 adoptopenjdk/openjdk11为模板
docker pull adoptopenjdk/openjdk11
Java代码示例,简单处理,没有包名,通过java Demo运行
- public class Demo {
- public static void main(String[] args) throws InterruptedException {
- System.out.println("start>>>>>>>>>>>>");
- Thread.sleep(500000000000l);
- }
- }
编写Dockerfile
- FROM adoptopenjdk/openjdk11:latest
-
- WORKDIR /opt
-
- ADD Demo.class .
-
- ENTRYPOINT ["java","Demo","> /opt/demo.log"]
docker build . -t java-demo:1.0
编译后
docker run -d --name java-demo java-demo:1.0
docker exec -it java-demo bash
模拟需要临时启动一些进程,在虚拟机上非常常见。
echo 'java Demo' > start.sh
执行chmod +x start.sh
./start.sh &
通过另外的bash进入容器,可以看到进程的父子关联,出现僵尸进程
僵尸进程产生跟父进程ID为1有关,父进程id为1,在容器下kill会产生僵尸进程。
为了验证是否产生僵尸进程跟语言的关系,又写了一个rust代码
- fn main() {
- println!("Hello, world!》》》》》》》》》》");
- use std::{thread, time};
- let ten_millis = time::Duration::from_millis(10000000000000);
- thread::sleep(ten_millis);
- }
安装交叉编译环境,以macOS为例
# macOS rustup target add x86_64-unknown-linux-musl brew install filosottile/musl-cross/musl-cross#win
rustup target add x86_64-pc-windows-gnu brew install mingw-w64
配置config
vim ~/.cargo/config 或者在项目的.cargo/config,一般就是编译Linux环境,本地环境不需要交叉编译
[target.x86_64-unknown-linux-musl]
linker = "x86_64-linux-musl-gcc"
cargo build --release --target x86_64-unknown-linux-musl
在target下
在ubuntu运行
镜像准备,控制变量,还是用原来的基底。
- FROM adoptopenjdk/openjdk11:latest
-
- WORKDIR /opt
-
- ADD hello .
-
- RUN chmod +x hello
-
- ENTRYPOINT ["./hello","> /opt/demo.log"]
执行额外进程操作
再进行exec
执行kill -9,出现僵尸进程。
结论是僵尸进程的产生实际上跟语言没关系。
在容器下进程id为1的进程是不能被kill的,而且id为0是所有进程的父进程id
当kill时,实际上kill -15一样会出现,跟-9没关系
1. kill进程,当父进程id为1时,出现僵尸进程
2. kill进程,如果父进程先被kill(包括正常退出),当前进程的父进程id在容器下会被设置为1,即基底容器的运行进程
根据总结的2条结果,要解决也很简单,kill的过程安装创建进程的逆过程即可
先kill子进程,再kill父进程,知道全部kill掉。
尝试java-demo,果然创建的逆过程无僵尸进程产生,这个设计实际上在Kubernetes的发布过程经常出现
rust-demo
在容器下进程id为1的进程是不能kill掉的,当容器下需要创建进程,运行时启动,那么创建进程的过程非常重要,因为销毁需要逆过程,否则会出现僵尸进程。容器下当新创建的进程(id为1的进程除外)的父进程被kill或者自然退出,当前进程的父进程id会被设为1,即容器下不能被kill的进程,导致僵尸进程的产生。