本章作为书籍《自己动手写Docker》的读书笔记

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

0、命令行工具的使用

github.com/urfave/cli提供命令行工具。

main.go:

package main

import (
	log "github.com/Sirupsen/logrus"
	"github.com/urfave/cli"
	"os"
)

const usage = `mydocker is a simple container runtime implementation.
			   The purpose of this project is to learn how docker works and how to write a docker by ourselves
			   Enjoy it, just for fun.`

func main() {
	app := cli.NewApp()
	app.Name = "mydocker"
	app.Usage = usage

	app.Commands = []cli.Command{
		initCommand,
		runCommand,
		commitCommand,
	}

	app.Before = func(context *cli.Context) error {
		// Log as JSON instead of the default ASCII formatter.
		log.SetFormatter(&log.JSONFormatter{})

		log.SetOutput(os.Stdout)
		return nil
	}

	if err := app.Run(os.Args); err != nil {
		log.Fatal(err)
	}
}

main_command.go:

var runCommand = cli.Command{
	Name: "run",
	Usage: `Create a container with namespace and cgroups limit
			mydocker run -ti [command]`,
	Flags: []cli.Flag{
		cli.BoolFlag{
			Name:  "ti",
			Usage: "enable tty",
		},
		cli.StringFlag{
			Name:  "v",
			Usage: "volume",
		},
	},
	Action: func(context *cli.Context) error {
		if len(context.Args()) < 1 {
			return fmt.Errorf("Missing container command")
		}
		var cmdArray []string
		for _, arg := range context.Args() {
			cmdArray = append(cmdArray, arg)
		}
		tty := context.Bool("ti")
		volume := context.String("v")
		Run(tty, cmdArray, volume)
		return nil
	},
}

var initCommand = cli.Command{
	Name:  "init",
	Usage: "Init container process run user's process in container. Do not call it outside",
	Action: func(context *cli.Context) error {
		log.Infof("init come on")
		err := container.RunContainerInitProcess()
		return err
	},
}

var commitCommand = cli.Command{
	Name:  "commit",
	Usage: "commit a container into image",
	Action: func(context *cli.Context) error {
		if len(context.Args()) < 1 {
			return fmt.Errorf("Missing container name")
		}
		imageName := context.Args().Get(0)
		//commitContainer(containerName)
		commitContainer(imageName)
		return nil
	},
}

cli.app创建一个主命令行工具,cli.command是子命令,也就是附加属性。使用方法就是./mydocker run xxx /commit xxx

[root@k8s-node3 mydocker]# ./mydocker
NAME:
   mydocker - mydocker is a simple container runtime implementation.
         The purpose of this project is to learn how docker works and how to write a docker by ourselves
         Enjoy it, just for fun.

USAGE:
   mydocker [global options] command [command options] [arguments...]

VERSION:
   0.0.0

COMMANDS:
     init     Init container process run user's process in container. Do not call it outside
     run      Create a container with namespace and cgroups limit
                  mydocker run -ti [command]
     commit   commit a container into image
     help, h  Shows a list of commands or help for one command

GLOBAL OPTIONS:
   --help, -h     show help
   --version, -v  print the version

1、使用AUFS包装busybox

Docker 在使用镜像启动一个容器时,会新建两个layer: write layer 和container-init layer。

write layer 是容器唯一的可读写层;而 container-init layer 是为容器新建的只读层,用来存储容器启动时传入的系统信息(前面也提到过,在实际的场景下,它们并不是 write layer 和 container-init layer 命名的)。最后把 write layer、container-init layer 和相关镜像的 layers 都 mount 到一个 mnt 目录下,然后把这个 mnt 目录作为容器启动的根目录。

NewWorkSpace 函数是用来创建容器文件系统的,它包括 CreateReadOnlyLayer、CreateWriteLayer、CreateMountPoint:

  • CreateReadOnlyLayer 函数新建 busybox 文件夹,将 busybox.tar 解压到 busybox 目录下,作为容器的只读层。

  • CreateWriteLayer 函数创建了名为 writeLayer 的文件夹,作为容器唯 一的可写层。

  • 在 CreateMountPoint 函数中,首先创建了 mnt 文件夹,作为挂载点,然后把 writeLayer 目录和 busybox 目录 mount 到 mnt 目录下。

