本文介绍runC和Libcontainer,解开Docker容器运行时底层的面纱。
Libcontainer是一个开源的Linux容器管理库,它是由Docker团队开发的,用于支持Docker容器引擎的底层。Libcontainer提供了一个接口,使得应用程序可以直接访问Linux内核中的容器相关功能,例如命名空间、控制组、文件系统等。
而命名空间(通过Linux Namespace)、控制组(Cgroups)、文件系统(rootfs)正是实现容器的核心。
Namespace主要实现访问隔离,它是针对一类资源进行抽象,并将其进行封装提供给一个容器使用,因为每个容器都有自己独立的抽象,所以他们值之间是不可见的。
Cgroup是control group的缩写,它可以实现将一组进程放进一个控制组,通过给这个控制组分配对应的资源,从而实现这个组下的资源控制。
Libcontainer具有以下主要功能:
总结来说,Libcontainer利用Linux内核的命名空间和控制组特性,将容器与主机隔离开来。在容器启动时,Libcontainer会创建新的命名空间和控制组,然后在这些隔离环境中启动容器进程。容器进程只能访问到隔离环境中的资源,不能访问到主机的资源。同时,Libcontainer还支持文件系统和网络的隔离,使得容器可以拥有自己的文件系统和网络配置。
runC是一个基于Libcontainer的容器运行时工具,它是Docker容器引擎的一个组件。runC利用Libcontainer提供的接口,将容器镜像转换为容器实例,并在隔离环境中启动容器进程。runC还提供了对容器的生命周期管理,包括启动、停止、重启等。因此,可以说runC是Libcontainer的一个应用程序,用于管理和运行容器。同时,runC也是一个独立的开源项目,可以用于在各种容器环境中运行容器。
runC在github上进行了开源,其源码也包括了libcontainer部分,github开源项目为opencontainers/runc
,目前已经有了10.1K Star,项目还是非常受欢迎的。
使用runC也能直接进行容器管理,下面演示一下直接使用runC创建容器以及容器的启停。
第一步:从github上下载runC并解压缩,使用make和make install进行安装
目前runC的最新版本是V1.1.4,可以直接从github上下载,根据你的服务器的不同平台下载不同的版本,这里选择通用Linux版本。
[root@node1 docker]# wget https://github.com/opencontainers/runc/releases/download/v1.1.4/runc.tar.xz
[root@node1 docker]# tar -xvf runc.tar.xz
runc的安装需要依赖于git和go,Go的版本需要1.19或以上的版本,所以不能直接使用yum进行安装,因为目前yum的最新版本为1.18。
另外runc的运行还需要系统支持seccomp,所以还需要安装相关的依赖。如果没有依赖的依赖,直接执行make
可以发现会有如下的异常。
[root@node1 runc-1.1.4]# make
make: go: Command not found
make: git: Command not found
make: git: Command not found
go build -trimpath -tags "seccomp" -ldflags "-X main.gitCommit= -X main.version=1.1.4 " -o runc .
/bin/sh: go: command not found
make: *** [runc] Error 127
如果是在Centos则执行以下命令即可,其他平台大家自行百度,这个比较简单。
[root@node1 runc-1.1.4]# yum install git
[root@node1 runc-1.1.4]# git --version
git version 1.8.3.1
[root@node1 runc-1.1.4]# git config --global user.name "lucas"
[root@node1 runc-1.1.4]# git config --global user.email [email protected]
[root@node1 runc-1.1.4]# git config --list
user.name=lucas
user.email=[email protected]
runc就是基于go开发的,所以对于golang是强依赖。其实解压缩可以看到以下的目录结构,很多都是go文件。
从官网下载最新版本的Go
[root@node1 go]# wget https://go.dev/dl/go1.20.2.linux-amd64.tar.gz
[root@node1 go]# tar -zxvf go1.20.2.linux-amd64.tar.gz -C /usr/local
将go添加到环境变量,修改/etc/profile,在文件最后面添加如下内容:
export GO111MODULE=on
export GOROOT=/usr/local/go
export GOPATH=/home/go
export PATH=$PATH:$GOROOT/bin:$GOPATH/bin
使配置生效并且进行验证go已经正常安装
[root@node1 go]# source /etc/profile
[root@node1 go]# go version
go version go1.20.2 linux/amd64
为了支持seccomp,在Centos上应该安装libseccomp-devel
,在Ubuntu上安装libseccomp-dev
。
[root@node1 go]# yum install libseccomp-devel
完成依赖的安装以后,使用make
和make install
进行runC的安装,最终runc会被安装到/usr/local/sbin/runc
。
使用runc -version可以查看到runc的版本代表已经成功安装。
[root@node1 runc-1.1.4]# make
fatal: Not a git repository (or any parent up to mount point /home)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: Not a git repository (or any parent up to mount point /home)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
go build -trimpath "-buildmode=pie" -tags "seccomp" -ldflags "-X main.gitCommit= -X main.version=1.1.4 " -o runc .
[root@node1 runc-1.1.4]# make install
fatal: Not a git repository (or any parent up to mount point /home)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: Not a git repository (or any parent up to mount point /home)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
install -D -m0755 runc /usr/local/sbin/runc
[root@node1 runc-1.1.4]# runc -version
runc version 1.1.4
spec: 1.0.2-dev
go: go1.20.2
libseccomp: 2.3.1
终于到了容器创建的环节了,在正式开始前,再校验一下环境。解压runc以后进入script目录,执行check-config
脚本,如果Generally Necessary段都是提示enabled,则表示已经满足条件。
下面开始创建容器,为了演示方便,使用docker(docker请自行安装)和busybox(是一系列的Linux工具集合)来支持生成rootfs,rootfs文件系统结构是容器运行的必要内容。
使用如下命令,先创建test-container
目录,用来作为容器的主目录,接着创建rootfs目录存储使用docker和busybox生成的rootfs结构。
[root@node1 docker]# mkdir test-container
[root@node1 docker]# cd test-container
[root@node1 test-container]# mkdir rootfs
[root@node1 test-container]# docker export $(docker create busybox) | tar -C rootfs -xvf -
Unable to find image 'busybox:latest' locally
Trying to pull repository docker.io/library/busybox ...
latest: Pulling from docker.io/library/busybox
1487bff95222: Pulling fs layer
1487bff95222: Verifying Checksum
1487bff95222: Download complete
1487bff95222: Pull complete
Digest: sha256:c118f538365369207c12e5794c3cbfb7b042d950af590ae6c287ede74f29b7d4
Status: Downloaded newer image for docker.io/busybox:latest
.dockerenv
bin/
bin/[
bin/[[
bin/acpid
bin/add-shell
bin/addgroup
bin/adduser
bin/adjtimex
bin/ar
bin/arch
......
成功以后查看rootfs目录,可以看到已经自动生成了如下的目录结构:
使用runc spec
生成配置模板(符合OCI标准),执行完毕以后可以看到config.json,这就是容器运行的配置文件。
[root@node1 test-container]# runc spec
[root@node1 test-container]# ls
config.json rootfs
使用如下命令,就可以直接启动容器并且进入容器内部终端拉!
[root@node1 test-container]# runc run containerid
以上演示了直接启动容器并且进入容器Terminal,其实更多时候我们需要的是容器创建、启动、停止、删除等动作进行容器的生命周期管理。runc同样提供了这样子的能力。
我们首先修改config.json,把terminal和args这两个配置项按照如下进行修改,修改完启动不会直接进入terminal。
"terminal": false
"args": ["sleep", "5"]
[root@node1 test-container]# runc create containerid
[root@node1 test-container]# runc create containerid
[root@node1 test-container]# runc list
ID PID STATUS BUNDLE CREATED OWNER
containerid 8033 created /home/docker/test-container 2023-03-14T05:26:56.97917356Z root
可以看到创建完成的状态是created。
[root@node1 test-container]# runc start containerid
启动以后的状态在使用runc list
就可以发现已经变成了running状态。
这个命令可以暂停容器内的所有进程。
[root@node1 test-container]# runc pause containerid
[root@node1 test-container]# runc delete containerid
总结一下,再来捋一捋Docker和runC、Libcontainer的关系。Docker和runC、Docker的默认容器引擎是Docker Engine。Docker Engine是一个开源的容器运行时引擎,它可以运行在Linux、Windows和macOS等操作系统上。Docker Engine利用Linux内核中的命名空间和控制组特性,创建隔离的容器环境,并在这些容器环境中运行应用程序。
Docker Engine、runC和Libcontainer之间存在如下关系:
因此,可以说Docker Engine是基于runC和Libcontainer来运行和管理容器的,同时,runC和Libcontainer也是独立的开源项目,它们可以被其他容器运行时引擎使用。
我们可以通过查看docker进程,发现它们的关系,如下查看docker进程,可以发现--default-runtime=docker-runc
指定了默认运行时为docker-runc,而unix:///var/run/docker/libcontainerd/docker-containerd.sock
也暴露了libcontainer。
[root@node1 ~]# ps -ef|grep docker
root 8241 1 1 16:54 ? 00:00:03 /usr/bin/dockerd-current --add-runtime docker-runc=/usr/libexec/docker/docker-runc-current --default-runtime=docker-runc --exec-opt native.cgroupdriver=systemd --userland-proxy-path=/usr/libexec/docker/docker-proxy-current --init-path=/usr/libexec/docker/docker-init-current --seccomp-profile=/etc/docker/seccomp.json --selinux-enabled --log-driver=journald --signature-verification=false --storage-driver overlay2
root 8247 8241 0 16:54 ? 00:00:01 /usr/bin/docker-containerd-current -l unix:///var/run/docker/libcontainerd/docker-containerd.sock --metrics-interval=0 --start-timeout 2m --state-dir /var/run/docker/libcontainerd/containerd --shim docker-containerd-shim --runtime docker-runc --runtime-args --systemd-cgroup=true
root 8391 8144 0 17:01 pts/0 00:00:00 grep --color=auto docker
说到runC,我们就顺便讲一下开放容器计划Open Container Initiative(简称OCI),因为runC现在就是由这个组织在维护的。它是一个开放的治理组织,其明确目的是围绕容器格式和运行时创建开放的行业标准。
OCI 于 2015 年 6 月由 Docker 等容器行业的领导者共同创立,目前包含三个规范:运行时规范(runtime-spec)、镜像规范(image-spec)和分发规范(distribution-spec)。runtime-spec概述了如何运行在磁盘上解压的“文件系统包”。后来的很多容器相关项目,很多都是遵循OCI的标准的,例如containerd、CRI-O等,所以我们有必要保持关注。
最后再说一下,我们可以发现Linux Namespace、Cgroups、rootfs这些容器最底层的技术都不是Docker公司创新发明的新玩意,但是Docker公司通过开发了libcontainer、runC,制定了镜像分层格式等规范,从而带来了容器技术的火爆。有时候不是一定要从0到1才是创新,基于现有的基础做整合或者改动一些流程规范让软件的运行生态更好,也是一种伟大的创新。