使用Golang实现自己的Docker(二)

五、构建镜像

5.1 使用busybox创建容器

5.1.1 busybox

在之前,容器的挂载点继承自父进程的所有挂载点,因为缺少了镜像。

busybox 是一个集合了非常多UNIX工具的箱子,它可以提供非常多在UNIX环境下经常使用的命令,可以说 busybox 提供了一个非常完整而且小巧的系统。

获得 busybox 文件系统的 rootfs 很简单,可以使用 docker export 将一个镜像打成一个 tar包。

1
2
3
4
docker pull busybox
docker run -d busybox top -b
docker export -o busybox.tar [容器id]

5.1.2 pivot_root

pivot_root是一个系统调用,主要功能是去改变当前的 root 文件系统pivot_root 可以将当前进程的 root 文件系统移动到 put_old 文件夹中,然后使 new_root成为新的 root 文件系统。new_rootput_old 必须不能同时存在 当前 root 的同 一个文件系统中pivot_rootchroot 的主要区别是, pivot_root 是把整个系统切换到一个新的 root 目录 ,而移除对之前 root 文件系统的依赖,这样你就能够 umount原先的 root 文件系统。而 chroot 是针对某个进程,系统的其他部分依旧运行于老的 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
31
32
33
func pivotRoot(root string) error {
/**
为了使当前root的老 root 和新 root 不在同一个文件系统下,我们把root重新mount了一次
bind mount是把相同的内容换了一个挂载点的挂载方法
*/
if err := syscall.Mount(root, root, "bind", syscall.MS_BIND|syscall.MS_REC, ""); err != nil {
return fmt.Errorf("Mount rootfs to itself error: %v", err)
}
// 从新创建的rootfs进行隔离,才可以使用pivot_root
syscall.Mount("", "/", "", syscall.MS_REC|syscall.MS_PRIVATE, "")
// 创建 rootfs/.pivot_root 存储 old_root
pivotDir := filepath.Join(root, ".pivot_root")
if err := os.Mkdir(pivotDir, 0777); err != nil {
return err
}
// pivot_root 到新的rootfs, 现在老的 old_root 是挂载在rootfs/.pivot_root
// 挂载点现在依然可以在mount命令中看到
if err := syscall.PivotRoot(root, pivotDir); err != nil {
return fmt.Errorf("pivot_root %v", err)
}
// 修改当前的工作目录到根目录
if err := syscall.Chdir("/"); err != nil {
return fmt.Errorf("chdir / %v", err)
}

pivotDir = filepath.Join("/", ".pivot_root")
// umount rootfs/.pivot_root
if err := syscall.Unmount(pivotDir, syscall.MNT_DETACH); err != nil {
return fmt.Errorf("unmount pivot_root dir %v", err)
}
// 删除临时文件夹
return os.Remove(pivotDir)
}

这里oldroot就是之前容器启动后挂载的linux文件目录,也就是当前目录,现在把当前目录作为一个新挂载点,以前的挂载点是现在挂载点的子目录chdir("/")切换到当前挂载后,就可以把原来的挂载卸载掉删除,就只剩新挂载点了。

虽然旧的挂载点和新的挂载点是同一个目录,但是其实是在两个不同的Mount Namespace中,所以容器内修改文件就不会影响宿主机的目录文件。这才是上面工作的最终目的。

pivot_root官方手册

  1. pivot_root改变当前进程所在mount namespace内的所有进程的root mount移到put_old,然后将new_root作为新的root mount;
  2. pivot_root并没有修改当前调用进程的工作目录,通常需要使用chdir("/")来实现切换到新的root mount的根目录。

rount mount可以理解为rootfs,也就是“/”,pivot_root将所在mount namespace的“/”改为了new_root 注意,pivot_root没有改变当前调用进程的工作目录 注意,pivot_root的调用前提需要明确在fork进程时指定mount namespace参数

主要约束条件:

  1. new_root和put_old都必须是目录
  2. new_root和put_old不在同一个mount namespace中
  3. put_old必须是new_root,或者是new_root的子目录
  4. new_root必须是mount point,且不能是当前mount namespace的“/”

注意,pivot_root(new_root, put_old),且chdir("/")后,put_old是“/”的子目录,可以unmount

