"docker数据卷初始化分析"

  "docker容器数据卷"

Posted by Xu on July 28, 2017

Volumn数据卷初始化过程分析

实验过程中由于将容器文件系统迁移过去之后,恢复过程中报出未发现volumn的异常,所以为了分析这个错误的原因,今天这一个章节就主要了解一下Volumn数据卷是如何初始化的,在daemon的启动过程NewDaemon中有一行代码就做了这部分工作

volStore, err := d.configureVolumes(rootUID, rootGID)

VolumeInit

该函数的实现:

func (daemon *Daemon) configureVolumes(rootUID, rootGID int) (*store.VolumeStore, error) {
	volumesDriver, err := local.New(daemon.configStore.Root, rootUID, rootGID)//根据本地信息获取volumn信息并对驱动进行配置,生成一个local的本地根volume驱动器。因为有的volume可能在外部目录需要其他的驱动来支持
	if err != nil {
		return nil, err
	}

	volumedrivers.RegisterPluginGetter(daemon.PluginStore)

	if !volumedrivers.Register(volumesDriver, volumesDriver.Name()) {
	//注册该驱动,相当于将驱动填充到映射关系drivers.extensions中去
		return nil, errors.New("local volume driver could not be registered")
	}
	return store.New(daemon.configStore.Root)//新建一个数据卷存储,存放一些数据卷相关的计数信息
}

第一个步骤:获取volumn信息并配置驱动

func New(scope string, rootUID, rootGID int) (*Root, error) {
	rootDirectory := filepath.Join(scope, volumesPathName)//首先获取volumn的根路径

	if err := idtools.MkdirAllAs(rootDirectory, 0700, rootUID, rootGID); err != nil {
	//如果不存在则根据根路径创建一个目录
		return nil, err
	}

	r := &Root{
		scope:   scope,
		path:    rootDirectory,
		volumes: make(map[string]*localVolume),
		rootUID: rootUID,
		rootGID: rootGID,
	}//定义一个驱动对象,volumn就是数据卷id和数据卷对象的映射

	dirs, err := ioutil.ReadDir(rootDirectory)//读取该目录下的所有子目录,返回排过序子目录数组
	if err != nil {
		return nil, err
	}

	mountInfos, err := mount.GetMounts()//根据/proc/self/mountinfo文件解析并获取该运行的进程所有的挂载信息
	if err != nil {
		logrus.Debugf("error looking up mounts for local volume cleanup: %v", err)
	}

	for _, d := range dirs {//遍历子目录
		if !d.IsDir() {
			continue
		}

		name := filepath.Base(d.Name())//获取该目录最后一个/后的字符串,也就是volumn的id
		v := &localVolume{
			driverName: r.Name(),
			name:       name,
			path:       r.DataPath(name),
		}//定义一个本地数据卷对象
		r.volumes[name] = v//构建数据卷id和数据卷对象的关系
		optsFilePath := filepath.Join(rootDirectory, name, "opts.json")//数据卷配置信息opt.json的路径(/docker/volumn/wea212312(id)/opts.json)
		if b, err := ioutil.ReadFile(optsFilePath); err == nil {
		//读取数据卷配置文件数据
			opts := optsConfig{}
			if err := json.Unmarshal(b, &opts); err != nil {
				return nil, errors.Wrapf(err, "error while unmarshaling volume options for volume: %s", name)
			}//将文件opts.json文件中的内容读取成json格式存放在optsConfig对象中并添加到数据卷对象中去
			// Make sure this isn't an empty optsConfig.
			// This could be empty due to buggy behavior in older versions of Docker.
			if !reflect.DeepEqual(opts, optsConfig{}) {
				v.opts = &opts
			}

			// unmount anything that may still be mounted (for example, from an unclean shutdown)
			for _, info := range mountInfos {
				if info.Mountpoint == v.path {
					mount.Unmount(v.path)
					break
				}
			}
		}
	}

	return r, nil
}

新建一个数据卷存储对象

// New initializes a VolumeStore to keep
// reference counting of volumes in the system.
func New(rootPath string) (*VolumeStore, error) {
	vs := &VolumeStore{
		locks:   &locker.Locker{},
		names:   make(map[string]volume.Volume),//存储Volume对象
		refs:    make(map[string]map[string]struct{}),
		labels:  make(map[string]map[string]string),//存储volume元数据中的Label
		options: make(map[string]map[string]string),
	}//新建数据卷存储器对象//存储volume元数据中的options数据

	if rootPath != "" {
		// initialize metadata store
		volPath := filepath.Join(rootPath, volumeDataDir)//获得volume默认的根路径
		if err := os.MkdirAll(volPath, 750); err != nil {
			return nil, err
		}

		dbPath := filepath.Join(volPath, "metadata.db")//元数据的信息文件

		var err error
		vs.db, err = bolt.Open(dbPath, 0600, &bolt.Options{Timeout: 1 * time.Second})//创建或启动数据库boltdb(一种k/v数据库,只能单点写入)
		if err != nil {
			return nil, errors.Wrap(err, "error while opening volume store metadata database")
		}

		// initialize volumes bucket
		if err := vs.db.Update(func(tx *bolt.Tx) error {
			if _, err := tx.CreateBucketIfNotExists(volumeBucketName); err != nil {//创建一个名为volume的bucket存储篮对象
				return errors.Wrap(err, "error while setting up volume store metadata database")
			}
			return nil
		}); err != nil {
			return nil, err
		}
	}//对创建的数据库进行读写操作,返回nil则进行commit提交,若返回error则进行数据回滚

	vs.restore()//根据metadata.db中的元数据信息填充到VolumeStore

	return vs, nil
}

根据metadata.db中的元数据来恢复VolumeStore存储器对象中内容

