Docker镜像存储

本次将分析Docker v1.12.3的AUFS镜像存储目录结构。首先来看镜像的存储录/var/lib/docker/image/aufs,目录下主要有:

  • Distribution: 目前还不知道该目录的用途;
  • Imagedb: 镜像数据库;
  • Layerdb: 镜像层数据库;
  • Repositories.json: 镜像管理入口。

为了分析的简便,我们只导入nginx:v1镜像,为了不影响之前的Docker环境,我们从/var/lib/docker1启动Docker,命令如下:

1
dockerd -H 0.0.0.0:4243 -H unix:///var/run/docker.sock -g /var/lib/docker1

Imagedb

Imagedb下有两个子目录:

  • Content: 存储镜像的config文件;
  • Metadata: 里面存有parent信息。

所以,进入/var/lib/docker1/image/aufs/imagedb/content/sha256/后,可以看到镜像的config文件:

1
2
root@fankang:/var/lib/docker1/image/aufs/imagedb/content/sha256# ls
3034d86f1f0b0379092bb99565d72c1dd4a94b41c1e83bb2d0714bb98720ac16

config文件的名字就是镜像的id号。

1
2
3
root@fankang:/var/lib/docker1/image/aufs/imagedb/content/sha256# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx v1 3034d86f1f0b 2 weeks ago 250.3 MB

那么,该config文件是从来的呢。我们可以把镜像的tar包解压:

1
2
3
4
5
6
7
8
root@fankang:/home/fankang/nginx# ls
01a90859e4ca4e3138adbadb3a76be9014b0ea5b89b6ef24988e70e7d7bfe12b 90627bd0fa0408d7488e79525d6b2810987c5ed579f7f649993ca29a1d819a69
0cfdb29ee5612340e908c304954decd7a2c001405abeb475d8e773f66071211b a599739916ea730466ba3a21e5493faf34808cbddc8c3570933048d604cde37d
2a9bb8e69f5a34ead65daecf9ff156975e13c5a29bb27b0c93de879c2787d616 aa1b48ac3dd21b33515f37763a1f4cafce662496c6ed3c13326a818d8669f06a
3034d86f1f0b0379092bb99565d72c1dd4a94b41c1e83bb2d0714bb98720ac16.json e27a3a829a7d9d07bec30f000c722c47e428f9c4cc975dbe2b0dbffda189a8f7
3fbeaf15931df30802b43e1147f6c40d3cbe2839bb91e49cf068e58dadb3ec90 manifest.json
707f1f25f845bd495c21c368a57666f6c4e1b0358868e43e0376fd55512f8734 nginx.tar
77b9c04c054e859ffc5d0bb6d89084dd543ffc2386ffbe17da7bc43147c10d49 repositories

所以该config文件就是镜像中的
3034d86f1f0b0379092bb99565d72c1dd4a94b41c1e83bb2d0714bb98720ac16.json
其中,3034d86f1f0b0379092bb99565d72c1dd4a94b41c1e83bb2d0714bb98720ac16直接可以通过config的内容计算sha256得出。
在config文件中,我们关心的是rootfs字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
"rootfs": {
"diff_ids": [
"sha256:8aa4fcad5eeb286fe9696898d988dc85503c6392d1a2bd9023911fb0d6d27081",
"sha256:25e0901a71b8c6a9df21590604a70517eb7b074071ef6af1033d50037baf3dd5",
"sha256:625c7a2a783b4736cf488efd1fafc41736b9998ec087e20da946c30522ec9ad7",
"sha256:9c42c2077cdea659ac116f148b63ded203d0e83f017accb6d7987377d1363673",
"sha256:a09947e71dc0d591901870f2fd3c18565339b960f6bb32793d14604a85d67393",
"sha256:1f73bd8df68529ff86d8ab3b3455abba4963022300efd4a04d95f1ef3a481dee",
"sha256:fc244f26c293eb5d1356b722a32f71276905297c8786ee1496ce8c903f4025a4",
"sha256:1f6eae413b5cfb474c168aa0e310f5584997f143d2fcea55f20014984c8cd2ca",
"sha256:f17d12637d6ffe31ae5ba3c35efa3ebd672b43ff81767960a65c1982bf7d279d",
"sha256:25d2e7a667f6eadfdc3da7f1f21ab0a6068337a338482f9e587d9bac2d4157ad"
],
"type": "layers"
}