在docker中,使用pivot_root实现rootfs切换和隔离,也遵循pivot_root的使用约束

  1. 首先创建一个new_root的临时子目录作为put_old,然后调用pivot_root实现切换
  2. chdir("/")
  3. umount put_old and clear

运行过程中会出现pivotRoot error pivot_root invalid argument 的报错,可以先执行 unshare -m命令,然后将 busybox/.pivot_root 文件夹删除,注意这个文件必须手动删除,不然启动会报错,再次重试即可。

原因是systemd会将 fs 修改为 sharedpivot root 不允许 parent mount pointnew mount pointshared

docker runC中也给出了解决方案:// Make parent mount PRIVATE if it was shared.

1
2
if err := syscall.Mount(root, root, "bind", syscall.MS_BIND|syscall.MS_REC|syscall.MS_PRIVATE, ""); err != nil {
...

或者修改为MS_SLAVE也可。

unshare有一个选项--propagation private|shared|slave|unchanged可控制创建mnt namespace时挂载点的共享方式。

  • private:表示新创建的mnt namespace中的挂载点的shared subtrees属性都设置为private,即ns1和ns2的挂载点互不影响
  • shared:表示新创建的mnt namespace中的挂载点的shared subtrees属性都设置为shared,即ns1或ns2中新增或移除子挂载点都会同步到另一方
  • slave:表示新创建的mnt namespace中的挂载点的shared subtrees属性都设置为slave,即ns1中新增或移除子挂载点会影响ns2,但ns2不会影响ns1
  • unchanged:表示拷贝挂载点信息时也拷贝挂载点的shared subtrees属性,也就是说挂载点A原来是shared,在mnt namespace中也将是shared
  • 不指定--progapation选项时,创建的mount namespace中的挂载点的shared subtrees默认值是private

可以看到创建容器后pwd结果为/,且新增一个aaa.txt在宿主机镜像的目录下没有影响,说明容器的文件系统不会影响宿主机了。

image-20221022232012119

5.2 使用OverlayFS挂载容器镜像

5.2.1 前置工作

书中使用的是AUFS进行挂载,但是AUFS已经稍有过时了,所以使用OverlayFS进行挂载,大体结构相同。

我们预期想要达到的效果是:通过./GoDocker run -it -image busybox.tar sh命令,能自动去镜像目录寻找镜像,并且根据Overlayfs的结构自动创建相应的容器工作目录,并自动执行overlay挂载,使得容器内Mount Namespace隔离,并且只能操作容器镜像目录的文件内容,无法看到容器外的文件。

步骤如下:

首先定义固定的工作目录:

1
2
3
4
5
6
7
8
9
10
const ImageLowerName = "lower-layer"
const UpperLayerName = "container-layer"
const WorkLayerName = "work"
const MergeLayerName = "merge"

// ImageRootPath 镜像根目录
const ImageRootPath = "/root/godocker/images/"

// ContainerRootPath 容器根目录
const ContainerRootPath = "/root/godocker/containers/"

ImageRootPath下约定存放所有的镜像文件,当然用户也可以通过命令行指定镜像,ContainerRootPath约定存放容器的工作目录,我们会给每个容器生成一个id,容器的所有文件都存放在这个文件夹下。

上面的四行约定为OverlayFS需要的四个文件夹名称。

5.2.2 初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func NewWorkSpace(imageUrl string) string {
CreateRootDir()

id := GenerateContainerId(24)
log.Infof("container id:%s", id)
containerDir := ContainerRootPath + id + "/"
if err := os.Mkdir(containerDir, 0777); err != nil {
log.Errorf("Mkdir container dir %s error. %v", containerDir, err)
}

CreateWorkAndMergeDir(containerDir)
err := CreateLowerLayer(containerDir, imageUrl)
if err != nil {
return ""
}
CreateUpperLayer(containerDir)
ExecuteMountOverlayFS(containerDir)
return containerDir
}

通过NewWorkSpace()进行初始化工作,包括创建根目录、生成容器id、创建OverlayFS目录、挂载OverlayFS。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func CreateLowerLayer(containerDir string, imageUrl string) error {
// 寻找镜像
imageExists, err := PathExists(imageUrl)
if err != nil {
log.Errorf("founding image error!")
return err
}

if imageExists == false {
panic("image not found!")
}
// 创建lowerlayer目录并解压
lowerLayerDir := containerDir + ImageLowerName
if err := os.Mkdir(lowerLayerDir, 0777); err != nil {
log.Errorf("Mkdir dir %s error. %v", lowerLayerDir, err)
return err
}
if _, err := exec.Command("tar", "-xvf", imageUrl, "-C", lowerLayerDir).CombinedOutput(); err != nil {
log.Errorf("Untar dir %s error %v", lowerLayerDir, err)
return err
}
log.Infof("untar image %s success!", imageUrl)
return nil
}

CreateLowerLayer()创建只读的lowerlayer,并将镜像解压至该目录(非容器的工作目录)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func ExecuteMountOverlayFS(containerDir string) {
log.Infof("pwd: %s", containerDir)

mountCmd := exec.Command("mount", "-t", "overlay", "overlay", "-o", "lowerdir=lower-layer,upperdir=container-layer,workdir=work", "merge")
// 切换到容器目录,方便进行挂载
mountCmd.Dir = containerDir

mountCmd.Stdout = os.Stdout
mountCmd.Stderr = os.Stderr
if err := mountCmd.Run(); err != nil {
panic(err)
}
log.Infof("mount overlayfs success!")
}

创建完需要的文件夹,就需要进行挂载。

首先将mount的命令配置好,然后切换到生成的包含了四个文件夹的容器目录,在这个目录执行挂载。

挂载完成后,merge目录就包含了所有layer的文件,merge目录也就是容器的工作目录。

然后最后需要进行容器的清理工作,包括卸载挂载、删除工作目录等。

注意到有这行代码:

unshare := exec.Command("unshare", "-m") 因为本人在使用过程中,只有先执行这句代码,pivot_root系统调用才会成功,否则会报错invalid Argument

意思是取消共享父进程的Mount NameSpace,按理说创建了Mount Namepsace就应该是隔离的了,不知道为什么要手动取消共享。

但是这样会导致只在容器内挂载overlayfs,这样是不符合要求的。

所以正确做法应该是:

先执行一次syscall.Mount("", "/", "", syscall.MS_REC|syscall.MS_PRIVATE, "")来真正隔离。

1
2
3
4
5
6
7
8
9
10
11
12
func pivotRoot(root string) error {
/**
为了使当前root的老 root 和新 root 不在同一个文件系统下,我们把root重新mount了一次
bind mount是把相同的内容换了一个挂载点的挂载方法
*/
if err := syscall.Mount(root, root, "bind", syscall.MS_BIND|syscall.MS_REC, ""); err != nil {
log.Errorf("Moun
t rootfs to itself error: %v", err)
return fmt.Errorf("Mount rootfs to itself error: %v", err)
}
// 从新创建的rootfs进行隔离,才可以使用pivot_root
syscall.Mount("", "/", "", syscall.MS_REC|syscall.MS_PRIVATE, "")

5.2.3 完整核心代码

核心代码如下:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
// container/container_process.go
package container

import (
log "github.com/sirupsen/logrus"
"math/rand"
"os"
"os/exec"
"strings"
"syscall"
"time"
)

const ImageLowerName = "lower-layer"
const UpperLayerName = "container-layer"
const WorkLayerName = "work"
const MergeLayerName = "merge"

// ImageRootPath 镜像根目录
const ImageRootPath = "/root/godocker/images/"

// ContainerRootPath 容器根目录
const ContainerRootPath = "/root/godocker/containers/"

type OverlayParameter struct {
lowerDir string
upperDir string
workDir string
mergeDir string
}

type UserParameter struct {
ImageName string
Root string
}

// NewParentProcess 自己调用自己fork一个新进程,使用namespace隔离资源,如果指定了 -it 就需要将当前的输入输出导入到os的标准输入输出上
func NewParentProcess(tty bool, parameter *UserParameter) (*exec.Cmd, *os.File, string) {
readPipe, writePipe, err := NewPipe()
if err != nil {
log.Errorf("New pipe error %v", err)
return nil, nil, ""
}
// 自己调用自己,对创建出来的进程进行初始化
cmd := exec.Command("/proc/self/exe", "init")
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS |
syscall.CLONE_NEWNET | syscall.CLONE_NEWIPC,
}
if tty {
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
}

// 将管道文件读取端的句柄传给子进程。
cmd.ExtraFiles = []*os.File{readPipe}

imageUrl := ImageRootPath
// 从是否有路径分割来判断是否是用户指定的绝对地址,否则在默认目录下寻找
if strings.Index(parameter.ImageName, "/") > 0 {
imageUrl = parameter.ImageName
} else {
// 否则只指定了镜像名称
imageUrl += parameter.ImageName
}
// 执行这句命令pibot_root才会成功
// unshare := exec.Command("unshare", "-m")
// unshare.Run()

workDir := NewWorkSpace(imageUrl)
if len(workDir) <= 1 {
panic("create workspace error!")
}
// 容器根目录为merge目录
cmd.Dir = workDir + MergeLayerName
return cmd, writePipe, workDir
}

func NewPipe() (*os.File, *os.File, error) {
read, write, err := os.Pipe()
if err != nil {
return nil, nil, err
}
return read, write, nil
}

func NewWorkSpace(imageUrl string) string {
CreateRootDir()

id := GenerateContainerId(24)
log.Infof("container id:%s", id)
containerDir := ContainerRootPath + id + "/"
if err := os.Mkdir(containerDir, 0777); err != nil {
log.Errorf("Mkdir container dir %s error. %v", containerDir, err)
}

CreateWorkAndMergeDir(containerDir)
err := CreateLowerLayer(containerDir, imageUrl)
if err != nil {
return ""
}
CreateUpperLayer(containerDir)
ExecuteMountOverlayFS(containerDir)
return containerDir
}

func CreateRootDir() {
// 镜像根目录
imageExists, err := PathExists(ImageRootPath)
if err != nil {
panic("founding image root dir error!")
}
if imageExists == false {
if err := os.Mkdir(ImageRootPath, 0777); err != nil {
log.Errorf("Mkdir dir %s error. %v", ImageRootPath, err)
panic(err)
}
}

// 容器根目录
containerExists, err := PathExists(ContainerRootPath)
if err != nil {
panic("founding container root dir error!")
}
if containerExists == false {
if err := os.Mkdir(ContainerRootPath, 0777); err != nil {
log.Errorf("Mkdir dir %s error. %v", ContainerRootPath, err)
panic(err)
}
}

}

// CreateLowerLayer 创建OverlayFS的只读lower目录,可以有多层,创建根目录和镜像目录并解压镜像到该目录
func CreateLowerLayer(containerDir string, imageUrl string) error {
// 寻找镜像
imageExists, err := PathExists(imageUrl)
if err != nil {
log.Errorf("founding image error!")
return err
}

if imageExists == false {
panic("image not found!")
}
// 创建lowerlayer目录并解压
lowerLayerDir := containerDir + ImageLowerName
if err := os.Mkdir(lowerLayerDir, 0777); err != nil {
log.Errorf("Mkdir dir %s error. %v", lowerLayerDir, err)
return err
}
if _, err := exec.Command("tar", "-xvf", imageUrl, "-C", lowerLayerDir).CombinedOutput(); err != nil {
log.Errorf("Untar dir %s error %v", lowerLayerDir, err)
return err
}
log.Infof("untar image %s success!", imageUrl)
return nil
}

func CreateUpperLayer(containerDir string) {
upperDir := containerDir + UpperLayerName
if err := os.Mkdir(upperDir, 0777); err != nil {
log.Errorf("Mkdir dir %s error. %v", upperDir, err)
}
log.Infof("mkdir upperlayer %s success!", upperDir)
}

// CreateWorkAndMergeDir 创建固定的work和merge目录
func CreateWorkAndMergeDir(containerDir string) {
// 创建work目录
workUrl := containerDir + WorkLayerName
exist, err := PathExists(workUrl)
if err != nil {
log.Errorf("fail to judge whether dir if exist! %v", err)
}
if exist == false {
if err := os.Mkdir(workUrl, 0777); err != nil {
log.Errorf("Mkdir dir %s error. %v", workUrl, err)
}
log.Infof("mkdir workdir %s success!", workUrl)
}

// 创建merge目录
mergeUrl := containerDir + MergeLayerName
mexist, err := PathExists(mergeUrl)
if err != nil {
log.Errorf("fail to judge whether dir if exist! %v", err)
panic(err)
}
if mexist == false {
if err := os.Mkdir(mergeUrl, 0777); err != nil {
log.Errorf("Mkdir dir %s error. %v", mergeUrl, err)
panic(err)
}
log.Infof("mkdir mergedir %s success!", mergeUrl)
}

}

func ExecuteMountOverlayFS(containerDir string) {

// 切换到容器目录,方便进行挂载

log.Infof("pwd: %s", containerDir)
//cmdStr := "-t overlay overlay -o lowerdir=" + ImageLowerName + ",upperdir=" + UpperLayerName + ",workdir=" + WorkLayerName
mountCmd := exec.Command("mount", "-t", "overlay", "overlay", "-o", "lowerdir=lower-layer,upperdir=container-layer,workdir=work", "merge")
mountCmd.Dir = containerDir
//log.Infof("mount cmd: mount -t overlay overlay -o %s merge ", cmdStr)
mountCmd.Stdout = os.Stdout
mountCmd.Stderr = os.Stderr
if err := mountCmd.Run(); err != nil {
panic(err)
}
log.Infof("mount overlayfs success!")
}

// 容器退出前清理工作
func DeleteWorkSpace(containerDir string) {
DeleteMountPoint(containerDir)
DeleteUpperLayer(containerDir)
}

func DeleteMountPoint(containerDir string) {
mergeUrl := containerDir + MergeLayerName
cmd := exec.Command("umount", mergeUrl)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
log.Errorf("%v", err)
}
if err := os.RemoveAll(mergeUrl); err != nil {
log.Errorf("Remove dir %s error %v", mergeUrl, err)
}
log.Infof("MountPoint Clear success!")
}

func DeleteUpperLayer(containerDir string) {
upperLayerUrl := containerDir + UpperLayerName
if err := os.RemoveAll(upperLayerUrl); err != nil {
log.Errorf("Remove dir %s error %v", upperLayerUrl, err)
}
log.Infof("Upperlayer Clear success!")
}
func PathExists(path string) (bool, error) {
_, err := os.Stat(path)
if err == nil {
return true, nil
}
if os.IsNotExist(err) {
return false, nil
}
return false, err
}

func verifyRootUrl(rootUrl string) string {
if strings.Compare("/", string(rootUrl[len(rootUrl)-1])) != 0 {
rootUrl += "/"
}
return rootUrl
}

// GenerateContainerId returns a random string with a fixed length
func GenerateContainerId(n int) string {
rand.Seed(time.Now().Unix())
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
b := make([]rune, n)
for i := range b {
b[i] = letters[rand.Intn(len(letters))]
}
return string(b)
}

5.2.4 运行效果

测试效果如下:

image-20221024122818616
image-20221024122850707

可以看到容器初始化成功,创建了工作目录,并且环境进行了隔离、只能看到工作目录的内容。

并且在容器外可以看到成功挂载了Overlayfs

image-20221024122931403

我们尝试在容器内修改文件。

image-20221024123130754

能够按照OverlayFS的原理进行修改。

退出后也能够自动卸载挂载、完成清理工作。

image-20221024123236297

5.2.5 总结

由于Mount Namespaceshared subtrees问题,书中也没有说明,真是踩了大坑,不过了解原理后就熟悉了。

原因是:首先进行了Mount bind,把root重新mount了一次,然后调用pivot_root。原因就在于这里,重新mount后由于shared subtrees,调用肯定会不成功。要么手动执行unshare -m,但是这样是在运行程序前执行的,还是和原来的root隔离了,所以后面overlayfs挂载只能挂载在容器上,而宿主机就看不到挂载,自然merge目录也就没内容了。

由于shared subtrees,之前创建mount namepsace后挂载proc其实是将宿主机的proc挂载过来了,所以会发现宿主机很多命令都没法用了。

然后得知mount --make-private /可以将挂载树变为private,就不会再共享操作了。

然后第一次在Mount bind前进行了调用,结果当时看似一切都解决了,能够完美运行,结果发现重启就崩了,因为把宿主机原始的rootfs给隔离了,导致系统启动找不到文件了。还好虚拟机有快照。

才明白应该在Mount bind之后,pivot_root之前进行调用,并且在创建子进程之前进行挂载,这样就能完美解决了。

5.3 实现Volume数据卷

我们上一节实现了挂载了镜像文件系统,但是如果退出容器会卸载挂载点、删除upperlayer,那么在容器中的操作就无法进行持久化了,Volume数据卷就可以实现持久化,退出容器后,容器的内容仍然保存在宿主机上。

5.3.1 建立挂载

因为用户可能挂载也可能不挂载,所以在创建容器的时候需要进行判断是否进行挂载,并且校验挂载的路径是否输入正确。

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
36
37
38
39
40
41
// container/container_process.go
func NewWorkSpace(imageUrl string, parameter *UserParameter) string {
// 创建容器根目录及镜像容器目录
CreateRootDir()
id := GenerateContainerId(24)
log.Infof("container id:%s", id)
containerDir := ContainerRootPath + id + "/"
if err := os.Mkdir(containerDir, 0777); err != nil {
log.Errorf("Mkdir container dir %s error. %v", containerDir, err)
}

// 创建OverlayFS文件夹及挂载
CreateWorkAndMergeDir(containerDir)
err := CreateLowerLayer(containerDir, imageUrl)
if err != nil {
return ""
}
CreateUpperLayer(containerDir)
ExecuteMountOverlayFS(containerDir)

// 判断是否需要挂载数据卷
volume := parameter.Volume
if volume != "" {
// /root:/root/abc
volumeURLs := volumeUrlExtract(volume)
length := len(volumeURLs)
if length == 2 && volumeURLs[0] != "" && volumeURLs[1] != "" {
MountVolume(containerDir, volumeURLs)
} else {
log.Infof("Volume parameter input is not correct.")
}
}
return containerDir
}

// : 分割,前面是宿主机的路径,后面是容器内的路径
func volumeUrlExtract(volume string) []string {
var volumeURLs []string
volumeURLs = strings.Split(volume, ":")
return volumeURLs
}

如果需要挂载,我们就通过MountVolume(containerDir, volumeURLs)来进行挂载。

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
// MountVolume 将宿主机的目录挂载到容器
func MountVolume(containerDir string, volumeURLs []string) {
// 宿主机目录
parentUrl := volumeURLs[0]
exists, err := PathExists(parentUrl)
if err != nil {
return
}
// 如果存在就直接挂载,不存在就创建文件夹
if exists == false {
if err := os.Mkdir(parentUrl, 0777); err != nil {
log.Errorf("Mkdir parent dir %s error. %v", parentUrl, err)
}
}
// 容器目录,注意这里是在主进程中进行挂载,所以需要指定容器的工作目录的完整路径。
containerVolumeUrl := volumeURLs[1]
containerVolumeURL := containerDir + "merge" + containerVolumeUrl
if err := os.Mkdir(containerVolumeURL, 0777); err != nil {
log.Errorf("Mkdir container dir %s error. %v", containerVolumeURL, err)
}
// 通过 mount --bind 来进行挂载
cmd := exec.Command("mount", "--bind", parentUrl, containerVolumeURL)
log.Infof("rootUrl: %s, containerVulumeUrl: %s", parentUrl, containerVolumeURL)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
log.Errorf("Mount volume failed. %v", err)
panic(err)
}
log.Infof("mount volume success!")
}

