Docker学习:Image的本地存储结构

写在前面

在使用Docker时候,针对镜像的操做通常就是docker pulldocker builddocker commit(刚开始接触Docker的时候,还不会Dockerfile,常用这个命令,可是经历了一次血的教训,我已经放弃这个命令好久)这些操做,大概都知道Images在Docker中是由无数个Layer组成,可是,Image在本地是如何存储的?上述操做又会对本地存储带来怎样的变化?抱着学习的态度,我从刚刚安装完docker开始,一步一步研究docker image的目录结构和含义。
本人也只是docker初学者,写文章的目的也是但愿本身不只仅停留在会使用docker的阶段,还可以边用边学边总结,一方面加深本身的理解,另外一方面但愿经过这种方式与一块儿学习Docker的童鞋们交流。若有错误,欢迎批评指正,谢谢。python

背景:Image 大小没法压缩引出的问题

之前基本都在本地服务器上使用Dockerfile构建镜像,通常来讲磁盘的空间都是足够的,并且基本不须要docker save,应用场景也不存在频发启动容器的状况,因此无论是空间仍是效率的角度,都没有刻意去压缩构建出来的镜像大小。可是,最近由于须要在VPS上构建,可用的空间严重受限,所以,以为重写Dockerfile来压缩镜像大小。本觉得应该是一件很简单的事情,果真太年轻。直接从dockerfile提及:mysql

