Docker容器:基本原理
几个基础概念
container
容器本质上是受到资源限制,彼此间相互隔离的若干个linux进程的集合
OCI
Open Container Initiative
Linux 基金会于 2015 年 6 月成立 OCI(Open Container Initiative)组织,旨在围绕容器格式和运行时制定一个开放的工业化标准;
containerd
2016年12月14日,Docker公司宣布将containerd从Docker中分离,并捐赠到一个开源社区独立发展和运营; containerd可以作为daemon程序运行在Linux和Windows上,管理机器上所有容器的生命周期;
containerd并不是直接面向最终用户的,而是主要用于集成到更上层的系统里,如Swarm、Kubernetes、Mesos等容器编排系统; containerd以daemon的形式运行在系统上,通过unix domain socket暴露底层的gRPC API,上层系统可以通过这些API管理机器上的容器;
每个containerd只负责一台机器,Pull镜像、对容器的启动停止等操作、网络、存储都是由containerd完成的;
具体运行容器由runC负责,实际上只要是符合OCI规范的容器都可以支持;
特性
支持OCI运行时(runC);
支持镜像的pull/push操作;
容器运行时和生命周期管理;
网络原语:创建/修改/删除接口;
让容器加入已有的network namespace;
使用“内容可寻址”存储支持全局镜像多租户共享;
containerd与Docker
Docker包含containerd,containerd提供的API偏底层,对普通用户来说,可以继续使用Docker;容器编排系统的开发者才需要containerd,比如云服务厂商容器服务;
containerd专注于运行时的容器管理,而Docker除了容器管理之外,还可以完成镜像构建之类的功能;
containerd、OCI与runC
OCI是一个标准化的容器规范,包括运行时规范和镜像规范;
runC是基于OCI规范的一个参考实现,Docker贡献了runC的主要代码;
containerd比runC的层次更高,containerd可以使用runC启动容器,还可以下载镜像,管理网络;
containerd与容器编排系统
在开源编排系统中,Kubernetes直接使用Docker,将来的版本将直接使用containerd;
Mesos和其他编排引擎也可以使用containerd而不是直接使用Docker;
对云计算服务厂商,可以直接基于containerd提供定制化的容器网络、容器存储和编排方案;
containerd的目标是提供一个更加开放、稳定的容器运行基础设施;客户最终将受益于一个稳定且有良好支持的容器基础设施;各家厂商也可以利用containerd作为一个标准化、灵活 的容器操作层,可以方便的提供定制化的网络、存储和容器编排;这对构建一个开放和健康的容器生态具有重要意义;
runc
runc,是对于OCI标准的一个参考实现,是一个可以用于创建和运行容器的CLI(command-line interface)工具;
runc直接与容器所依赖的cgroup/linux kernel等进行交互,负责为容器配置cgroup/namespace等启动容器所需的环境,创建启动容器的相关进程;
runC是一个轻量级的容器运行引擎,包括所有Docker使用的和容器相关的系统调用的代码; 其基本功能点:
-
完全支持Linux Namespace,包括User Namespace;
-
原生支持所有Linux提供的安全特性:Selinux、Apparmor、Seccomp、control groups、capablity、pivot_root等;
-
一份正式的容器标准,由Open Container Project管理挂靠在Linux基金会下;
runC的目标就是构造到处可以运行的标准容器;
OCI标准包(bundle)
一个标准的容器运行时需要文件系统,即镜像;
OCI是怎么定义一个基本的容器运行包的?这个容器标准包的定义仅仅考虑如何把容器和它的配置数据存储到磁盘上以便运行时读取;
一般来说包含两个模块:
-
config.json:包括容器的配置数据,这个文件必须在容器的root文件系统内;
-
一个文件夹:容器的root文件系统;
config.json
包含容器必须的元信息,主要包括容器需要运行的进程、环境变量、沙盒环境等;
ociVersion:指定OCI容器的版本号;
root:配置容器的root文件系统;
path指定root文件系统的路径;readonly如果为true,那么root文件系统在容器内就是只读的,默认是false;
mounts
配置额外的挂载点;
destination:挂载点在容器内的目标位置,绝对路径;
type:需要挂载的文件系统类型,如minix、ext2、ext3、jfs、xfs、reiserfs、proc、nfs;
source:设备名或文件名;
options:挂载点需要的额外信息;
process
配置容器进程信息;
terminal:是否需要连接一个终端到此进程,默认false;
consoleSize:terminal连接时,用来指定控制台的大小;
cwd:可执行文件的工作目录,绝对路径;
env:需要传递给进程的环境变量;KEY=value格式;
args:传递可执行文件的参数;
capabilities:一系列指定给容器进程的capabilities值;
rlimits:限制容器内执行的进程资源使用量;
user
指定容器内运行进程的用户信息;
uid:指定用户ID
gid:指定group ID;
additionalGids:指定附加的groups ID;
hostname
配置容器的主机名,只有容器创建了UTS namespace才可以指定;
platform
指定容器运行的系统信息;
os:容器运行的系统类型;
arch:指定系统的架构;
钩子(Hook)
扩展容器运行态的动作,可以在容器运行前和停止后执行一些命令;帮助用户做一些复杂的网络配置和volume的垃圾收集等动作;
prestart:pre-start钩子在容器进程创建后执行,但在用户还没有开始执行前触发;在Linux上,它是在Namespace创建成功后触发的,可以提供一个配置容器初始化环境的机会;
poststart:post-start在用户进程启动之后执行,这个钩子可以用来告诉用户进程已经启动起来了;
poststop:在容器停止后执行,可以用来清理容器运行中产生的垃圾;
容器引擎
Docker原理
namespace–资源隔离
cgroups–资源限制
Linux Cgroups提供了对一组进程及将来子进程的资源限制、控制和统计的能力,这些资源包括CPU、内存、存储、网络等; 通过Cgroups,可以方便的限制某个进程的资源占用,并且可以实时的监控
Union File System
简称UnionFS,是一种为Linux、FreeBSD、和NetBSD操作系统设计的把其他文件系统联合到一个挂载点的文件系统服务;
容器管理–libcontainer
在2013年Docker刚发布的时候,它是一款基于LXC的开源容器管理引擎; 把LXC复杂的容器创建与使用方式简化为Docker自己的一套命令体系;
随着Docker的不断发展,它开始有了更为远大的目标,那就是反向定义容器的实现标准,将底层实现都抽象化到libcontainer的接口;
libcontainer是一套实现容器管理的Go语言解决方案;
libcontainer模块分析
Docker容器范畴内一切与Linux内核相关的技术实现,都可以认为是通过libcontainer来完成的;
- namespace
容器的创建,由于容器的本质也是进程,故在派生进程时, Docker Daemon传入了与namespace相关的flag参数,实现namespace的创建, 确保容器(进程)被执行之后,也就是在进程在运行时达到namespace隔离的效果;
- cgroups
cgroup是Linux内核的一个特性,此特性可以帮助用户对一组进程进行资源使用的限制、统计以及隔离; Docker领域也不例外,Docker利用对cgroup的支持,完成对Docker容器进程组的资源限制、统计以及隔离;
Docker对于cgroup的支持,主要有以下5方面:
1.设备:用户选择容器可以使用的设备;
2.内存:为容器的运行设定用量限额;
3.CPU:支持容器进程之间拥有相对的运行时间片;
4.Freezer:可以使容器挂起,节省CPU资源, Docker命令中pause与unpause命令即采用了cgroup的Freezer子系统。 需要注意的是,容器进程组挂起,并不意味着进程已经终止,从Linux内核的角度来看,被挂起的进程拥有完整的task struct,占用相应的内存, 但是Freezer子系统保证容器中的进程不会被CPU调度到
5.systemd:Slice属性;
- 网络
关于容器网络的管理,底层实现全部借助libcontainer的network包来完成;
network包定义了Docker容器的网络枝类型; Docker容器的网络模式有: bridge模式、 host模式、 other container模式以及none模式;
- 挂载
Docker容器不仅限于镜像所提供的rootfs; volume的存在(包括bind-mount以及data volume)使得容器的文件系统可以共享宿主机的资源;
- 设备
默认情况下,libcontainer会为容器创建某些必备的设备, 如/dev/null、 /dev/zero、 /dev/full、 /dev/tty、 /dev/urandom、 /dev/console等;
- nsinit
nsinit是一个功能强大的应用程序,该应用程序巧妙利用了libcontainer,使得容器的创建变得简单; 若需要创建一个容器,则按照libcontainer的API接口,必须要向其提供容器的rootfs以及容器的参数配置; 因此,nsinit的运行需要提供一个rootfs以及一个容器的配置文件container.json
- 其他模块
Netlink作为Linux内核的一套接口,提供进程间用户态与内核态之间的通信方式;
容器的安全方面,libcontainer由security模块负责;Docker可以由用户来自定义配置容器的Capablity;
标准化容器执行引擎–runC
容器探究
查看容器或image信息
docker inspect image_id
# docker inspect xxxxxxx
[
{
"Id": "sha256:xxxxxxxxxxxx",
"RepoTags": [
"test_image:latest"
],
"RepoDigests": [],
"Parent": "sha256:xxxxxxxxxx",
"Comment": "",
"Created": "2020-09-07T09:14:06.522141761Z",
"Container": "xxxxxx",
"ContainerConfig": {
"Hostname": "qwq",
"Domainname": "",
"User": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"Tty": true,
"OpenStdin": true,
"StdinOnce": false,
"Env": [
"PORT=9000",
"DBCON=root:xxxxxxxxx",
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"Cmd": null,
"ArgsEscaped": true,
"Image": "web_backend",
"Volumes": null,
"WorkingDir": "",
"Entrypoint": [
"/bin/sh",
"-x",
"/tmp/scripts/start_web_backend.sh"
],
"OnBuild": null,
"Labels": {
"org.label-schema.build-date": "20190801",
"org.label-schema.license": "GPLv2",
"org.label-schema.name": "CentOS Base Image",
"org.label-schema.schema-version": "1.0",
"org.label-schema.vendor": "CentOS"
}
},
"DockerVersion": "1.13.1",
"Author": "",
"Config": {
"Hostname": "",
"Domainname": "",
"User": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"PORT=8088",
"DBCON=root:xxxxxxxxxxxxxx",
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"Cmd": null,
"ArgsEscaped": true,
"Image": "",
"Volumes": null,
"WorkingDir": "",
"Entrypoint": [
"/bin/sh",
"-x",
"/tmp/scripts/start_web_backend.sh"
],
"OnBuild": null,
"Labels": {
"org.label-schema.build-date": "20190801",
"org.label-schema.license": "GPLv2",
"org.label-schema.name": "CentOS Base Image",
"org.label-schema.schema-version": "1.0",
"org.label-schema.vendor": "CentOS"
}
},
"Architecture": "amd64",
"Os": "linux",
"Size": 225592240,
"VirtualSize": 225592240,
"GraphDriver": {
"Name": "overlay2",
"Data": {
"LowerDir": "/var/lib/docker/overlay2/2a170b941a585e6641052856289d95c19a4d47380bf58fa4bf8abe840da5b116/diff:/var/lib/docker/overlay2/6d285580af0cdac32dbd3f8efb69a0f7f4ca124b4ea2f96bb61a716aa0c80ac8/diff:/var/lib/docker/overlay2/7963b898227182f49d783948dc4d96ea271056c2299fa95f986d820c7391dfba/diff:/var/lib/docker/overlay2/d0c5b76ffa96883fc9758a2ce6aa8de7e5289e8194fb6bb33bc540e95ee3e308/diff:/var/lib/docker/overlay2/bc7cbb3bea5f8bfd943c199f42f1e62f0c3fe032884e26c1be2f570b262d1b6a/diff:/var/lib/docker/overlay2/427a43c169e607012d1c04472d17ee73523066d4bcd28cd5c10f75ca0acbce70/diff",
"MergedDir": "/var/lib/docker/overlay2/xxxxxx/merged",
"UpperDir": "/var/lib/docker/overlay2/xxxxxx/diff",
"WorkDir": "/var/lib/docker/overlay2/xxxxxx/work"
}
},
"RootFS": {
"Type": "layers",
"Layers": [
"sha256:877b494a9",
"sha256:a32ca1709",
"sha256:ee91f2b79",
"sha256:0ebd24ff",
"sha256:1aa1fd2f",
"sha256:38a48a37",
"sha256:2f4fc4636"
]
}
}
]
docker inspect container_id
[
{
"Id": "3aewq1n37f05272d5cbf8cd1e903ed19590d4697a2903853a79ca141989df047",
"Created": "2020-05-27T06:57:23.523587346Z",
"Path": "/bin/sh",
"Args": [
"-c",
"jekyll serve --host 0.0.0.0 --port 80"
],
"State": {
"Status": "running",
"Running": true,
"Paused": false,
"Restarting": false,
"OOMKilled": false,
"Dead": false,
"Pid": 5227,
"ExitCode": 0,
"Error": "",
"StartedAt": "2020-10-28T11:41:39.903049896Z",
"FinishedAt": "2020-10-28T11:41:39.461026327Z"
},
"Image": "sha256:c5b55e0c914f227f970bb4c829f3244099be0697fed178b042e9bceb0a70bfb1",
"ResolvConfPath": "/var/lib/docker/containers/3aewq1n37f05272d5cbf8cd1e903ed19590d4697a2903853a79ca141989df047/resolv.conf",
"HostnamePath": "/var/lib/docker/containers/3aewq1n37f05272d5cbf8cd1e903ed19590d4697a2903853a79ca141989df047/hostname",
"HostsPath": "/var/lib/docker/containers/3aewq1n37f05272d5cbf8cd1e903ed19590d4697a2903853a79ca141989df047/hosts",
"LogPath": "",
"Name": "/myblog_pre_view",
"RestartCount": 0,
"Driver": "overlay2",
"MountLabel": "",
"ProcessLabel": "",
"AppArmorProfile": "",
"ExecIDs": null,
"HostConfig": {
"Binds": [
"/tmp/xxxx"
],
"ContainerIDFile": "",
"LogConfig": {
"Type": "journald",
"Config": {}
},
"NetworkMode": "default",
"PortBindings": {
"80/tcp": [
{
"HostIp": "",
"HostPort": "9000"
}
]
},
"RestartPolicy": {
"Name": "no",
"MaximumRetryCount": 0
},
"AutoRemove": false,
"VolumeDriver": "",
"VolumesFrom": null,
"CapAdd": null,
"CapDrop": null,
"Dns": [],
"DnsOptions": [],
"DnsSearch": [],
"ExtraHosts": null,
"GroupAdd": null,
"IpcMode": "",
"Cgroup": "",
"Links": null,
"OomScoreAdj": 0,
"PidMode": "",
"Privileged": false,
"PublishAllPorts": false,
"ReadonlyRootfs": false,
"SecurityOpt": null,
"UTSMode": "",
"UsernsMode": "",
"ShmSize": 67108864,
"Runtime": "docker-runc",
"ConsoleSize": [
0,
0
],
"Isolation": "",
"CpuShares": 0,
"Memory": 0,
"NanoCpus": 0,
"CgroupParent": "",
"BlkioWeight": 0,
"BlkioWeightDevice": null,
"BlkioDeviceReadBps": null,
"BlkioDeviceWriteBps": null,
"BlkioDeviceReadIOps": null,
"BlkioDeviceWriteIOps": null,
"CpuPeriod": 0,
"CpuQuota": 0,
"CpuRealtimePeriod": 0,
"CpuRealtimeRuntime": 0,
"CpusetCpus": "",
"CpusetMems": "",
"Devices": [],
"DiskQuota": 0,
"KernelMemory": 0,
"MemoryReservation": 0,
"MemorySwap": 0,
"MemorySwappiness": -1,
"OomKillDisable": false,
"PidsLimit": 0,
"Ulimits": null,
"CpuCount": 0,
"CpuPercent": 0,
"IOMaximumIOps": 0,
"IOMaximumBandwidth": 0
},
"GraphDriver": {
"Name": "overlay2",
"Data": {
"LowerDir": "/var/lib/docker/overlay2/fbf93a97574b7337f96ae7c1894d38c740acebf935f3d98bf8c9904e5cb4cb7c-init/diff:/var/lib/docker/overlay2/3895446ebd62fb3980cf4611100ec2cc32288191d71ee56fba602dae11f3fa8a/diff:/var/lib/docker/overlay2/1a38cadd0f52ac21a7de9ae46237e55094175b7fe4ba49183fc0fa270efc6297/diff:/var/lib/docker/overlay2/9b1f58ee24287265e3ef3f7d0bf9a99e6cfc7f74d466b50a0a89fc946388c948/diff:/var/lib/docker/overlay2/167065adeed2e0e6f903f22ca80488128f526e315203f002ba2fab94d7ce6726/diff:/var/lib/docker/overlay2/3d6d2d363a7532c2a9b2405308b2951b83859856ad5f254b222085e8e0d79cd8/diff:/var/lib/docker/overlay2/4f5d91df12b733e7321665cc9e1f8d16bc69fa2b1b7d1bd8df206dda79a0cdac/diff:/var/lib/docker/overlay2/cc15a6750c371dda4cdf14238430c2fe66c76912bfc3f41952515776af6383c1/diff:/var/lib/docker/overlay2/33e0fb633c9e54b1701694095648aef4c5a239cd9929ea11067dcbf6325545b2/diff:/var/lib/docker/overlay2/db66d6ef1611bcf93ac87b4e2debea5ad715b8a5ab7a23d3de3ca2d1af711c00/diff:/var/lib/docker/overlay2/f209d619545845e32827d903658cf547e51f4fa6c0907661c0fd693ad714898d/diff",
"MergedDir": "/var/lib/docker/overlay2/fbf93a97574b7337f96ae7c1894d38c740acebf935f3d98bf8c9904e5cb4cb7c/merged",
"UpperDir": "/var/lib/docker/overlay2/fbf93a97574b7337f96ae7c1894d38c740acebf935f3d98bf8c9904e5cb4cb7c/diff",
"WorkDir": "/var/lib/docker/overlay2/fbf93a97574b7337f96ae7c1894d38c740acebf935f3d98bf8c9904e5cb4cb7c/work"
}
},
"Mounts": [
{
"Type": "bind",
"Source": "/root/xxx",
"Destination": "/tmp/xxxxx",
"Mode": "",
"RW": true,
"Propagation": "rprivate"
}
],
"Config": {
"Hostname": "3aewq1n37f05",
"Domainname": "",
"User": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"ExposedPorts": {
"80/tcp": {}
},
"Tty": true,
"OpenStdin": true,
"StdinOnce": false,
"Env": [
"PATH=/usr/local/bundle/bin:/usr/local/bundle/gems/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"RUBY_MAJOR=2.6",
"RUBY_VERSION=2.6.3",
"RUBY_DOWNLOAD_SHA256=11a83f85c03d3f0fc9b8a9b6cad1b2674f26c5aaa43ba858d4b0fcc2b54171e1",
"GEM_HOME=/usr/local/bundle",
"BUNDLE_PATH=/usr/local/bundle",
"BUNDLE_SILENCE_ROOT_WARNING=1",
"BUNDLE_APP_CONFIG=/usr/local/bundle"
],
"Cmd": [
"/bin/sh",
"-c",
"jekyll serve --host 0.0.0.0 --port 80"
],
"ArgsEscaped": true,
"Image": "blog:v20200526",
"Volumes": null,
"WorkingDir": "/tmp/mecacho.github.io",
"Entrypoint": null,
"OnBuild": null,
"Labels": {}
},
"NetworkSettings": {
"Bridge": "",
"SandboxID": "34ffa38aac2cee18a958c797cc4ca56bbbbfc80d33f2e8389d75fa6c73f8b1d6",
"HairpinMode": false,
"LinkLocalIPv6Address": "",
"LinkLocalIPv6PrefixLen": 0,
"Ports": {
"80/tcp": [
{
"HostIp": "0.0.0.0",
"HostPort": "9000"
}
]
},
"SandboxKey": "/var/run/docker/netns/34ffa38aac2c",
"SecondaryIPAddresses": null,
"SecondaryIPv6Addresses": null,
"EndpointID": "e238a05aca01c2e5beb12723cb18dfdf9ddb7ef313ec3e8f2f3e2f42d970bd57",
"Gateway": "172.17.0.1",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"IPAddress": "172.17.0.3",
"IPPrefixLen": 16,
"IPv6Gateway": "",
"MacAddress": "02:42:ac:11:00:03",
"Networks": {
"bridge": {
"IPAMConfig": null,
"Links": null,
"Aliases": null,
"NetworkID": "ed3023f62f645e10ddfaecb43240149619e5260a8a7dd4b79dfd8ee8d7b3f3da",
"EndpointID": "e238a05aca01c2e5beb12723cb18dfdf9ddb7ef313ec3e8f2f3e2f42d970bd57",
"Gateway": "172.17.0.1",
"IPAddress": "172.17.0.3",
"IPPrefixLen": 16,
"IPv6Gateway": "",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"MacAddress": "02:42:ac:11:00:03"
}
}
}
}
]
refs
https://github.com/opencontainers/runtime-spec
https://www.infoq.cn/article/sgS3SxhjboTLEdmbJx4H
https://github.com/containerd/containerd