不启动容器挂载容器文件系统的方法

内容简介

在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。
0328091.png
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目录,即可看到指定容器内部的数据,可以修改也可以做数据备份等

posted @ 2016-01-06 21:21  珞珈小学子  阅读(1189)  评论(0编辑  收藏  举报