会玩,使用 init 进程运行 Container

0c071edb12395754fc9ea82e8f133fb8.gif

背景

关注过 Bare Metal 相关项目的同学应该都了解过系统的启动流程、如何快速的置备一台物理服务器等之类的实现方式,通常都需要运行一个 LiveOS 来实现某些动作。在 Tinkerbell 项目中,使用 Linuxkit 来作为 LiveOS,Plunder 项目中使用 BOOTy 来作为  LiveOS。前几天 @thebsdbox 将 BOOTy 中的一部分抽离了出来,作为 ginit  展示主要的实现方式,可以更好的让我们理解安装环节中的具体细节。今天来看一下这个项目。

如果安装一个 CentOS,那么通常是会通过 kernel + initramfs.img 启动,initramfs.img 中会包含  systemd 、anaconda、dracut 等一些列组件,然后通过 systemd 指定不同的 Target 所属/依赖/顺序来完成最终  Anaconda 调用。Anaconda 通过解析 /proc/cmdline 中的 KickStart 参数来决定自己的安装方式。

ginit 项目展示了以下内容:

  • 制作 initramfs.img

  • 通过 Container image 制作一个 RAW image

  • 通过 QEMU 使用 RAW image 和 Linux Kernel 来运行一个虚拟机

  • ginit 自动运行 Container 中 entrypoint 指令

流程演示

通过 Container image 制作一个 RAW image

RAW image 中最终不会包含 Kernel 部分,以 Nginx Container 为例。提取 nginx:latest image 中的 Entrypoint ,通过 dd 置备一个 RAW image,并格式化为 ext4 ,raw image 作为 loop 设备挂载到本地,通过 docker export 将 Nginx Image 拷贝到挂载点下,卸载挂载点,最终 RAW image 包含了 Nginx Container 的所有内容。这里的 RAW image 因为不包含 kernel,所以无法直接启动,只是作为后续动作的依赖。

Nginx Container 默认的 Entrypointdocker-entrypoint.sh ,通过这个脚本来做一些参数检查动作。

#!/bin/bash

echo "Lets build you a disk image!"
docker pull $1
ENTRYPOINT=$(docker inspect -f '{{.Config.Entrypoint}}' $1 | sed 's/[][]//g')
echo "Creating a 200MB Disk"
dd if=/dev/zero of=disk.img bs=1024k count=200
mkfs.ext4 -F disk.img
mkdir -p /tmp/disk
mount -t ext4 -o loop disk.img /tmp/disk/
echo "Converting $1 to disk image"
docker create --name exporter $1 null
docker export exporter | tar xv -C /tmp/disk
docker rm exporter
umount /tmp/disk
echo The command $ENTRYPOINT will start this container

使用 ginit 制作 initramfs.img

静态编译 ginit;下载并编译 busybox ,将 ginit 编译结果 init 放置到 / 路径下,通过 cpio 将 busybox 归档,使用 gzip 进行压缩。所有流程完成后,将最终得到的 initramfs.cpio.gz 拷贝到项目路径下。initramfs 最终包含的是 busybox + ginit 。

# syntax=docker/dockerfile:experimental

# Build ginit as an init
FROM golang:1.17-alpine as dev
RUN apk add --no-cache git ca-certificates gcc linux-headers musl-dev
COPY . /go/src/github.com/thebsdbox/ginit/
WORKDIR /go/src/github.com/thebsdbox/ginit
ENV GO111MODULE=on
RUN --mount=type=cache,sharing=locked,id=gomod,target=/go/pkg/mod/cache \
    --mount=type=cache,sharing=locked,id=goroot,target=/root/.cache/go-build \
    CGO_ENABLED=1 GOOS=linux go build -a -ldflags "-linkmode external -extldflags '-static' -s -w" -o init
    

# Build Busybox
FROM gcc:10.1.0 as Busybox
RUN apt-get update; apt-get install -y cpio
RUN curl -O https://busybox.net/downloads/busybox-1.31.1.tar.bz2
RUN tar -xf busybox*bz2
WORKDIR busybox-1.31.1
RUN make defconfig; make LDFLAGS=-static CONFIG_PREFIX=./initramfs install

