本文为容器化技术系列文章的第二篇。本文将把容器技术与文件系统,压缩文件相结合,并且实现commit命令,用于保存镜像文件。
前置知识
容器镜像与持久化 容器镜像 overlay文件系统 从本文开始,我们将把参考资料《自己动手写docker》的原版中实现的镜像构造进行改造,其中最根本原因是原书采用了AUFS,而在笔者的系统上,没有AUFS,因而采用了docker也在使用的overlayfs进行替代。 overlayfs与AUFS类似,在linux 3.18内核便被支持。overlay允许一个读写目录树覆盖到另一个只读目录树上,所有对文件的修改都在上层的可写层进行。它与其他的UFS不同,在打开文件后的所有操作都转换到底层或者上层的文件系统。这使得文件系统的实现得到简化。 overlay简单的用法如下所示。
1 mount -t overlay overlay -o lowerdir=/lower,upperdir=/upper,workdir=/work /merged
从上述的指令可以看出,指令指定了底层文件只读层,可以有多层;上层文件可写层;工作文件是提供给文件系统便于生成。merged则是最后合并的挂载目录。注意底层,顶层与工作的目录的文件系统,overlay在这方面有很多限制。 在执行完成上述挂载指令后,我们可以在文件夹中进行读写文件的尝试。具体不在本文中呈现。下面我们将其挂入容器系统之中。
busybox基础镜像 这里我们采用busybox作为基础的镜像。busybox是一个精简的镜像,包含了很多UNIX环境的常用命令。busybox可以使用docker export得到tar,并且解压得到新的rootfs。 我们实现的基础思路是:使用pivot_root将整个root文件系统移动进busybox所提供的目录,使用overlay将其创建为只读层,然后创建只写层,提供给容器进行运行操作。在容器结束运行后,还原这一切。 首先,我们将busybox解压,得到其rootfs。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func CreateReadOnlyLayer (rootURL string ) error { busyboxURL := rootURL + "busybox/" busyboxTarURL := rootURL + "busybox.tar" exist, err := PathExists(busyboxURL) if err != nil { return fmt.Errorf("path exist error:%v" , err) } if exist == false { if err := os.Mkdir(busyboxURL, 0777 ); err != nil { return fmt.Errorf("mkdir root error: %v" , err) } if _, err := exec.Command("tar" , "-vxf" , busyboxTarURL, "-C" , busyboxURL).CombinedOutput(); err != nil { return fmt.Errorf("tar error: %v" , err) } } return nil }
然后,创建读写层与工作层,将其挂入目录。此处不必多说。 之后,在mount各种需要的东西之前,我们要使用pivot_root系统调用,将整个系统的根文件移动进入该文件夹。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 func pivotRoot (root string ) error { fmt.Println("pivot rooting" ) if err := syscall.Mount(root, root, "bind" , syscall.MS_BIND|syscall.MS_REC, "" ); err != nil { return fmt.Errorf("mount bind error: %v" , err) } pivotDir := filepath.Join(root, ".pivot_root" ) if err := os.Mkdir(pivotDir, 0777 ); err != nil { return fmt.Errorf("mkdir error: %v" , err) } if err := syscall.Mount("" , "/" , "" , syscall.MS_PRIVATE|syscall.MS_REC, "" ); err != nil { fmt.Printf("mount / error: %v\n" , err) return err } if err := syscall.PivotRoot(root, pivotDir); err != nil { return fmt.Errorf("pivotroot error: %v" , err) } if err := syscall.Chdir("/" ); err != nil { return fmt.Errorf("chdir error: %v" , err) } pivotDir = filepath.Join("/" , ".pivot_root" ) if err := syscall.Unmount(pivotDir, syscall.MNT_DETACH); err != nil { return fmt.Errorf("umount error: %v" , err) } return os.Remove(pivotDir) }
在上述函数中,我们首先为了使得老root和新root不再一个文件系统下,将root重新mount了一次,使得新老root不在同一个文件系统下。然后进行系统调用,更改当前目录,删除临时文件。 在此之后,就像之前的文件系统一样,我们挂载必要的,如proc等。我们得到了以根目录为虚拟环境的容器进程。 至于卸载方面,我们只需要umount我们所挂载的目录,并且删除文件夹,即可。在此也不过多阐述。 但我们需要的不仅是虚拟的环境,我们还有持久化的需求。这里我们引入数据卷的概念,来存放我们持久化的数据。
持久化 mount –bind 就像标题所写,这里我们采用mount –bind的方法来进行持久化。mount –bind相当于将两个目录连接起来。我们可以在得到持久化的目录名后,使用bind将容器外的目录与容器内的目录连接起来。同时在最后退出的时候,先卸载对应目录,此时文件便留在了容器外的目录。我们便实现了持久化的目标。 同时我们要注意,我们需要增加新的command选项,解析字符串,在这里便不过多展示。
volume代码实现 这里展示的仅仅是开始mount和结束umount的过程,增加命令与解析由于篇幅暂不展示。原理如上述所说。由于go的异常处理,代码会显得颇为冗杂。但本意如上所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 func MountVolume (rootURL string , mntURL string , volumeURLs []string ) error { parentURL := volumeURLs[0 ] if err := os.Mkdir(parentURL, 0777 ); err != nil { if exist, _ := PathExists(parentURL); exist == false { return fmt.Errorf("error in mkdir parentURL: %v" , err) } } containerURL := volumeURLs[1 ] containerVolumeURL := mntURL + containerURL fmt.Println(containerVolumeURL) if err := os.Mkdir(containerVolumeURL, 0777 ); err != nil { return fmt.Errorf("error in mkdir containerVolumeURL: %v" , err) } cmd := exec.Command("mount" , "--bind" , parentURL, containerVolumeURL) cmd.Stderr = os.Stderr cmd.Stdout = os.Stdout if err := cmd.Run(); err != nil { return fmt.Errorf("Mount volume error :%v" , err) } return nil } func DeleteVolumeMountPoint (mntURL string , volumnURL string ) error { cmd := exec.Command("umount" , "-v" , mntURL+volumnURL) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { return fmt.Errorf("delete mount volume error : %v" , err) } return nil }
在完成这些工作后,我们可以尝试数据卷是否工作正常。下面是本机的一个运行实例。可以看见,在完成数据卷工作后,数据确实得到了持久化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 root@wqy:/media/wqy/新加卷/projekts/Go/src/mydocker# sudo ./mydockerdev run -ti -v /volume:/containerVolume sh start runCommand New parent mounting /home/wqy/mnt//containerVolume [/volume /containerVolume] your command is sh start initCommand reading user command the command is sh mount start /home/wqy/mnt pivot rooting the current wd is / / # ls bin etc root usr containerVolume home sys var dev proc tmp / # cd containerVolume/ /containerVolume # ls a.txt /containerVolume # echo "sfsfsf" >b.txt /containerVolume # exit wqy@wqy:/volume$ cat b.txt sfsfsf
镜像打包 镜像打包也是我们需要的一项功能。在这里我们提供一个简单的实现方法:在为退出前,将所有文件打包成tar保存到外面即可。 在具体实现上,我们还需要增加指令,解析参数。我们这里使用commit命令,在容器运行时候进行打包。
1 2 3 4 5 6 7 8 9 10 func commitContainer (imageName string ) error { mntURL := "/home/wqy/mnt/" rootURL := "/home/wqy/" imageTar := rootURL + imageName + ".tar" fmt.Println(imageTar) if _, err := exec.Command("tar" , "-czf" , imageTar, "-C" , mntURL, "." ).CombinedOutput(); err != nil { return fmt.Errorf("tar error: %v " , err) } return nil }
这里提供的简单的打包解决方案。同样,解析命令的操作便不在此展示。 至此,我们实现了一些与容器文件相关的简单操作。
下一步做什么 在下一步,我们将继续扩展容器的功能,实现一些常用的docker指令,使得我们的容器更加接近docker,可用性更强。在下一篇文章中,我们将探讨这些功能。
总结与后记 这属于容器化技术第二篇文章。本文只介绍了一些简单的文件功能以及实现。文章较上一篇篇幅明显减少,主要原因还是实现的功能不太复杂,原理也不需要过多介绍。完成时间也较于上一篇文章所预期的时间晚了整整一个星期。这期间也有参加比赛,拖了一下的缘故。 写完本文主要是对文件的知识有所回顾。在本文中,我尝试了第一次自己设计解决方案,如使用bind,这是在原本书上没有写的。书上的解决方案在本机上无法实现,便自己想了一个。后来才发现,docker也使用了这样的技术。 下一篇技术含量会比本篇要更高。但预计时间也更长,可能是一个月左右,敬请期待。