可以看到最终是通过Mount --bind来进行挂载的。书中是通过aufs进行挂载,但是我觉得没有必要,普通的挂载就可以了,Mount --bind相当于将两个文件夹指向同一个inode.

https://blog.csdn.net/allway2/article/details/122136813

5.3.2 清理工作

下面就是退出容器时的清理工作。

注意,这里我们有两个挂载点:

  1. 容器工作目录containerDir/merge
  2. 容器内挂载点containerDir/merge/{mountdir}

所以卸载挂载的时候需要先卸载容器内挂载点,再卸载容器工作目录挂载点。否则会报错mount device busy

1
2
3
4
5
6
7
8
9
10
11
12
13
func UmountVolume(containerDir string, volumeUrl string) {
// 删除数据卷挂载点
containerVolumeUrl := containerDir + "merge" + volumeUrl
cmd := exec.Command("umount", containerVolumeUrl)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
log.Errorf("Umount volume failed. %v", err)
}
// 这里需要先卸载一次容器目录
mergeUrl := containerDir + MergeLayerName
DeleteMergeMountPoint(mergeUrl)
}

注意的是,在测试中发现,如果挂载了数据卷,mount会出现两次容器目录的挂载,如果卸载数据卷挂载点后不卸载一次容器目录,那么后面卸载工作目录的时候只卸载一次,就会导致文件夹删除不了。所以这里需要卸载一次工作目录。