FROM alpine
........
RUN apk -U upgrade && \
    apk -v add --no-cache bash curl && \
    apk -v add --no-cache --virtual .build-deps gcc make && \
    apk -v add --no-cache mysql-client libc-dev mariadb-dev && \
    rm -rf /var/cache/apk/*
COPY ./startService.sh /
........
RUN make clean && make && make install && \
    apk del .build-deps
    
CMD ["/bin/bash", "/startService.sh"]

实验发现上面的写法,apk del .build-deps这一句加不加,大小都是同样的,也就是说彻底没有像预期的同样,卸载环境就能够压缩大小。一通googole,问题获得了解决,你们给出来的缘由基本能够总结为:“Image是由多个Layer组成的,后面的Layer没办法修改前面的Layer”,修改一下dockerfile就能解决:linux

FROM alpine
........
RUN apk -U upgrade && \
    apk -v add --no-cache bash curl && \
    apk -v add --no-cache mysql-client libc-dev mariadb-dev && \
    rm -rf /var/cache/apk/*
COPY ./startService.sh /
........
RUN apk -v add --no-cache --virtual .build-deps gcc make && \
    make clean && make && make install && \
    apk del .build-deps
    
CMD ["/bin/bash", "/startService.sh"]

问题确实解决了,也大概能体会到在dockerfile中,最好把中间过程写在一块儿,减小Layer,可是,为何会这样?很明显,从最开始的错误理解和如今的不理解,都是由于对Image的实现原理不清楚,因此,决定从Image本地目录结构的角度来分析和理解。若是只想看结论,直接跳到最后吧。git

环境

  • Centos 7.4
  • Docker 18.09.0

由于不一样的Docker版本,目录结构有一些差别,下面的操做都是针对V18.09.0,而不一样的操做系统会影响默认的存储方式等,这里使用的是Centos 7.4
接下来的内容,首先根据最初始的Docker环境,拉去一个alpine镜像分析本地目录结构,以及每个目录或文件的含义;而后基于alpine镜像,从dockerfile中构建一个简单的test-image镜像,完成构建以后进一步分析和验证目录或文件的含义,并分析Image和Layer的关联关系在本地文件系统是如何实现关联的。github

从最简单的docker pull alpine了解本地目录结构

通常默认安装启动Docker,全部相关的文件都会存储在/var/lib/docker下面,可使用tree /var/lib/docker 查看目录结构,而与Image相关的目录主要包括两个:imageoverlay2,须要注意overlay2,是存储驱动,不一样的操做系统和docker版本可能不太一致,因此在查看目录的时候要结合本身的环境:sql

/var/lib/docker
├── builder
│   └── fscache.db
├── buildkit
│   ├── cache.db
│   ├── content
│   │   └── ingest
│   ├── executor
│   ├── metadata.db
│   └── snapshots.db
├── containerd
│   └── daemon
│       ├── ........
│       └── tmpmounts
├── containers
├── image
│   └── overlay2
│       ├── distribution
│       ├── imagedb
│       │   ├── content
│       │   │   └── sha256
│       │   └── metadata
│       │       └── sha256
│       ├── layerdb
│       └── repositories.json
├── network
│   └── files
│       └── local-kv.db
├── overlay2
│   ├── backingFsBlockDev
│   └── l
├── plugins
│   ├── storage
│   │   └── blobs
│   │       └── tmp
│   └── tmp
├── runtimes
├── swarm
├── tmp
├── trust
└── volumes
    └── metadata.db

由于上面是刚安装完的状态,并无pull或者build任何镜像,因此目前image目录下只有一些默认的文件或者目录,并且文件和目录也没有存什么有用的信息。如今咱们使用docker pull alpine获取一个最简单的镜像。docker

[root@docker-learn docker]# docker pull alpine
Using default tag: latest
latest: Pulling from library/alpine
cd784148e348: Pull complete 
Digest: sha256:46e71df1e5191ab8b8034c5189e325258ec44ea739bba1e5645cff83c9048ff1
Status: Downloaded newer image for alpine:latest

上面拉去过程只会产生一个Layer,咱们能够经过docker images --digests命令查看拉取的镜像,注意Image ID和digest的区别。json

[root@docker-learn docker]# docker images --digests
REPOSITORY          TAG                 DIGEST                                                                    IMAGE ID            CREATED             SIZE
alpine              latest              sha256:46e71df1e5191ab8b8034c5189e325258ec44ea739bba1e5645cff83c9048ff1   3f53bb00af94        8 days ago          4.41MB

此时,咱们能够再看文件系统的变化,为了方便,只展现image目录:bash

image/
└── overlay2
    ├── distribution
    │   ├── diffid-by-digest
    │   │   └── sha256
    │   │       └── cd784148e3483c2c86c50a48e535302ab0288bebd587accf40b714fffd0646b3
    │   └── v2metadata-by-diffid
    │       └── sha256
    │           └── 7bff100f35cb359a368537bb07829b055fe8e0b1cb01085a3a628ae9c187c7b8
    ├── imagedb
    │   ├── content
    │   │   └── sha256
    │   │       └── 3f53bb00af943dfdf815650be70c0fa7b426e56a66f5e3362b47a129d57d5991
    │   └── metadata
    │       └── sha256
    ├── layerdb
    │   ├── sha256
    │   │   └── 7bff100f35cb359a368537bb07829b055fe8e0b1cb01085a3a628ae9c187c7b8
    │   │       ├── cache-id
    │   │       ├── diff
    │   │       ├── size
    │   │       └── tar-split.json.gz
    │   └── tmp
    └── repositories.json

repositories.json

这个文件存储了本地的全部images列表,里面目前包含了两个,"alpine:latest""alpine@sha256:46e71df1e5191ab8b8034c5189e325258ec44ea739bba1e5645cff83c9048ff1",其实这两个是同一个镜像,你能够在刚刚docker images --digests看到,前者是tag,后者是digest(docker inspect 3f53bb00af94也能够看到相同的效果)。服务器

{
    "Repositories": {
        "alpine": {
            "alpine:latest": "sha256:3f53bb00af943dfdf815650be70c0fa7b426e56a66f5e3362b47a129d57d5991",
            "alpine@sha256:46e71df1e5191ab8b8034c5189e325258ec44ea739bba1e5645cff83c9048ff1": "sha256:3f53bb00af943dfdf815650be70c0fa7b426e56a66f5e3362b47a129d57d5991"
        }
    }
}

imagedb目录

imagedb/
├── content
│   └── sha256
│       └── 3f53bb00af9......
└── metadata
    └── sha256

该目录存储了镜像的相关信息,每一个镜像的内容都包含在本身的目录下,目录名即为该镜像的Image ID
首先是metadata目录,该目录保存每一个镜像的parent镜像ID,由于这里的alpine:lasted镜像没有更上层的镜像,因此目录为空,后续咱们使用docker build构建一个镜像,再进一步分析。
其次是content目录,该目录下存储了镜像的JSON格式描述信息:

{
    "architecture": "amd64",
    "config": {
        "ArgsEscaped": true,
        "AttachStderr": false,
        "AttachStdin": false,
        "AttachStdout": false,
        "Cmd": [
            "/bin/sh"
        ],
        "Domainname": "",
        "Entrypoint": null,
        "Env": [
            "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
        ],
        "Hostname": "",
        "Image": "sha256:49573004c44f9413c7db63cbab336356e7a8843139fca5e68f92d84a56f0e6df",
        "Labels": null,
        "OnBuild": null,
        "OpenStdin": false,
        "StdinOnce": false,
        "Tty": false,
        "User": "",
        "Volumes": null,
        "WorkingDir": ""
    },
    "container": "c44d11fa67899a984d66f5542092b474f11ca95cc9b03b1470546f16ec8ce74f",
    "container_config": {
        "ArgsEscaped": true,
        "AttachStderr": false,
        "AttachStdin": false,
        "AttachStdout": false,
        "Cmd": [
            "/bin/sh",
            "-c",
            "#(nop) ",
            "CMD [\"/bin/sh\"]"
        ],
        "Domainname": "",
        "Entrypoint": null,
        "Env": [
            "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
        ],
        "Hostname": "c44d11fa6789",
        "Image": "sha256:49573004c44f9413c7db63cbab336356e7a8843139fca5e68f92d84a56f0e6df",
        "Labels": {},
        "OnBuild": null,
        "OpenStdin": false,
        "StdinOnce": false,
        "Tty": false,
        "User": "",
        "Volumes": null,
        "WorkingDir": ""
    },
    "created": "2018-12-21T00:21:30.122610396Z",
    "docker_version": "18.06.1-ce",
    "history": [
        {
            "created": "2018-12-21T00:21:29.97055571Z",
            "created_by": "/bin/sh -c #(nop) ADD file:2ff00caea4e83dfade726ca47e3c795a1e9acb8ac24e392785c474ecf9a621f2 in / "
        },
        {
            "created": "2018-12-21T00:21:30.122610396Z",
            "created_by": "/bin/sh -c #(nop)  CMD [\"/bin/sh\"]",
            "empty_layer": true
        }
    ],
    "os": "linux",
    "rootfs": {
        "diff_ids": [
            "sha256:7bff100f35cb359a368537bb07829b055fe8e0b1cb01085a3a628ae9c187c7b8"
        ],
        "type": "layers"
    }
}

解释如下主要的几个部分:

  • config: 将来根据这个image启动container时,config里面的配置就是运行container时的默认参数。
  • container: 此处为一个容器ID,通常咱们执行docker build构建镜像时,能够看见是不断地生成新的container,而后提交为新的image,此处的容器ID即生成该镜像时临时容器的ID,后面经过docker build构建镜像会进一步验证。
  • container_config:上述临时容器的配置,能够对比containner_configconfig的内容,字段彻底一致,验证了config的做用。
  • history:构建该镜像的全部历史命令
  • rootfs:该镜像包含的layer层的diff id。

layerdb目录

imagedb目录同样,根据命名便可理解该目录主要用来存储Docker的Layer信息,在只有一个alpine:lasted镜像的状况下,目录结构以下:

layerdb/
├── sha256
│   └── 7bff100f35cb359a368537bb07829b055fe8e0b1cb01085a3a628ae9c187c7b8
│       ├── cache-id
│       ├── diff
│       ├── size
│       └── tar-split.json.gz
└── tmp

在咱们docker pull alpine:lasted的时候,能够发现只pull了一层,而在上面imagedb/content中的镜像信息中,rootfs中也只有一个diff,所以,与此处的一个Layer层吻合。可是,须要注意此处的 7bff100f35... 与rootfs中的diff_id 7bff100f35...虽然值同样,可是含义并不相同,此处标识Layer的Chain ID,之因此此处一致,是由于在只有一层Layer,没有parent时,diff id与chain id相等,后面咱们构建test-image后再来分析便可看出区别。
在改Layer的目录下,包含四个文件:

  • diff:该Layer层的diff id

    [root@docker-learn overlay2]# cat layerdb/sha256/7bff100f35cb359a368537bb07829b055fe8e0b1cb01085a3a628ae9c187c7b8/diff
    sha256:7bff100f35cb359a368537bb07829b055fe8e0b1cb01085a3a628ae9c187c7b8

    如上面所述,最底层的Layer具备相同的chain id 和 diff id

  • size:该Layer的大小,单位字节

    [root@docker-learn overlay2]# cat layerdb/sha256/7bff100f35cb359a368537bb07829b055fe8e0b1cb01085a3a628ae9c187c7b8/size
    4413428

    在docker images中,咱们能够看到alpine镜像的大小为4.41MB,将此处的大小进行换算 4413428/(1024*1024),发现大小不一致,第一反应是Image相对于Layer还增长了其余信息,可是理论上彷佛没法解释,因而使用docker inspect alpine查看了镜像的具体信息,发现其中Size: 4413428,与该处数值一直,那么4.41M应该是4413428/(1000000)计算得来,后面咱们会使用test-image镜像进一步验证。

  • tar-split.json.gz:layer层数据tar压缩包的split文件

    该文件生成须要依赖tar-split,经过这个文件能够还原layer的tar包。

  • cache_id:内容为一个uuid,指向Layer本地的真正存储位置。

    [root@docker-learn layerdb]# cat sha256/7bff100f35cb359a368537bb07829b055fe8e0b1cb01085a3a628ae9c187c7b8/cache-id 
    281c53a74496be2bfcf921ceee5ec2d95139c43fec165ab667a77899b3691d56

    那么Layer本地真正的存储位置又在何处呢?即是上面提到的/var/lib/docker/overlay2目录下:

    [root@docker-learn overlay2]# ls
    281c53a74496be2bfcf921ceee5ec2d95139c43fec165ab667a77899b3691d56  backingFsBlockDev  l

须要注意,layerdb目录下除了diff、size、cache_id和tar-split.json.gz文件,还应该包括一个parent文件,文件存储了当前Layer层的父层chain_id,由于当前alpine镜像只有一层,因此也就没有parent。

distribution目录

该目录包含了Layer层diif id和digest之间的对应关系。

[root@docker-learn overlay2]# tree distribution/
distribution/
├── diffid-by-digest
│   └── sha256
│       └── cd784148e3483c2c86c50a48e535302ab0288bebd587accf40b714fffd0646b3
└── v2metadata-by-diffid
    └── sha256
        └── 7bff100f35cb359a368537bb07829b055fe8e0b1cb01085a3a628ae9c187c7b8

4 directories, 2 files

v2metadata-by-diffid目录下,咱们能够经过Layer的diff id找到对应的digest,而且包含了生成该digest的源仓库。

[
    {
        "Digest": "sha256:cd784148e3483c2c86c50a48e535302ab0288bebd587accf40b714fffd0646b3",
        "HMAC": "",
        "SourceRepository": "docker.io/library/alpine"
    }
]

diffid-by-digest目录则与v2metadata-by-diffid相反

[root@docker-learn overlay2]# cat distribution/diffid-by-digest/sha256/cd784148e3483c2c86c50a48e535302ab0288bebd587accf40b714fffd0646b3 
sha256:7bff100f35cb359a368537bb07829b055fe8e0b1cb01085a3a628ae9c187c7b8

到这里为止,基于最简单的alpine镜像,咱们看到了Image的本地目录结构,以及每个目录或文件大概的做用。可是,由于该镜像只有一层,不少关联关系并无很好的体现,接下来用一个稍微复杂的镜像再过一遍上述过程。

基于docker build test-image进一步理解目录结构

一个简单的dockerfile构建test-image

FROM alpine

LABEL name="test-image"

RUN apk -v add --no-cache bash 
RUN apk -v add --no-cache curl
COPY ./startService.sh /

CMD ["/bin/bash", "/startService.sh"]

构建过程输出以下:

[root@docker-learn docker]# docker build -t test-image .
Sending build context to Docker daemon  3.072kB
Step 1/6 : FROM alpine
 ---> 3f53bb00af94
Step 2/6 : LABEL name="test-image"
 ---> Running in 3bd6320fc291
Removing intermediate container 3bd6320fc291
 ---> bb97dd1fb1a1
Step 3/6 : RUN apk -v add --no-cache bash
 ---> Running in f9987ff57ad7
fetch http://dl-cdn.alpinelinux.org/alpine/v3.8/main/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.8/community/x86_64/APKINDEX.tar.gz
(1/5) Installing ncurses-terminfo-base (6.1_p20180818-r1)
(2/5) Installing ncurses-terminfo (6.1_p20180818-r1)
(3/5) Installing ncurses-libs (6.1_p20180818-r1)
(4/5) Installing readline (7.0.003-r0)
(5/5) Installing bash (4.4.19-r1)
Executing bash-4.4.19-r1.post-install
Executing busybox-1.28.4-r2.trigger
OK: 18 packages, 136 dirs, 2877 files, 13 MiB
Removing intermediate container f9987ff57ad7
 ---> a5635f1b1d00
Step 4/6 : RUN apk -v add --no-cache curl
 ---> Running in c49fb2e4b311
fetch http://dl-cdn.alpinelinux.org/alpine/v3.8/main/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.8/community/x86_64/APKINDEX.tar.gz
(1/5) Installing ca-certificates (20171114-r3)
(2/5) Installing nghttp2-libs (1.32.0-r0)
(3/5) Installing libssh2 (1.8.0-r3)
(4/5) Installing libcurl (7.61.1-r1)
(5/5) Installing curl (7.61.1-r1)
Executing busybox-1.28.4-r2.trigger
Executing ca-certificates-20171114-r3.trigger
OK: 23 packages, 141 dirs, 3040 files, 15 MiB
Removing intermediate container c49fb2e4b311
 ---> 9156d1521a2f
Step 5/6 : COPY ./startService.sh /
 ---> 704626646baf
Step 6/6 : CMD ["/bin/bash", "/startService.sh"]
 ---> Running in 1c5e6e861264
Removing intermediate container 1c5e6e861264
 ---> 6cd0a66e83f1
Successfully built 6cd0a66e83f1
Successfully tagged test-image:latest

镜像build过程能够理解为基于一个镜像启动一个容器,在容器内执行Dockerfile里的一条命令,生成一个新的镜像。根据上述的输入,test-image的构建过程能够表示为:

test-image构建过程

最终生成的test-image镜像ID为 6cd0a66e83f1,咱们从该镜像开始,再一次分析本地目录。首先查看镜像的基本信息:

[root@docker-learn docker]# docker images --digests test-image
REPOSITORY          TAG                 DIGEST              IMAGE ID            CREATED             SIZE
test-image          latest              <none>              6cd0a66e83f1        About an hour ago   9.88MB

如前面所述,digest是有docker repository生成,由于本地构建完以后并无推送至远程仓库,因此为None。此时,image目录发生了以下变化:

image/
└── overlay2
    ├── distribution
    │   ├── diffid-by-digest
    │   │   └── sha256
    │   │       └── cd784148e3483c2c86c50a48e535302ab0288bebd587accf40b714fffd0646b3
    │   └── v2metadata-by-diffid
    │       └── sha256
    │           └── 7bff100f35cb359a368537bb07829b055fe8e0b1cb01085a3a628ae9c187c7b8
    ├── imagedb
    │   ├── content
    │   │   └── sha256
    │   │       ├── 3f53bb00af943dfdf815650be70c0fa7b426e56a66f5e3362b47a129d57d5991
    │   │       ├── 6cd0a66e83f133a2bad37103ed03f6480330fa3c469368eb5871320996d3b924
    │   │       ├── 704626646baf8bdea82da237819cded076a0852eb97dba2fc731569dd85ae836
    │   │       ├── 9156d1521a2fd50d972e1e1abc30d37df7c8e8f7825ca5955170f3b5441b3341
    │   │       ├── a5635f1b1d0078cd926f21ef3ed77b357aa899ac0c8bf80cae51c37129167e3a
    │   │       └── bb97dd1fb1a10b717655594950efb4605ff0d3f2f631feafc4558836c2b34c3c
    │   └── metadata
    │       └── sha256
    │           ├── 6cd0a66e83f133a2bad37103ed03f6480330fa3c469368eb5871320996d3b924
    │           │   ├── lastUpdated
    │           │   └── parent
    │           ├── 704626646baf8bdea82da237819cded076a0852eb97dba2fc731569dd85ae836
    │           │   └── parent
    │           ├── 9156d1521a2fd50d972e1e1abc30d37df7c8e8f7825ca5955170f3b5441b3341
    │           │   └── parent
    │           ├── a5635f1b1d0078cd926f21ef3ed77b357aa899ac0c8bf80cae51c37129167e3a
    │           │   └── parent
    │           └── bb97dd1fb1a10b717655594950efb4605ff0d3f2f631feafc4558836c2b34c3c
    │               └── parent
    ├── layerdb
    │   ├── mounts
    │   ├── sha256
    │   │   ├── 0e88764cdf90e8a5d6597b2d8e65b8f70e7b62982b0aee934195b54600320d47
    │   │   │   ├── cache-id
    │   │   │   ├── diff
    │   │   │   ├── parent
    │   │   │   ├── size
    │   │   │   └── tar-split.json.gz
    │   │   ├── 7bff100f35cb359a368537bb07829b055fe8e0b1cb01085a3a628ae9c187c7b8
    │   │   │   ├── cache-id
    │   │   │   ├── diff
    │   │   │   ├── size
    │   │   │   └── tar-split.json.gz
    │   │   ├── 80fe1abae43103e3be54ac2813114d1dea6fc91454a3369104b8dd6e2b1363f5
    │   │   │   ├── cache-id
    │   │   │   ├── diff
    │   │   │   ├── parent
    │   │   │   ├── size
    │   │   │   └── tar-split.json.gz
    │   │   └── db7c15c2f03f63a658285a55edc0a0012ccd0033f4695d4b428b1b464637e655
    │   │       ├── cache-id
    │   │       ├── diff
    │   │       ├── parent
    │   │       ├── size
    │   │       └── tar-split.json.gz
    │   └── tmp
    └── repositories.json

能够看到,相比于只有alpine镜像的时候,在imagedb的content和metadata下,增长了build过程当中产生的镜像(镜像ID可以一一对应),在layerdb下增长了三个Layer,目前还看不出来关联关系,后续分析。

repositories.json

[root@docker-learn docker]# cat image/overlay2/repositories.json | python -m json.tool
{
    "Repositories": {
        "alpine": {
            "alpine:latest": "sha256:3f53bb00af943dfdf815650be70c0fa7b426e56a66f5e3362b47a129d57d5991",
            "alpine@sha256:46e71df1e5191ab8b8034c5189e325258ec44ea739bba1e5645cff83c9048ff1": "sha256:3f53bb00af943dfdf815650be70c0fa7b426e56a66f5e3362b47a129d57d5991"
        },
        "test-image": {
            "test-image:latest": "sha256:6cd0a66e83f133a2bad37103ed03f6480330fa3c469368eb5871320996d3b924"
        }
    }
}

能够看见,增长了test-image这一项,包含其tag和id。

imagedb目录

[root@docker-learn overlay2]# tree imagedb/
imagedb/
├── content
│   └── sha256
│       ├── 3f53bb00af943dfdf815650be70c0fa7b426e56a66f5e3362b47a129d57d5991
│       ├── 6cd0a66e83f133a2bad37103ed03f6480330fa3c469368eb5871320996d3b924
│       ├── 704626646baf8bdea82da237819cded076a0852eb97dba2fc731569dd85ae836
│       ├── 9156d1521a2fd50d972e1e1abc30d37df7c8e8f7825ca5955170f3b5441b3341
│       ├── a5635f1b1d0078cd926f21ef3ed77b357aa899ac0c8bf80cae51c37129167e3a
│       └── bb97dd1fb1a10b717655594950efb4605ff0d3f2f631feafc4558836c2b34c3c
└── metadata
    └── sha256
        ├── 6cd0a66e83f133a2bad37103ed03f6480330fa3c469368eb5871320996d3b924
        │   ├── lastUpdated
        │   └── parent
        ├── 704626646baf8bdea82da237819cded076a0852eb97dba2fc731569dd85ae836
        │   └── parent
        ├── 9156d1521a2fd50d972e1e1abc30d37df7c8e8f7825ca5955170f3b5441b3341
        │   └── parent
        ├── a5635f1b1d0078cd926f21ef3ed77b357aa899ac0c8bf80cae51c37129167e3a
        │   └── parent
        └── bb97dd1fb1a10b717655594950efb4605ff0d3f2f631feafc4558836c2b34c3c
            └── parent

9 directories, 12 files

在构建test-image以前,metadata目录为空,由于alpine没有parent,在build以后,新增了5个目录,分别对应docker build test-image所产生的5个镜像,每一层以上一层为parent,即parent文件中存储了上一层image id。
content目录中,相比于构建test-image以前,也多了上述5个镜像的内容,以test-image为例(目前的最底层镜像),查看其描述信息:

{
    "architecture": "amd64",
    "config": {
        "ArgsEscaped": true,
        "AttachStderr": false,
        "AttachStdin": false,
        "AttachStdout": false,
        "Cmd": [
            "/bin/bash",
            "/startService.sh"
        ],
        "Domainname": "",
        "Entrypoint": null,
        "Env": [
            "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
        ],
        "Hostname": "",
        "Image": "sha256:704626646baf8bdea82da237819cded076a0852eb97dba2fc731569dd85ae836",
        "Labels": {
            "name": "test-image"
        },
        "OnBuild": null,
        "OpenStdin": false,
        "StdinOnce": false,
        "Tty": false,
        "User": "",
        "Volumes": null,
        "WorkingDir": ""
    },
    "container": "1c5e6e861264654f79a190eba5157dd4dedce59ab3de098a3625fb4e5b6f1d98",
    "container_config": {
        "ArgsEscaped": true,
        "AttachStderr": false,
        "AttachStdin": false,
        "AttachStdout": false,
        "Cmd": [
            "/bin/sh",
            "-c",
            "#(nop) ",
            "CMD [\"/bin/bash\" \"/startService.sh\"]"
        ],
        "Domainname": "",
        "Entrypoint": null,
        "Env": [
            "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
        ],
        "Hostname": "1c5e6e861264",
        "Image": "sha256:704626646baf8bdea82da237819cded076a0852eb97dba2fc731569dd85ae836",
        "Labels": {
            "name": "test-image"
        },
        "OnBuild": null,
        "OpenStdin": false,
        "StdinOnce": false,
        "Tty": false,
        "User": "",
        "Volumes": null,
        "WorkingDir": ""
    },
    "created": "2019-01-01T02:29:19.701494089Z",
    "docker_version": "18.09.0",
    "history": [
        {
            "created": "2018-12-21T00:21:29.97055571Z",
            "created_by": "/bin/sh -c #(nop) ADD file:2ff00caea4e83dfade726ca47e3c795a1e9acb8ac24e392785c474ecf9a621f2 in / "
        },
        {
            "created": "2018-12-21T00:21:30.122610396Z",
            "created_by": "/bin/sh -c #(nop)  CMD [\"/bin/sh\"]",
            "empty_layer": true
        },
        {
            "created": "2019-01-01T02:29:06.530296297Z",
            "created_by": "/bin/sh -c #(nop)  LABEL name=test-image",
            "empty_layer": true
        },
        {
            "created": "2019-01-01T02:29:14.182236016Z",
            "created_by": "/bin/sh -c apk -v add --no-cache bash"
        },
        {
            "created": "2019-01-01T02:29:19.327280058Z",
            "created_by": "/bin/sh -c apk -v add --no-cache curl"
        },
        {
            "created": "2019-01-01T02:29:19.549474383Z",
            "created_by": "/bin/sh -c #(nop) COPY file:fff66db7f2d773b25215edcc9d5697d84813835e3b731e5a6afe9a9b9647ecec in / "
        },
        {
            "created": "2019-01-01T02:29:19.701494089Z",
            "created_by": "/bin/sh -c #(nop)  CMD [\"/bin/bash\" \"/startService.sh\"]",
            "empty_layer": true
        }
    ],
    "os": "linux",
    "rootfs": {
        "diff_ids": [
            "sha256:7bff100f35cb359a368537bb07829b055fe8e0b1cb01085a3a628ae9c187c7b8",
            "sha256:b1ddbff022577cd249a074285a1a7eb76d7c9139132ba5aa4272fc115dfa9e36",
            "sha256:9edc93f4dcf640f272ed73f933863dbefae6719745093d09c6c6908f402b1c34",
            "sha256:a6c8828ba4b58628284f783d3c918ac379ae2aba0830f4c926a330842361ffb6"
        ],
        "type": "layers"
    }
}

主要注意一下几个参数:

  • container: 这里的容器ID能够与图XX中生成该image的容器ID对应上。
  • container_config:容器配置状态,能够看出是执行完dockerfile中命令以后的容器状态。
  • rootfs:镜像包含的Layer的diff id,能够看出test-image镜像包含了四个Layer。当我第一次分析这里的时候,有一点点困惑,在我想象中Dockerfile中的每一条命令对应一个Layer,也就是一个diff id,可是在dockerfile中包含6条命令,这里却只有四层,进一步整理并分析每个image的rootfs,以下图2所示。能够看到,在 LABEL name="test-image"CMD ["/bin/bash", "/startService.sh"]这两行并未产生新的Layer。事实上,若是咱们认为镜像是一个打包的静态OS,那么Layer能够认为是描述该OS的fs变化,即文件系统中文件或者目录发生的改变,很明显上述两行命令并不会引发fs的变化,只是会写入该镜像的config中,在生成容器时读取便可,天然也就不存在diff id。

Image与Layer diif id

至此,解释完了Image相关的目录,总结一下,单个Image的配置信息在content目录中,以image id为文件名存储,Image之间关联信息在metadata中,以parent文件存储。而后,咱们根据image生成容器的时候,但是生成了一个文件系统,可是上述这些信息并不包含fs的数据。由于真正的fs数据是存储在Layer中的。如前面所述,Layer的信息存储在layerdb目录下,因此咱们转战layerdb目录。

layerdb目录

[root@docker-learn overlay2]# tree layerdb/
layerdb/
├── mounts
├── sha256
│   ├── 0e88764cdf90e8a5d6597b2d8e65b8f70e7b62982b0aee934195b54600320d47
│   │   ├── cache-id
│   │   ├── diff
│   │   ├── parent
│   │   ├── size
│   │   └── tar-split.json.gz
│   ├── 7bff100f35cb359a368537bb07829b055fe8e0b1cb01085a3a628ae9c187c7b8
│   │   ├── cache-id
│   │   ├── diff
│   │   ├── size
│   │   └── tar-split.json.gz
│   ├── 80fe1abae43103e3be54ac2813114d1dea6fc91454a3369104b8dd6e2b1363f5
│   │   ├── cache-id
│   │   ├── diff
│   │   ├── parent
│   │   ├── size
│   │   └── tar-split.json.gz
│   └── db7c15c2f03f63a658285a55edc0a0012ccd0033f4695d4b428b1b464637e655
│       ├── cache-id
│       ├── diff
│       ├── parent
│       ├── size
│       └── tar-split.json.gz
└── tmp

相比于只有alpine镜像时,首先,layerdb目录多了一个mounts目录,简单来讲,当由镜像生成容器时,该目录下会生成容器的可读可写两个layer,可读即为由镜像生成,而可写就是将来对容器的修改都会放在着了,由于本文只讨论镜像,这个目录就再也不深刻分析。

其次,目前Layer已经增长至4个,这与咱们上一节中看到的test-image的rootfs配置中有4个layer diif id可以对应上,而后,很显然除了第一层的"7bff100f35cb"能对应上,其余三个彻底不一样。进一步研究才知道这里目录名实际上是layer的chain id,而非diff id,关于这两这个的区别,咱们能够理解为diff id用来描述单个变化,而chain id用来便于一些列的变化,diff id和chain id之间的计算公式能够在image-spec中看到。

ChainID(A) = DiffID(A)
ChainID(A|B) = Digest(ChainID(A) + " " + DiffID(B))
ChainID(A|B|C) = Digest(ChainID(A|B) + " " + DiffID(C))

在这里以test-image的rootfs验证分析他们是如何关联的。

"rootfs": {
    "diff_ids": [
        "sha256:7bff100f35cb359a368537bb07829b055fe8e0b1cb01085a3a628ae9c187c7b8",
        "sha256:b1ddbff022577cd249a074285a1a7eb76d7c9139132ba5aa4272fc115dfa9e36",
        "sha256:9edc93f4dcf640f272ed73f933863dbefae6719745093d09c6c6908f402b1c34",
        "sha256:a6c8828ba4b58628284f783d3c918ac379ae2aba0830f4c926a330842361ffb6"
    ],
    "type": "layers"
}
ChainID(A) = DiffID(A) = sha256:7bff100f35cb359a368537bb07829b055fe8e0b1cb01085a3a628ae9c187c7b8

ChainID(A|B) = Digest(ChainID(A) + " " + DiffID(B))
ChainID(A) = sha256:7bff100f35cb359a368537bb07829b055fe8e0b1cb01085a3a628ae9c187c7b8
DiffID(B) = sha256:b1ddbff022577cd249a074285a1a7eb76d7c9139132ba5aa4272fc115dfa9e36
计算:
[root@docker-learn overlay2]# echo -n "sha256:7bff100f35cb359a368537bb07829b055fe8e0b1cb01085a3a628ae9c187c7b8 sha256:b1ddbff022577cd249a074285a1a7eb76d7c9139132ba5aa4272fc115dfa9e36" | sha256sum -
db7c15c2f03f63a658285a55edc0a0012ccd0033f4695d4b428b1b464637e655  -

结果:
ChainID(A|B) = sha256:db7c15c2f03f63a658285a55edc0a0012ccd0033f4695d4b428b1b464637e655
chainID(A|B|C) = sha256:0e88764cdf90e8a5d6597b2d8e65b8f70e7b62982b0aee934195b54600320d47
chainID(A|B|C|D) = sha256:80fe1abae43103e3be54ac2813114d1dea6fc91454a3369104b8dd6e2b1363f5

![Layer chain id][3]

所以,test-image的第二层Layer对应的目录为:layerdb/sha256/db7c15c2f03f63a658285a55edc0a0012ccd0033f4695d4b428b1b464637e655,查看该Layer的信息:

[root@docker-learn sha256]# ls db7c15c2f03f63a658285a55edc0a0012ccd0033f4695d4b428b1b464637e655/
cache-id  diff  parent  size  tar-split.json.gz

与上一节中相比多了parent,包含了上一层Layer的chain id。

destribution目录

[root@docker-learn overlay2]# tree distribution/
distribution/
├── diffid-by-digest
│   └── sha256
│       └── cd784148e3483c2c86c50a48e535302ab0288bebd587accf40b714fffd0646b3
└── v2metadata-by-diffid
    └── sha256
        └── 7bff100f35cb359a368537bb07829b055fe8e0b1cb01085a3a628ae9c187c7b8

4 directories, 2 files

目前来看,destribution目录和只有alpine镜像时并无什么区别,这是由于digest是由镜像仓库生成,本地构建的镜像在没有push到仓库以前,天然也就没有digest。使用docker push命令将test-image推送至dockerhub。

[root@docker-learn distribution]# docker push backbp/test-image:lasted
The push refers to repository [docker.io/backbp/test-image]
a6c8828ba4b5: Pushed 
9edc93f4dcf6: Pushed 
b1ddbff02257: Pushed 
7bff100f35cb: Mounted from library/alpine 
lasted: digest: sha256:3dc66a43c28ea3e994e4abf6a2d04c7027a9330e8eeab5c609e4971a8c58f0b0 size: 1156

根据过程输出,咱们能够看到虽然test-image镜像包括四层Layer,可是由于最底层的7bff100f35cb原本就是在docker pull alpine时,拉取得来,天然也就不须要再push,所以真正push的只有三层。而如今destribution目录也已经增长了对应Layer的digest,Image的digest能够在上面的过程输出中看到。

distribution/
├── diffid-by-digest
│   └── sha256
│       ├── 2826782ee82560ec5f90a8a9da80880d48dd4036763f5250024fab5b3ef8e8cf
│       ├── 8e905c02e6908fbb0e591cea285470208920d32408735bd6a8fcaf85ffba9089
│       ├── a5bec9983f6902f4901b38735db9c427190ffcb3734c84ee233ea391da81081b
│       └── cd784148e3483c2c86c50a48e535302ab0288bebd587accf40b714fffd0646b3
└── v2metadata-by-diffid
    └── sha256
        ├── 7bff100f35cb359a368537bb07829b055fe8e0b1cb01085a3a628ae9c187c7b8
        ├── 9edc93f4dcf640f272ed73f933863dbefae6719745093d09c6c6908f402b1c34
        ├── a6c8828ba4b58628284f783d3c918ac379ae2aba0830f4c926a330842361ffb6
        └── b1ddbff022577cd249a074285a1a7eb76d7c9139132ba5aa4272fc115dfa9e36

拓展思考

docker tag命令发生了什么?

咱们经过基于test-image生成一个新的tag,test-image-tag

[root@docker-learn overlay2]# docker tag test-image test-image-tag
[root@docker-learn overlay2]# docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
test-image-tag      latest              6cd0a66e83f1        4 hours ago         9.88MB
test-image          latest              6cd0a66e83f1        4 hours ago         9.88MB
alpine              latest              3f53bb00af94        11 days ago         4.41MB

查看repositories.json文件会发现两个tag的镜像都指向同一个image id,因此这个命令至关于只有修改了repositories.json。

[root@docker-learn overlay2]# cat repositories.json | python -m json.tool
{
    "Repositories": {
        "alpine": {
            "alpine:latest": "sha256:3f53bb00af943dfdf815650be70c0fa7b426e56a66f5e3362b47a129d57d5991",
            "alpine@sha256:46e71df1e5191ab8b8034c5189e325258ec44ea739bba1e5645cff83c9048ff1": "sha256:3f53bb00af943dfdf815650be70c0fa7b426e56a66f5e3362b47a129d57d5991"
        },
        "test-image": {
            "test-image:latest": "sha256:6cd0a66e83f133a2bad37103ed03f6480330fa3c469368eb5871320996d3b924"
        },
        "test-image-tag": {
            "test-image-tag:latest": "sha256:6cd0a66e83f133a2bad37103ed03f6480330fa3c469368eb5871320996d3b924"
        }
    }
}

Image Size是如何计算的?

上述的alpine镜像和test-image镜像大小分别是:4.41M和9.88M。为了更准确的分析,可使用docker inspect 镜像查看详细大小,分别为4413428和9876099。
再次查看每层Layer的大小(layerdb/layer chain id/size),分别为

  • Layer1:4413428
  • Layer2:3815516
  • Layer3:1647117
  • Layer4:38

alpine只有一层,因此image size与Layer size相等;test-image有四层,因此image size = sum(Layer1+Layer2+Layer3+Layer4)=9876099

回到最初的问题

完成上面的分析,如今再回头看最初的问题,在apk del .build-deps这一层中,执行完以后生成的Layer只是记录相对于上一层删除了安装包,在计算Image size时(或者说在根据Layer合并Image时),由于安装.build-deps而带来的变换依然存在于app add这一层Layer中,因此大小并不会减少。若是将add和del放在同一个命令内,那么生成的该层Layer记录的是相对于上一层的变化,.build-deps的安装和卸载相对于上一层而言,根本就不存在,因此Layer中也就彻底不存在。
说到底,仍是OCI Image的原理所致,最须要记住的是每个Layer记录的是与上一层Layer相比的变化。

参考

image-spec

相关文章
相关标签/搜索