//Create a AUFS filesystem as container root workspace
func NewWorkSpace(rootURL string, mntURL string) {
	CreateReadOnlyLayer(rootURL)
	CreateWriteLayer(rootURL)
	CreateMountPoint(rootURL, mntURL)
}

func CreateReadOnlyLayer(rootURL string) {
	busyboxURL := rootURL + "busybox/"
	busyboxTarURL := rootURL + "busybox.tar"
	exist, err := PathExists(busyboxURL)
	if err != nil {
		log.Infof("Fail to judge whether dir %s exists. %v", busyboxURL, err)
	}
	if exist == false {
		if err := os.Mkdir(busyboxURL, 0777); err != nil {
			log.Errorf("Mkdir dir %s error. %v", busyboxURL, err)
		}
		if _, err := exec.Command("tar", "-xvf", busyboxTarURL, "-C", busyboxURL).CombinedOutput(); err != nil {
			log.Errorf("Untar dir %s error %v", busyboxURL, err)
		}
	}
}

func CreateWriteLayer(rootURL string) {
	writeURL := rootURL + "writeLayer/"
	if err := os.Mkdir(writeURL, 0777); err != nil {
		log.Errorf("Mkdir dir %s error. %v", writeURL, err)
	}
}

func CreateMountPoint(rootURL string, mntURL string) {
	if err := os.Mkdir(mntURL, 0777); err != nil {
		log.Errorf("Mkdir dir %s error. %v", mntURL, err)
	}
	dirs := "dirs=" + rootURL + "writeLayer:" + rootURL + "busybox"
	cmd := exec.Command("mount", "-t", "aufs", "-o", dirs, "none", mntURL)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	if err := cmd.Run(); err != nil {
		log.Errorf("%v", err)
	}
}

Docker 会在删除容器的时候,把容器对应 Write Layer 和 Container-init Layer 删除,而保留镜像所有的内容。在容器退出的时候删除 WriteLayer。DeleteWorkSpace 函数包括 DeleteMountPoint、DeleteWriteLayer:

  • 首先,在 DeleteMountPoint 函数中 umount mnt 目录。

  • 然后,删除 mnt 目录。

  • 最后,在 DeleteWriteLayer 函数中删除 writeLayer 文件夹。

//Delete the AUFS filesystem while container exit
func DeleteWorkSpace(rootURL string, mntURL string){
	DeleteMountPoint(rootURL, mntURL)
	DeleteWriteLayer(rootURL)
}

func DeleteMountPoint(rootURL string, mntURL string){
	cmd := exec.Command("umount", mntURL)
	cmd.Stdout=os.Stdout
	cmd.Stderr=os.Stderr
	if err := cmd.Run(); err != nil {
		log.Errorf("%v",err)
	}
	if err := os.RemoveAll(mntURL); err != nil {
		log.Errorf("Remove dir %s error %v", mntURL, err)
	}
}

func DeleteWriteLayer(rootURL string) {
	writeURL := rootURL + "writeLayer/"
	if err := os.RemoveAll(writeURL); err != nil {
		log.Errorf("Remove dir %s error %v", writeURL, err)
	}
}

就是在执行 mydocker run 启动容器的时候,就会在/root下生成3个文件夹,busybox文件夹就是只读层,writelayer就是可写层,还有mnt目录,就是busybox和writelayer挂载过来的。

在AUFS挂载时,默认第一个参数是可写层,在容器内写文件就会写到writelayer目录下。在退出容器时,取消挂载并删除writelayer和mnt目录,只留下了busybox目录。

2、volume数据卷

上一小节介绍了如何包装 busybox ,从而实现容器和镜像的分离。但是一旦容器退出,容器可写层的所有内容都会被删除。那么,如果用户需要持久化容器里的部分数据该怎么办呢? volume 就是用来解决这个问题的。本节将会介绍如何实现将宿主机的目录作为数据卷挂载到容器中,井且在容器退出后,数据卷中的内容仍然能够保存在宿主机上。

