手动实现 Docker 容器,从根上理解 Docker 容器技术

小熊 Docker评论7,5371字数 15778阅读52分35秒阅读模式

手动实现 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 容器技术

手动实现 Docker 容器,从根上理解 Docker 容器技术

个人简介

瞿鹏志(小熊),就职于腾讯云私有云全栈云团队,监控云负责人。

前言

很高兴你订阅我的 Chat , 这篇文章由浅入深,带你了解 Docker 的概念、常用命令、尝试用 shell 实现 Docker ,这样一套下来想必会对容器和 Docker 有进一步的认识。

本文有一定门槛,你至少需要对 Linux 系统有所了解
本文使用环境 centos8
本文核心内容为实现 Docker 的交互,进一步学习原理

对于现有的 Docker 资料而言,上来就是写原理+实战,很少有人考虑过 Docker 底层所使用的技术,作为一个工作中常用容器和编排技术的人,不如就实现一个看看效果,用 Linux 本身的功能和命令手把手实现一个 Docker 的所有功能,做到真正意义上的理解。

优势

更高效的利用系统资源

可以看到下图右边的虚拟机会用到硬件虚拟化技术,还必须安装完整的操作系统。

手动实现 Docker 容器,从根上理解 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 容器,从根上理解 Docker 容器技术

  • 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 请求

安装说明

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 镜像操作命令实战(镜像拉取、镜像查看、导入导出、删除、重命名)

当然官方提供了很多现成的镜像以使用,可以在此搜索想要的镜像

手动实现 Docker 容器,从根上理解 Docker 容器技术

镜像分为镜像名称和标签(tag),如图是我搜索 nginx 镜像的结果

手动实现 Docker 容器,从根上理解 Docker 容器技术

拉取命令,不加 tag 默认为 latesttag 就是镜像的版本号

官方介绍

$ 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 容器技术

如上图,镜像仓库不止 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

使用场景:

  1. 离线导入导出
  2. 分享个人定制镜像

导入镜像

官方介绍

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 容器,从根上理解 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 镜像中安装和配置需要的软件构建出来的。

手动实现 Docker 容器,从根上理解 Docker 容器技术

从上图可以看到,新镜像是从 base 镜像一层一层叠加生成的。每安装一个软件,就在现有镜像的基础上增加一层。

Docker 镜像为什么分层?

镜像分层最大的一个好处就是共享资源。

比如说有多个镜像都从相同的 base 镜像构建而来,那么 Docker Host 只需在磁盘上保存一份 base 镜像;同时内存中也只需加载一份 base 镜像,就可以为所有容器服务了。而且镜像的每一层都可以被共享。

如果多个容器共享一份基础镜像,当某个容器修改了基础镜像的内容,比如 /etc 下的文件,这时其他容器的 /etc 是不会被修改的,修改只会被限制在单个容器内。这就是容器 Copy-on-Write 特性。

可写的容器层

当容器启动时,一个新的可写层被加载到镜像的顶部。这一层通常被称作“容器层”,“容器层”之下的都叫“镜像层”。

手动实现 Docker 容器,从根上理解 Docker 容器技术

所有对容器的改动 - 无论添加、删除、还是修改文件都只会发生在容器层中。只有容器层是可写的,容器层下面的所有镜像层都是只读的。

所有对容器的改动 - 无论添加、删除、还是修改文件都只会发生在容器层中。只有容器层是可写的,容器层下面的所有镜像层都是只读的。

容器层的细节说明

镜像层数量可能会很多,所有镜像层会联合在一起组成一个统一的文件系统。如果不同层中有一个相同路径的文件,比如 /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 的区别

CMDENTRYPOINT 命令

相同点:

  • 为启动的容器指定默认要运行的程序,程序运行结束,容器也就结束,所以如果想要容器长期运行就让这个命令指定的命令长期运行。
  • CMD 指令,仅最后一个生效。

不同点:

  • CMD 指令指定的程序可被 docker run 命令行参数中指定要运行的程序所覆盖,但 ENTRYPOINT 不会。

高级用法,如果是使用 [] 来标记后面执行的命令,功能有所不同

命令加参数的形式

ENTRYPOINT [ "echo", "a" ]
$ docker run  test
a

加参数,但是不会替换

ENTRYPOINT [ "echo", "a" ]
$ docker run  test b
a b

CMDENTRYPOINT 提供默认参数

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

手动实现 Docker 容器,从根上理解 Docker 容器技术

使用 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 列表。

手动实现 Docker 容器,从根上理解 Docker 容器技术

而且,所有调用者通过 uname 系统调用返回的系统相关信息(包括系统名称和有关内核的一些信息)都是相同的。

手动实现 Docker 容器,从根上理解 Docker 容器技术

用户 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

查看所有 cgoupssubsystem

$ 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 的脚本

手动实现 Docker 容器,从根上理解 Docker 容器技术

创建 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 容器,从根上理解 Docker 容器技术

手动实现 Docker

快速安装

这里是用 shell 来实现 Docker 的基本功能,参考网上的bocker项目,我做了一些轻量的改动,先快速安装完体验一下,下一节再分析原理

我们知道Docker 使用了cgroupnamespace,在网络上又使用了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

安装undockerutil-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

最终效果

拉取

手动实现 Docker 容器,从根上理解 Docker 容器技术

列出镜像、运行、查看容器

手动实现 Docker 容器,从根上理解 Docker 容器技术

提交

手动实现 Docker 容器,从根上理解 Docker 容器技术

总结

可以看到bocker项目包括镜像拉取,镜像查看,容器启动,容器删除,容器查看,容器资源限制,镜像删除的功能,因为实现的比较简单,拉镜像的时候不是很灵活、使用的时候也有略微的小差异,不要要求太高。

我们的目的是从 100 行代码中学会他用到的技术。

整个 docker 使用到的技术就是cgroup作资源限制、namespace做各种隔离,在网络上使用了iptable做网络转发实现不同的网络模式(连通内部和外部的网络)

引用

weinxin
公众号
扫码订阅最新深度技术文,回复【资源】获取技术大礼包
小熊