zl程序教程

您现在的位置是:首页 >  工具

当前栏目

【博客618】docker容器重启后读写层数据并不丢失的原理

Docker博客容器数据原理 读写 重启 丢失
2023-09-14 09:12:53 时间

docker容器重启后读写层数据并不丢失的原理

1、场景

当我们对docker容器执行restart后,其实容器中原本读写层里对临时数据还在。只有我们删除了这个容器,重新创建的容器是基于镜像的只读层然后挂载上新的空的读写层,此时临时数据是不在的

2、前置知识

镜像,静态容器,运行时容器之间的区别

  • Image:统一只读文件系统)

  • 静态容器 :未运行的容器,统一可读写文件系统

  • 运行时容器:运行中的容器,进程空间(包括进程)+ 统一可读写文件系统

docker run,create,start之间的区别

docker run相当于执行了两步操作:将镜像放入容器中(docker create),然后将容器启动,使之变成运行时容器(docker start)。而docker start的作用是,重新启动已存在的镜像。也就是说,如果使用这个命令,我们必须事先知道这个容器的ID,或者这个容器的名字,我们可以使用docker ps找到这个容器的信息。
在这里插入图片描述

docker常见命令的区别:

  • docker create < image-id >

    该命令即为在只读文件系统上添加一层可读写层「Top Layer」,并生成可读写文件系统。该命令状态下容器为静态容器,并没有运行。

  • docker start | restart < container-id >

    该命令即为在可读写文件系统添加一个进程空间和运行的进程,并生成一个动态容器。

  • docker run < image-id >

    docker run = docker create + docker start

  • docker stop < container-id >

    该指令向运行中的容器发一个 SIGTERM 信号,然后停止所有的进程。即为 docker start 的逆过程。

  • docker kill < container-id >

    该指令向容器发送一个不友好的 SIGKILL 信号,相当于快速强制关闭容器。与 docker stop 的区别是 docker stop 是先发 SIGTERM 信号来清理进程,然后再发 SIGKILL 信号退出,整个进程是正常关闭的。

  • docker pause < container-id >

    该指令用作暂停容器中的所有进程,使用 cgroup 的 freezer 顺序暂停容器里的所有进程。

  • docker commit < container-id >

    该指令用作把容器的可读写层转化成只读层,即从容器状态「可读写文件系统」变为镜像状态「只读文件系统」,可理解为固化。

  • docker build

    docker build = docker run 「运行容器 + 进程修改数据」+ docker commit「固化数据」,整个过程不断循环直至生成所需镜像。循环一次便会形成一个新的层(新镜像 = 原镜像层 + 已固化的可读写层)。docker build 过程一般通过 dockerfile 文件来实现。

docker容器生命周期

在这里插入图片描述

3、docker容器重启后读写层数据并不丢失的原理

容器创建与启动的流程:
在这里插入图片描述

docker创建容器和运行容器源码剖析:

docker run命令其实是由两部分组成:create和start:

  • 创建容器的逻辑(create):

    • 获取镜像ID GetImage

    • 合并容器配置

    • 合并日志配置

    • 创建容器对象 newContainer

      if container, err = daemon.newContainer(params.Name, params.Config, imgID, managed); err != nil {
      return nil, err
      }

    • 设置安全选项

    • 设置容器读写层

      if err := daemon.setRWLayer(container); err != nil {
      return nil, err
      }

    • 创建文件夹保存容器配置信息

      //创建文件夹,用于保存容器的配置信息,在/var/lib/docker/containers/id下,并赋予容器进程的读写权限
      if err := idtools.MkdirAs(container.Root, 0700, rootUID, rootGID); err != nil {
      return nil, err
      }

      //把配置文件保存到磁盘
      if err := daemon.setHostConfig(container, params.HostConfig); err != nil {
      return nil, err
      }

    • 保存到硬盘

      if err := daemon.updateContainerNetworkSettings(container, endpointsConfigs); err != nil {
      return nil, err
      }

    • 注册到daemon

  • 启动容器的逻辑(start):

    • 找到容器对象实例

      container, err := daemon.GetContainer(name)
      if err != nil {
      return err
      }

    • 判断如果暂停的容器不能启动,先unpause再启动

      if container.IsPaused() {
      return fmt.Errorf(“Cannot start a paused container, try unpause instead.”)
      }

    • 判断如果是运行的容器不用启动

      if container.IsRunning() {
      err := fmt.Errorf(“Container already started”)
      return errors.NewErrorWithStatusCode(err, http.StatusNotModified)
      }

    • 确认hostconfig与当前系统是否一致

      if _, err = daemon.verifyContainerSettings(container.HostConfig, nil, false, validateHostname); err != nil {
      return err
      }

    • 调整旧版容器设置:主要是cpu、内存限制的校验和设置

      if err := daemon.adaptContainerSettings(container.HostConfig, false); err != nil {
      return err
      }

    • 开始启动容器

      • 容器对象加锁

        container.Lock()
        defer container.Unlock()

      • 状态校验,如该已经运行,直接返回

        if container.Running {
        return nil
        }

      • 挂载读写层

        dir, err := container.RWLayer.Mount(container.GetMountLabel())
        if err != nil {
        return err
        }

      • 初始化网络

      • containerd 调用runc

      • 进入runc启动容器

4、创建容器读写层源码

