• 日常搜索
  • 百度一下
  • Google
  • 在线工具
  • 搜转载

Docker 从头开始​​:理解镜像

docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的镜像中,然后发布到任何流行的 Linux或Windows操作系统的机器上,也可以实现虚拟化。Docker 容器作为部署和管理云原生分布式系统的最佳实践正在兴起。容器是 Docker 镜像的实例。事实证明,关于镜像有很多东西需要了解和理解。 

Docker 从头开始​​:理解镜像  第1张

在这个分为两部分的教程中,我将深入介绍 Docker 镜像。在这一部分中,我将从基本原则开始,然后我将继续考虑设计和检查镜像内部结构。在第二部分中,我将介绍构建您自己的镜像、故障排除以及使用镜像存储库。 

当你站在另一边时,你将对 Docker 镜像到底是什么以及如何在你自己的应用程序和系统中有效地利用它们有一个深刻的理解。

了解层

Docker 使用后端存储驱动程序管理镜像。有多种受支持的驱动程序,例如 AUFS、BTRFS 和覆盖。镜像由有序层组成。您可以将层视为一组文件系统更改。当您获取所有图层并将它们堆叠在一起时,您会得到一个包含所有累积更改的新镜像。 

订购的部分很重要。如果你在一层添加一个文件并在另一层删除它,你最好按正确的顺序进行。Docker 跟踪每一层。一张图片可以由几十层组成(目前限制为127层)。每一层都很轻。图层的好处是镜像可以共享图层。 

如果你有很多基于相似层的镜像,比如基础操作系统或通用包,那么所有这些通用层将只存储一次,每个镜像的开销将只是该镜像的独特层。

写时复制

当从镜像创建新容器时,所有镜像层都是只读的,并且在顶部添加了一个薄的读写层。对特定容器所做的所有更改都存储在该层中。 

现在,这并不意味着容器不能从其镜像层修改文件。绝对可以。但它会在其顶层创建一个副本,从那时起,任何试图访问该文件的人都将获得顶层副本。当文件或目录从较低层移除时,它们会隐藏起来。原始镜像层由基于内容的加密哈希值标识。容器的读写层由 UUID 标识。

这允许镜像和容器的写时复制策略。Docker 尽可能重复使用相同的项目。只有当一个项目被修改时,Docker 才会创建一个新的副本。

Docker 镜像的设计注意事项

独特的分层组织和写时复制策略促进了一些创建和合成 Docker 镜像的最佳实践。

最小镜像:少即是多

Docker 镜像越小,从稳定性、安全性和加载时间的角度来看,它们就会获得巨大的好处。您可以为生产目的创建非常小的镜像。如果您需要排除故障,您始终可以在容器中安装工具。 

如果您只将数据、日志和其他所有内容写入已安装的卷,那么您可以在主机上使用整个调试和故障排除工具库。我们很快就会看到如何非常小心地控制将哪些文件放入 Docker 映像。

合并图层

层很棒,但有一个限制,并且存在与层相关的开销。太多的层可能会影响容器内的文件系统访问(因为每一层都可能添加或删除了一个文件或目录),并且它们只会弄乱您自己的文件系统。

例如,如果您安装了一堆包,您可以为每个包创建一个层,方法是在 Dockerfile 中将每个包安装在单独的 RUN 命令中:

RUN apt-get update
 
RUN apt-get -y install package_1
 
RUN apt-get -y install package_2
 
RUN apt-get -y install package_3

或者您可以使用单个 RUN 命令将它们组合成一层。

RUN apt-get update && \
 
    apt-get -y install package_1 && \
 
    apt-get -y install package_2 && \
 
    apt-get -y install package_3

选择基本镜像

您的基础镜像(实际上没有人从头开始构建镜像)通常是一个重大决定。它可能包含很多层并添加很多功能,但也有很多重量。图片的质量和作者也很关键。您不希望您的镜像基于一些不稳定的镜像,您不确定其中到底包含什么以及您是否可以信任作者。

许多发行版、编程语言数据库和运行时环境都有官方镜像。有时选择是压倒性的。慢慢来,做出明智的选择。

