容器镜像与持久化

  本文为容器化技术系列文章的第二篇。本文将把容器技术与文件系统,压缩文件相结合,并且实现commit命令,用于保存镜像文件。

前置知识

  • Linux
  • Go

容器镜像与持久化

容器镜像

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 {
//注意文件位置 将tar放到指定位置
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")
//重新挂在本目录 便于使得新老root不在同一文件系统下
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)
}
//卸载原rootfs 清除临时文件
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 {
//volumeURLs格式为 [容器外目录,容器内目录]
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也使用了这样的技术。
  下一篇技术含量会比本篇要更高。但预计时间也更长,可能是一个月左右,敬请期待。

Author

王钦砚

Posted on

2020-12-01

Licensed under

CC BY-NC-SA 4.0

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×