func (s *VolumeStore) restore() {
	var ls []volumeMetadata
	s.db.View(func(tx *bolt.Tx) error {
		ls = listMeta(tx)
		return nil
	})//db.View只读模式,读取metadata中的元数据(key,value)

	chRemove := make(chan *volumeMetadata, len(ls))
	var wg sync.WaitGroup
	for _, meta := range ls {//遍历每一个元数据k/v
		wg.Add(1)
		// this is potentially a very slow operation, so do it in a goroutine
		go func(meta volumeMetadata) {
			defer wg.Done()

			var v volume.Volume
			var err error
			if meta.Driver != "" {
				v, err = lookupVolume(meta.Driver, meta.Name)//从指定的驱动中获取对应volume名称的Volume
				if err != nil && err != errNoSuchVolume {
					logrus.WithError(err).WithField("driver", meta.Driver).WithField("volume", meta.Name).Warn("Error restoring volume")
					return
				}
				if v == nil {
					// doesn't exist in the driver, remove it from the db
					chRemove <- &meta
					return
				}
			} else {
				v, err = s.getVolume(meta.Name)//如果没有指定驱动则从VolumeStore中获取Volume
				if err != nil {
					if err == errNoSuchVolume {
						chRemove <- &meta
					}
					return
				}

				meta.Driver = v.DriverName()//然后填写该Volume的元数据中的驱动信息
				if err := s.setMeta(v.Name(), meta); err != nil {//并更新元数据内容到boltdb数据库
					logrus.WithError(err).WithField("driver", meta.Driver).WithField("volume", v.Name()).Warn("Error updating volume metadata on restore")
				}
			}

			// increment driver refcount
			volumedrivers.CreateDriver(meta.Driver)

			// cache the volume
			s.globalLock.Lock()
			s.options[v.Name()] = meta.Options//根据元数据中的Labels及Options更新VolumeStore对象中的数据
			s.labels[v.Name()] = meta.Labels
			s.names[v.Name()] = v
			s.globalLock.Unlock()
		}(meta)
	}

	wg.Wait()
	close(chRemove)
	s.db.Update(func(tx *bolt.Tx) error {
		for meta := range chRemove {
			if err := removeMeta(tx, meta.Name); err != nil {
				logrus.WithField("volume", meta.Name).Warnf("Error removing stale entry from volume db: %v", err)//根据chRemove从metadata.db移除无用的Volume元数据
			}
		}
		return nil
	})
}

在容器恢复的过程中加载数据卷

前面两个章节分析到恢复过程中的四个步骤的前两步,今天进入第三个阶段即:daemon.createSpec,这个阶段对容器所特有的属性都进行设置,例如:资源限制,命名空间,安全模式等等配置信息,其中

ms, err := daemon.setupMounts(c)

完成了对容器数据卷的挂载,该函数的实现如下:

func (daemon *Daemon) setupMounts(c *container.Container) ([]container.Mount, error) {
	var mounts []container.Mount
	// TODO: tmpfs mounts should be part of Mountpoints
	tmpfsMounts := make(map[string]bool)
	tmpfsMountInfo, err := c.TmpfsMounts()//获取该容器所有临时文件系统的挂载信息
	if err != nil {
		return nil, err
	}
	for _, m := range tmpfsMountInfo {
		tmpfsMounts[m.Destination] = true
	}
	for _, m := range c.MountPoints {//遍历容器的数据卷挂载点,填充所有挂载点是否存在对应的数据卷
		if tmpfsMounts[m.Destination] {
			continue//如果为临时文件系统,则跳过
		}
		if err := daemon.lazyInitializeVolume(c.ID, m); err != nil {
			return nil, err//初始化一个挂载点的数据卷,通过数据卷驱动获取数据卷对象信息,即drivers.extensions[m].volumn[c.ID],填充到挂载点m对象中去
		}
		rootUID, rootGID := daemon.GetRemappedUIDGID()
		path, err := m.Setup(c.MountLabel, rootUID, rootGID)//挂载点安装
		if err != nil {
			return nil, err
		}
		if !c.TrySetNetworkMount(m.Destination, path) {//设置网络挂载,判断是否是一个网络挂载文件,不是则执行下面例行操作
			mnt := container.Mount{
				Source:      path,
				Destination: m.Destination,
				Writable:    m.RW,
				Propagation: string(m.Propagation),
			}
			if m.Volume != nil {
				attributes := map[string]string{
					"driver":      m.Volume.DriverName(),
					"container":   c.ID,
					"destination": m.Destination,
					"read/write":  strconv.FormatBool(m.RW),
					"propagation": string(m.Propagation),
				}
				daemon.LogVolumeEvent(m.Volume.Name(), "mount", attributes)
			}
			mounts = append(mounts, mnt)
		}
	}

	mounts = sortMounts(mounts)//将所有的挂载信息进行排序
	netMounts := c.NetworkMounts()//根据前面的设置,挂载网络文件
	// if we are going to mount any of the network files from container
	// metadata, the ownership must be set properly for potential container
	// remapped root (user namespaces)
	rootUID, rootGID := daemon.GetRemappedUIDGID()
	for _, mount := range netMounts {
		if err := os.Chown(mount.Source, rootUID, rootGID); err != nil {
			return nil, err
		}
	}
	return append(mounts, netMounts...), nil//将网络文件的挂载信息也添加到挂载信息中去并返回
}

上述代码中,container.MountPoint对象非常重要,他包含了容器所有的数据卷挂载信息,它的初始化在daemon的启动start阶段加载容器的时候就已经被赋值,主要是通过读取config.json.v2给容器contaienr对象设置,该配置文件中就已经包含MountPoint字段