WORKDIR initramfs 
COPY --from=dev /go/src/github.com/thebsdbox/ginit/init .

# Package initramfs
RUN find . -print0 | cpio --null -ov --format=newc > ../initramfs.cpio 
RUN gzip ../initramfs.cpio
RUN mv ../initramfs.cpio.gz /

FROM scratch
COPY --from=Busybox /initramfs.cpio.gz .

通过 QEMU 运行 Container 中的 EntryPoint 指令

到目前状态,我们得到了 initramfs.img ,得到了 raw image,但是还缺少 Linux Kernel 。可以直接从 Ubuntu 提供的 netboot[1] 下载 boot executable bzImage 文件。

现在所有的准备工作都进行完成了,我们可以直接通过 QEMU 来运行虚拟机,其中 Nginx 所需运行环境在 RAW Image 中,ginit 所需运行环境在 initramfs 中。

前面有提到,Nginx Container 默认的 Entrypointdocker-entrypoint.sh,用来做一些参数包装,所以这里我将参数改为了 /usr/sbin/nginx

$ qemu-system-x86_64 -nographic \
  -kernel ./linux \
  -append "entrypoint=/usr/sbin/nginx root=/dev/sda console=ttyS0" \
  -initrd ./initramfs.cpio.gz \
  -hda ./disk.img \
  -m 1G

虚拟机 console 是 ttyS0 ,通过终端运行可以直接查看启动日志:

...
[    1.469920] rtc_cmos 00:00: setting system clock to 2022-03-05T06:36:19 UTC (1646462179)
[    1.525397] ata1.00: ATA-7: QEMU HARDDISK, 2.5+, max UDMA/100
[    1.525579] ata1.00: 409600 sectors, multi 16: LBA48 
[    1.532980] ata2.00: ATAPI: QEMU DVD-ROM, 2.5+, max UDMA/100
[    1.540741] scsi 0:0:0:0: Direct-Access     ATA      QEMU HARDDISK    2.5+ PQ: 0 ANSI: 5
[    1.545673] sd 0:0:0:0: [sda] 409600 512-byte logical blocks: (210 MB/200 MiB)
[    1.547063] sd 0:0:0:0: [sda] Write Protect is off
[    1.547515] sd 0:0:0:0: Attached scsi generic sg0 type 0
[    1.548188] sd 0:0:0:0: [sda] Write cache: enabled, read cache: enabled, doesn't support DPO or FUA
[    1.550227] scsi 1:0:0:0: CD-ROM            QEMU     QEMU DVD-ROM     2.5+ PQ: 0 ANSI: 5
[    1.568178] sd 0:0:0:0: [sda] Attached SCSI disk
[    1.578345] sr 1:0:0:0: [sr0] scsi3-mmc drive: 4x/4x cd/rw xa/form2 tray
[    1.578736] cdrom: Uniform CD-ROM driver Revision: 3.20
[    1.582611] sr 1:0:0:0: Attached scsi generic sg1 type 5
[    1.595655] Freeing unused decrypted memory: 2040K
[    1.666044] Freeing unused kernel image memory: 2712K
[    1.666482] Write protecting the kernel read-only data: 22528k
[    1.669246] Freeing unused kernel image memory: 2008K
[    1.670507] Freeing unused kernel image memory: 1192K
[    1.742691] x86/mm: Checked W+X mappings: passed, no W+X pages found.
[    1.743002] Run /init as init process
INFO[0000] Folder created [dev] -> [/dev]          
INFO[0000] Folder created [proc] -> [/proc]        
INFO[0000] Folder created [sys] -> [/sys]          
INFO[0000] Folder created [tmp] -> [/tmp]          
INFO[0000] Mounted [dev] -> [/dev]                 
INFO[0000] Mounted [proc] -> [/proc]               
INFO[0000] Mounted [sys] -> [/sys]                 
INFO[0000] Mounted [tmp] -> [/tmp]                 
INFO[0000] Starting DHCP client                    
INFO[0000] Starting ginit                          
ERRO[0000] Error finding adapter [Link not found]  
[    2.209227] tsc: Refined TSC clocksource calibration: 2893.182 MHz
[    2.209573] clocksource: tsc: mask: 0xffffffffffffffff max_cycles: 0x29b41aa25d4, max_idle_ns: 440795325238 ns
[    2.209984] clocksource: Switched to clocksource tsc
INFO[0002] Beginning provisioning process          
ERRO[0002] route ip+net: no such network interface 
INFO[0002] Folder created [root] -> [/mnt]         
[    3.902861] random: fast init done
[    3.912319] EXT4-fs (sda): recovery complete
[    3.913757] EXT4-fs (sda): mounted filesystem with ordered data mode. Opts: (null)
[    3.914463] ext4 filesystem being mounted at /mnt supports timestamps until 2038 (0x7fffffff)
INFO[0002] Mounted [root] -> [/mnt]                
INFO[0002] Mounted [dev] -> [/mnt/dev]             
INFO[0002] Mounted [proc] -> [/mnt/proc]           
INFO[0002] Starting Shell                          
INFO[0002] Waiting for command to finish...        
/ #

