这是《自己动手写Docker》的读书笔记,本篇为第一、二章的笔记,主要的内容为Docker的简介、安装,以及它涉及到的一些Linux系统知识。
实验环境参数如下
- 虚拟机 Parallel14
- Ubuntu 16.04
- Linux内核 4.10.0-28-generic
- Golang v1.12.4
Docker介绍及安装
一键安装Docker
1
| sudo curl -sSL https://get.docker.com | sh
|
几句话概括特点:
- 容器共享内核
- 容器之间相互隔离
- 不和任何基础设施绑定
- 就像集装箱一样,帮你打包好了一切,开箱即用
Linux Namespace
用来隔离一系列资源,以下为其六个可隔离的Namespace
Namespace类型 |
系统调用参数 |
内核版本 |
Mount |
CLONE_NEWNS |
2.4.19 |
UTS |
CLONE_NEWUTS |
2.6.19 |
IPC |
CLONE_NEWIPC |
2.6.19 |
PID |
CLONE_NEWPID |
2.6.24 |
NetWork |
CLONE_NEWNET |
2.6.29 |
User |
CLONE_NEWUSER |
3.8 |
以下为三个系统调用
- clone() 创建新进程
- unshare() 将进程移出某个Namespace
- setns() 将进程加入某个Namespace中
UTS
隔离 nodename
和 domainname
这两个系统标识,每个Namespace允许用于自己的 hostname
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| package main
import ( "log" "os" "os/exec" "syscall" )
func main() { cmd := exec.Command("sh") cmd.SysProcAttr = &syscall.SysProcAttr{ Cloneflags: syscall.CLONE_NEWUTS, } cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil { log.Fatal(err) } }
|
执行程序后将进入一个被fork出的sh环境中,在其中执行 echo $$
查看当前被fork出的进程的id,同时查看父进程的id,假设分别为7906和7902。执行如下命令
1 2 3 4
| sudo readlink /proc/7906/ns/uts
sudo readlink /proc/7902/ns/uts
|
发现两者所属的uts不一样。也可以修改子进程中的hostname,会发现父进程不受影响
IPC
隔离System V IPC和POSIX message queues,每一个IPC命名空间都有自己的System V IPC和POSIX message queues。以下为两个IPC指令
- 创建IPC message queue:
ipcmk -Q
- 查看IPC message queue:
ipcs -q
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| package main
import ( "log" "os" "os/exec" "syscall" )
func main() { cmd := exec.Command("sh") cmd.SysProcAttr = &syscall.SysProcAttr{ Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC, } cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil { log.Fatal(err) } }
|
PID
隔离PID,也就是进程的。容器中,前台允许的进程PID为1,但是在容器外,同样的进程却有着不同的ID。
可以使用pstree -pl
查看进程树
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
| package main
import ( "log" "os" "os/exec" "syscall" )
func main() { cmd := exec.Command("sh") cmd.SysProcAttr = &syscall.SysProcAttr{ Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID, } cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil { log.Fatal(err) } }
|
运行代码进入sh环境后,执行echo $$
,就会发现其根进程的PID为1
Mount
隔离各个进程看到的挂载点视图,执行 mount()
和 umount()
只会影响到当前Namespace内的文件系统,高配版chroot
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
| package main
import ( "log" "os" "os/exec" "syscall" )
func main() { cmd := exec.Command("sh") cmd.SysProcAttr = &syscall.SysProcAttr{ Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS, } cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil { log.Fatal(err) } }
|
在按照书中执行 mount -t proc proc /proc
后会出现一些小问题,详见这个issue
User
User Namespace主要是用于隔离用户的用户组ID。宿主机上非root用户映射成命名空间内的root用户
这里也不能按照书中的代码写,因为内核版本不一致,需要做一些就该,详见这个issue
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
| package main
import ( "log" "os" "os/exec" "syscall" )
func main() { cmd := exec.Command("sh") cmd.SysProcAttr = &syscall.SysProcAttr{ Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS | syscall.CLONE_NEWUSER, UidMappings: []syscall.SysProcIDMap{ { ContainerID: 1234, HostID: 0, Size: 1, }, }, GidMappings: []syscall.SysProcIDMap{ { ContainerID: 1234, HostID: 0, Size: 1, }, }, } cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil { log.Fatal(err) } os.Exit(-1) }
|
运行代码,可以使用id
来查看用户的信息,会发现他的UID
发生了改变
Network
隔离网络设备、UP地址端口等网络栈的Namespace。让每个容器拥有自己独立的(虚拟的)网络设备,并且容器内的应用可以绑定到自己的端口。
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
| package main
import ( "log" "os" "os/exec" "syscall" )
func main() { cmd := exec.Command("sh") cmd.SysProcAttr = &syscall.SysProcAttr{ Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS | syscall.CLONE_NEWUSER| syscall.CLONE_NEWNET, UidMappings: []syscall.SysProcIDMap{ { ContainerID: 1234, HostID: 0, Size: 1, }, }, GidMappings: []syscall.SysProcIDMap{ { ContainerID: 1234, HostID: 0, Size: 1, }, }, } cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { log.Fatal(err) } os.Exit(-1) }
|
执行 ifconfig
查看子进程的网络栈发现为空
Linux Cgroups
提供了对一组进程及将来子进程的资源限制、控制和统计的能力。包括CPU、内存、存储、网络等
三个组件
- cgroup 对进程分组管理的一种机制
- subsystem 一组资源控制模块
- hierarchy 把一组cgroup串成一个树状结构
三者的关系
- 系统创建了新的
hierarchy
之后,系统中的所有进程都会加入这个 hierarchy
的 cgroup
根节点,这个 cgroup
根节点是 hierarchy
默认创建的。
- 一个
subsystem
只能附加到一个 hierarchy
上面
- 一个
hierarchy
可以附加到多个 subsystem
上面
- 父进程fork出子进程,两者在用一个
cgroup
中
命令行实战
1 2
| mkdir cgroup-test sudo mount -t cgroup -o none,name=cgroup-test cgroup-test ./cgroup-test
|
此时 cgroup-test 文件夹中生成了一些列默认文件
- cgroup.clone_children 内容为1时子进程才会继承父进程的cpuset
- cgroup.procs 当前节点cgroup的进程组ID
- notify_on_release 和 release_agent 一起使用,详细用法略
- tasks 标识该cgroup下的进程ID
在这个文件夹内执行如下shell命令
1 2
| sudo mkdir cgroup-1 sudo mkdir cgroup-2
|
这样创建了两个子cgroup,可以将当前bash进程加入到子cgroup中
1 2
| sudo sh -c "echo $$ >> tasks" cat /proc/${PID}/cgroup
|
限制内存资源使用实战
执行如下shell指令
/sys/fs/cgroup/memory
目录挂在了 memory subsystem
的 hierarchy
上
在 memory
下创建一个子cgroup,并加入内存限制
1 2 3 4 5 6 7 8 9 10 11 12
| sudo mkdir test-limit-memory cd test-limit-memor
sudo sh -c ” echo ” lOOm ” > memory.limit_in_bytes ” sudo sh -c "echo $$ > tasks"
sudo sh -c "echo 1 >> memory.oom_control"
stress --vm-bytes 200m --vm-keep -m 1
|
可以在 /sys/fs/cgroup/memory/docker
下看到Docker使用的CGroup
使用Golang
有一个bug还未修复
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
| const cgroupMemoryHierarchMount = "/sys/fs/cgroup/memory"
func main() { if os.Args[0] == "/proc/self/exe" { log.Printf("current pid %d\n", syscall.Getpid()) cmd := exec.Command("sh", "-c", `stress --vm-bytes 200m --vm-keep -m 1`) cmd.SysProcAttr = &syscall.SysProcAttr{} cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { log.Fatal("stress has error", err) } }
cmd := exec.Command("/proc/self/exe") cmd.SysProcAttr = &syscall.SysProcAttr{ Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS, } cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Start(); err != nil { log.Fatal(err) } else { log.Printf("%v\n", cmd.Process.Pid) cgName := "testmemorylimit" var err error
if err = os.Mkdir(path.Join(cgroupMemoryHierarchMount, cgName), 0755); err != nil { if !strings.Contains(fmt.Sprintln(err), "file exists") { log.Fatal("mkdir error", err) } else { log.Println("mkdir warning ", err) } } if err = ioutil.WriteFile(path.Join(cgroupMemoryHierarchMount, cgName, "tasks"), []byte(strconv.Itoa(cmd.Process.Pid)), 0644); err != nil { log.Fatal("write to tasks error", err) }
fspath := path.Join(cgroupMemoryHierarchMount, cgName, "memory.oom_control") if err= exec.Command("echo", "1 >>"+ fspath).Run(); err != nil { log.Fatal("write to memory.oom_control error", err) }
if err = ioutil.WriteFile(path.Join(cgroupMemoryHierarchMount, cgName, "memory.limit_in_bytes"), []byte("100m"), 0644); err != nil { log.Fatal("write to memory.limit_in_bytes error", err) } } if _, err := cmd.Process.Wait(); err != nil { log.Fatal("cmd.Process.Wait error", err) } }
|
Union File System
概述
把其他文件系统联合到一个联合挂载点的文件系统服务。使用到 copy-on-write 技术,减少资源的占用。
AUFS
为其的升级版,为Docker支持的一种存储驱动类型。每一个Docker image 都是由一系列read-only layer组成的。image layer的内容存储在 /var/lib/docker/aufs/diff
下;layer的metadata信息在 /var/lib/docker/aufs/layers
下。
命令行模拟
执行一下bash脚本生成测试目录结构
1 2 3 4 5 6 7 8 9 10 11 12 13
| mkdir mnt container-layer touch container-layer/container-layer.txt echo "I am container layer" >> container-layer/container-layer.txt
for i in {1..4} do dirname=image-layer${i} filename=${dirname}/image-layer${i}.txt
mkdir $dirname touch ${filename} echo "I am image layer ${i}" >> ${filename} done
|
之后再执行挂载至mnt下的操作
1
| sudo mount -t aufs -o dirs=./container-layer:./image-layer4:./image-layer3:./image-layer2:./image-layer1 none ./mnt
|
在我个人的电脑上查看aufs的信息
1 2 3 4 5 6
| cat /sys/fs/aufs/si_a236c06529c6ac94/* | less
|
只有第一个是可写的
修改 mnt/image-layer4.txt
中的信息,会发现在文件,也就是 image-layer4/image-layer4.txt
中的内容并没有改变,而是在 container-layer/
下,也就是可写层中多出了一个最新的 image-layer4.txt
文件