同时需要修改一下,如果用户没有进行挂载,那么就不卸载数据卷挂载。

1
2
3
4
5
6
7
8
9
10
11
12
func DeleteWorkSpace(containerDir string, volumeMount string) {
// 如果有数据卷挂载,则先卸载数据卷挂载
if volumeMount != "" {
volumeURLs := volumeUrlExtract(volumeMount)
length := len(volumeURLs)
if length == 2 && volumeURLs[0] != "" && volumeURLs[1] != "" {
UmountVolume(containerDir, volumeURLs[1])
}
DeleteMountPoint(containerDir)
DeleteUpperLayer(containerDir)
}
}

5.3.3 测试效果

通过./GoDocker run -it -image busybox.tar -v /root/busybox:/busybox sh启动容器,表示通过镜像busybox创建容器,并且将宿主机/root/busybox目录挂载到容器内/busybox目录。

image-20221025114916172

可以看到宿主机/root/busybox有3个文件,容器启动成功后能够在根目录看到这三个文件。

如果宿主机目录不存在的话,也会自动创建空文件夹然后进行挂载。

我们增加一个文件ddd.txt

image-20221025115106229

可以看到宿主机挂载的目录也能够同步更新。

然后我们退出容器。

image-20221025115146253

可以看到退出容器后,宿主机挂载的目录仍然存在,如果我们重新启动一个容器,将宿主机这个目录再次挂载,那么在容器内又可以看到这4个文件。这样就实现了数据卷的挂载。