其中 [ 1.743002] Run /init as init process 中的 /init 已经是我们上面编译的 ginitginit 运行的日志输出为:

INFO[0000] Folder created [dev] -> [/dev]          
INFO[0000] Folder created [proc] -> [/proc]        
INFO[0000] Folder created [sys] -> [/sys]          
INFO[0000] Folder created [tmp] -> [/tmp]          
INFO[0000] Mounted [dev] -> [/dev]                 
INFO[0000] Mounted [proc] -> [/proc]               
INFO[0000] Mounted [sys] -> [/sys]                 
INFO[0000] Mounted [tmp] -> [/tmp]                 
INFO[0000] Starting DHCP client                    
INFO[0000] Starting ginit                          
ERRO[0000] Error finding adapter [Link not found]  
[    2.209227] tsc: Refined TSC clocksource calibration: 2893.182 MHz
[    2.209573] clocksource: tsc: mask: 0xffffffffffffffff max_cycles: 0x29b41aa25d4, max_idle_ns: 440795325238 ns
[    2.209984] clocksource: Switched to clocksource tsc
INFO[0002] Beginning provisioning process          
ERRO[0002] route ip+net: no such network interface 
INFO[0002] Folder created [root] -> [/mnt]         
[    3.902861] random: fast init done
[    3.912319] EXT4-fs (sda): recovery complete
[    3.913757] EXT4-fs (sda): mounted filesystem with ordered data mode. Opts: (null)
[    3.914463] ext4 filesystem being mounted at /mnt supports timestamps until 2038 (0x7fffffff)
INFO[0002] Mounted [root] -> [/mnt]                
INFO[0002] Mounted [dev] -> [/mnt/dev]             
INFO[0002] Mounted [proc] -> [/mnt/proc]           
INFO[0002] Starting Shell                          
INFO[0002] Waiting for command to finish...

主要做了几件事情:创建必要的路径,创建对应的设备,启动一个 DHCP Client 来获取 IP 地址,挂载 RAW Image 到 /mnt 下,通过 chroot 运行 entrypoint 参数中指定的程序,在这里是 /usr/sbin/nginx ,最终提供一个 Shell 环境给用户。我们可以通过 ps 命令查看当前所运行的进程:

/ # ps -ef |grep -v '\['
PID   USER     TIME  COMMAND
    1 0         0:01 /init
  178 0         0:00 nginx: master process /usr/sbin/nginx
  179 0         0:00 /bin/sh
  180 101       0:00 nginx: worker process
  193 0         0:00 ps -ef
/ # df 
Filesystem           1K-blocks      Used Available Use% Mounted on
devtmpfs                497020         4    497016   0% /dev
tmpfs                   502392         0    502392   0% /tmp
/dev/sda                181984    150940     16708  90% /mnt
devtmpfs                497020         4    497016   0% /mnt/dev
/ # ls /mnt/docker-entrypoint.sh 
/mnt/docker-entrypoint.sh
/ # ls /mnt/usr/sbin/nginx
/mnt/usr/sbin/nginx
/ # ls -hl /init
-rwxr-xr-x    1 0        0           3.4M Mar  5 04:20 /init

