docker 学习笔记1:什么是 runc

2019-12-06  本文已影响0人  董泽润

TL;DR runc 是启动容器的最后一步,设置 cgroup, 隔离 namespaces 并启动程序。最早 docker 比较轻量,只有一个单一的 dockerd 进程,后来诞生了 OCI 标准, 用于统一容器运行时接口和镜像文件。而 runc 就是 docker 贡献给社区的一个运行时实现。

docker 架构

运行时标准 runtime spec

当前 OCI 有两个标准:runtime-specimage-spec,实际上就是为了兼容性及移植,而规定了镜像制作的标准,和如何启动解压过的 filesystem bundle ,而 runc 就是一种 runtime spec 的实现,其它的实现参见列表。其中有 c 实现的,号称比 go 的快一倍,貌似没啥用

1. bundle

不太好翻译,filesystem bundle 就是一个目录,提供 config.json 文件和 rootfs 文件系统,参照官网可以用如下命令生成

root@myali1:~# mkdir mycontainer; cd mycontainer
root@myali1:~/mycontainer# mkdir rootfs
root@myali1:~/mycontainer# docker export $(docker create busybox) | tar -C rootfs -xvf -
root@myali1:~/mycontainer# runc spec
root@myali1:~/mycontainer# ls -l
total 8
-rw-r--r--  1 root root 2618 Dec  4 17:44 config.json
drwxr-xr-x 12 root root 4096 Dec  4 17:44 rootfs

2. config

config 描述了当前容器的配置: OCI 版本,启动程序路径与参数,挂载哪些文件系统,平台相关的比如 cgroup, namespaces, cpu quota 等等。具体可以参考 spec config.go

3. 状态

runc 启动的容器,都会把状态文件 state.json 存到一个地方,默认路径是 /run/runc/${container_id},通过 runc state 获取的状态来自于这个文件,里面内容非常多,暂时不看。

root@myali1:~/mycontainer# runc state mycontainerid4
{
  "ociVersion": "1.0.1-dev",
  "id": "mycontainerid4",
  "pid": 13246,
  "status": "running",
  "bundle": "/root/mycontainer",
  "rootfs": "/root/mycontainer/rootfs",
  "created": "2019-12-04T07:06:58.828453173Z",
  "owner": ""
}

Hello World

1. 前台启动

我们先看下刚才生成的 config.json,发现启动了 terminal,运行命令是 sh,然后在 bundle 目录下运行命令 runc run mycontainerid

root@myali1:~/mycontainer# runc run mycontainerid
/ # ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
/ # ls
bin   dev   etc   home  proc  root  sys   tmp   usr   var
/ # df -h
Filesystem                Size      Used Available Use% Mounted on
/dev/vda1                39.2G      4.6G     32.8G  12% /
tmpfs                    64.0M         0     64.0M   0% /dev
shm                      64.0M         0     64.0M   0% /dev/shm
tmpfs                   996.7M         0    996.7M   0% /sys/fs/cgroup
tmpfs                    64.0M         0     64.0M   0% /proc/kcore
tmpfs                    64.0M         0     64.0M   0% /proc/timer_list
tmpfs                    64.0M         0     64.0M   0% /proc/sched_debug
tmpfs                   996.7M         0    996.7M   0% /sys/firmware
tmpfs                   996.7M         0    996.7M   0% /proc/scsi

可以看到容器内生成了 lo 网卡,文件系统也换成了 rootfs 的


ps axjf
root@myali1:~/mycontainer# lsns
        NS TYPE   NPROCS   PID USER            COMMAND
......
4026532273 mnt         1 13610 root            sh
4026532274 uts         1 13610 root            sh
4026532275 ipc         1 13610 root            sh
4026532276 pid         1 13610 root            sh
4026532278 net         1 13610 root            sh

然后我们在宿主机查看下进程和 ns,可以看到 sh 的父进程是 runc,并且开启了 uts, ipc, pid, net namespace,但是细心的发现并没有 user ns

2. 后台启动

修改下 config.json, 将启动命令换成 sleep,并且将 terminate 置为 false