检查镜像

让我们看一些图片。这是我的机器上当前可用的镜像列表:

REPOSITORY      TAG    IMAGE ID     CREATED      SIZE
python          latest 775dae9b960e 12 days ago  687 MB
d4w/nsenter     latest 9e4f13a0901e 4 months ago 83.8 kB
ubuntu-with-ssh latest 87391dca396d 4 months ago 221 MB
ubuntu          latest bd3d4369aebc 5 months ago 127 MB
hello-world     latest c54a2cc56cbb 7 months ago 1.85 kB
alpine          latest 4e38e38c8ce0 7 months ago 4.8 MB
nsqio/nsq       latest 2a82c70fe5e3 8 months ago 70.7 MB

存储库和标签为人类识别镜像。如果您只是尝试使用存储库名称运行或拉取而不指定标签,则默认使用“最新”标签。镜像 ID 是唯一标识符。

让我们深入研究一下 hello-world 镜像:

> docker inspect hello-world
[
    {
        "Id": "sha256:c54a2cc56cbb2f...e7e2720f70976c4b75237dc",
        "RepoTags": [
            "hello-world:latest"
        ],
        "RepoDigests": [
            "hello-world@sha256:0256e8a3...411de4cdcf9431a1feb60fd9"
        ],
        "Parent": "",
        "Comment": "",
        "Created": "2016-07-01T19:39:27.532838486Z",
        "Container": "562cadb4d17bbf30b58a...bf637f1d2d7f8afbef666",
        "ContainerConfig": {
            "Hostname": "c65bc554a4b7",
            "domainname": "",
            "User": "",
            "AttachStdin": false,
            "AttachStdout": false,
            "AttachStderr": false,
            "Tty": false,
            "OpenStdin": false,
            "StdinOnce": false,
            "Env": [
                "PATH=/usr/bin:/usr/sbin:/usr/bin:/sbin:/bin"
            ],
            "Cmd": [
                "/bin/sh",
                "-c",
                "#(nop) CMD [\"/hello\"]"
            ],
            "Image": "sha256:0f9bb7da10de694...5ab0fe537ce1cd831e",
            "Volumes": null,
            "WorkingDir": "",
            "Entrypoint": null,
            "OnBuild": null,
            "Labels": {}
        },
        "DockerVersion": "1.10.3",
        "Author": "",
        "Config": {
            "Hostname": "c65bc554a4b7",
            "Domainname": "",
            "User": "",
            "AttachStdin": false,
            "AttachStdout": false,
            "AttachStderr": false,
            "Tty": false,
            "OpenStdin": false,
            "StdinOnce": false,
            "Env": [
                "PATH=/usr/sbin:/usr/bin:/sbin:/bin"
            ],
            "Cmd": [
                "/hello"
            ],
            "Image": "sha256:0f9bb7da10de694b...b0fe537ce1cd831e",
            "Volumes": null,
            "WorkingDir": "",
            "Entrypoint": null,
            "OnBuild": null,
            "Labels": {}
        },
        "Architecture": "amd64",
        "Os": "linux",
        "Size": 1848,
        "VirtualSize": 1848,
        "GraphDriver": {
            "Name": "aufs",
            "data": null
        },
        "RootFS": {
            "Type": "layers",
            "Layers": [
                "sha256:a02596fdd012f22b03a...079c3e8cebceb4262d7"
            ]
        }
    }
]

查看每个镜像关联了多少信息很有趣。我不会详细介绍每一项。我将仅提及一个有趣的花絮,即“container”和“containerConfig”条目用于 Docker 在构建映像时创建的临时容器。在这里,我想重点介绍“RootFS”的最后一节。您可以使用 inspect 命令的 Go 模板支持来获取这一部分:

> docker inspect -f '{{.RootFS}}' hello-world
{layers [sha256:a02596fdd012f22b03af6a...8357b079c3e8cebceb4262d7] }

它有效,但我们失去了漂亮的格式。我更喜欢使用jq:

