《自己动手写Docker》学习笔记2
1 前言
由于本人毕业设计与云原生领域相关,因此最近在学习Docker相关知识,《自己动手写Docker》涵盖了Docker底层的各类知识,还提供了各类实验及代码Demo,是入门云原生的不错之书。本文主要是记录本人的实验过程以及一些总结感悟。
本文所涉及的实验环境:
Vmware Workstation 16 搭建Ubuntu20.04环境
Linux内核为5.10.x
Go版本1.17.1
2 第二章 基础技术
2.1 Linux Namespace
Linux Namespace 是Kernel的一个功能,它可以隔离一系列的系统资源,比如 PID ( Process ID )、User ID、Network 等。
当前 Linux 共实现了6种不同类型的 Namespace。
Namespace API 主要使用如下 个系统调用
-
clone ()创建新进程。根据系统调用参数来判断哪些类型的 Namespace 被创建,而且它们的子进程也会被包含到这些 Namespace 中。
-
unshare()将进程移出某个 Namespace。
-
setns ()将进程加入到 Namespace 中。
UTS NameSpace
#在/src 目录下创建main.go文件进行实验
vim main.go
#写入
package main
import (
"os/exec"
"syscall"
"os"
"log"
)
func main() {
cmd := exec.Command("sh")//指定被fork()出来的新进程内的初始化进程
cmd.SysProcAttr = &syscall.SysProcAttr{
//已经封装clone,直接进行调用就好
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)
}
}
#退出,并保存
#输入go run main.go 进入namespace 空间
go run main.go
IPC NameSpace
IPC Namespace 是用来隔离 System V IPC 和POSIX message queues.每一个IPC Namespace都有他们自己的System V IPC 和POSIX message queue。
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, #此处添加IPC命名空间
}
cmd.Stdin=os.Stdin
cmd.Stdout=os.Stdout
cmd.Stderr=os.Stderr
if err :=cmd.Run(); err!=nil{
log.Fatal(err)
}
}
PID NameSpacce
PID namespace是用来隔离进程 id。同样的一个进程在不同的 PID Namespace 里面可以拥有不同的 PID。
可以这样理解,在 docker container 里面,我们使用ps -ef
发现,容器内在前台跑着的那个init进程的 PID 是1,但是我们在容器外,使用ps -ef
会发现同样的进程却有不同的 PID,这就是PID namespace 干的事情。
修改前
修改
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, #此处添加PID命名空间
}
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run();err!=nil{
log.Fatal(err)
}
}
修改后,进入命名空间
Mount NameSpace
之前的Domo中,暂时不能使用top和ps查看,因为其会使用/proc内容
Linux系统上的/proc目录是一种文件系统,即proc文件系统。与其它常见的文件系统不同的是,/proc是一种伪文件系统(也即虚拟文件系统),存储的是当前内核运行状态的一系列特殊文件,用户可以通过这些文件查看有关系统硬件及当前正在运行进程的信息,甚至可以通过更改其中某些文件来改变内核的运行状态。
基于/proc文件系统如上所述的特殊性,其内的文件也常被称作虚拟文件
修改代码,添加MNT命名空间
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,#此处添加命名空间mnt
}
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run();err!=nil{
log.Fatal(err)
}
}
未挂载/proc前
挂载/proc
mount -t proc proc/proc
ps -ef
User NameSpace
User namespace 主要是隔离用户的用户组ID。也就是说,一个进程的User ID 和Group ID 在User namespace 内外可以是不同的。
比较常用的是,在宿主机上以一个非root用户运行创建一个User namespace,然后在User namespace里面却映射成root 用户。
这个进程在User namespace里面有root权限,但是在User namespace外面却没有root的权限。
这里需要注意: CentOS 默认关闭了User namespace,而且内核版本太高会导致代码无法运行,请确认内核版本之后再修改。
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, #此处增加User namespace
}
cmd.SysProcAttr.Credential = &syscall.Credential{Uid: uint32(1), Gid: uint32(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)
}
NetWork NameSpace
Network namespace 是用来隔离网络设备,IP地址端口等网络栈的namespace。Network namespace 可以让每个容器拥有自己独立的网络设备(虚拟的),而且容器内的应用可以绑定到自己的端口,每个 namesapce 内的端口都不会互相冲突。在宿主机上搭建网桥后,就能很方便的实现容器之间的通信,而且每个容器内的应用都可以使用相同的端口。
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,
}
cmd.SysProcAttr.Credential = &syscall.Credential{Uid: uint32(1), Gid: uint32(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)
}
宿主机网络
命名空间内网络
可以看到宿主机上有lo, eth0, eth1 等网络设备,而在Namespace 里面什么网络设备都没有。这样就能展现 Network namespace 与宿主机之间的网络隔离。
2.2 Cgroups
Linux Cgroups (Control Groups )提供了对一组进程及将来子进程的资源限制、控制和统计的能力,这些资源包括 CPU、内存、存储、网络等。
Cgroups 中的3个组件
- cgroup : 对一组经常进行管理,将一组进程和一组subsystem关联起来。
- subsystem:一组资源控制模块,主要包含对内存和CPU的限制
- hierarchy:将一组cgroup串成一个树状结构,通过树状结构cgroup可以做到继承。
用go语言实现通过cgroup限制容器的资源
package main
import (
"os/exec"
"path"
"os"
"fmt"
"io/ioutil"
"syscall"
"strconv"
)
const cgroupMemoryHierarchyMount = "/sys/fs/cgroup/memory"
func main() {
if os.Args[0] == "/proc/self/exe" {
//容器进程
fmt.Printf("current pid %d", syscall.Getpid())
fmt.Println()
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 {
fmt.Println(err)
os.Exit(1)
}
}
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 {
fmt.Println("ERROR", err)
os.Exit(1)
} else {
//得到fork出来进程映射在外部命名空间的pid
fmt.Printf("%v", cmd.Process.Pid)
// 在系统默认创建挂载了memory subsystem的Hierarchy上创建cgroup
os.Mkdir(path.Join(cgroupMemoryHierarchyMount, "testmemorylimit"), 0755)
// 将容器进程加入到这个cgroup中
ioutil.WriteFile(path.Join(cgroupMemoryHierarchyMount, "testmemorylimit", "tasks") , []byte(strconv.Itoa(cmd.Process.Pid)), 0644)
// 限制cgroup进程使用
ioutil.WriteFile(path.Join(cgroupMemoryHierarchyMount, "testmemorylimit", "memory.limit_in_bytes") , []byte("100m"), 0644)
}
cmd.Process.Wait()
}
通过对Cgroups虚拟文件系统的配置,我们让容器中的把stress进程的内存占用限制到了100m
。
2.3 Union File System
主要涉及以下几个重点内容:
- 写时复制( copy-on-write ,下文简称 CoW ),也叫隐式共享,是一种对可修改资源实现高效复制的资源管理技术,通俗的讲,就是每次写都是对原文件的副本进行操作,原文件并不会被修改,而且之后的修改都是对这一个副本进行操作,即只在第一次修改时进行复制。
- Docker镜像的分层文件系统,该结构是利用AUFS机制,AUFS具有快速启动容器、高效利用存储和内存的优点。每个镜像具有树状结构,上层镜像利用底层镜像。
- Docker 使用 CoW 技术来实现 image layer和减少磁盘空间占用。Cow 意味着一旦某个文件只有很小的部分有改动,也需要复制整个文件。不过也不用过度担心,对于每个容器而言 ,每个 image layer 最多只需要复制1次。后续的改动都会在第一次拷贝的 container layer 上进行。