现在我们已经将一个 Container Image 中要运行的指令,通过 Linux kernel 配合 initramfs  来运行了起来,在 Bare Metal 场景下,我们可以将 Nginx 内置到 initramfs 中,将 Nginx 替换为 Docker  或者 Container 然后暴露出去,物理服务器作为 Docker Server,置备服务器作为 Docker Client  连接物理服务器进行指定容器的运行,最终完成物理服务器 OS 的安装,这也是目前 TinkerBell 的实现方式。

ginit 具体实现

创建系统设备并挂载

DefaultMountsDefaultDevices 中定义了一些必须的设备如 /dev/null, /dev/random, /dev/urandom ,和挂载点,如 /dev,/proc, /tmp, /sys

urandom := Device{
  CreateDevice: false,

  Name:  "urandom",
  Path:  "/dev/urandom",
  Mode:  syscall.S_IFCHR,
  Major: 1,
  Minor: 9,
 }

dev := Mount{
  CreateMount: false,
  EnableMount: false,
  Name:        "dev",
  Source:      "devtmpfs",
  Path:        "/dev",
  FSType:      "devtmpfs",
  Flags:       syscall.MS_MGC_VAL,
  Mode:        0777,
 }
 m.Mount = append(m.Mount, dev)

//cmd.Execute()
m := realm.DefaultMounts()
d := realm.DefaultDevices()
dev := m.GetMount("dev")
dev.CreateMount = true
dev.EnableMount = true

proc := m.GetMount("proc")
proc.CreateMount = true
proc.EnableMount = true

tmp := m.GetMount("tmp")
tmp.CreateMount = true
tmp.EnableMount = true

sys := m.GetMount("sys")
sys.CreateMount = true
sys.EnableMount = true

// Create all folders
m.CreateFolder()
// Ensure that /dev is mounted (first)
m.MountNamed("dev", true)

// Create all devices
d.CreateDevice()

// Mount any additional mounts
m.MountAll()

在基本环境准备完成后,启动 DHCP Client,获取 IP 地址:

log.Println("Starting DHCP client")
 go realm.DHCPClient()

 // HERE IS WHERE THE MAIN CODE GOES
 log.Infoln("Starting ginit")
 time.Sleep(time.Second * 2)

 log.Infoln("Beginning provisioning process")

 mac, err := realm.GetMAC()
 if err != nil {
  log.Errorln(err)
  //realm.Shell()
 }
 fmt.Print(mac)

现在系统环境准备好了,网络也准备好了,那么可以运行具体的指令了,获取指令的方式是通过解析 /proc/cmdline/proc/cmdline 是通过我们在创建 VM 的时候通过 --append 传递的:

在解析到 rootentrypoint 参数值后,通过 Mountroot 挂载到对应的挂载点,通过 chroot 运行 entrypoint

stuffs, err := ParseCmdLine(CmdlinePath)
if err != nil {
 log.Errorln(err)
}
_, err = realm.MountRootVolume(stuffs["root"])
if err != nil {
 log.Errorf("Disk Error: [%v]", err)
}

cmd := exec.Command("/usr/sbin/chroot", []string{"/mnt", stuffs["entrypoint"]}...)
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr

err = cmd.Start()
if err != nil {
 log.Errorf("command error [%v]", err)
}
err = cmd.Wait()
if err != nil {
 log.Errorf("error [%v]", err)
}

realm.Shell()

所有程序运行完成后,提供一个Shell 环境给用户:

// Shell will Start a userland shell
func Shell() {
 // Shell stuff
 log.Println("Starting Shell")

 // TTY hack to support ctrl+c
 cmd := exec.Command("/usr/bin/setsid", "cttyhack", "/bin/sh")
 cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr

 err := cmd.Start()
 if err != nil {
  log.Errorf("Shell error [%v]", err)
 }
 log.Printf("Waiting for command to finish...")
 err = cmd.Wait()
 if err != nil {
  log.Errorf("Shell error [%v]", err)
 }
}

