docker架构细看(2)-镜像

​ 上一章讲了Docker服务端的启动,这一章我们来看Docker中的镜像,需要对容器镜像分层存储,容器存储驱动有一定了解,参考

容器技术原理(一):从根本上认识容器镜像

​ Docker篇之镜像存储-OverlayFS和联合挂载技术

手撕docker文件结构 —— overlayFS,image,container文件结构详解

​ 先给出Docker中镜像存储相关架构图,接下来一一介绍架构图中各部分。

镜像打包存储格式

​ Docker镜像打包存储格式目前为Docker image v2,这是兼容OCI (Open Container Initiative) Image Specification的,所以Docker打包的镜像可以和符合OCI标准的容器运行时一起工作,Docker的容器运行时为containerd,比较常见的还有OCI-R。containerd可以加载 Docker打包的镜像运行,并导出OCI image spec格式打包的镜像。不过我用Docker加载OCI image spec格式打包的镜像,可以加载,但是缺失一些信息(如容器名等),另外也运行不了。

OCI image-spec

​ 好了,让我们先看一看Docker中镜像打包后的存储格式,使用docker image save -o path [image],可以将镜像打包为tar格式,这里保存了一个mysql:8的镜像,解包tar文件后,目录格式如下:

# 每个目录代表镜像的一层(layer),目录名为层的sha2560a8df9b6baa9a...41cabb1147bae0d5366b52a/-json //该层设置-layer.tar //该层内容的归档文件-VERSION33c2db18737d9...52df5fe7b59230a25ed51fd/4261e1b5e194d...65082b7f399f202e3c6efa6/5d011c2d54d47...6ab4401cb753e97d9a6e789/7fb59873cff82...a51b0de05109ddbe2823f03/8690ce19e0a42...5708640d81b533aa86d9e9e/8e0a3834fee65...9aa228469d7164996b2b7d8/a995401364fdc...edabbef15d8ca6f82cc556d/ad8a3181e4cd9...5aab16b354a738cbbc75ae5/f53d394a7abb4...46ac8911a23e5ad72cdf9cd/ff702031cdc68...5d94addbe4dc88fa6629eda/# 镜像设置文件,Docker可以通过该文件初始化一个image(描述镜像的结构体)b939d379d46e3...14ad27b7d9da27f26774965610.json# 镜像元数据,保存了镜像运行平台,镜像设置文件(即上面的Json文件),镜像包含哪些层(上面那些层目录中的layer.tar文件)manifest.jsonrepositories。

​ 打包文件存储了所有的层,所有层联合挂载即可提供镜像文件系统。值得注意的是各层之间是有父子关系的,子层是基于父层做了一些修改(文件,目录的增删改),在各层目录的json文件中记录了该层的父层。而manifest中记录了所有层,且排列顺序为各层按父子关系连成链的顺序,另外manifest中还记录了镜像配置文件名称,镜像配置文件中记录了rootfs信息,其中记录了组成镜像每层的diffID,diffID为layer.tar内容的SHA256签名,也代表了该层与父层的差异。下图展示了层之间的关系,以及各种ID。这些ID在Docker中会用到。

