在开始写docker之前要明白GOPATH是代码的路径,GO寻找依赖包会根据GOPATH来找,下面有三个目录,pkg存放的是依赖包,src存放源代码,bin存放编译后的可执行文件
1. Linux NameSpace
namespace是一种 隔离技术,允许容器实现系统资源的隔离,例如PID(process ID),UID(User ID),Network。
举个例子:与普通的linux用户隔离不同,linux的用户的操作有时候需要用到root权限。而namespace里的用户可以被赋予在这个命名空间的内的root权限,实现了UID的隔离,这才是用户的真正隔离。
除了UsrNamespace,PID也是可以被虚拟的,在每一个子命名空间内,都会有自己的init进程等等进程,映射到父命名空间中。
1.1 UTS Namespace
UTS namespace是用来隔离nodename和domainname两个系统标识,在UTS namespace里每个namespace允许拥有自己的hostname
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.Stdin=os.Stdin
cmd.Stderr=os.Stderr
if err:=cmd.Run();err!=nil{
log.Fatal(err)
}
}
复制代码
os/exec用来运行外部指令,它有一个type是Cmd
可以方便的开启进程使用通道通信和一些io口的读写。
这其中的Command函数命令返回Cmd结构以执行带有给定参数的命名程序。
它只设置返回结构中的路径和参数。
如果name不包含路径分隔符,Command将使用LookPath尽可能将name解析为完整路径。否则它直接使用name作为Path。返回的Cmd的Args字段是由命令名后跟arg元素构造的,所以arg不应该包括命令名本身。例如Command(“echo”, “hello”)。参数[0]总是名称,而不是可能解析的路径。
sysprocttr是一些参数,具体用途未知
log.Fatal触发后 打印输出内容、 退出应用程序、 defer函数不会执行。而panic是会执行defer的并且递归到上一级。go和panic的区别
这段代码fork什么是fork?(英语:fork,又译作派生、分支)是UNIX或类UNIX中的分叉函数,fork函数将运行着的程序分成2个(几乎)完全一样的进程,每个进程都启动一个从代码的同一位置开始执行的线程。这两个进程中的线程继续执行,就像是两个用户同时启动了该应用程序的两个副本
出了一个新进程的sh,他使用的是 fork/exec /usr/bin/sh下的sh需要有root权限。echo $$
打印出了当前的PID
我们想要查看父进程和子进程是不是在同一个namespace中,pstree -pl
列出所有的进程的树形图关系,发现sh父进程就是我们的projectname awesomeProject(PID 51903)。
使用hostname -b zhouxiaohao
改变主机名称发现和外面的hostname没有发声变化。说明被隔离了。
1.2 IPC Namespace
IPC Namespace用来隔离system V IPC和POSIX mesage(消息队列的一种)。
system V 是一种unix的内核架构,它引入了三种高级进程通信机制(interative process communication?)消息队列,共享内存,信号量。而这些都是IPC对象,通过独自的ID调用他们。
ipcs -q: 只显示消息队列。
ipcs -s: 只显示信号量。
ipcs -m: 只显示共享内存。
ipcs –help: 其他的参数
复制代码
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.Stdin=os.Stdin
cmd.Stderr=os.Stderr
if err:=cmd.Run();err!=nil{
log.Fatal(err)
}
}
复制代码
在主机上创建一个消息队列,在我们的容器上是看不到的,说明linux namespace也隔离了ipc namespace,因为一般消息队列是所有进程共享的,都能看到的。
1.3 PID Namespace
PID Namespace 是用来隔离进程 ID 。同样一 进程在不 同的 PID Namespace 里可有不同的 PID,同样的再把之前的程序稍微改一下。然后再主机里查看pstree -pl
在容器内使用echo $$
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.Stdin=os.Stdin
cmd.Stderr=os.Stderr
if err:=cmd.Run();err!=nil{
log.Fatal(err)
}
}
复制代码
可以看到主机上的当前PID和我在容器内的PID编码不一样了。
1.4 mount Namespace
我们首先要知道:Linux系统上的/proc目录是一种文件系统,即proc文件系统。与其它常见的文件系统不同的是,/proc是一种伪文件系统(也即虚拟文件系统),存储的是当前内核运行状态的一系列特殊文件,用户可以通过这些文件查看有关系统硬件及当前正在运行进程的信息,甚至可以通过更改其中某些文件来改变内核的运行状态。
mount Namespace是用来隔离挂载点的,在我们的容器内部看到的文件目录结构和主机的文件结构不同,在不同的mount Namespace中看到的文件层次是不一样的,在mount namespace 中调用Mount()和 unmount()仅仅会影响当前的namespace内的文件系统,而对全局的文件系统不会产生什么影响。有点类似于chroot
命令,将一个节点变成根节点,只是更加安全灵活。
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.Stdin=os.Stdin
cmd.Stderr=os.Stderr
if err:=cmd.Run();err!=nil{
log.Fatal(err)
}
}
复制代码
使用ls /proc
会发现很多内容,因为有一些是宿主机的,使用mount -t proc proc /proc
把这个/proc mount 到当前的容器内,会少很多,然后就能用ps -ef
查看进程
1.5 User Namespace
User Namespace 主要是隔离用户用户组 ID,就是说 一个进程的 uer ID、Group ID、User Namespace 内外 是不同。较常用是,在宿主机上以一个非 root 用户运行创建一个 User Namespace 然后在 User Namespace 里面却映射成 root 用户。这意味着这个进程在 User Namespace 里面有 root 权限,但是在 User mespace 外面却没有 root 的权限。从Linux Kernel 3.8开始 root 进程也可 User Name pace 并且此用户在 namespoce 里面可以被映射成 root 且在 Namespace有root 权限。
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,
}
//让指定的系统用户去打开子进程
cmd.SysProcAttr.Credential=&syscall.Credential{Uid:uint32(1),Gid:uint32(1)}
cmd.Stdin=os.Stdin
cmd.Stdout=os.Stdout
cmd.Stdin=os.Stdin
cmd.Stderr=os.Stderr
if err:=cmd.Run();err!=nil{
log.Fatal(err)
}
os.Exit(-1)
}
复制代码
按照上面的程序会发生permission deny的问题,vim /etc/passwd
中列出了用户的信息
用户名:密码:UID:GID:用户信息:HOME目录路径:用户shell,发现打开子进程的是daemon守护进程用户。所以有可能是这个原因?
可以看到UID不同说明user namespace生效了。
参考的博客linux 下查看用户和用户组
1.6 Network Namespace
Network Namespace 是用来隔离网络设备、 IP 地址端口 等网络械的 Namespace Network
Namespace 可以让每个容器拥有自己独立的(虚拟的)网络设备,而且容器内的应用可以绑定
到自己的端口,每个 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 | syscall.CLONE_NEWNET,
}
//cmd.SysProcAttr.Credential=&syscall.Credential{Uid:uint32(1),Gid:uint32(1)}
cmd.Stdin=os.Stdin
cmd.Stdout=os.Stdout
cmd.Stdin=os.Stdin
cmd.Stderr=os.Stderr
if err:=cmd.Run();err!=nil{
log.Fatal(err)
}
os.Exit(-1)
}
复制代码
可以在主机和容器内分别查看ifconfig
,会发现内部没有网络设备,但是我们会看到有warning
2. Linux Cgroup
2.1 什么是Control Group?
www.hangdaowangluo.com/archives/24…
zhuanlan.zhihu.com/p/81668069
tech.meituan.com/2015/03/31/…
Cgroups全称Control Groups,是Linux内核提供的物理资源隔离机制,通过这种机制,可以实现对Linux进程或者进程组的资源限制、隔离和统计功能。(修改配置文件/etc/cgconfig.conf及/etc/cgrules.conf )
比如可以通过cgroup限制特定进程的资源使用,比如使用特定数目的cpu核数和特定大小的内存,如果资源超限的情况下,会被暂停或者杀掉。
我找了很多参考文档,最后整理出自己的理解如下
需要简单知道的几个概念:
- 任务(task): 在cgroup中,任务就是一个进程。
- 控制组(control group): cgroup的资源控制是以控制组的方式实现,控制组指明了资源的配额限制。进程可以加入到某个控制组,也可以迁移到另一个控制组。
- 层级(hierarchy): 控制组有层级关系,类似树的结构,子节点的控制组继承父控制组的属性(资源配额、限制等)。
- 子系统(subsystem): 一个子系统其实就是一种资源的控制器,比如memory子系统可以控制进程内存的使用。子系统需要加入到某个层级,然后该层级的所有控制组,均受到这个子系统的控制。
子系统的类别:
- cpu: 限制进程的 cpu 使用率。
- cpuacct 子系统,可以统计 cgroups 中的进程的 cpu 使用报告。
- cpuset: 为cgroups中的进程分配单独的cpu节点或者内存节点。
- memory: 限制进程的memory使用量。
- blkio: 限制进程的块设备io。
- devices: 控制进程能够访问某些设备。
- net_cls: 标记cgroups中进程的网络数据包,然后可以使用tc模块(traffic control)对数据包进行控制。
- net_prio: 限制进程网络流量的优先级。
- huge_tlb: 限制HugeTLB的使用。
- freezer:挂起或者恢复cgroups中的进程。
- ns: 控制cgroups中的进程使用不同的namespace。
各个概念之间的关系:
一个Cgroup内可以控制很多种资源的配置,就是可以配置上很多种subsystem。而我们的hierarchy通过了不同的cgroup分层,定义了很多种资源的分配组合,我们可以让我们的task去引用不同的hierarchy中的子节点来得到这种分配的组合方式。但是注意同一种资源配置不应该有多个定义。所以有了一下几个规则
- 不同的hierarchy之间不能拥有同一种subsystem
- 一个task不能引用一个hierarchy的里的多个cgroup
- 刚刚fork出来的父子进程在同一个cgroup内,但是这种关系随后可以改变
两个任务组成了一个 Task Group,并使用了 CPU 和 Memory 两个子系统的 cgroup,用于控制 CPU 和 MEM 的资源隔离
查看subsystem需要安装cgroup的toolapt install cgroup-tools
,然后查看子系统lssubsys -a
,发现打不开,是因为这个mounts文件夹为红色( 红色:压缩文件. 绿色:可执行文件,包括jar. 白色:文本文件. 红色闪烁:错误的符号链接. 淡蓝色:符号链接. 黄色:设备文件)
解决办法:使用mount -t proc proc /proc
,把外面的proc文件装载到容器内,然后再来查看子系统,为什么每次都要装载一下呢?我们的top命令,以及pstree,以及我们的lssubsys都需要用到proc文件夹。
2.2 Linux Kernel如何实现cgroup
Linux用一个树状的虚拟文件系统来表示hierarchy结构,通过层级的文件目录来表示cgroup,具体操作如下图
- cgroup.clone_children,这个配置文件的数值默认是0,如果为1的时候就是子节点继承父结点的属性
- cgroup.procs可以查看到当前节点cgroup的进程组ID,当前是根节点,会看到整个hierarchy内的所有进程
- notify_no_release是一个标注,表示当前cgroup最后一个进程退出后是否执行了release_agent,而当前的release_agent则是一个路径,通常用来在进程退出后清理掉不在使用的cgroup
- tasks标识了当前节点下的进程id,如果把一个PID放进里面,则表示加入了这个cgroup
可以看到子节点继承了父节点的属性
把当前的terminal的移动到一个cgroup中去,如下
但是做到这一步并没有关联到任何subsystem,也就是对我们的cgroup没有任何限制,其实系统已经对每个subsystem创建了一个hierarchy如下图,从下图中我们看大memory subsystem被挂载在了/sys/fs/cgroup/memory下面,我们可以直接在这个hierarchy下面创建cgroup来限制进程的内存。
创建一个stress占用200m内存,然后把他放在我们的限制100m内存的cgroup中查看效果
这是我们还没开始限制内存的时候的stress进程用top
查看的结果
具体操作如下图
这是再来使用top
看看内存占用,发现变成了一半
2.3 GOlang实现cgroup
这一步就是将上面那个限制memroy的kernel操作通过一个go脚本实现
package main
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"strconv"
"syscall"
)
const cgroupMemoryHierarchyMounted="/sys/fs/cgroup/memory"
func main(){
if os.Args[0]=="/proc/self/exe"{
//thread of container
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.Stdin=os.Stdin
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_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS | syscall.CLONE_NEWUSER | syscall.CLONE_NEWNET,
}
//cmd.SysProcAttr.Credential=&syscall.Credential{Uid:uint32(0),Gid:uint32(0)}
cmd.Stdin=os.Stdin
cmd.Stdout=os.Stdout
cmd.Stdin=os.Stdin
cmd.Stderr=os.Stderr
if err:=cmd.Run();err!=nil{
fmt.Println(err)
os.Exit(1)
}else {
fmt.Printf("%v", cmd.Process.Pid)
//mkdir the cgroup-1 on the hierarchy of the default memory subsystem
os.Mkdir(path.Join(cgroupMemoryHierarchyMounted,"testmemroylimits"),0755)
//add the container into the cgroup
ioutil.WriteFile(path.Join(cgroupMemoryHierarchyMounted,"testmemorylimits","tasks"),[]byte(strconv.Itoa(cmd.Process.Pid)),0644)
//limit the cgroup usage
ioutil.WriteFile(path.Join(cgroupMemoryHierarchyMounted,"testmemroylimits","memory.limit_in_bytes"),[]byte("100m"),0644)
}
//Wait waits for the Process to exit, and then returns a ProcessState describing its status and an error, if any. Wait releases any resources associated with the Process. On most operating systems, the Process must be a child of the current process or an error will be returned.
cmd.Process.Wait()
}
复制代码
在第二个exec:=Command()已经fork出了一个子进程,那么把当前的这个子进程放入这个limit里,stress进程应该是不会收到影响的?
由于之前在user namespace的报错可以看出来,控制台是从/proc/self/exe启动的?第一次不会执行if语句内的内容,因为args[0]不符合条件,然后fork出来的子进程就是进入到当前main的二进制文件内,满足了因为是一个新的进程,从main开始,然后可以执行if语句中的内容,然后就推退出了,最后还是没能把内容stress加入到限制中。从top得到的结果可见一斑,memeory还是5%。
把args[0]打印出来,就知道执行顺序了
参考的一些操作文档:
mount: www.linuxprobe.com/mount-detai…
3.Union File System
什么是UFS?早期docker使用的是AUFS是UFS的进阶版。它让一个容器内有许多种文件系统,方便的利用和管理资源。
AUFS下文件读操作
- 1、文件存在于container-layer:直接从container-layer进行读取;
- 2、文件不存在于container-layer:自container-layer下一层开始,逐层向下搜索,找到该文件,并从找到文件的那一层layer中读取文件;
- 3、当文件同时存在于container-layer和image-layer,读取container-layer中的文件。
简而言之,从container-layer开始,逐层向下搜索,找到该文件即读取,停止搜索。
AUFS下修改文件或目录
写操作
- 1、对container-layer中已存在的文件进行写操作:直接在该文件中进行操作(新文件直接在container-layer创建&修改);
- 2、对image-layers中已存在的文件进行写操作:将该文件完整复制到container-layer,然后在container-layer对这份copy进行写操作;
删除
- 1、删除container-layer中的文件/目录:直接删除;
- 2、删除image-layers中的文件:在container-layer中创建whiteoutfile,image-layer中的文件不会被删除,但是会因为whiteout而变得对container而言不可见;
- 3、删除image-layers中的目录:在container-layer中创建opaquefile,作用同whiteout;
重命名
- 1、container-layer文件/目录重命名:直接操作;
- 2、image-layer文件重命名:
- 3、image-layer目录重命名:在docker的AUFS中没有支持,会触发EXDEV。
写时复制技术:当开启一个进程的副本,比如说一个父进程开启了一个子进程,不会立刻复制用到的资源,而是共享他们,直到子进程对某个资源进行了写操作,才会复制一个资源副本,这个资源副本属于子进程的地址空间,而不会影响到父进程。Docker正式基于此去创建images和containers
由于现在的docker用的是overlay2不再是aufs了,想要看aufs如何使用就只能自己创建一个aufs
3.1 自己动手写aufs
新建一个aufs目录,让它拥有以下结构,并且每个层内都有一个txt文件
发现我的kernel已经不支持aufs了
采用dmesag查看错误的真正原因
docker0是什么呢?是一个本地回环的路由器,用来控制dokcer内容器的通信的