这些diff_id可以由具体镜像层的tar包哈希得到。
在系统中,rootfs的类型为layers,所以我们将进入到layerdb目录的分析。

Layerdb

Layerdb下有如下目录:

  • Mounts: 记录容器的挂载点,即启动目录;
  • Sha256: 存储layer信息;
  • Tmp: 临时目录,存储layer信息用。

在Mounts目录中,有:

  • Init-id: 容器的init目录;
  • Mount-id: 容器的启动目录;
  • Parent: 容器的最后一层的chain-id。

现在以nginx:v1启动一个容器,则在Mounts目录中生成该容器挂载点的信息。

1
2
3
4
5
6
root@fankang:/var/lib/docker1/image/aufs/layerdb/mounts# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
9971a7b3da13 nginx:v1 "/usr/bin/supervisord" 56 seconds ago Up 55 seconds 80/tcp high_goldberg
root@fankang:/var/lib/docker1/image/aufs/layerdb/mounts# ls
9971a7b3da13cc592fc86c7da752cc5b88ec7e7e11a89f12b7376040d1416815

在9971a7b3da13cc592fc86c7da752cc5b88ec7e7e11a89f12b7376040d1416815中:
Init-id: 017e4953d35ca28c875c4c88373be77ddcc499e7d1252188d68decbd2ae1bd68-init
Mount-id: 017e4953d35ca28c875c4c88373be77ddcc499e7d1252188d68decbd2ae1bd68
Parent: sha256:238adf0419800e0628fc8b69d0c9087847754e9155f871d2169920995bc987c6
显然,init-id和mount-id在aufs存储中都能找到对应的目录;parent在layerdb中也能找到目录。

在sha256中有:

1
2
3
4
5
6
root@fankang:/var/lib/docker1/image/aufs/layerdb/sha256# ls
238adf0419800e0628fc8b69d0c9087847754e9155f871d2169920995bc987c6 508ceb742ac26b43bdda819674a5f1d33f7b64c1708e123a33e066cb147e2841
2f72738e9826d32bca9f97513aac63c16d9214e00a18f43b6240bc024910d96e 5270b8fe5160d690ece1e6713d6570c31bd3d2462ffb02046a29e7495d7427cd
364dc483ed8e64e16064dc1ecf3c4a8de82fe7f8ed757978f8b0f9df125d67b3 8aa4fcad5eeb286fe9696898d988dc85503c6392d1a2bd9023911fb0d6d27081
476e873df78db8c0ec59a9759d9a19a66a818d89f5256942da7254bcb211c616 a2982ed568b83721067818425290d6958823ec7bb690b43d412aad6c273dbd23
4f10a8fd56139304ad81be75a6ac056b526236496f8c06b494566010942d8d32 cb5450c7bb149c39829e9ae4a83540c701196754746e547d9439d9cc59afe798