总结

ginit 作为一个最小实现方便我们快速了解 init 具体做了什么,将 ginit 替换为 systemd 同理,但是直接看 systemd bootup 容易迷失在成堆的 Target 依赖中。在查找资料的过程中还看到了 https://github.com/QuentinPerez/busygox 做了类似的事情,可以作为参考。

参考链接

  • https://github.com/thebsdbox/ginit

  • https://unix.stackexchange.com/questions/146284/minimal-linux-with-kernel-and-busybox-etc-inittab-is-ignored-only-init-is-ex/147688#147688

  • https://github.com/QuentinPerez/busygox

引用链接

[1]

netboot: http://archive.ubuntu.com/ubuntu/dists/focal-updates/main/installer-amd64/current/legacy-images/netboot/ubuntu-installer/amd64/

原文链接:https://zdyxry.github.io/2022/03/05/%E4%BD%BF%E7%94%A8-init-%E8%BF%9B%E7%A8%8B%E8%BF%90%E8%A1%8C-Container/

a1c443d9482b79dd1baabbe47b3d605f.gif

afb1982248997cddd137ef268b88cb73.png

你可能还喜欢

点击下方图片即可阅读

48f42c90614b35b930f3e7a711e14399.png

Koyeb 容器云——Heroku 的继承者?

c2901615ba4228bc35be22b17658864d.gif

云原生是一种信仰 🤘

关注公众号

后台回复◉k8s◉获取史上最方便快捷的 Kubernetes 高可用部署工具,只需一条命令,连 ssh 都不需要!

d7cbb36cc46274f8b1745d645c50ed8b.gif

1c912db9ec0fc00bf0a49085897d361a.gif

点击 "阅读原文" 获取更好的阅读体验!

发现朋友圈变“安静”了吗?

7d41e9569ef638a43685eb6673bb86f5.gif