5.3.4 容器启动和清理工作流程图

image-20221025121212620 ## 5.4 实现简单镜像打包

镜像打包原理很简单,因为容器退出后会删除掉merge目录,我们将merge目录的内容打包就可以了。

注意的是,我们不能在容器里面进行打包,因为容器里面的挂载和宿主机是隔离的,所以只能在容器正在运行的时候宿主机进行打包,因为容器退出就会进行清理。

类似docker,我们也是在宿主机进行打包的,因为docker的容器支持后台运行,我们现在还暂不支持。

首先我们实现命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var commitCommand = cli.Command{
Name: "commit",
Usage: "commit a container to image",
Action: func(ctx *cli.Context) error {
//缺少run参数
if len(ctx.Args()) < 1 {
return fmt.Errorf("missing container command")
}
containerId := ctx.Args().Get(0)
imageName := ctx.Args().Get(1)
commitContainer(containerId, imageName)
return nil
},
}

这里我们会使用./GoDocker commit [容器id] [镜像名称]进行打包,所以直接根据ctx获取参数即可。

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
// src/commit.go
func commitContainer(containerId, imageName string) {
containerDir := container.ContainerRootPath + containerId + "/merge"
// 校验容器id
containerExist, err := container.PathExists(containerDir)
if containerExist == false {
fmt.Printf("container %s not found!\n", containerId)
return
}
imageDir := container.ImageRootPath + imageName + ".tar"
// 如果镜像名称存在则返回
exists, err := container.PathExists(imageDir)
if err != nil {
return
}
if exists == true {
fmt.Printf("image name %s exists!\n", imageName)
return
}
_, err = exec.Command("tar", "-czf", imageDir, containerDir).CombinedOutput()
if err != nil {
fmt.Println("tar image fail!")
return
}
fmt.Printf("commit image to %s success\n", imageName)
}

5.4.1 测试效果

首先启动docker,然后进行打包。

image-20221026115835199
image-20221026115933282

可以看到参数不正确进行了提示,打包成功后出现在了镜像目录下。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!