//Create a AUFS filesystem as container root workspace
func NewWorkSpace(rootURL string, mntURL string, volume string) {
	CreateReadOnlyLayer(rootURL)
	CreateWriteLayer(rootURL)
	CreateMountPoint(rootURL, mntURL)
	if(volume != ""){
		volumeURLs := volumeUrlExtract(volume)
		length := len(volumeURLs)
		if(length == 2 && volumeURLs[0] != "" && volumeURLs[1] !=""){
			MountVolume(rootURL, mntURL, volumeURLs)
			log.Infof("%q",volumeURLs)
		}else{
			log.Infof("Volume parameter input is not correct.")
		}
	}
}

func MountVolume(rootURL string, mntURL string, volumeURLs []string)  {
	parentUrl := volumeURLs[0]
	if err := os.Mkdir(parentUrl, 0777); err != nil {
		log.Infof("Mkdir parent dir %s error. %v", parentUrl, err)
	}
	containerUrl := volumeURLs[1]
	containerVolumeURL := mntURL + containerUrl
	if err := os.Mkdir(containerVolumeURL, 0777); err != nil {
		log.Infof("Mkdir container dir %s error. %v", containerVolumeURL, err)
	}
	dirs := "dirs=" + parentUrl
	cmd := exec.Command("mount", "-t", "aufs", "-o", dirs, "none", containerVolumeURL)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	if err := cmd.Run(); err != nil {
		log.Errorf("Mount volume failed. %v", err)
	}

}
//Delete the AUFS filesystem while container exit
func DeleteWorkSpace(rootURL string, mntURL string, volume string){
	if(volume != ""){
		volumeURLs := volumeUrlExtract(volume)
		length := len(volumeURLs)
		if(length == 2 && volumeURLs[0] != "" && volumeURLs[1] !=""){
			DeleteMountPointWithVolume(rootURL, mntURL, volumeURLs)
		}else{
			DeleteMountPoint(rootURL, mntURL)
		}
	}else {
		DeleteMountPoint(rootURL, mntURL)
	}
	DeleteWriteLayer(rootURL)
}

func DeleteMountPointWithVolume(rootURL string, mntURL string, volumeURLs []string){
	containerUrl := mntURL + volumeURLs[1]
	cmd := exec.Command("umount", containerUrl)
	cmd.Stdout=os.Stdout
	cmd.Stderr=os.Stderr
	if err := cmd.Run(); err != nil {
		log.Errorf("Umount volume failed. %v",err)
	}

	cmd = exec.Command("umount", mntURL)
	cmd.Stdout=os.Stdout
	cmd.Stderr=os.Stderr
	if err := cmd.Run(); err != nil {
		log.Errorf("Umount mountpoint failed. %v",err)
	}

	if err := os.RemoveAll(mntURL); err != nil {
		log.Infof("Remove mountpoint dir %s error %v", mntURL, err)
	}
}

在通过命令 ./mydocker run -it -v /root/volume:/containerVolume sh启动容器时,就会在宿主机/root下创建一个volume目录,在容器根目录也就是/root/mnt下创建一个containerVolume,并完成挂载。这样容器内在containervolume内创建文件,会自动同步到/root/volume下。

在退出容器时,删除Volume挂载点和容器内的containerVolume,但是留下/root/volume。

3、镜像打包

容器退出时会删除所有可写层的内容,mydocker commit命令把运行状态容器的内容存储成镜像保存下来。

func commitContainer(imageName string){
	mntURL := "/root/mnt"
	imageTar := "/root/" + imageName + ".tar"
	fmt.Printf("%s",imageTar)
	if _, err := exec.Command("tar", "-czf", imageTar, "-C", mntURL, ".").CombinedOutput(); err != nil {
		log.Errorf("Tar folder %s error %v", mntURL, err)
	}
}

打包就是把mnt目录用tar压缩出来。

所以docker就是一个命令行工具的名字,里面实现了很多cli.command,根据附加参数,执行对应的action函数。run 函数启动一个子进程,并利用namespace做资源隔离,cgroup做资源限制,ufs完成文件挂载。