<think>好的,用户现在想知道如何在Dockerfile中使用init参数。之前他们问过关于Alpine离线安装Tini的问题,所以可能他们现在是在构建Docker镜像时需要集成Tini作为init进程。 首先,我需要确定用户所说的“init参数”具体指什么。在Docker的上下文中,这可能涉及到两个方面的内容:一个是使用Docker自带的--init选项,另一个是在Dockerfile中配置Tini作为ENTRYPOINT或CMD的一部分。用户可能是在运行容器时想启用init进程,但希望在构建镜像时就配置好,而不是每次运行都加参数。 接下来,我需要回顾之前的对话历史。用户之前已经了解了如何离线安装Tini到Alpine系统,并且有在Dockerfile中复制Tini二进制文件的步骤。所以现在他们可能想进一步了解如何在Dockerfile中正确设置,使得容器启动时自动使用Tini作为init进程。 可能需要解释两种方法:一种是直接使用Docker的--init参数,这会在运行时自动注入一个轻量级init进程(tini),另一种是用户自己在镜像中安装Tini并配置ENTRYPOINT,这样不需要每次运行都加参数。 需要比较这两种方法的优缺点。例如,使用--init参数不需要修改Dockerfile,但每次运行都要指定;而集成到Dockerfile中则更方便,适合需要始终使用init进程的场景。用户可能在寻求如何在Dockerfile中实现后者,因为他们之前已经安装了Tini。 另外,需要注意Docker版本的问题。Docker自带的--init参数需要版本1.13+,并且docker run命令需要加上--init。如果用户希望镜像本身包含Tini,那可能需要详细说明如何复制二进制文件、设置权限、配置ENTRYPOINT等步骤,确保用户能够正确编写Dockerfile。 同时,用户可能对信号处理和僵尸进程回收感兴趣,所以需要强调Tini的作用,比如正确处理SIGTERM信号,避免僵尸进程等,以及为什么在容器中使用init进程是重要的。 还要考虑可能的错误情况,比如架构不匹配、权限问题,或者Dockerfile中的路径错误。可能需要提醒用户验证二进制文件的兼容性,确保执行权限正确,以及ENTRYPOINT和CMD的正确写法,避免使用字符串格式而是使用JSON数组格式。 最后,是否需要提供示例Dockerfile,以及运行命令的示例,比如如何构建镜像和运行容器,特别是如果用户选择集成Tini到镜像中,或者选择使用Docker的--init参数时的不同方法。这可以帮助用户更清晰地理解步骤,避免混淆。</think>--- ### Dockerfile 集成 Init 参数的两种方式 在 Docker 中为容器添加 `init` 进程(如 Tini),主要用于**解决容器内 PID 1 进程的信号处理问题**和**僵尸进程回收**。以下是两种实现方式: --- #### **方式 1:使用 Docker 自带的 `--init` 参数(无需修改 Dockerfile)** ##### **原理** - Docker 内置了 `tini` 作为轻量级 init 进程,通过 `--init` 参数自动注入到容器 - **适用场景**:临时测试或无需固化到镜像的场景 ##### **操作步骤** 1. **运行容器时添加 `--init` 参数**: ```bash docker run --init -d your-image ``` 2. **验证是否生效**: ```bash docker exec -it <container-id> ps -ef ``` 输出中应看到 `tini` 作为 PID 1 进程: ``` PID USER TIME COMMAND 1 root 0:00 /dev/init -- /your/app/command ``` --- #### **方式 2:在 Dockerfile 中直接集成 Tini(推荐固化到镜像)** ##### **原理** - **手动将 Tini 二进制文件打包到镜像**,并通过 `ENTRYPOINT` 设置为主进程 - **适用场景**:需要长期稳定运行的容器或自定义 Tini 版本 ##### **操作步骤** 1. **Dockerfile 示例**: ```dockerfile # 选择基础镜像(如 Alpine) FROM alpine:3.18 # 下载并安装 Tini(在线环境) # 若离线安装,需提前将二进制文件复制到镜像 ENV TINI_VERSION v0.19.0 ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-static /usr/bin/tini RUN chmod +x /usr/bin/tini # 设置 Tini 为入口点,并指定容器主进程 ENTRYPOINT ["/usr/bin/tini", "--"] CMD ["/your/app/command"] ``` 2. **构建镜像并运行**: ```bash docker build -t my-app . docker run -d my-app ``` --- #### **关键配置说明** | 配置项 | 作用 | |-----------------|----------------------------------------------------------------------| | `ENTRYPOINT` | 定义容器启动时的固定命令(此处启动 Tini) | | `CMD` | 定义 Tini 启动的子进程(即你的应用) | | `["--"]` | Tini 参数分隔符,后续命令由 Tini 托管 | --- #### **参数扩展(Tini 高级用法)** 通过 `ENTRYPOINT` 传递额外参数控制 Tini 行为: ```dockerfile # 示例:设置子进程超时回收 ENTRYPOINT ["/usr/bin/tini", "-g", "--", "your-app"] ``` 常用参数: - `-g`: 允许子进程接收信号(如 `SIGTERM`) - `-w`: 等待子进程结束后再退出 - `-v`: 输出详细日志 --- #### **验证方法** 1. **检查进程树**: ```bash docker exec -it <container-id> ps -ef ``` 确认 `tini` 是 PID 1,你的应用是子进程。 2. **测试信号传递**: ```bash docker kill --signal=SIGTERM <container-id> ``` 观察容器是否能正常退出。 --- #### **注意事项** 1. **版本兼容性**: - Docker `--init` 要求 Docker 1.13+(默认使用内置 Tini) - 手动集成时需选择与系统架构匹配的 Tini 二进制文件 2. **权限控制**: - 如果以非 root 用户运行容器,需确保 Tini 有执行权限: ```dockerfile USER appuser RUN chmod +x /usr/bin/tini ``` 3. **避免重复注入**: - 若 Dockerfile 中已配置 Tini,**不要同时使用 `--init`**,否则会导致双重托管 --- 通过以上配置,可确保容器内进程的优雅终止和资源回收。**推荐生产环境固化 Tini 到镜像**,减少对外部参数的依赖。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值