不启动容器挂载容器文件系统的方法
内容简介
在docker实践中,有时候会出现一些稳定性问题,例如docker容器无法启动等,尤其是版本较早的docker。另外,在容器镜像 内部有时候也会因为配置不当而导致容器启动不起来,这个时候,如果了解了docker所采用的存储技术的原理,那么就可以实现不启动容器,就把容器的数据 卷挂载到宿主机的指定目录上,然后再到其中做修改或备份数据工作,使容器恢复正常并恢复重要数据
技术原理
1、docker镜像的典型结构如下图所示。传统的Linux加载bootfs时会先将rootfs设为read-only,然后在系统自检之后将 rootfs从read-only改为read-write,然后我们就可以在rootfs上进行写和读的操作了。但docker的镜像却不是这样,它在 bootfs自检完毕之后并不会把rootfs的read-only改为read-write。而是利用union mount(UnionFS的一种挂载机制)将一个或多个read-only的rootfs加载到之前的read-only的rootfs层之上。在加载 了这么多层的rootfs之后,仍然让它看起来只像是一个文件系统,在docker的体系里把union mount的这些read-only的rootfs叫做Docker的镜像。但是,此时的每一层rootfs都是read-only的,我们此时还不能对 其进行操作。当我们创建一个容器,也就是将Docker镜像进行实例化,系统会在一层或是多层read-only的rootfs之上分配一层空的 read-write的rootfs。
docker镜像这种层次化的组织方式带来了很多好处,首先是节约了镜像在物理机上占用的空间,其次是创建一个新的空的rootfs很容易,这就意味着容器相比其它虚拟化技术可以更加快速启动。
2、docker社区推荐使用AUFS组织它层次化的镜像结构,但是,在centos平台上,因为AUFS被未被纳入内核,因此docker采用的 是devicemapper作为镜像和容器的底层存储技术。devicemapper是linux内核中支持逻辑卷管理的通用设备映射机制,简单来说,就 是通过devicemapper提供的接口,可以创建一些逻辑块设备以及对应的映射表,这些逻辑块设备可以挂载给docker容器使用,当容器的IO请求 落到指定的块设备时,devicemapper的内核模块就会根据之前创建好的映射表把IO请求定位到指定的物理块设备上,完成IO操作。docker镜 像的每一层,都会对应devicemapper中的某一逻辑块设备。
3、devicemapper提供了多种的映射管理方案,其中有一种称为Thin-Provisioning的快照机制,具体细节可参照参考资料 3,简而言之,就是devicemapper满足了docker对镜像分层的组织要求,即每一层镜像都对应了一个dm设备,且这些dm设备可以被 mount成一个文件系统供容器使用,而且dm设备在层次叠加上是没有深度限制的
4、下面从docker代码层面分析docker是如何通过devicemapper提供的机制来管理docker镜像的,主要代码都在docker源码目录的pkg/devicemapper/devicemapper.go文件中:
a、在docker daemon初始化的时候,会首先通过devicemapper提供的接口创建一个thin pool,后续创建的每一个逻辑设备都是从这个pool中分配出来的:
func CreatePool(poolName string, dataFile, metadataFile *os.File, poolBlockSize uint32) error {
......
params := fmt.Sprintf("%s %s %d 32768 1 skip_block_zeroing", metadataFile.Name(), dataFile.Name(), poolBlockSize)
if err := task.AddTarget(0, size/512, "thin-pool", params); err != nil {
return fmt.Errorf("Can't add target %s", err)
}
......
if err := task.Run(); err != nil {
return fmt.Errorf("Error running DeviceCreate (CreatePool) %s", err)
}
return nil
}
以上代码等同于在用户空间执行了类似这样的命令:
dmsetup create docker-253:0-756230-pool --table "0 209715200 thin-pool /dev/sdb1 /dev/sdb2 128 32768 1 skip_block_zeroing"
创建了一个指定扇区数大小的pool,之后可以从该pool中创建名为dm-X的逻辑设备
b、从pool中创建一个基础的thin设备,代码如下:
func CreateDevice(poolName string, deviceId int) error {
......
if err := task.SetMessage(fmt.Sprintf("create_thin %d", deviceId)); err != nil {
return fmt.Errorf("Can't set message %s", err)
}
......
}
以上代码等同于在用户空间执行了以下命令:
dmsetup message docker-253:0-756230-pool 0 "create_thin 1"
创建了一个baseimage(id为1),后续所有的镜像层,都是基于这个baseimage形成的快照
c、之后创建的每一个镜像层,执行的都是创建快照的动作:
func CreateSnapDevice(poolName string, deviceId int, baseName string, baseDeviceId int) error {
......
if err := task.SetMessage(fmt.Sprintf("create_snap %d %d", deviceId, baseDeviceId)); err != nil {
if doSuspend {
ResumeDevice(baseName)
}
return fmt.Errorf("Can't set message %s", err)
}
......
}
以上代码等同于在用户空间执行了以下命令:
dmsetup message docker-253:0-756230-pool 0 "create_snap 2 1"
这就会在内核空间中注册一个新的设备(id为2),该设备是基于上步中创建的设备打快照得来的
d、之后再激活快照设备,用户空间中就可以看到该设备,然后挂载给容器使用:
func ActivateDevice(poolName string, name string, deviceId int, size uint64) error {
......
params := fmt.Sprintf("%s %d", poolName, deviceId)
if err := task.AddTarget(0, size/512, "thin", params); err != nil {
return fmt.Errorf("Can't add target %s", err)
}
......
}
以上代码等同于在用户空间执行了:
dmsetup create dm-1 --table "0 20971520 thin /dev/mapper/docker-253:0-756230-pool 2"
执行后会在系统中看到/dev/dm-1设备,之后docker就可以将该设备mount给指定容器使用。容器关闭的时候,会对应地删除dm-1这个逻辑设备,但实际上内核中注册的id为2的设备依然存在,并不会真正删除。
具体步骤
了解了docker是如何通过devicemapper管理镜像层次结构的原理后,我们就可以通过以下方法将无法启动的容器的文件系统挂载出来,其实也就是把没有删除的容器在内核中对应的id号的设备给映射出来:
1、docker ps -a查询要挂载的虚拟机的id号
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
1bd4cc1aed92 mgj-base-20150807:latest "/usr/bin/python2.6 5 hours ago Up 5 hours 10.13.130.56
bcee5c0e31ea mgj-base-20150807:latest "/usr/bin/python2.6 5 hours ago Up 5 hours 10.13.130.57
2、获取容器对应的设备号信息,例如:
cat /var/lib/docker/devicemapper/metadata/1bd4cc1aed92fae13cdf3ee586460a0319d9ea4b95c039b2ec0325a9ff9ff437
得到以下内容:
{"device_id":6,"size":107374182400,"transaction_id":111,"initialized":false}
可知devicemapper中的设备号是6,后面第4步要用到
3、执行dmsetup ls查看docker pool的信息:
docker-8:1-44303307-pool (253:0)
第4步要用到以上查询结果
4、第2步中,获取到的size是107374182400字节,而dmsetup配置时是以扇区数计算的,因此size应该是107374182400/512 = 209715200扇区,执行以下命令:
dmsetup create tmp --table "0 209715200 thin /dev/mapper/docker-8:1-44303307-pool 6"
创建了名为tmp的临时dm设备,映射到的device_id是6,即执行容器对应的设备号
此时/dev/mapper/目录下会多出一个名为tmp的目录
5、执行
mount /dev/mapper/tmp /home/tmp
进入/home/tmp/rootfs目录,即可看到指定容器内部的数据,可以修改也可以做数据备份等