可以发现,sha256中的目录个数(chain-id)和镜像config rootfs中的layer个数一致。接下来将说明如何从镜像的diff_ids计算出chain-id。
先再来看一遍diff_ids:
“sha256:8aa4fcad5eeb286fe9696898d988dc85503c6392d1a2bd9023911fb0d6d27081”,
“sha256:25e0901a71b8c6a9df21590604a70517eb7b074071ef6af1033d50037baf3dd5”,
“sha256:625c7a2a783b4736cf488efd1fafc41736b9998ec087e20da946c30522ec9ad7”,
“sha256:9c42c2077cdea659ac116f148b63ded203d0e83f017accb6d7987377d1363673”,
“sha256:a09947e71dc0d591901870f2fd3c18565339b960f6bb32793d14604a85d67393”,
“sha256:1f73bd8df68529ff86d8ab3b3455abba4963022300efd4a04d95f1ef3a481dee”,
“sha256:fc244f26c293eb5d1356b722a32f71276905297c8786ee1496ce8c903f4025a4”,
“sha256:1f6eae413b5cfb474c168aa0e310f5584997f143d2fcea55f20014984c8cd2ca”,
“sha256:f17d12637d6ffe31ae5ba3c35efa3ebd672b43ff81767960a65c1982bf7d279d”,
“sha256:25d2e7a667f6eadfdc3da7f1f21ab0a6068337a338482f9e587d9bac2d4157ad”
第一个diff_id为”sha256:8aa4fcad5eeb286fe9696898d988dc85503c6392d1a2bd9023911fb0d6d27081”,
则其对应的chain_id为”8aa4fcad5eeb286fe9696898d988dc85503c6392d1a2bd9023911fb0d6d27081”;
第二个diff_id为”sha256:25e0901a71b8c6a9df21590604a70517eb7b074071ef6af1033d50037baf3dd5”,先把该diff_id与前面的chain_id合并:
“sha256:8aa4fcad5eeb286fe9696898d988dc85503c6392d1a2bd9023911fb0d6d27081 sha256:25e0901a71b8c6a9df21590604a70517eb7b074071ef6af1033d50037baf3dd5”
然后哈希得到对应的chain_id: 508ceb742ac26b43bdda819674a5f1d33f7b64c1708e123a33e066cb147e2841。
第三个diff_id为”sha256:625c7a2a783b4736cf488efd1fafc41736b9998ec087e20da946c30522ec9ad7”,先合并:
“sha256:508ceb742ac26b43bdda819674a5f1d33f7b64c1708e123a33e066cb147e2841 sha256:625c7a2a783b4736cf488efd1fafc41736b9998ec087e20da946c30522ec9ad7”
再哈希得:4f10a8fd56139304ad81be75a6ac056b526236496f8c06b494566010942d8d32。
依此类推。
具体如何进行hash见附录。
其计算代码如下,核心是一个递归:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// CreateChainID returns ID for a layerDigest slice
func CreateChainID(dgsts []DiffID) ChainID {
    return createChainIDFromParent("", dgsts...)
}
func createChainIDFromParent(parent ChainID, dgsts ...DiffID) ChainID {
    if len(dgsts) == 0 {
        return parent
    }
    if parent == "" {
        return createChainIDFromParent(ChainID(dgsts[0]), dgsts[1:]...)
    }
    // H = "H(n-1) SHA256(n)"
    dgst := digest.FromBytes([]byte(string(parent) + " " + string(dgsts[0])))
    return createChainIDFromParent(ChainID(dgst), dgsts[1:]...)
}

知道了如何计算chain-id之后,我们来看下layerdb中具体的文件,以8aa4fcad5eeb286fe9696898d988dc85503c6392d1a2bd9023911fb0d6d27081目录为例:

1
2
root@fankang:/var/lib/docker1/image/aufs/layerdb/sha256/8aa4fcad5eeb286fe9696898d988dc85503c6392d1a2bd9023911fb0d6d27081# ls
cache-id diff size tar-split.json.gz

其中:

Cache-id是一个随机生成的值,是layer和AUFS存储的纽带。

再来看来如何计算Diff值,Diff的值即之前提到过的diff_ids的值。
在解压开的镜像中,有manifest.json文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[
{
"Config": "3034d86f1f0b0379092bb99565d72c1dd4a94b41c1e83bb2d0714bb98720ac16.json",
"Layers": [
"707f1f25f845bd495c21c368a57666f6c4e1b0358868e43e0376fd55512f8734/layer.tar",
"3fbeaf15931df30802b43e1147f6c40d3cbe2839bb91e49cf068e58dadb3ec90/layer.tar",
"a599739916ea730466ba3a21e5493faf34808cbddc8c3570933048d604cde37d/layer.tar",
"0cfdb29ee5612340e908c304954decd7a2c001405abeb475d8e773f66071211b/layer.tar",
"aa1b48ac3dd21b33515f37763a1f4cafce662496c6ed3c13326a818d8669f06a/layer.tar",
"90627bd0fa0408d7488e79525d6b2810987c5ed579f7f649993ca29a1d819a69/layer.tar",
"2a9bb8e69f5a34ead65daecf9ff156975e13c5a29bb27b0c93de879c2787d616/layer.tar",
"01a90859e4ca4e3138adbadb3a76be9014b0ea5b89b6ef24988e70e7d7bfe12b/layer.tar",
"e27a3a829a7d9d07bec30f000c722c47e428f9c4cc975dbe2b0dbffda189a8f7/layer.tar",
"77b9c04c054e859ffc5d0bb6d89084dd543ffc2386ffbe17da7bc43147c10d49/layer.tar"
],
"RepoTags": [
"nginx:v1"
]
}
]