​ 接下来,解析Docker加载镜像打包文件,创建新镜像的过程,主要加载函数为image/tarexport/load.go::Load(inTar io.ReadCloser),函数主要流程如下:

  1. 解包镜像打包文件,读取manifest.json文件,获取镜像配置文件路径和组成镜像所有层的路径。

  2. 读取镜像配置文件到img(存放镜像配置信息的结构体)中,读取其中Rootfs信息,获取所有层的diffID。

  3. Rootfs中的diffID和manifest.json中的层路径都是按父->子的关系排列好的,所以两者一一对应。之后就是注意检查每一层是否在Docker中已存储,若没有则从layer.tar加载到Docker存储中。其过程类似于不断扩展一条链,按拓扑序遍历层:

    1. 将该层diffID加入diffID列表,根据列表中diffID计算chainID。
    2. 查询LayerStore中是否存储有chainID代表的链。有,直接跳到d。没有进行c。
    3. 从layerPath加载缺失的层到LayerStore。
    4. 修改内存数据结构,回到a处理下一层
    for i, diffID := range img.RootFS.DiffIDs {// 获取layer.tar所在路径layerPath, err := safePath(tmpDir, m.Layers[i])//不断加入diffID,计算当前已加入的层组成的链的chainID//查询docker是否已存有当前链(在这里等价于当前层)r.Append(diffID)newLayer, err := l.lss.Get(r.ChainID())//docker没有存储,则从layer.tar加载当前层if err != nil {newLayer, err = l.loadLayer(layerPath)}//为内存中的img的rootfs添加diffID rootFS.Append(diffID)}
  4. 根据镜像配置文件,在ImageStore内创建镜像文件,imgID为config文件的SHA256签名。

    imgID, err := l.is.Create(config)
  5. todo: 镜像引用,标签

​ 以上就是Docker加载打包镜像的大体流程。从中可以看到几点,镜像实际上是由一条layer链和镜像配置文件组成的,值得注意的是,layer(层)是Docker镜像存储中基本单位,但链才是检索的基本单位,在Load函数中可以看到,是通过查询以层结尾的链是否存在来确定是否加载该层。另外在加载过程中也可以窥见LayerStore,ImageStore的部分内容,下面会详细介绍。

LayerStore 和 ImageStore

​ 先放上图,对两者有一个初步认识,也方便下面论述。

LayerStore

​ LayerStore是Docker中存储层的结构,当然,后面会讲到,实际它只存储了层的元数据,可以通过元数据索引到实际层存储位置。先看一下LayerStore内存数据结构。

type LayerStore struct {store *fileMetadataStore// 存储驱动,常用overlay2drivergraphdriver.DriveruseTarSplit bool// 记录了Docker存储的所有层,使用chainID索引,也可以说所有链。layerMap map[ChainID]*roLayerlayerL sync.Mutexmounts map[string]*mountedLayermountL sync.Mutex// protect *RWLayer() methods from operating on the same name/idlocker *locker.Locker}// readOnlyLayer 代表一个只读层type roLayer struct {//从根layer到当前layer的链的 链IDchainIDChainID// layer.tar文件 sha256diffID DiffIDparent *roLayercacheIDstringsize int64LayerStore *LayerStoredescriptor distribution.DescriptorreferenceCount intreferences map[Layer]struct{}}

​ 接下来进一步解析上面load()函数中用到的loadlayer(layerPath string)函数。通过理解把layer加载进LayerStore的过程,进一步理解LayerStore。

  1. 调用LayerStore.driver(存储驱动),创建层的存储目录,目录名称为层的cacheID。

    ls.driver.Create(layer.cacheID);
  2. 将layer.tar解包,并写入步骤1创建目录中的diff目录下,即该层和父层的差异。

    ls.driver.ApplyDiff(layer.cacheID, parent, rdr)
  3. 将层的元数据存入LayerStore。

    storeLayer(layer)
  4. 修改内存数据结构

    ls.layerMap[layer.chainID] = layer

​ 光说可能不是很明白,我们可以看一看这些目录。我是用win基于wsl运行Docker,Docker数据存储位置为\\wsl$\docker-desktop-data\data\docker,Docker使用overlay2作为存储驱动。则其中image目录是ImageStore和LayerStore的存储位置,而overlay2目录则是层的实际存储位置。

image\overlay2\layerdb\sha256目录为层元数据存储目录,其下也包含一批目录,目录名称为层的chainID,其中存储着cacheID,diffID,parent等信息。

overlay2目录为层实际存储目录,其下也保存了一批目录,目录名称为cacheID。

​ 所以读取层的元数据信息,就可以获得parent层元数据目录,即可获取一整条链的元数据。同时也可以获取层的cacheID,从而锁定层的实际存储目录,这就将层的元数据和层关联了起来。

​ 层的元数据(LayerStore)已经和层(overlay2)关联起来了,接下来介绍image是如何和层关联起来的。

ImageStore

​ ImageStore就简单多了,实际上只存储了镜像的配置信息。

​ 以下为ImageStore相关数据结构,可以看到ImageStore在内存中记录了imageID和imageMeta的映射关系,而imageMeta中记录了组成镜像的层链中的最后一层,可以通过沿parent爬获取整条链。

type store struct {sync.RWMutexlss LayerGetReleaserimagesmap[ID]*imageMetafsStoreBackenddigestSet *digestset.Set}type imageMeta struct {layerlayer.Layerchildren map[ID]struct{}}imageID: image config sha256实际上镜像就是config,记录了层信息// Image stores the image configuration// 可以用镜像配置信息初始化type Image struct {V1Image// Parent is the ID of the parent image.//// Depending on how the image was created, this field may be empty and// is only set for images that were built/created locally. This field// is empty if the image was pulled from an image registry.Parent ID `json:"parent,omitempty"` //nolint:govet// RootFS contains information about the image's RootFS, including the// layer IDs.RootFS*RootFS `json:"rootfs,omitempty"`History []History `json:"history,omitempty"`// OsVersion is the version of the Operating System the image is built to// run on (especially for Windows).OSVersionstring `json:"os.version,omitempty"`OSFeatures []string `json:"os.features,omitempty"`// rawJSON caches the immutable JSON associated with this image.rawJSON []byte// computedID is the ID computed from the hash of the image config.// Not to be confused with the legacy V1 ID in V1Image.computedID ID}

​ 存储的实际目录为image\overlay2\imagedb\content\sha256,目录下存储了一批文件,文件名称为对应镜像的imageID,也就是配置文件的sha256签名。文件中存储的即镜像配置信息和打包镜像文件中的镜像配置信息一致。

​ 最后,解释一下store初始化时,从磁盘读取已存储的镜像,如何通过镜像配置文件和组成镜像的层联系起来:镜像配置文件中记录有rootfs结构,其中记录了每一层的diffID,由此可以计算出组成镜像的层链的chainID,通过chainID。而存储层元信息的目录名称就是chainID,SO,镜像就和层联系起来了。

总结

​ 本章细看了Docker中的镜像存储,下一章将会细看容器部分。