书中提到,docker最核心的就是通过Linux NameSpace,Cgroups以及Union FS构造的。这里首先记录一下NameSpace。
Linux NameSpace
Linux Namespace 是kernel 的一个功能,它可以隔离一系列系统的资源,比如PID(Process ID),User ID, Network等等。
类似与chroot
允许把当前目录变成根目录一样(被隔离开来的),Namesapce也可以在一些资源上,将进程隔离起来,这些资源包括进程树,网络接口,挂载点等等。
书中提到一个生动的例子。一家公司向外界出售自己的计算资源,公司有一台性能还不错的服务器,每个用户买到一个tomcat实例用来运行它们自己的应用。有些调皮的客户可能不小心进入了别人的tomcat实例,修改或者关闭了其中的某些资源,这样就会导致各个客户之间互相干扰。
为此,使用Namespace,
我们就可以做到UID级别的隔离,也就是说,我们可以以UID为n的用户,虚拟化出来一个namespace,在这个namespace里面,用户是具有root权限的。但是在真实的物理机器上,
他还是那个UID为n的用户。这只是NameSpace的一个子功能。
除了User Namespace ,PID也是可以被虚拟的。命名空间建立系统的不同视图, 对于每一个命名空间,从用户看起来,应该像一台单独的Linux计算机一样,有自己的init进程(PID为1),
其他进程的PID依次递增,A和B空间都有PID为1的init进程,子容器的进程映射到父容器的进程上,父容器可以知道每一个子容器的运行状态,而子容器与子容器之间是隔离的。从图中我们可以看到,进程3在父命名空间里面PID 为3,但是在子命名空间内,他就是1.也就是说用户从子命名空间 A 内看进程3就像 init 进程一样,以为这个进程是自己的初始化进程,但是从整个 host 来看,他其实只是3号进程虚拟化出来的一个空间而已。
当前Linux一共实现六种不同类型的namespace。
Namespace类型 | 系统调用参数 | 内核版本 |
---|---|---|
Mount namespaces | CLONE_NEWNS | 2.4.19 |
UTS namespaces | CLONE_NEWUTS | 2.6.19 |
IPC namespaces | CLONE_NEWIPC | 2.6.19 |
PID namespaces | CLONE_NEWPID | 2.6.24 |
Network namespaces | CLONE_NEWNET | 2.6.29 |
User namespaces | CLONE_NEWUSER | 3.8 |
Namesapce 的API主要使用三个系统调用
-
clone()
- 创建新进程。根据系统调用参数来判断哪种类型的namespace被创建,而且它们的子进程也会被包含到namespace中 -
unshare()
- 将进程移出某个namespace -
setns()
- 将进程加入到namespacef中
UTS NameSpace
下面我们将使用Go来做一个UTS Namespace 的例子。
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)
}
}
IPC NameSpace
IPC Namespace 是用来隔离 System V IPC 和POSIX message queues.每一个IPC Namespace都有他们自己的System V IPC 和POSIX message queue。
可以看到我们仅仅增加syscall.CLONE_NEWIPC
代表我们希望创建IPC 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,
}
cmd.Stdin=os.Stdin
cmd.Stdout=os.Stdout
cmd.Stderr=os.Stderr
if err :=cmd.Run(); err!=nil{
log.Fatal(err)
}
}
下面我们需要打开两个shell 来演示隔离的效果。
查看现有的ipc Message Queues
root@taroballs-PC:/home/taroballs/GoglandProjects/awesomeDockerProject/IPC# ipcs -q
------ Message Queues --------
key msqid owner perms used-bytes messages
下面我们创建一个message queue 然后再查看一下
root@taroballs-PC:/home/taroballs/GoglandProjects/awesomeDockerProject/IPC# ipcmk -Q
Message queue id: 0
root@taroballs-PC:/home/taroballs/GoglandProjects/awesomeDockerProject/IPC# ipcs -q
------ Message Queues --------
key msqid owner perms used-bytes messages
0xb06f1d25 0 root 644 0 0
发现是可以看到一个queue了。下面我们使用另外一个shell去运行我们的程序。
通过这里我们可以发现,在新创建的Namespace里面,我们看不到宿主机上已经创建的message queue,说明我们的 IPC Namespace 创建成功,IPC 已经被隔离。
PID NameSpacce
PID namespace是用来隔离进程 id。同样的一个进程在不同的 PID Namespace 里面可以拥有不同的 PID。
可以这样理解,在 docker container 里面,我们使用ps -ef
发现,容器内在前台跑着的那个init进程的 PID 是1,但是我们在容器外,使用ps -ef
会发现同样的进程却有不同的 PID,这就是PID namespace 干的事情。
同上,我们添加了一个syscall.CLONE_NEWPID
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)
}
}
可以看到,我们打印了当前namespace的pid,发现是1,也就是说。这个20190 PID 被映射到 namesapce 里面的 PID 为1.这里还不能使用ps 来查看,因为ps 和 top 等命令会使用/proc内容
Mount NameSpace
上一小节讲到,暂时不能使用top和ps查看,因为其会使用/proc内容
- 何为/proc文件呢
Linux系统上的/proc目录是一种文件系统,即proc文件系统。与其它常见的文件系统不同的是,/proc是一种伪文件系统(也即虚拟文件系统),存储的是当前内核运行状态的一系列特殊文件,用户可以通过这些文件查看有关系统硬件及当前正在运行进程的信息,甚至可以通过更改其中某些文件来改变内核的运行状态。
基于/proc文件系统如上所述的特殊性,其内的文件也常被称作虚拟文件
所谓Mount NameSpace隔离,是用来隔离各个进程看到的挂载点视图。
在不同namespace中的进程看到的文件系统层次是不一样的。在mount namespace 中调用mount()
和umount()
仅仅只会影响当前namespace内的文件系统,而对全局的文件系统是没有影响的。
看到这里,也许就会想到chroot()
。它也是将某一个子目录变成根节点。但是mount namespace不仅能实现这个功能,而且能以更加灵活和安全的方式实现。
我们针对上面的代码做了一点改动,代码中增加了NEWNS 标识。
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)
}
}
首先我们运行代码后,查看一下/proc的文件内容。proc 是一个文件系统,它提供额外的机制可以从内核和内核模块将信息发送给进程。
下面我们将/proc mount到我们自己的namesapce下面来。
可以看到,在当前namesapce里面,我们的sh 进程是PID 为1 的进程。这里就说明,我们当前的Mount namesapce 里面的mount 和外部空间是隔离的,mount 操作并没有影响到外部。Docker volume 也是利用了这个特性。
User NameSpace
User namespace 主要是隔离用户的用户组ID。也就是说,一个进程的User ID 和Group ID 在User namespace 内外可以是不同的。
比较常用的是,在宿主机上以一个非root用户运行创建一个User namespace,然后在User namespace里面却映射成root 用户。
这个进程在User namespace里面有root权限,但是在User namespace外面却没有root的权限。
从Linux kernel 3.8开始,非root进程也可以创建User namespace ,并且此进程在namespace里面可以被映射成 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.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
log.Fatal(err)
}
os.Exit(-1)
}
在原来的基础上增加了syscall.CLONE_NEWUSER
。首先我们以root来运行这个程序,运行前在宿主机上我们看一下当前用户和用户组。
可以看到。它们的UID是不同的,亦即User NameSpace生效了.
NetWork NameSpace
Network namespace 是用来隔离网络设备,IP地址端口等网络栈的namespace。Network namespace 可以让每个容器拥有自己独立的网络设备(虚拟的),而且容器内的应用可以绑定到自己的端口,每个 namesapce 内的端口都不会互相冲突。在宿主机上搭建网桥后,就能很方便的实现容器之间的通信,而且每个容器内的应用都可以使用相同的端口。
同样,我们在原来的代码上增加一点。我们增加了syscall.CLONE_NEWNET
这里标识符。
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 与宿主机之间的网络隔离。
可以发现,实现LinuxNameSpace隔离就是如此简单。如有勘误,欢迎斧正~