root@myali1:~/mycontainer# cat config.json
......
    "process": {
        "terminal": false,
        "args": [
            "sleep", "500"
        ],
......

然后在 bundle 目录下先创建容器,不启动

root@myali1:~/mycontainer# runc create backgroundc
root@myali1:~/mycontainer# runc list
ID            PID         STATUS      BUNDLE              CREATED                          OWNER
backgroundc   13700       created     /root/mycontainer   2019-12-04T11:09:10.091216191Z   root

然后进入容器的状态目录 /run/runc/backgroundc

root@myali1:~# cd /run/runc/backgroundc
root@myali1:/run/runc/backgroundc# ls
exec.fifo  state.json

注意这里多了一个 exec.fifo 文件,这是个很重要的用于同步的,稍后会讲

root@myali1:/run/runc/backgroundc# ps aux | grep runc
root     13700  0.0  0.5 494616 12188 ?        Ssl  Dec04   0:00 runc init
root     14724  0.0  0.0  16148  1060 pts/0    S+   10:49   0:00 grep --color=auto runc
root@myali1:/run/runc/backgroundc# lsof -p 13700
COMMAND     PID USER   FD      TYPE DEVICE SIZE/OFF    NODE NAME
runc:[2:I 13700 root  cwd       DIR  252,1     4096 1310876 /
runc:[2:I 13700 root  rtd       DIR  252,1     4096 1310876 /
runc:[2:I 13700 root  txt       REG  252,1 13869512  669613 /
runc:[2:I 13700 root    0u      CHR  136,1      0t0       4 /dev/pts/1 (deleted)
runc:[2:I 13700 root    1u      CHR  136,1      0t0       4 /dev/pts/1 (deleted)
runc:[2:I 13700 root    2u      CHR  136,1      0t0       4 /dev/pts/1 (deleted)
runc:[2:I 13700 root    4u     FIFO   0,24      0t0    1563 /run/runc/backgroundc/exec.fifo
runc:[2:I 13700 root    5w      CHR    1,3      0t0       6 /null
runc:[2:I 13700 root    6u  a_inode   0,13        0    9567 [eventpoll]

查看进程,发现当前存在一个 runc init,并且打的文件描述符 4u 就是上面提到的 exec.fifo

root@myali1:/run/runc/backgroundc# lsns
        NS TYPE   NPROCS   PID USER            COMMAND
......
4026532273 mnt         1 13700 root            runc init
4026532274 uts         1 13700 root            runc init
4026532275 ipc         1 13700 root            runc init
4026532276 pid         1 13700 root            runc init
4026532278 net         1 13700 root            runc init

再查看当前机器的 namespace, 发现己经创建了容器的 ns,只不过没有启动容器的 cmd. 最后我们启动这个容器

root@myali1:/run/runc/backgroundc# runc start backgroundc
root@myali1:/run/runc/backgroundc# ls
state.json
root@myali1:/run/runc/backgroundc# runc list
ID            PID         STATUS      BUNDLE              CREATED                          OWNER
backgroundc   13700       running     /root/mycontainer   2019-12-04T11:09:10.091216191Z   root

可以看到 exec.fifo 文件没了,再查看下进程

root@myali1:/run/runc/backgroundc# ps axjf | grep -A 4 -B 4 sleep
    1 32068 32068 32068 ?           -1 Ss       0   0:00 /lib/systemd/systemd --user
32068 32069 32068 32068 ?           -1 S        0   0:00  \_ (sd-pam)
    1 13700 13700 13700 ?           -1 Ss       0   0:00 sleep 500

当前进程启动了,但是发现他的父进程是 1,被 init 托管了。如果是 docker 启动的话,那么父进程应该是 shim

实现原理

一句话总结:runc run 根据提供的 filesystem bundle 生成创建容器所需要各种配置,然后创建子进程 runc init,同时父进程 runc run 设置子进程 runc init 的 cgroup, namespaces 等等。子进程 runc init 也要做一部份容器内的初始化,比如创建网络接口路由等等,最后 runc init 系统调用 exec 执行真正的 cmd,而 runc run 退出后,cmd 进程要么由操作系统 1 号进程接管,要么在 docker 环境中被 containerd-shim 接管。

如下图所示 docker 启动 nginx 的例子,另外也可以看到 --runtime-root 参数,其实就是保存 runc 状态的位置

docker run nginx
上一篇下一篇

猜你喜欢

热点阅读