func (daemon *Daemon) setRWLayer(container *container.Container) error {
       var layerID layer.ChainID
       if container.ImageID != "" {
              img, err := daemon.stores[container.Platform].imageStore.Get(container.ImageID)
              layerID = img.RootFS.ChainID()
       }

       rwLayerOpts := &layer.CreateRWLayerOpts{
              MountLabel: container.MountLabel,
              InitFunc:   daemon.getLayerInit(),
              StorageOpt: container.HostConfig.StorageOpt,
       }

       rwLayer, err := daemon.stores[container.Platform].layerStore.CreateRWLayer(container.ID, layerID, rwLayerOpts)
       container.RWLayer = rwLayer

       return nil
}
func (ls *layerStore) CreateRWLayer(name string, parent ChainID, opts *CreateRWLayerOpts) (RWLayer, error) {
       if opts != nil {
              mountLabel = opts.MountLabel
              storageOpt = opts.StorageOpt
              initFunc = opts.InitFunc
       }

       ls.mountL.Lock()
       defer ls.mountL.Unlock()
       m, ok := ls.mounts[name]

       if string(parent) != "" {
              p = ls.get(parent)
              if p == nil {
                     return nil, ErrLayerDoesNotExist
              }
              pid = p.cacheID
       }

       m = &mountedLayer{
              name:       name,
              parent:     p,
              mountID:    ls.mountID(name),
              layerStore: ls,
              references: map[RWLayer]*referencedRWLayer{},
       }

       if initFunc != nil {
              pid, err = ls.initMount(m.mountID, pid, mountLabel, initFunc, storageOpt)
              m.initID = pid
       }

       createOpts := &graphdriver.CreateOpts{
              StorageOpt: storageOpt,
       }

       if err = ls.driver.CreateReadWrite(m.mountID, pid, createOpts); err != nil {
     
       if err = ls.saveMount(m); err != nil {
     
       return m.getReference(), nil
}
func (ls *layerStore) initMount(graphID, parent, mountLabel string, initFunc MountInit, storageOpt map[string]string) (string, error) {
       // Use "<graph-id>-init" to maintain compatibility with graph drivers
       // which are expecting this layer with this special name. If all
       // graph drivers can be updated to not rely on knowing about this layer
       // then the initID should be randomly generated.
       initID := fmt.Sprintf("%s-init", graphID)

       createOpts := &graphdriver.CreateOpts{
              MountLabel: mountLabel,
              StorageOpt: storageOpt,
       }

       if err := ls.driver.CreateReadWrite(initID, parent, createOpts); err != nil {
              return "", err
       }
       p, err := ls.driver.Get(initID, "")
       if err != nil {
              return "", err
       }

       if err := initFunc(p); err != nil {
              ls.driver.Put(initID)
              return "", err
       }

       if err := ls.driver.Put(initID); err != nil {
              return "", err
       }

       return initID, nil
}

CreateReadWrite 函数如下, 在 /var/lib/docker/aufs 目录下创建两个文件 mnt 和 diff,创建 /var/lib/docker/aufs/layers/${id} 文件,获得该层的父层,记录所有父层 id 该文件

func (a *Driver) CreateReadWrite(id, parent string, opts *graphdriver.CreateOpts) error {
       return a.Create(id, parent, opts)
}

// Create three folders for each id
// mnt, layers, and diff
func (a *Driver) Create(id, parent string, opts *graphdriver.CreateOpts) error{
       if err := a.createDirsFor(id); err != nil {
              return err
       }
       // Write the layers metadata
       f, err := os.Create(path.Join(a.rootPath(), "layers", id))

       if parent != "" {
              ids, err := getParentIDs(a.rootPath(), parent)

              if _, err := fmt.Fprintln(f, parent); err != nil {
         
              for _, i := range ids {
                     if _, err := fmt.Fprintln(f, i); err != nil {
                    
       }

       return nil
}

saveMount 函数是在 /var/lib/image/aufs/layerdb/mounts目录下操作,如下所示:

func (ls *layerStore) saveMount(mount *mountedLayer) error {
       if err := ls.store.SetMountID(mount.name, mount.mountID); err != nil {
   
       if mount.initID != "" {
              if err := ls.store.SetInitID(mount.name, mount.initID); err != nil {

       if mount.parent != nil {
              if err := ls.store.SetMountParent(mount.name, mount.parent.chainID); err != nil {

       ls.mounts[mount.name] = mount

       return nil
}

SetMountID 函数位置 layer/filestore.go,主要是在 /var/lib/docker/image/aufs/layerdb/mounts 目录下创建层,将 ${mount-id} 写入 mount-id 文件

func (fms *fileMetadataStore) SetMountID(mount string, mountID string) error {
       if err := os.MkdirAll(fms.getMountDirectory(mount), 0755); err != nil {
              return err
       }
       return ioutil.WriteFile(fms.getMountFilename(mount, "mount-id"), []byte(mountID), 0644)
}

SetInitID 主要是在 ${mount-id}-init 写入 init-id 文件

func (fms *fileMetadataStore) SetInitID(mount string, init string) error {
       if err := os.MkdirAll(fms.getMountDirectory(mount), 0755); err != nil {
              return err
       }
       return ioutil.WriteFile(fms.getMountFilename(mount, "init-id"), []byte(init), 0644)
}

SetMountParent 将父层 image 记录 parent 文件

func (fms *fileMetadataStore) SetMountParent(mount string, parent ChainID) error {
       if err := os.MkdirAll(fms.getMountDirectory(mount), 0755); err != nil {
              return err
       }
       return ioutil.WriteFile(fms.getMountFilename(mount, "parent"), []byte(digest.Digest(parent).String()), 0644)
}

总结

docker run的时候其实是由create和start来完成的,create创建容器的时候会调用setRWLayer(container)创建读写层,start的时候会调用container.RWLayer.Mount(container.GetMountLabel())挂载读写层。restart的时候,则会使用新的镜像只读层 + 挂载当前容器的读写层,因为容器重启并不会丢失那些临时修改