手动实现 Docker 容器,从根上理解 Docker 容器技术
Docker 是一个开源的应用容器引擎,基于 Go 语言 并遵从 Apache2.0 协议开源。
Docker 可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化。容器是完全使用沙箱机制,相互之间不会有任何接口(类似 iPhone 的 app), 更重要的是容器性能开销极低。
对于现有的 Docker 资料而言,上来就是写原理+实战,很少有人考虑过 Docker 底层所使用的技术,作为一个工作中常用容器和编排技术的人,不如就实现一个看看效果,用 linux 本身的功能和命令手把手实现一个 Docker 的所有功能,做到真正意义上的理解。
在本场 Chat 中,会讲到如下内容:
- Docker 优势
- Docker 安装
- Docker 镜像操作命令实战(镜像拉取、镜像查看、导入导出、删除、重命名)
- Dockerfile 的编写及其常用 11 个命令(FROM、MAINTAINER、RUN、CMD、EXPOSE、ENV、ADD、COPY、ENTRYPOINT、VOLUME、USER)的使用
- Docker 的常用命令实战(创建、启动、停止、进入、删除、暴露端口)
- 创建私有仓库
- Docker 容器隔离原理
- Docker 资源限制原理
- Docker 镜像分层原理
- 手动实现 docker(复现镜像拉取、镜像查看、容器启动、容器删除、容器查看、容器资源限制、镜像删除功能)
适合人群:对深层了解 Docker 容器技术有兴趣的技术人员
手动实现 Docker 容器,从根上理解 Docker 容器技术
个人简介
瞿鹏志(小熊),就职于腾讯云私有云全栈云团队,监控云负责人。
- 个人博客: 机智的程序员小熊
- github: minibear2333
前言
很高兴你订阅我的 Chat
, 这篇文章由浅入深,带你了解 Docker
的概念、常用命令、尝试用 shell
实现 Docker
,这样一套下来想必会对容器和 Docker
有进一步的认识。
本文有一定门槛,你至少需要对 Linux 系统有所了解
本文使用环境 centos8
本文核心内容为实现 Docker 的交互,进一步学习原理
对于现有的 Docker 资料而言,上来就是写原理+实战,很少有人考虑过 Docker 底层所使用的技术,作为一个工作中常用容器和编排技术的人,不如就实现一个看看效果,用 Linux 本身的功能和命令手把手实现一个 Docker 的所有功能,做到真正意义上的理解。
优势
更高效的利用系统资源
可以看到下图右边的虚拟机会用到硬件虚拟化技术,还必须安装完整的操作系统。
而容器非常轻量,无论是应用执行速度、内存损耗或者文件存储速度,都要比传统虚拟机技术更高效。因此,相比虚拟机技术,一个相同配置的主机,往往可以运行更多数量的应用。
特性 | 容器 | 虚拟机 |
---|---|---|
启动 | 秒级 | 分钟级 |
硬盘使用 | 一般为 MB | 一般为 GB |
性能 | 接近原生 | 弱于 |
系统支持量 | 单机支持上千个容器 | 一般几十个 |
一致的运行环境
开发过程中一个常见的问题是环境一致性问题。由于开发环境、测试环境、生产环境不一致,导致有些 bug 并未在开发过程中被发现。而 Docker 的镜像提供了除内核外完整的运行时环境,确保了应用运行环境一致性,从而不会再出现 「这段代码在我机器上没问题啊」 这类问题。
持续交付和部署
对开发和运维(DevOps)人员来说,最希望的就是一次创建或配置,可以在任意地方正常运行。
使用 Docker 可以通过定制应用镜像来实现持续集成、持续交付、部署。开发人员可以通过 Dockerfile 来进行镜像构建,并结合 持续集成(Continuous Integration) 系统进行集成测试,而运维人员则可以直接在生产环境中快速部署该镜像,甚至结合 持续部署(Continuous Delivery/Deployment) 系统进行自动部署。
而且使用 Dockerfile 使镜像构建透明化,不仅仅开发团队可以理解应用运行环境,也方便运维团队理解应用运行所需条件,帮助更好的生产环境中部署该镜像。
更轻松的迁移
由于 Docker 确保了执行环境的一致性,使得应用的迁移更加容易。Docker 可以在很多平台上运行,无论是物理机、虚拟机、公有云、私有云,甚至是笔记本,其运行结果是一致的。因此用户可以很轻易的将在一个平台上运行的应用,迁移到另一个平台上,而不用担心运行环境的变化导致应用无法正常运行的情况。
更轻松的维护和扩展
Docker 使用的分层存储以及镜像的技术,使得应用重复部分的复用更为容易,也使得应用的维护更新更加简单,基于基础镜像进一步扩展镜像也变得非常简单。此外,Docker 团队同各个开源项目团队一起维护了一大批高质量的 官方镜像,既可以直接在生产环境使用,又可以作为基础进一步定制,大大的降低了应用服务的镜像制作成本。
Docker 安装
Docker 需要操作系统、管理工具、runtime
- 操作系统推荐使用 centos/unbunt ,而 windows 和 mac 在使用过程中会有一些网络问题,作为服务器使用 Linux 也更为轻便和通用
- 管理工具 - Docker Engine
- runtime (runc)是 OCI 容器运行时规范的参考实现,runc 生来只有一个作用——创建容器
Docker Engine 包括
- Docker CLI — docker 向外暴露的命令行接口(Command Line API)
- Docker Daemon — docker 的守护进程,属于 C/S 中的 server;向外暴露了 REST 接口(docker REST API),包含 containerd
- containerd 在对 Docker daemon 的功能进行拆解后,所有的容器执行逻辑被重构到一个新的名为 containerd(发音为 container-dee)的工具中,它的主要任务是容器的生命周期管理
因此,客户端访问服务端的方式有两种
- 一种是使用命令行工具,比如 docker run, docker ps....
- 另一种就是直接通过调用 REST API,比如发送一个 curl http 请求
安装说明
- 前提:能上网
- 命令行版本:CentOS、Debian、Fedora、Raspbian、Ubuntu
- 桌面版: macOS、Windows
- 安装网址:https://docs.docker.com/engine/install/
Docker 支持以下的 64 位 CentOS 版本:
- CentOS 7
- CentOS 8
- 更高版本...
使用官方安装脚本自动安装
安装命令如下:
curl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun
也可以使用国内 daocloud 一键安装命令:
curl -sSL https://get.daocloud.io/docker | sh
安装完毕,我们就开始实战吧!
Docker 镜像
前面我们有聊过镜像是 Docker 从众多容器中脱颖而出的秘密武器,当然镜像也是 Docker 的重要角色。
实际上镜像可以理解为一种特殊的封装,最佳实践就是一个应用程序(在跑起来就是一个进程)打一个镜像,把这个应用程序所需要的文件系统打包起来,把多多余的东西摈弃掉。
这样就可以得到一个最小运行环境包,这个包足够轻量,在不同环境、不同云平台迁移都可以完美还原,保持一致性。
Docker 镜像操作命令实战(镜像拉取、镜像查看、导入导出、删除、重命名)
当然官方提供了很多现成的镜像以使用,可以在此搜索想要的镜像
镜像分为镜像名称和标签(tag),如图是我搜索 nginx
镜像的结果
拉取命令,不加 tag
默认为 latest
, tag
就是镜像的版本号
$ docker pull debian
Using default tag: latest
latest: Pulling from library/debian
fdd5d7827f33: Pull complete
a3ed95caeb02: Pull complete
Digest: sha256:e7d38b3517548a1c71e41bffe9c8ae6d6d29546ce46bf62159837aad072c90aa
Status: Downloaded newer image for debian:latest
下载完成后,我们可以直接使用这个镜像来运行容器。比如:
官方介绍$ docker run nginx
刚刚我们是从 Docker 官方镜像仓库拉取的镜像,镜像仓库就是存储镜像的地方,看起来就是这样
如上图,镜像仓库不止 Docker 官方仓库一家,当前我们也可以定制自己的镜像并推送到远程仓库里
镜像查看
官方介绍$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
<none> <none> 77af4d6b9913 19 hours ago 1.089 GB
committ latest b6fa739cedf5 19 hours ago 1.089 GB
<none> <none> 78a85c484f71 19 hours ago 1.089 GB
docker latest 30557a29d5ab 20 hours ago 1.089 GB
<none> <none> 5ed6274db6ce 24 hours ago 1.089 GB
postgres 9 746b819f315e 4 days ago 213.4 MB
postgres 9.3 746b819f315e 4 days ago 213.4 MB
postgres 9.3.5 746b819f315e 4 days ago 213.4 MB
postgres latest 746b819f315e 4 days ago 213.4 MB
各个选项说明:
- REPOSITORY:表示镜像的仓库源
- TAG:镜像的标签
- IMAGE ID:镜像 ID
- CREATED:镜像创建时间
- SIZE:镜像大小
导出镜像
官方介绍可以把本地 docker images
列出的镜像导出成文件,方便在离线导入
$ docker save busybox > busybox.tar
$ ls -sh busybox.tar
2.7M busybox.tar
导出并压缩
docker save myimage:latest | gzip > myimage_latest.tar.gz
使用场景:
- 离线导入导出
- 分享个人定制镜像
导入镜像
官方介绍docker load -i 名称.tar
删除
官方介绍镜像删除使用 docker rmi 命令,比如我们删除 nginx
镜像:
$ docker rmi nginx
重命名
docker tag IMAGEID(镜像id) REPOSITORY:TAG(仓库:标签)
比如
docker tag nginx:latest minibear2333/nginx:1.0.0
docker tag ca1b6b825289 registry.cn-xx.com/xxx:v1.0
搜索
官方介绍支持直接在命令行搜索官方镜像
搜索查询默认最多返回 25 个结果。
通过镜像名字搜索:
$ docker search busybox
NAME DESCRIPTION STARS OFFICIAL AUTOMATED
busybox Busybox base image. 1494 [OK]
progrium/busybox 68 [OK]
hypriot/rpi-busybox-httpd Raspberry Pi compatible Docker Image with a … 45
radial/busyboxplus Full-chain, Internet enabled, busybox made f… 21 [OK]
.....
查看镜像的历史
官方介绍显示镜像的历史,显示镜像每层的变更内容
使用
docker history [OPTIONS] IMAGE
如:
$ docker history istio/mixer:0.7.1 --no-trunc
IMAGE CREATED CREATED BY SIZE COMMENT
251b0d3d2b93 10 months ago /bin/sh -c #(nop) CMD ["--configStoreURL=fs… 0B
<missing> 10 months ago /bin/sh -c #(nop) ENTRYPOINT ["/usr/local/b… 0B
<missing> 10 months ago /bin/sh -c #(nop) ADD file:04fbcb926d6c80af5… 53.7MB
<missing> 10 months ago /bin/sh -c #(nop) ADD file:f74cafb0f2a94a0b9… 252kB
Docker 的常用命令实战
如下图就是所有常用命令
创建(不常用)
官方介绍根据镜像生成一个新的容器,但是不启动。
$ docker create fedora
声明一个容器,使用 fedora
镜像,打开一个命令行终端。
$ docker create -it fedora bash
6d8af538ec541dd581ebc2a24153a28329acb5268abe5ef868c1f1a261221752
-t
分配一个 TTY(终端)-i
保持终端不关闭bash
启动后运行的命令,也可以是其他
启停
官方介绍-start$ docker start my_container
官方介绍-stop
$ docker stop my_container
run 命令
官方介绍-run创建、启动容器并在新的容器中运行命令。
进入容器
还是用刚才的例子
$ docker run --name test -it debian
root@d6c0fe130dba:/# exit
$ docker ps -a | grep test
d6c0fe130dba debian:7 "/bin/bash" 26 seconds ago Exited (13) 17 seconds ago test
--name
是指定容器名字docker ps -a
:列出全部容器,包括停止的容器
暴露容器端口
有的容器会对其他应用提供服务,容器内部使用了一套网络,外部想要访问需要把端口映射到外部。
把内部 80 端口映射到主机上的 8080 端口
$ docker run -d -p 80:8080 --name webserver nginx
-d
后台运行此应用
删除容器
官方介绍docker rm <container id>
也可以按名称删除,比如
docker rm webserver
docker rm d6c0fe130dba
查看容器日志
官方介绍注意,此命令是打印标准输入输出流
$ docker run --name test -d busybox sh -c "while true; do $(echo date); sleep 1; done"
$ date
Tue 14 Nov 2017 16:40:00 CET
$ docker logs -f --until=2s test
Tue 14 Nov 2017 16:40:00 CET
Tue 14 Nov 2017 16:40:01 CET
Tue 14 Nov 2017 16:40:02 CET
-f
参数代表实时等待--untils=2s
展示容器启动后多长时间的日志
更多命令见 官网 Docker CLI
Docker 镜像分层
镜像是一层一层的架构模式, Docker
支持通过扩展现有镜像,创建新的镜像。实际上, Docker Hub
中 99% 的镜像都是通过在 base
镜像中安装和配置需要的软件构建出来的。
从上图可以看到,新镜像是从 base 镜像一层一层叠加生成的。每安装一个软件,就在现有镜像的基础上增加一层。
Docker 镜像为什么分层?
镜像分层最大的一个好处就是共享资源。
比如说有多个镜像都从相同的 base 镜像构建而来,那么 Docker Host 只需在磁盘上保存一份 base 镜像;同时内存中也只需加载一份 base 镜像,就可以为所有容器服务了。而且镜像的每一层都可以被共享。
如果多个容器共享一份基础镜像,当某个容器修改了基础镜像的内容,比如 /etc 下的文件,这时其他容器的 /etc 是不会被修改的,修改只会被限制在单个容器内。这就是容器 Copy-on-Write 特性。
可写的容器层
当容器启动时,一个新的可写层被加载到镜像的顶部。这一层通常被称作“容器层”,“容器层”之下的都叫“镜像层”。
所有对容器的改动 - 无论添加、删除、还是修改文件都只会发生在容器层中。只有容器层是可写的,容器层下面的所有镜像层都是只读的。
所有对容器的改动 - 无论添加、删除、还是修改文件都只会发生在容器层中。只有容器层是可写的,容器层下面的所有镜像层都是只读的。
容器层的细节说明
镜像层数量可能会很多,所有镜像层会联合在一起组成一个统一的文件系统。如果不同层中有一个相同路径的文件,比如 /a,上层的 /a 会覆盖下层的 /a,也就是说用户只能访问到上层中的文件 /a。在容器层中,用户看到的是一个叠加之后的文件系统。
文件操作 | 说明 |
---|---|
添加文件 | 在容器中创建文件时,新文件被添加到容器层中。 |
读取文件 | 在容器中读取某个文件时,Docker 会从上往下依次在各镜像层中查找此文件。一旦找到,立即将其复制到容器层,然后打开并读入内存。 |
修改文件 | 在容器中修改已存在的文件时,Docker 会从上往下依次在各镜像层中查找此文件。一旦找到,立即将其复制到容器层,然后修改之。 |
删除文件 | 在容器中删除文件时,Docker 也是从上往下依次在镜像层中查找此文件。找到后,会在容器层中记录下此删除操作。(只是记录删除操作) |
只有当需要修改时才复制一份数据,这种特性被称作 Copy-on-Write。可见,容器层保存的是镜像变化的部分,不会对镜像本身进行任何修改。
这样就解释了我们前面提出的问题:容器层记录对镜像的修改,所有镜像层都是只读的,不会被容器修改,所以镜像可以被多个容器共享。
Dockerfile 的编写及其常用 11 个命令的使用
当我们从 docker 镜像仓库中下载的镜像不能满足我们的需求时,我们可以通过以下两种方式对镜像进行更改。
1、从已经创建的容器中更新镜像,并且提交这个镜像( docker commit
命令)
2、使用 Dockerfile 指令来创建一个新的镜像
这里要说的就是第二种,所涉及到的 11 个 Dockerfile
构建使用到的命令为(FROM、MAINTAINER、RUN、CMD、EXPOSE、ENV、ADD、COPY、ENTRYPOINT、VOLUME、USER)
FROM、MAINTAINER、COPY、RUN、ADD
这里非常简单,我用几个样例把全部都用上,并且写上注解,一下就能看懂。
简单例子:
# 基础镜像信息
FROM nginx:1.8
# 维护者信息
MAINTAINER minibear2333 coding3min@foxmail.com
# 镜像操作指令
# 将当前目录的全部文件拷贝到容器中的目录中
COPY . /usr/share/nginx/html
# 运行命令
RUN echo hello minibear2333
创建镜像
$ docker build -t minibear2333/nginx:hello .
[+] Building 0.7s (8/8) FINISHED
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 304B 0.0s
=> [internal] load metadata for docker.io/library/nginx:1.8 0.0s
=> [internal] load build context 0.3s
=> => transferring context: 359B 0.3s
=> [1/3] FROM docker.io/library/nginx:1.8 0.0s
=> => resolve docker.io/library/nginx:1.8 0.0s
=> [2/3] COPY . /usr/share/nginx/html 0.0s
=> [3/3] RUN echo hello minibear2333 0.4s
=> exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:089ad90ddb16a53d5f5cdfc05c8fb0f47a81213fb7c75 0.0s
=> => naming to docker.io/minibear2333/nginx:hello 0.0s
查看
$ docker images |grep nginx
minibear2333/nginx hello 089ad90ddb16 14 minutes ago 133MB
COPY 和 ADD 的区别
COPY
命令和 ADD
类似,唯一的不同是 ADD
会自动解压压缩包/还可以直接下载 url 中的文件
但是官方建义使用 wget
或者 curl
代替 ADD
# 拷贝并解压
ADD nickdir.tar.gz .
# 仅拷贝
ADD https://example.com/big.tar.xz /usr/src/things/
RUN tar -xJf /usr/src/things/big.tar.xz -C /usr/src/things
应该改成这样子
RUN mkdir -p /usr/src/things \
&& curl -SL https://example.com/big.tar.xz \
| tar -xJC /usr/src/things \
&& make -C /usr/src/things all
ARG、ENV、WORKDIR、CMD
下面了两阶段构建 golang
程序包的 Dockerfile
文件
FROM golang:1.14-alpine3.12 AS builder
# ARG 在build时候是可以从外部以 `--build-arg` 带入的变量
ARG service_name
# ENV 是运行时会使用到的变量
ENV service_name={service_name}
# 容器中的工作目录,后来的命令全部做把此目录作为当前目录
WORKDIR /build/src/{service_name}
RUN adduser -u 10001 -D app-runner
ENV GO111MODULE on
ENV GOPATH /build
# 使用了 WORKDIR 设置的目录
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o {service_name} -mod=vendor cmd/server/server.go
# Production stage
FROM alpine:3.12 AS final
# ...... 省略
# CMD 为容器启动时执行的入口命令,容器会监听这个进程
CMD{service_name} -app.config.path=/etc/${service_name}/appconfig.json
完整
Dockerfile
可以看我以前写过的文章 两阶段构建 golang 项目 dockerfile
构建
docker build --build-arg service_name=服务名 -t 服务名:版本号 .
ENTRYPOINT 和 CMD 的区别
CMD
和 ENTRYPOINT
命令
相同点:
- 为启动的容器指定默认要运行的程序,程序运行结束,容器也就结束,所以如果想要容器长期运行就让这个命令指定的命令长期运行。
- CMD 指令,仅最后一个生效。
不同点:
- CMD 指令指定的程序可被
docker run
命令行参数中指定要运行的程序所覆盖,但ENTRYPOINT
不会。
高级用法,如果是使用 []
来标记后面执行的命令,功能有所不同
命令加参数的形式
ENTRYPOINT [ "echo", "a" ]
$ docker run test
a
加参数,但是不会替换
ENTRYPOINT [ "echo", "a" ]
$ docker run test b
a b
CMD
为 ENTRYPOINT
提供默认参数
ENTRYPOINT [ "echo", "a" ]
CMD ["b"]
$ docker run test
a b
加参数 c
会替换 CMD
提供的参数
ENTRYPOINT [ "echo", "a" ]
CMD ["b"]
$ docker run test c
a c
VOLUME (不常用命令)
VOLUME
指定匿名挂载点,会自动挂载在主机上(可以使用 docker inspect
查看自动生成的目录),否则容器销毁数据就会丢失。比如
VOLUME ["/data1"]
$ docker inspect test
......
"Mounts": [
{
"Name": "d411f6b8f17f4418629d4e5a1ab69679dee369b39e13bb68bed77aa4a0d12d21",
"Source": "/var/lib/docker/volumes/d411f6b8f17f4418629d4e5a1ab69679dee369b39e13bb68bed77aa4a0d12d21/_data",
"Destination": "/data1",
"Driver": "local",
"Mode": "",
"RW": true
},
......
EXPOSE(不常用命令)
EXPOSE
指令是声明运行时容器提供服务端口,这只是一个声明,在运行时并不会因为这个声明应用就会开启这个端口的服务。
在 Dockerfile 中写入这样的声明有两个好处
- 帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射
- 在运行时使用随机端口映射时,也就是
docker run -P
时,会自动随机映射 EXPOSE 的端口。
要将 EXPOSE
和在运行时使用 -p <宿主端口>:<容器端口>
区分开来。 -p
,是映射宿主端口和容器端口,换句话说,就是将容器的对应端口服务公开给外界访问,而 EXPOSE
仅仅是声明容器打算使用什么端口而已,并不会自动在宿主进行端口映射。
USER(不常用命令)
USER
指令和 WORKDIR
相似,都是改变环境状态并影响以后的层。 WORKDIR
是改变工作目录, USER
则是改变之后层的执行 RUN
, CMD
以及 ENTRYPOINT
这类命令的身份。
注意, USER
只是帮助你切换到指定用户而已,这个用户必须是事先建立好的,否则无法切换。
RUN groupadd -r redis && useradd -r -g redis redis
USER redis
RUN [ "redis-server" ]
如果以 root
执行的脚本,在执行期间希望改变身份,比如希望以某个已经建立好的用户来运行某个服务进程,不要使用 su
或者 sudo
,这些都需要比较麻烦的配置,而且在 TTY
缺失的环境下经常出错。建议使用 gosu
。
# 建立 redis 用户,并使用 gosu 换另一个用户执行命令
RUN groupadd -r redis && useradd -r -g redis redis
# 下载 gosu
RUN wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/1.12/gosu-amd64" \
&& chmod +x /usr/local/bin/gosu \
&& gosu nobody true
# 设置 CMD,并以另外的用户执行
CMD [ "exec", "gosu", "redis", "redis-server" ]
创建个人仓库
创建个人仓库非常容易,只需要在官网上注册帐号,再到此页面创建就可以了 https://hub.docker.com/repositories
使用 tag
重命令镜像,再使用 docker push
推送到远程仓库
$ docker login -u 用户名 -p 密码
$ docker tag minibear2333/gitbook-export:0.0.4 minibear2333/gitbook-export:latest
$ docker push minibear2333/gitbook-export:latest
Docker 容器隔离原理
Namespace
是 Linux 用来隔离系统资源的方式,实际上一个容器本身就是一个进程,只不过是做了进程级别的隔离。
容器可以拥有自己的 root
文件系统、自己的网络配置、自己的进程空间,甚至自己的用户 ID
空间。容器内的进程是运行在一个隔离的环境里,使用起来,就好像是在一个独立于宿主的系统下操作一样。这种特性使得容器封装的应用比直接在宿主运行更加安全。
每个 namespace
里面的资源对其他 namespace
都是彼此透明,互不干扰,改变一个 namespace
中的系统资源只会影响当前 namespace
里的进程,对其他 namespace
中的进程没有影响。
在原先 Linux 中,许多资源是全局管理的。例如,系统中的所有进程按照惯例是通过 PID 标识的,这意味着内核必须管理一个全局的 PID 列表。
而且,所有调用者通过 uname 系统调用返回的系统相关信息(包括系统名称和有关内核的一些信息)都是相同的。
用户 ID 的管理方式类似,即各个用户是通过一个全局唯一的 UID 号标识。
cat /etc/passwd|grep -v nologin|grep -v halt|grep -v shutdown|awk -F":" '{ print 1"|"3"|"$4 }'|more
root|0|0
sync|5|0
syslog|996|994
git|1000|1000
ppp|1001|1000
Namespace
提供了一种不同的解决方案,前面所述的所有全局资源都通过 namespace
封装、抽象出来。建立了系统的不同视图,上面说到的全部资源都会包含进来,这种主要是 chroot 系统调用。该方法可以将进程限制到文件系统的某一部分。
所以对于这个容器进程来说,就是一个完全独立的系统。
列出 pid
为 1 的进程的 namespace
ls -l /proc/1/ns
total 0
lrwxrwxrwx 1 root root 0 Nov 9 21:56 ipc -> ipc:[4026531839]
lrwxrwxrwx 1 root root 0 Nov 9 21:56 mnt -> mnt:[4026531840]
lrwxrwxrwx 1 root root 0 Nov 9 21:56 net -> net:[4026531956]
lrwxrwxrwx 1 root root 0 Nov 9 21:56 pid -> pid:[4026531836]
lrwxrwxrwx 1 root root 0 Nov 9 21:56 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 Nov 9 21:56 uts -> uts:[4026531838]
列出 容器
的 namespace
$ docker inspect --format '{{.State.Pid}}' 13dc62bb443a
14141
$ ls -l /proc/14141/ns
total 0
lrwxrwxrwx. 1 root root 0 Oct 30 11:36 ipc -> ipc:[4026532462]
lrwxrwxrwx. 1 root root 0 Oct 30 12:11 mnt -> mnt:[4026532460]
lrwxrwxrwx. 1 root root 0 Oct 30 11:36 net -> net:[4026531956]
lrwxrwxrwx. 1 root root 0 Oct 30 12:11 pid -> pid:[4026532463]
lrwxrwxrwx. 1 root root 0 Oct 30 12:11 user -> user:[4026531837]
lrwxrwxrwx. 1 root root 0 Oct 30 12:11 uts -> uts:[4026532461]
Docker 资源限制原理
cgroup
是 Linux 内核中的一项功能,它可以对进程进行分组,并在分组的基础上对进程组进行资源分配(如 CPU 时间、系统内存、网络带宽等)。通过 cgroup
,系统管理员在分配、排序、拒绝、管理和监控系统资源等方面,可以对硬件资源进行精细化控制。
cgroup
是做资源限制的,而 namespace
是做环境隔离的。
cgroup
技术就是把系统中所有进程组织成一颗进程树,进程树都包含系统的所有进程,树的每个节点是一个进程组。 cgroup
中的资源被称为 subsystem
,进程树可以和一个或者多个 subsystem
系统资源关联。
系统中可以有很多颗进程树,每棵树都和不同的 subsystem
关联,一个进程可以属于多颗树,即一个进程可以属于多个进程组,只是这些进程组和不同的 subsystem
关联。
进程树的作用是将进程分组,而 subsystem
的作用是监控、调度或限制每个进程组的资源。目前 Linux 支持 12 种 subsystem
,比如限制 CPU 的使用时间、内存、统计 CPU 的使用情况等。
也就是 Linux 里面最多可以建 12 棵进程树,每棵树关联一个 subsystem
,当然也可以只建一棵树,然后让这棵树关联所有的 subsystem
。
查看所有 cgoups
的 subsystem
$ lssubsys -M
cpuset /sys/fs/cgroup/cpuset
cpu,cpuacct /sys/fs/cgroup/cpu,cpuacct
memory /sys/fs/cgroup/memory
devices /sys/fs/cgroup/devices
freezer /sys/fs/cgroup/freezer
net_cls,net_prio /sys/fs/cgroup/net_cls,net_prio
blkio /sys/fs/cgroup/blkio
perf_event /sys/fs/cgroup/perf_event
hugetlb /sys/fs/cgroup/hugetlb
pids /sys/fs/cgroup/pids
手动限制 cpu
,我写了一个消耗 cpu
的脚本
创建 cpu
限制方法
$ cgcreate -g cpu:mytest
$ ls /sys/fs/cgroup/cpu/mytest
cgroup.clone_children cgroup.procs cpuacct.usage cpu.cfs_period_us cpu.rt_period_us cpu.shares notify_on_release
cgroup.event_control cpuacct.stat cpuacct.usage_percpu cpu.cfs_quota_us cpu.rt_runtime_us cpu.stat tasks
绑定进程 pid: 1646, 并设置 cpu
限制
cgclassify -g cpu:mytest 1646
echo 3000 > /sys/fs/cgroup/cpu/mytest/cpu.cfs_quota_us
看 cpu
被限制到了 3%
手动实现 Docker
快速安装
这里是用 shell
来实现 Docker
的基本功能,参考网上的bocker
项目,我做了一些轻量的改动,先快速安装完体验一下,下一节再分析原理
- bocker 项目 bocker
- 我的修改 bocker
我们知道Docker
使用了cgroup
、namespace
,在网络上又使用了iptable
再做网络转,我们先来完成网络部分,Docker
本身有三种网络模式,这里使用的默认模式 bridge
echo 1 > /proc/sys/net/ipv4/ip_forward
ip link add bridge0 type bridge
ip addr add 10.0.0.1/24 dev bridge0
iptables --flush
iptables -t nat -A POSTROUTING -o bridge0 -j MASQUERADE
ip link set bridge0 up
- 开启内核转发
- 创建虚拟网卡
bridge0
等同于docker
在宿主机上创建的docker0
网卡 - 设置
bridge0
网卡可用网段 - 清空 iptable 防火墙规则
- 配置 nat 网络,利用 MASQUERADE 规则将容器的 ip 转换为宿主机出口网卡的 ip
- 启动这张网卡
安装依赖
核心是cgourp
, btrfs-progs
yum install -y -q autoconf automake btrfs-progs docker gettext-devel git libcgroup-tools libtool python3
jq
启动 docker,如果已经启动跳过这一步
systemctl enable docker.service
systemctl start docker.service
安装undocker
和util-linux
工具
wget https://files.pythonhosted.org/packages/3b/77/f8e0ee5758292ff5dc800419d7fd579b98a1b3299c903d17d4ebb2c228ca/undocker-7.tar.gz
tar zxvf undocker-7.tar.gz && cd undocker-7/ && python3 setup.py install && cd ..
wget https://github.com/karelzak/util-linux/archive/v2.25.2.tar.gz -O util-linux-2.25.2.tar.gz
tar zxvf util-linux-2.25.2.tar.gz && cd util-linux-2.25.2/
./autogen.sh && ./configure --without-ncurses --without-python && make
mv unshare /usr/bin/unshare
创建挂载文件系统
docker 镜像支持的一种文件结构,具体细节可以看链接
fallocate -l 10G ~/btrfs.img
mkdir /var/bocker
mkfs.btrfs ~/btrfs.img
mount -o loop ~/btrfs.img /var/bocker
- docker 在管理容器时,也会创建一个目录用来存储必要的文件,
/var/lib/docker
目录 - 镜像存储于
/var/lib/docker/image/
- 容器存储于
/var/lib/docker/containers/
解压 docker 的 centos 包,作为 base 包
docker pull centos
docker save centos | undocker -o base-image
安装 bocker
git clone https://github.com/minibear2333/bocker.git
cd bocker
chmod +x bocker
cp bocker /usr/local/bin
最终效果
拉取
列出镜像、运行、查看容器
提交
总结
可以看到bocker
项目包括镜像拉取,镜像查看,容器启动,容器删除,容器查看,容器资源限制,镜像删除的功能,因为实现的比较简单,拉镜像的时候不是很灵活、使用的时候也有略微的小差异,不要要求太高。
我们的目的是从 100 行代码中学会他用到的技术。
整个 docker 使用到的技术就是cgroup
作资源限制、namespace
做各种隔离,在网络上使用了iptable
做网络转发实现不同的网络模式(连通内部和外部的网络)
引用

评论