
二、构造镜像
本章作为书籍《自己动手写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完成文件挂载。