> docker inspect hello-world | jq .[0].RootFS
{
  "Type": "layers",
  "Layers": [
    "sha256:a02596fdd012f22b03af6a...7507558357b079c3e8cebceb4262d7"
  ]
}

可以看到type是“Layers”,只有一层。 

让我们检查 Python 镜像的层:

> docker inspect python | jq .[0].RootFS
{
  "Type": "layers",
  "Layers": [
    "sha256:a2ae92ffcd29f7ede...e681696874932db7aee2c",
    "sha256:0eb22bfb707db44a8...8f04be511aba313bdc090",
    "sha256:30339f20ced009fc3...6b2a44b0d39098e2e2c40",
    "sha256:f55f65539fab084d4...52932c7d4924c9bfa6c9e",
    "sha256:311f330fa783aef35...e8283e06fc1975a47002d",
    "sha256:f250d46b2c81bf76c...365f67bdb014e98698823",
    "sha256:1d3d54954c0941a8f...8992c3363197536aa291a"
  ]
}

哇。七层。但是那些层是什么?我们可以使用 history 命令来弄清楚:

IMAGE        CREATED     CREATED BY                             SIZE 
775dae9b960e 12 days ago /bin/sh -c #(nop)  CMD ["python3"]     0 B
<missing>    12 days ago /bin/sh -c cd /usr/local/bin  && { ... 48 B
<missing>    12 days ago /bin/sh -c set -ex  && buildDeps=' ... 66.9 MB
<missing>    12 days ago /bin/sh -c #(nop)  ENV PYTHON_PIP_V... 0 B
<missing>    12 days ago /bin/sh -c #(nop)  ENV PYTHON_VERSI...   0 B
<missing>    12 days ago /bin/sh -c #(nop)  ENV GPG_KEY=0D96... 0 B
<missing>    12 days ago /bin/sh -c apt-get update && apt-ge... 7.75 MB
<missing>    12 days ago /bin/sh -c #(nop)  ENV.UTF-8    0 B
<missing>    12 days ago /bin/sh -c #(nop)  ENV PATH=/usr/lo... 0 B
<missing>    13 days ago /bin/sh -c apt-get update && apt-ge... 323 MB
<missing>    13 days ago /bin/sh -c apt-get update && apt-ge... 123 MB
<missing>    13 days ago /bin/sh -c apt-get update && apt-ge... 44.3 MB
<missing>    13 days ago /bin/sh -c #(nop)  CMD ["/bin/bash"... 0 B
<missing>    13 days ago /bin/sh -c #(nop) ADD file:89ecb642... 123 MB

好的。不要惊慌。什么都不缺。这只是一个糟糕的用户界面。在 Docker 1.10 之前,层曾经有一个镜像 ID,但现在没有了。顶层的 ID 并不是该层的真正 ID。它是 Python 镜像的 ID。“CREATED BY”被截断了,但如果你通过,你可以看到完整的命令--no-trunc。由于需要极端换行的页面宽度限制,我将从这里的输出中拯救你。

你如何获得镜像?有以下三种方式:

  • 拉动/运行

  • 加载

  • 建造

当你运行一个容器时,你指定它的镜像。如果您的系统上不存在该映像,则它是从 Docker 注册表(默认情况下为 DockerHub)中提取的。或者,您可以直接拉取而不运行容器。

您还可以加载某人作为 tar 文件发送给您的镜像。Docker 原生支持它。

最后,也是最有趣的是,您可以构建自己的镜像,这是第二部分的主题。

结论

Docker 镜像基于分层文件系统,为容器设计的用例提供了许多优势和好处,例如轻量级和共享公共部分,因此可以经济地在同一台机器上部署和运行多个容器。 

但是有一些陷阱,您需要了解有效利用 Docker 镜像的原理和机制。Docker 提供了几个命令来了解可用的镜像及其结构。

这篇文章是名为docker from the Ground Up的系列文章的一部分

Docker 从头开始:创建镜像

文章目录
  • 了解层
    • 写时复制
  • Docker 镜像的设计注意事项
    • 最小镜像:少即是多
    • 合并图层
    • 选择基本镜像
  • 检查镜像
  • 结论
  • 发表评论