【Go 与云原生】让一个 Go 项目脱离原生的操作系统——我们开始使用 Docker 制造云容器进行时

文章目录

推荐一个零声教育学习教程,个人觉得老师讲得不错,分享给大家:[Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等技术内容,点击立即学习: https://github.com/0voice 链接

前言

我们在上一篇文章 《先从 Go 对与云原生的依赖关系讲起,再讲讲 一个简单的 Go 项目热热身》 中介绍了一个零声教育的商品信息服务项目案例,由于在代码中使用了 GRPC 框架,我们把项目变成了一个前后端分离、信息流紧凑结构化的程序。这个 go 程序是可以通过 docker 打包成镜像以达到脱离其操作系统环境的目的,而且极其之简单,因为 go 程序真不像 C/C++ 程序那样每次启动都需要程序员主动去连接动态库,它是一次过把所有的依赖都打包进了同一个程序之中。

因此,我想说明 C/C++ 程序的 Docker 镜像打包,完全可以做。但是需要程序员对库依赖文件具有高度敏锐的识别,程序员需要把所有依赖文件都找齐,比如我们使用了 Linux 的 epoll 事件驱动库,那就需要把这个文件找出来... 。这是一个非常费力的过程,要求程序员对操作系统有很高水平的认知和理解。

进入正题,我们在本篇文章要把之前做的商品信息服务系统打包成 Docker 镜像,然后运行成一个容器。通过一个具体的案例,我们是可以深刻领会 Docker 寂静是一个什么样的软件了。另外,docker 的下载请自行解决。


Docker 的技术边界

docker是容器化技术,针对的是应用及应用所依赖的环境做容器化。遵循单一原则,一个容器只运行一个主进程。多个进程都部署在一个容器中,弊端很多。比如更新某个进程的镜像时,其他进程也会被迫重启,如果一个进程出问题导致容器挂了,所有进程都将无法访问。再根据官网的提倡的原则而言,容器 = 应用 + 依赖的执行环境,而不是像虚拟机一样,把一堆进程都部署在一起。

docker解决了什么问题:

  1. 解决了应用程序本地运行环境与生产运行环境不一致的问题
  2. 解决了应用程序资源使用的问题,docker会一开始就为每个程序指定内存分配和CPU分配
  3. 让快速扩展、弹性伸缩变得简单

Docker 与虚拟机的区别

我们虚拟机是需要在配置的时候通过光盘映像文件安装操作系统(比如 Ubuntu,Centos 等),但是 Docker 只需要把所有依赖文件安装好了就行,整个 docker 容器就只运行一个程序即可,但虚拟机操作系统需要做到 "面面俱到",因而整体规模很大。

Hypervisor(虚拟机监控器,Virtual Machine Monitor,简称 VMM)是一种软件、硬件或固件,用于创建和运行虚拟机(VM)。它允许多个操作系统共享单一的物理主机,通过将主机的硬件资源(如 CPU、内存、存储和网络)虚拟化,使每个虚拟机都能独立运行,仿佛它们各自拥有独立的物理机。

对于 Docker 来说,它有 Docker 引擎管理镜像、存储数据持久化、网络区隔、容器运行时的资源分配。这个倒是和 Hypervisor 区别不大。Docker 容器镜像之内,就是一个小型王国,它有操作系统的一些性质,比如 "域名解析"、环境变量、路径、终端等等。


同一个镜像是可以运行成多个不同的容器,做不同的事情

镜像构建只创建只读的 LowerDir 层,每次运行容器时才会创建新的 UpperDirWorkDir,同一个镜像的多个容器实例有各自独立的 UpperDir


打包 Docker 镜像

将 Go 程序打包成 Docker 镜像,并不是那么难,因为 go 程序并没有那么的以来操作系统,也可以说是与操作系统生殖隔离(不同操作系统的差别并不大),实在是上手学习云原生部署的好对象。这点远远优于 C/C++ 程序,因为 c/c++ 是高度依赖于操作系统的各个架构与底层源代码的,比如 epoll 和 io-uring 等库文件都是 Linux 的底层代码,因而盘根错节,剪不断理还乱。

另外,我会先介绍 Dockerfile 的语法

Docker 语法命令 作用
FROM 设置镜像使用的基础镜像
MAINTAINER 设置镜像的作者
RUN 编译镜像时运行的脚步
CMD 设置容器的启动命令
LABEL 设置镜像标签
EXPOSE 设置镜像暴露的端口
ENV 设置容器的环境变量
ADD 编译镜像时复制上下文中文件到镜像中
COPY 编译镜像时复制上下文中文件到镜像中
ENTRYPOINT 设置容器的入口程序
VOLUME 设置容器的挂载卷
USER 设置运行 RUN CMD ENTRYPOINT的用户名
WORKDIR 设置 RUN CMD ENTRYPOINT COPY ADD 指令的工作目录
ARG 设置编译镜像时加入的参数
ONBUILD 设置镜像的ONBUILD 指令
STOPSIGNAL 设置容器的退出信号量

前端程序的 Dockerfile

我们先来看我们为前端程序准备的 Dockerfile

我们发现这个 Dockerfile 里面有两段文字两个 FROM,实质上该镜像的构建过程分成两个阶段 stage,我们查看上面那个 Dockerfile 文件语法表,便可以解读

复制代码
第一阶段
1、(基础)下载镜像 golang:1.20,本地没有的话,就远程拉取,就此进入第一阶段,记作 stage0。
2、(运行)设置 Docker 镜像容器内的环境变量 GOPROXY=https://proxy.golang.com.cn,https://goproxy.cn,direct
3、(复制)把当前路径下的所有文件都复制到容器的 /src/0voiceGateway 路径上
4、(进入某个容器镜像的某个目录)进入某个容器镜像的 /src/0voiceGateway 目录
5、(运行)编译 go 程序,跟我们上一篇文章类似, CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o 0voiceGateway .

第二阶段
1、(基础)下载镜像 alpine:3.18,本地没有的话,就远程拉取,就此进入第二阶段,记作 stage1。
2、(复制)把当前文件夹下的  ./curl-amd64 可执行文件复制到容器里面,并且指定路径与文件名 /usr/bin/curl 
3、(运行)将容器内的文件 /usr/bin/curl  设置可执行权限
4、(进入某个容器镜像的某个目录)进入某个容器镜像的 /app 目录
5、(复制)把当前路径下的 config.yaml 文件都复制到容器的 /app/config.yaml 路径上
6、(复制)把第一个阶段的编译成型文件 /src/0voiceGateway/0voiceGateway 移动到当前镜像的 /app 目录下
7、(入口程序)当我们把这个镜像启动成 docker 容器运行时的时候,第一步就是要执行这个文件

打包前端程序的镜像需要用到这个命令,docker build 是 docker 的镜像构架命令,-t--tag 的简写,用来给即将构建出的镜像取"名字+标签"(name:tag)。后面的 0voice-gateway:v0.5.0 就是镜像+标签,. 就是当前文件路径下的 Dockerfile。

bash 复制代码
docker build -t 0voice-gateway:v0.5.0 .

第一阶段生成镜像就是中间镜像,如果这个命令没有指明生成哪一个,就一定是最后一个阶段的镜像作为最终镜像

如果我们运行下面这个命令,那就不会再是默认的最后一个阶段变成最终镜像
$ docker build -t [输出镜像名字];[标签] --target [阶段名字] [Dockerfile的文件名]

于是,终端输出会显示整个构建过程,他是分阶段构建的,我们从 [stage0 3/5] ADD ./ /src/0voiceGateway[stage1 6/6] COPY --from=stage0 /src/0voiceGateway/0voiceGateway ./ 等等可以看出是分阶段构建,因为它有阶段名字与该阶段下的命令,还介绍了该阶段下运行的命令运行了多长时间。

bash 复制代码
qiming@k8s-master1:~/share/CTASK/docker/code/0voice-crm/0voiceGateway$ docker build -t 0voice-gateway:v0.5.0 .
[+] Building 351.7s (17/17) FINISHED                                                                                                                                                              docker:default
 => [internal] load build definition from Dockerfile                                                                                                                                                        0.2s
 => => transferring dockerfile: 1.29kB                                                                                                                                                                      0.2s
 => WARN: FromAsCasing: 'as' and 'FROM' keywords' casing do not match (line 1)                                                                                                                              0.2s
 => WARN: FromAsCasing: 'as' and 'FROM' keywords' casing do not match (line 7)                                                                                                                              0.2s
 => [internal] load metadata for docker.io/library/alpine:3.18                                                                                                                                              0.0s
 => [internal] load metadata for docker.io/library/golang:1.20                                                                                                                                             46.5s
 => [internal] load .dockerignore                                                                                                                                                                           0.1s
 => => transferring context: 2B                                                                                                                                                                             0.0s
 => [stage0 1/5] FROM docker.io/library/golang:1.20@sha256:8f9af7094d0cb27cc783c697ac5ba25efdc4da35f8526db21f7aebb0b0b4f18a                                                                                 0.0s
 => [stage1 1/6] FROM docker.io/library/alpine:3.18                                                                                                                                                         0.0s
 => CACHED [stage0 2/5] RUN go env -w GOPROXY=https://proxy.golang.com.cn,https://goproxy.cn,direct                                                                                                         0.0s
 => [internal] load build context                                                                                                                                                                          18.3s
 => => transferring context: 20.67MB                                                                                                                                                                       18.2s
 => [stage0 3/5] ADD ./ /src/0voiceGateway                                                                                                                                                                  1.7s
 => CACHED [stage1 2/6] ADD ./curl-amd64 /usr/bin/curl                                                                                                                                                      0.0s
 => CACHED [stage1 3/6] RUN chmod +x /usr/bin/curl                                                                                                                                                          0.0s
 => CACHED [stage1 4/6] WORKDIR /app/                                                                                                                                                                       0.0s
 => [stage1 5/6] ADD ./config.yaml /app/config.yaml                                                                                                                                                         0.5s
 => [stage0 4/5] WORKDIR /src/0voiceGateway                                                                                                                                                                 0.0s
 => [stage0 5/5] RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o 0voiceGateway .                                                                                                                    284.1s
 => [stage1 6/6] COPY --from=stage0 /src/0voiceGateway/0voiceGateway ./                                                                                                                                     0.2s 
 => exporting to image                                                                                                                                                                                      0.2s 
 => => exporting layers                                                                                                                                                                                     0.2s 
 => => writing image sha256:d5542708ce1bf33c6ddc8fa5da784ae957306de83cf263b0a597084a4016f8f1                                                                                                                0.0s 
 => => naming to docker.io/library/0voice-gateway:v0.5.0                                                                                                                                                    0.0s 
                                                                                                                                                                                                                 
 2 warnings found (use docker --debug to expand):
 - FromAsCasing: 'as' and 'FROM' keywords' casing do not match (line 1)
 - FromAsCasing: 'as' and 'FROM' keywords' casing do not match (line 7)

后端程序的 Dockerfile

我们先来看我们为后端程序准备的 Dockerfile(这个文件的解读也和上面的一样,大家跟着我的步骤来就行了)

打包前端程序的镜像需要用到这个命令

bash 复制代码
docker build -t goods:v0.5.0 .

于是,终端输出会显示整个构建过程(解读过程也跟前面的一样)

bash 复制代码
qiming@k8s-master1:~/share/CTASK/docker/code/0voice-crm/Goods$ docker build -t goods:v0.5.0 .
[+] Building 344.4s (17/17) FINISHED                                                                                                                                                              docker:default
 => [internal] load build definition from Dockerfile                                                                                                                                                        0.9s
 => => transferring dockerfile: 498B                                                                                                                                                                        0.4s
 => [internal] load metadata for docker.io/library/alpine:3.18                                                                                                                                              0.0s
 => [internal] load metadata for docker.io/library/golang:1.20                                                                                                                                             50.1s
 => [internal] load .dockerignore                                                                                                                                                                           0.0s
 => => transferring context: 2B                                                                                                                                                                             0.0s
 => [stage0 1/5] FROM docker.io/library/golang:1.20@sha256:8f9af7094d0cb27cc783c697ac5ba25efdc4da35f8526db21f7aebb0b0b4f18a                                                                                 0.0s
 => [internal] load build context                                                                                                                                                                           0.6s
 => => transferring context: 14.94MB                                                                                                                                                                        0.6s
 => [stage1 1/6] FROM docker.io/library/alpine:3.18                                                                                                                                                         0.0s
 => CACHED [stage0 2/5] RUN go env -w GOPROXY=https://proxy.golang.com.cn,https://goproxy.cn,direct                                                                                                         0.0s
 => [stage0 3/5] ADD ./ /src/goods                                                                                                                                                                          9.8s
 => [stage0 4/5] WORKDIR /src/goods                                                                                                                                                                         0.0s
 => [stage0 5/5] RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o goods .                                                                                                                            277.7s
 => CACHED [stage1 2/6] ADD ./grpc_health_probe-linux-amd64 /usr/bin/grpc_health_probe                                                                                                                      0.0s 
 => CACHED [stage1 3/6] RUN chmod +x /usr/bin/grpc_health_probe                                                                                                                                             0.0s 
 => CACHED [stage1 4/6] WORKDIR /app/                                                                                                                                                                       0.0s 
 => CACHED [stage1 5/6] ADD ./config.yaml /app/config.yaml                                                                                                                                                  0.0s 
 => [stage1 6/6] COPY --from=stage0 /src/goods/goods ./                                                                                                                                                     0.2s 
 => exporting to image                                                                                                                                                                                      2.1s 
 => => exporting layers                                                                                                                                                                                     1.7s
 => => writing image sha256:714feb39b527f08ebb24816e1ce9901dd97ecf42e7d5418da45aed48afbc162e                                                                                                                0.1s
 => => naming to docker.io/library/goods:v0.5.0                                                                                                                                                             0.1s

 3 warnings found (use docker --debug to expand):
 - FromAsCasing: 'as' and 'FROM' keywords' casing do not match (line 1)
 - FromAsCasing: 'as' and 'FROM' keywords' casing do not match (line 7)
 - MaintainerDeprecated: Maintainer instruction is deprecated in favor of using label (line 10)

近距离的观察 Docker 镜象

构建完镜像后,我们就可以通过 docker inspect 去近距离观察一个镜像,发现它的信息真的很多,但也无须着急,因为真正重要的也就是 GraphDriverRootFS 两个字段的内容。

bash 复制代码
qiming@k8s-master1:~$ docker inspect 0voice-gateway:v0.5.0
[
    {
        "Id": "sha256:d5542708ce1bf33c6ddc8fa5da784ae957306de83cf263b0a597084a4016f8f1",
        "RepoTags": [
            "0voice-gateway:v0.5.0"
        ],
        "RepoDigests": [],
        "Parent": "",
        "Comment": "buildkit.dockerfile.v0",
        "Created": "2025-11-08T18:53:43.29937097Z",
        "DockerVersion": "",
        "Author": "",
        "Architecture": "amd64",
        "Os": "linux",
        "Size": 34735574,
        "GraphDriver": {
            "Data": {
                "LowerDir": "/var/lib/docker/165536.165536/overlay2/r20wlwiybeqx425ote1slwbiu/diff:/var/lib/docker/165536.165536/overlay2/5o0xzo91v6dhwrrcf00m9r749/diff:/var/lib/docker/165536.165536/overlay2/t82x15extk3uuxhlrb8raiyya/diff:/var/lib/docker/165536.165536/overlay2/p645ylzduapfzr0yvm48k956s/diff:/var/lib/docker/165536.165536/overlay2/b9c3d4bf2bafc80dbf6081405bc051132bdbc6ab82eb989646a5777426c3c22e/diff",
                "MergedDir": "/var/lib/docker/165536.165536/overlay2/73wyqndlla6e3d7dd0gwuw0eq/merged",
                "UpperDir": "/var/lib/docker/165536.165536/overlay2/73wyqndlla6e3d7dd0gwuw0eq/diff",
                "WorkDir": "/var/lib/docker/165536.165536/overlay2/73wyqndlla6e3d7dd0gwuw0eq/work"
            },
            "Name": "overlay2"
        },
        "RootFS": {
            "Type": "layers",
            "Layers": [
                "sha256:f44f286046d9443b2aeb895c0e1f4e688698247427bca4d15112c8e3432a803e",
                "sha256:aaf1c64fc2a8e5857597d97ede0eb18beedc86d05b6e2bdda3b62571ea9d3af7",
                "sha256:fe695ece85edd2ab4d1c0c6115d8febce80f51d2cab1d1b656440b5754c16678",
                "sha256:be4ad4a66fe7c00a1c5d51ad3915879114f774399a111d4db5971659d11959b6",
                "sha256:6e819136eca5d4fa602bd1e8476786cdd429715c74c33b8536f5d25e554abad0",
                "sha256:b4cc20de0a7c46f6ca41cc8f3579f56291936d90dd70735608b967b26d6eb76f"
            ]
        },
        "Metadata": {
            "LastTagTime": "2025-11-08T18:53:43.50436954Z"
        },
        "Config": {
            "Cmd": null,
            "Entrypoint": [
                "./0voiceGateway"
            ],
            "Env": [
                "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
            ],
            "Labels": null,
            "OnBuild": null,
            "User": "",
            "Volumes": null,
            "WorkingDir": "/app/"
        }
    }
]

我们需要注意到 GraphDriver 字段的子字段 DataLowerDir 字段,即 GraphDriver.Data.LowerDir 字段,它里面有 5 个路径。这 5 个路径对应 Dockerfile 中的以下构建步骤:

复制代码
LowerDir[0]: /var/lib/docker/.../r20wlwiybeqx425ote1slwbiu/diff
    ↓
LowerDir[1]: /var/lib/docker/.../5o0xzo91v6dhwrrcf00m9r749/diff  
    ↓
LowerDir[2]: /var/lib/docker/.../t82x15extk3uuxhlrb8raiyya/diff
    ↓
LowerDir[3]: /var/lib/docker/.../p645ylzduapfzr0yvm48k956s/diff
    ↓
LowerDir[4]: /var/lib/docker/.../b9c3d4bf2bafc80dbf6081405bc051132bdbc6ab82eb989646a5777426c3c22e/diff

需要明确,任何镜像构建命令,没有指定哪一个阶段作为最终镜像,那就一定是最后一个阶段作为构建镜像的依据,即这个镜像的 5 个 LowerDir 就是仅由红色框的命令生成的

第1层(最底层):alpine:3.18 基础镜像

bash 复制代码
# LowerDir[4] - 基础操作系统层
/var/lib/docker/.../b9c3d4bf2bafc80dbf6081405bc051132bdbc6ab82eb989646a5777426c3c22e/diff

# 内容:Alpine Linux 3.18 的最小文件系统
/bin, /etc, /lib, /usr, /var, ...

第2层:添加 curl 二进制文件

bash 复制代码
# LowerDir[3] - 对应 Dockerfile 第8行
ADD ./curl-amd64 /usr/bin/curl

# 内容:/usr/bin/curl 文件
# 这创建了一个新层,包含您添加的 curl 二进制文件

第3层:设置 curl 权限

bash 复制代码
# LowerDir[2] - 对应 Dockerfile 第9行  
RUN chmod +x /usr/bin/curl

# 内容:/usr/bin/curl 文件的权限元数据变化
# 虽然看起来是"修改",但在分层文件系统中这会创建新层

第4层:设置工作目录和添加配置文件

bash 复制代码
# LowerDir[1] - 对应 Dockerfile 第10-11行
WORKDIR /app/
ADD ./config.yaml /app/config.yaml

# 内容:
# - 工作目录元数据
# - /app/config.yaml 配置文件

第5层:从 stage0 复制构建结果

bash 复制代码
# LowerDir[0] - 对应 Dockerfile 第12行
COPY --from=stage0 /src/0voiceGateway/0voiceGateway ./

# 内容:/app/0voiceGateway 可执行文件
# 这是从第一阶段构建的 Go 二进制文件

被优化的指令:

  • FROM golang:1.20 as stage0 - 这是第一阶段,不包含在最终镜像中
  • ENTRYPOINT ["./0voiceGateway"] - 元数据指令,不创建文件系统层

完整的镜像结构就是 GraphDriver.Data.MergedDir 字段下的那个

复制代码
+------------------------------------------------+
|  最终镜像(stage1)的 Merged View              |
+------------------------------------------------+
| UpperDir (可写层,如果有容器修改)              |
+------------------------------------------------+
| LowerDir[0]: 0voiceGateway 二进制文件         | ← COPY --from=stage0
+------------------------------------------------+
| LowerDir[1]: /app/config.yaml + WORKDIR       | ← ADD config.yaml
+------------------------------------------------+  
| LowerDir[2]: curl 文件权限设置                 | ← RUN chmod +x
+------------------------------------------------+
| LowerDir[3]: curl 二进制文件                   | ← ADD curl-amd64
+------------------------------------------------+
| LowerDir[4]: alpine:3.18 基础系统              | ← FROM alpine:3.18
+------------------------------------------------+

另外,RootFS 字段也很有意思

bash 复制代码
"RootFS": {
    "Type": "layers",
    "Layers": [
        "sha256:f44f286046d9443b2aeb895c0e1f4e688698247427bca4d15112c8e3432a803e",
        "sha256:aaf1c64fc2a8e5857597d97ede0eb18beedc86d05b6e2bdda3b62571ea9d3af7",
        "sha256:fe695ece85edd2ab4d1c0c6115d8febce80f51d2cab1d1b656440b5754c16678",
        "sha256:be4ad4a66fe7c00a1c5d51ad3915879114f774399a111d4db5971659d11959b6",
        "sha256:6e819136eca5d4fa602bd1e8476786cdd429715c74c33b8536f5d25e554abad0",
        "sha256:b4cc20de0a7c46f6ca41cc8f3579f56291936d90dd70735608b967b26d6eb76f"
    ]
}

核心区别

  • GraphDriver.Data.LowerDir:显示的是实际磁盘上的分层目录结构(Overlay2 驱动视角)
  • RootFS.Layers:显示的是镜像的链式层级关系(镜像元数据视角)

第1-2层:来自 golang:1.20 基础镜像

bash 复制代码
# golang:1.20 本身就是一个多层的镜像
Layers[0]: "sha256:f44f286046d9443b2aeb895c0e1f4e688698247427bca4d15112c8e3432a803e"
Layers[1]: "sha256:aaf1c64fc2a8e5857597d97ede0eb18beedc86d05b6e2bdda3b62571ea9d3af7"
# 这些是 golang:1.20 基础镜像的层,虽然最终镜像不包含文件内容,
# 但镜像元数据中保留了这些层的引用

第3-6层:来自 Dockerfile 构建

bash 复制代码
# 对应您的 Dockerfile 指令
Layers[2]: "sha256:fe695ece85edd2ab4d1c0c6115d8febce80f51d2cab1d1b656440b5754c16678"
    # FROM alpine:3.18 基础层

Layers[3]: "sha256:be4ad4a66fe7c00a1c5d51ad3915879114f774399a111d4db5971659d11959b6"  
    # ADD ./curl-amd64 /usr/bin/curl

Layers[4]: "sha256:6e819136eca5d4fa602bd1e8476786cdd429715c74c33b8536f5d25e554abad0"
    # RUN chmod +x /usr/bin/curl + WORKDIR /app/ + ADD ./config.yaml /app/config.yaml

Layers[5]: "sha256:b4cc20de0a7c46f6ca41cc8f3579f56291936d90dd70735608b967b26d6eb76f"
    # COPY --from=stage0 /src/0voiceGateway/0voiceGateway ./

虽然最终镜像(stage1)不包含 stage0 的文件内容,但镜像元数据中仍然保留了这些层的引用:

go 复制代码
// 镜像元数据结构
type ImageConfig struct {
    RootFS struct {
        Type    string   `json:"type"`
        Layers  []string `json:"layers"`  // 所有引用到的层,包括构建阶段的引用
    } `json:"rootfs"`
    History []History `json:"history"`  // 构建历史记录
}

您可以通过以下命令验证:

bash 复制代码
# 查看镜像的历史记录,会显示所有层的创建信息
docker history your-image-name

终端输出可以看到

bash 复制代码
qiming@k8s-master1:~$ docker history 0voice-gateway:v0.5.0
IMAGE          CREATED        CREATED BY                                      SIZE      COMMENT
d5542708ce1b   13 hours ago   ENTRYPOINT ["./0voiceGateway"]                  0B        buildkit.dockerfile.v0
<missing>      13 hours ago   COPY /src/0voiceGateway/0voiceGateway ./ # b...   20.2MB    buildkit.dockerfile.v0
<missing>      13 hours ago   ADD ./config.yaml /app/config.yaml # buildkit   356B      buildkit.dockerfile.v0
<missing>      4 weeks ago    WORKDIR /app/                                   0B        buildkit.dockerfile.v0
<missing>      4 weeks ago    RUN /bin/sh -c chmod +x /usr/bin/curl # buil...   3.58MB    buildkit.dockerfile.v0
<missing>      4 weeks ago    ADD ./curl-amd64 /usr/bin/curl # buildkit       3.58MB    buildkit.dockerfile.v0
<missing>      8 months ago   CMD ["/bin/sh"]                                 0B        buildkit.dockerfile.v0
<missing>      8 months ago   ADD alpine-minirootfs-3.18.12-x86_64.tar.gz ...   7.36MB    buildkit.dockerfile.v0

RootFS.Layers 有 6 个层的原因是:

  • 包含了多阶段构建的所有层引用,包括构建阶段(stage0)的层
  • 镜像元数据记录了完整的构建历史和层依赖关系
  • 内容寻址存储机制要求记录所有被引用的层哈希

GraphDriver.Data.LowerDir 只有 5 个路径是因为:

  • 只包含实际的文件系统层(最终镜像中存在的文件)
  • 反映了 Overlay2 存储驱动的实际磁盘结构

这种设计确保了:

  1. 层的高效共享(多个镜像可以引用相同的底层)
  2. 构建历史的完整性(可以追溯完整的构建过程)
  3. 存储的空间效率(实际只存储需要的文件数据)

镜像里面包含容器运行时的信息

我补充说明,GraphDriver.Data 字段的 UpperDirWorkDir 是容器运行时的概念。镜像构建只创建只读的 LowerDir 层,每次运行容器时才会创建新的 UpperDirWorkDir,同一个镜像的多个容器实例有各自独立的 UpperDir

UpperDir 的作用

1、容器的可写层(比如我们在容器里面编译一个文件,那这个文件就会放在这个文件夹下)

bash 复制代码
# 当您运行容器时:
docker run -it your-image /bin/sh

# Docker 会为这个容器实例创建一个 UpperDir
# 所有在容器内的修改都存储在这里

2.、写时复制(Copy-on-Write)

bash 复制代码
// 当容器修改文件时的流程:
func handleFileModification(filename string) {
    // 1. 检查文件是否在 UpperDir 中已存在
    if !existsInUpperDir(filename) {
        // 2. 如果不存在,从 LowerDir 复制到 UpperDir
        copyFromLowerToUpper(filename)
    }
    // 3. 在 UpperDir 中进行修改
    modifyFileInUpperDir(filename)
}

WorkDir 的作用(处理文件操作的事务性)

当容器进行文件操作时,WorkDir 用于:

  1. 文件重命名的原子操作
  2. 文件删除的标记处理
  3. 硬链接的临时处理
  4. 确保文件系统操作的一致性

类似于我们所理解的 C/C++ 原子操作,确保文件一致性不会被打破。实际工作流程是

bash 复制代码
# 当删除文件时:
1. 在 WorkDir 中创建 "whiteout" 文件(标记删除)
2. 在 MergedDir 中隐藏原文件
3. 确保操作是原子的和一致的

完整的 Overlay2 运行时结构

bash 复制代码
+------------------------------------------------+
|              MergedDir (合并视图)              |
|  容器看到的统一文件系统,包含所有修改          |
+------------------------------------------------+
| UpperDir (可写层)                             |
| - 新建的文件: /app/test.txt                   |
| - 修改的文件: /app/config.yaml (复制后修改)    |
| - 删除标记: /etc/nginx/nginx.conf.wh..wh..opq |
+------------------------------------------------+
| WorkDir (工作目录)                            |
| - 临时文件操作                                |
| - 原子操作准备                                |
+------------------------------------------------+
| LowerDir[0-4] (只读镜像层)                    |
| - 基础镜像文件                                |
| - 构建时添加的文件                            |
+------------------------------------------------+

想象图景

Docker 的文件系统可以这样想像



运行镜像,将其变成 Docker 容器运行时

上文,我们构建完镜像之后,通过 docker inspect 观察到了镜像的多层构建细节,从而理解了 Docker 的文件系统架构,最终理解了它的运行规律。接下来,我们通过这段命令

bash 复制代码
 docker run -d --name [容器命名] -p [监听宿主机的端口]:[容器内网络条件的端口] [镜像名字]:[标签]

来运行我们生成的两个镜像,生成容器运行时。

bash 复制代码
qiming@k8s-master1:~$ docker run -d --name qiming-goods -p 50051:50051 goods:v0.5.0
f0d04556bb8baffa6cf8ba09367998d7490264e47c2bf41ef057d0d51ba5e740
qiming@k8s-master1:~$ docker run -d --name qiming-0voice-gateway -p 8081:8081 0voice-gateway:v0.5.0
ee891850ff17a063f48c409e8842b9b11ca4237fc4bedbbc0ec36980158eafd7

--name qiming-0voice-gateway 是命名。-p 是端口映射,它非常之关键,因为没有它,就不能正常的监听宿主机的端口,他只会监听自己容器小空间内的端口,换言之是默认与容器内的程序通信,而非与宿主机通信。-d 是让容器以守护进程的方式运行。守护进程是在后台运行的计算机程序,它的特点是:

  • 不直接与用户交互
  • 没有控制终端
  • 长期运行提供服务
  • 通常在系统启动时自动运行

在上面的命令行中的一长串字符是 Docker 容器的完整 ID。

现在我们来检验一下,是否在正常运行,可以通过 docker ps 命令现实 docker 引擎正在管理的容器进程,注意 CONTAINER ID 只是显示了前面那一段,意思意思而已,ID 是同一个的。

bash 复制代码
qiming@k8s-master1:~$ docker ps
CONTAINER ID   IMAGE                                               COMMAND                  CREATED              STATUS              PORTS                                             NAMES
ee891850ff17   0voice-gateway:v0.5.0                               "./0voiceGateway"        6 seconds ago        Up 5 seconds        0.0.0.0:8081->8081/tcp, [::]:8081->8081/tcp       qiming-0voice-gateway
f0d04556bb8b   goods:v0.5.0                                        "./goods"                About a minute ago   Up About a minute   0.0.0.0:50051->50051/tcp, [::]:50051->50051/tcp   qiming-goods

而且事实上,他也是可以运行的

近距离观察 Docker 容器

我们可以通过这段命令

bash 复制代码
docker exec -it [容器名] [容器内的可执行文件]

进入容器这个小型的文件系统

bash 复制代码
qiming@k8s-master1:~$ docker exec -it qiming-0voice-gateway sh
/app # ls
0voiceGateway  config.yaml
/app # 

docker exec 命令的选项 -it 实质可分为 -i-t

  • -i--interactive:保持标准输入流(STDIN)打开,也就是不会立刻退出,保持交互性
  • -t--tty:分配一个伪终端(pseudo-TTY),也就是我们看到的终端

这个 docker 容器真的就是一个小型文件系统,他有很多的操作系统小工具(就是那个 alpine 镜像引进的),大家跟着我实验就好了

bash 复制代码
/app # cd ..
/ # ls
app    bin    dev    etc    home   lib    media  mnt    opt    proc   root   run    sbin   srv    sys    tmp    usr    var
/ #

结尾:先删除镜像,后删除容器

退出容器,使用 exit 命令

bash 复制代码
/ # exit
qiming@k8s-master1:~$

停止容器 docker stop [容器名]

bash 复制代码
qiming@k8s-master1:~$ docker stop qiming-goods
qiming-goods
qiming@k8s-master1:~$ docker stop qiming-0voice-gateway
qiming-0voice-gateway

关掉容器 docker rm [容器名]

bash 复制代码
qiming@k8s-master1:~$ docker rm qiming-goods
qiming-goods
qiming@k8s-master1:~$ docker rm qiming-0voice-gateway
qiming-0voice-gateway

删除镜像 docker rmi [镜像名]:[标签]

bash 复制代码
qiming@k8s-master1:~$ docker rmi 0voice-gateway:v0.5.0
Untagged: 0voice-gateway:v0.5.0
Deleted: sha256:d5542708ce1bf33c6ddc8fa5da784ae957306de83cf263b0a597084a4016f8f1
qiming@k8s-master1:~$ docker rmi goods:v0.5.0
Untagged: goods:v0.5.0
Deleted: sha256:714feb39b527f08ebb24816e1ce9901dd97ecf42e7d5418da45aed48afbc162e

检查容器情况、镜像的删除情况情况分别用 docker psdocker images,都删干净了

bash 复制代码
qiming@k8s-master1:~$ docker ps
CONTAINER ID   IMAGE                                               COMMAND                  CREATED       STATUS       PORTS     NAMES
qiming@k8s-master1:~$ docker images
REPOSITORY                                                        TAG               IMAGE ID       CREATED        SIZE
ghcr.io/flannel-io/flannel                                        v0.27.4           e83704a17731   5 weeks ago    91.4MB
ghcr.io/flannel-io/flannel-cni-plugin                             v1.8.0-flannel1   bb28ded63816   5 weeks ago    10.8MB
alpine                                                            3.18              802c91d52981   8 months ago   7.36MB
golang                                                            1.22.12           2fce09cfad57   9 months ago   823MB
registry.aliyuncs.com/google_containers/kube-apiserver            v1.28.2           cdcab12b2dd1   2 years ago    126MB
registry.aliyuncs.com/google_containers/kube-scheduler            v1.28.2           7a5d9d67a13f   2 years ago    60.1MB
registry.aliyuncs.com/google_containers/kube-controller-manager   v1.28.2           55f13c92defb   2 years ago    122MB
registry.aliyuncs.com/google_containers/kube-proxy                v1.28.2           c120fed2beb8   2 years ago    73.1MB
busybox                                                           latest            a416a98b71e2   2 years ago    4.26MB
quay.io/jetstack/cert-manager-webhook                             v1.12.0           5059a762fd74   2 years ago    48.9MB
quay.io/jetstack/cert-manager-controller                          v1.12.0           eb0fa758c994   2 years ago    63.7MB
quay.io/jetstack/cert-manager-cainjector                          v1.12.0           8ec112cada1b   2 years ago    41.6MB
registry.aliyuncs.com/google_containers/etcd                      3.5.9-0           73deb9a3f702   2 years ago    294MB
gcr.io/kubebuilder/kube-rbac-proxy                                v0.14.1           1da78bf35ce7   2 years ago    55.6MB
nginx                                                             1.22.1            0f8498f13f3a   2 years ago    142MB
registry.aliyuncs.com/google_containers/coredns                   v1.10.1           ead0a4a53df8   2 years ago    53.6MB
registry.aliyuncs.com/google_containers/pause                     3.9               e6f181688397   3 years ago    744kB
gcr.io/distroless/static                                          nonroot           ea2dc773e32d   N/A            2.45MB
相关推荐
资深web全栈开发4 小时前
[特殊字符]图解 Golang 反射机制:从底层原理看动态类型的秘密
开发语言·后端·golang
橙色云-智橙协同研发8 小时前
【PLM实施专家宝典】离散制造企业MBD与无纸化制造实施方案:从“图纸驱动”到“数据驱动”的革命
云原生·解决方案·数字化转型·plm·国产plm·专家经验·无纸化
victory04319 小时前
K8S重启之后无法启动故障排查 与 修复
云原生·容器·kubernetes
研究司马懿11 小时前
【ETCD】ETCD常用命令
网络·数据库·云原生·oracle·自动化·运维开发·etcd
java_logo11 小时前
SGLANG Docker容器化部署指南
linux·运维·docker·容器·eureka·1024程序员节
Tony Bai11 小时前
【Go模块构建与依赖管理】09 企业级实践:私有仓库与私有 Proxy
开发语言·后端·golang
Lucky小小吴11 小时前
开源项目5——Go版本快速管理工具
开发语言·golang·开源
Qayrup12 小时前
各个系统的 docker安装
运维·docker·容器
进化中的码农12 小时前
Go中的泛型编程和reflect(反射)
开发语言·笔记·golang