其中config字段为config的文件名,RepoTags标明镜像名,Layers为镜像tar包中的存储位置。Layers的个数与rootfs中的diff_ids一致,所以第一个layer对应的目录就是707f1f25f845bd495c21c368a57666f6c4e1b0358868e43e0376fd55512f8734/(该id计算方法见附录,hash的内容由代码生成),该目录下有layer.tar文件,计算哈希值得:

1
2
root@fankang:/home/fankang/nginx/707f1f25f845bd495c21c368a57666f6c4e1b0358868e43e0376fd55512f8734# sha256sum layer.tar
8aa4fcad5eeb286fe9696898d988dc85503c6392d1a2bd9023911fb0d6d27081 layer.tar

这与8aa4fcad5eeb286fe9696898d988dc85503c6392d1a2bd9023911fb0d6d27081目录下的diff文件中的值一样。
其他层也是如此。

Size就是layer的大小的字节表示,与ll命令查看的有些误差,但相差很小。

repositories.json

repositories.json标明了系统中的镜像:

1
2
3
4
5
6
7
8
root@fankang:/var/lib/docker1/image/aufs# cat repositories.json | python -mjson.tool
{
"Repositories": {
"nginx": {
"nginx:v1": "sha256:3034d86f1f0b0379092bb99565d72c1dd4a94b41c1e83bb2d0714bb98720ac16"
}
}
}

如上图所示,该文件说明系统中有nginx:v1镜像,镜像的id为3034d86f1f0b0379092bb99565d72c1dd4a94b41c1e83bb2d0714bb98720ac16。

总结

系统的Image存储相当于实现了一个Image数据库。其中从的索引为repositories.json,里面可以获取到镜像的config文件。从镜像的config文件,可以计算出镜像每层的chain-id,这样就可以从layerdb中找到该层在AUFS中的存储目录。其中镜像的挂载点也存储在layerdb中。

附录

GO语言如何求hash

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main
import (
    sha256 "crypto/sha256"
    "fmt"
)
func main() {
    s := "sha256:860708544664c614b8ab025d5d01ba8f37694e939fd02ce861538bfd7c64cc43 sha256:f370efdef91872f8e309b38636aec6fc3a51cae8ee8d44c59f6328a6c5d16def"
    h := sha256.New()
    h.Write([]byte(s))
    bs := h.Sum(nil)
    fmt.Printf("%x", bs)
}

用sha256sum求与GO语言求得到的结果不一样,因为cat或echo会在字符串后加’\n’。可以通过echo -n来解决。

镜像tar包中manifest.json中的layer值计算

Layer值依据镜像层的config生成。如sha256:ea9f151abb7e06353e73172dad421235611d4f6d0560ec95db26e0dc240642c1对应的configJSON为:

1
{"container_config":{"Hostname":"","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":null,"Cmd":null,"Image":"","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"created":"0001-01-01T00:00:00Z","layer_id":"sha256:ea9f151abb7e06353e73172dad421235611d4f6d0560ec95db26e0dc240642c1"}

该configJSON值从Docker源码中得到,对该configJSON求Hash(注意,此例子和nginx:v1无关系):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main
import (
    sha256 "crypto/sha256"
    "fmt"
)
func main() {
    s := "{\"container_config\":{\"Hostname\":\"\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":null},\"created\":\"0001-01-01T00:00:00Z\",\"layer_id\":\"sha256:ea9f151abb7e06353e73172dad421235611d4f6d0560ec95db26e0dc240642c1\"}"
    h := sha256.New()
    h.Write([]byte(s))
    bs := h.Sum(nil)
    fmt.Printf("%x", bs)
}

得到该层对应的值为:2b8a5cc36c07cfb2a5bd6e40f9d5dd52fb300ab1a7afb6e22b3d977e3cfcb885,与manifest.json中的值保持一致。