在上一文《Docker实战01|容器与开发语言》中主要介绍了Docker的基本概念与Docker安装、Go语言安装等实战技巧。
本文继续针对Namespace技术展开讲解并利用Go语言进行实践。
本系列所有代码均已经开源。关公众号回复「Go语言实现Docker」即可获得。
我们经常听到,Docker是一个使用了LinuxNamespace和Cgroups的虚拟化工具。但是,什么是LinuxNamespace,它在Docker内是怎么被使用的?说到这里,很多人就会迷茫。下面就先来介绍一下LinuxNamespace及它们是如何在容器中使用的。
比如,一家公司向外界出售自己的计算资源。公司有一台性能还不错的服务器,每个用户买到一个tomcat实例用来运行它们自己的应用。有些调皮的客户可能不小心进入了别人的tomcat实例,修改或关闭了其中的某些资源,这样就会导致各个客户之间互相干扰。 也许你会说,我们可以限制不同用户的权限,让用户只能访问自己名下的tomcat实例,但是,有些操作可能需要系统级别的权限,比如root权限。我们不可能给每个用户都授予root权限,也不可能给每个用户都提供一台全新的物理主机让他们互相隔离。因此,LinuxNamespace在这里就派上了用场。**使用Namespace,就可以做到uid级别的隔离,也就是说,可以以uid为n的用户,虚拟化出来一个Namespace,在这个Namespace里面,用户是具有root权限的。但是,在真实 的物理机器上,他还是那个以uid为n的用户,这样就解决了用户之间隔离的问题。**当然这只是Namespace其中的一个简单功能。
除了UserNamespace,PID也是可以被虚拟的。命名空间建立系统的不同视图,从用户的角度来看,每一个命名空间应该像一台单独的Linux计算机一样,有自己的init进程(PID为I),其他进程的PID依次递增,A和B空间都有PID为1的init进程,子命名空间的进程映射到父命名空间的进程上,父命名空间可以知道每一个子命名空间的运行状态,而子命名空间与子命名空间之间是隔离的。PID映射关系图中可以看到,进程3在父命名空间中的PID为3,但是在子命名空间内,它的PID就是1。也就是说用户从子命名空间A内看进程3就像init进程一样,以为这个进程是自己的初始化进程,但是从整个host来看,它其实只是3号进程虚拟化出来的一个空间而己。
当前Linux一共实现了6种不同类型的Namespace。
Namespace的API主要使用如下3个系统调用 。
UTS Namespace主要用来隔离nodename和domainname两个系统标识。在UTS Namespace里面,每个Namespace允许有自己的hostname。
下面将使用Go来做一个UTS Namespace的例子。其实对于Namespace这种系统调用,使用C语言来描述是最好的,但是本文的目的是去实现Docker,由于Docker就是使用Go开发的,所以就整体使用Go来讲解。先来看一下如下代码,非常简单。
package namespace
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.Fatalln(err)
}
}
执行go run main.go
root@i:~/go/src/docker# go run main.go
sh-5.1#
查看以下是否真的进入了新的 UTS Namespace。
执行pstree -pl
重点关注:
├─sh(2309337)───node(2309347)─┬─node(2309457)─┬─bash(2325085)───go(2328552)─┬─main(2328610)─┬─bash(2328614)───go(2329013)─┬─main(2329063)─┬─sh(2329067)───pstree(2329644)
main程序pid为2328610,后续新创建的sh pid为2329067,现在查看二者uts是否相同即可:
sh-5.1# readlink /proc/2328610/ns/uts
uts:[4026531838]
sh-5.1# readlink /proc/2329067/ns/uts
uts:[4026532362]
可以看到它们确实不在同一个UTS Namespace中。由于UTS Namespace对hostname做了隔离,所以在这个环境内修改hostname应该不影响外部主机,在这个sh环境内执行如下代码示例。
下面来做一下实验。
sh-5.1# hostname sh
sh-5.1# hostname
sh
新开一个宿主机窗口,执行hostname
root@i# hostname
iZ2zed7lj4oetgoal2c8v7Z
可以看到外部的 hostname 并没有被修改影响,由此可了解 UTS Namespace 的作用。
IPC Namespace 用来隔离 sys V IPC和 POSIX message queues。
每个 IPC Namespace 都有自己的 Sys V IPC 和 POSIX message queues。
微调一下程序,只是修改了 Cloneflags,新增了 CLONE_NEWIPC,表示同时创建 IPC Namespace。
// 注: 运行时需要 root 权限。
func main() {
cmd.SysProcAttr = &syscall.SysProcAttr{
// Cloneflags: syscall.CLONE_NEWUTS,
Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC,
}
运行并且进行测试
# 先查看宿主机上的 ipc message queue
root@iZ2zed7Z:~# ipcs -q
------ Message Queues --------
key msqid owner perms used-bytes messages
# 然后创建一个
root@iZ2zed7Z:~# ipcmk -Q
Message queue id: 0
# 再次查看,发现有了
root@iZ2zed7Z:~# ipcs -q
------ Message Queues --------
key msqid owner perms used-bytes messages
0x75c132a7 0 root 644 0 0
运行程序进入新的 shell
可以发现,在新的 Namespace 中已经看不到宿主机上的 message queue 了。说明 IPC Namespace 创建成功,IPC 已经被隔离。
PID Namespace是用来隔离进程ID的。
这样就可以理解,在docker container 里面,使用ps -ef经常会发现,在容器内,前台运行的那个进程PID是1,但是在容器外,使用ps -ef会发现同样的进程却有不同的PID,这就是PID Namespace做的事情。
调整程序,增加 PID flags。
cmd.SysProcAttr = &syscall.SysProcAttr{
// Cloneflags: syscall.CLONE_NEWUTS,
// Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC,
Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID,
}
运行并测试:
root@iZ:~/go/src/docker# go run main.go
sh-5.1# pstree -pl
main(2332072)─┬─sh(2332076)───pstree(2332172)
可以看到mian 的pid为2332072,sh的pid为2332076。
在程序开的sh中执行echo $$
sh-5.1# echo $$
1
发现pid 是1,说明再新开的 PID Namespace 中只有一个 bash 这个进程,而且被伪装成了 1 号进程。
Mount Namespace用来隔离各个进程看到的挂载点视图。
在不同Namespace的进程中,看到的文件系统层次是不一样的。 在Mount Namespace中调用mount()和umount()仅仅只会影响当前Namespace内的文件系统,而对全局的文件系统是没有影响的。
需要注意的是,Mount Namespace 的 flag 是CLONE_NEWNS,直接是 NEWNS 而不是 NEWMOUNT,因为 Mount Namespace 是 Linux 中实现的第一个 Namespace,当时也没想到后续会有很多类型的 Namespace 加入。
再次修改代码,增加 Mount Namespace 的 flag。
cmd.SysProcAttr = &syscall.SysProcAttr{
// Cloneflags: syscall.CLONE_NEWUTS,
// Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC,
// Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID,
Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS,
}
运行并测试:
root@iZ2zed:~/go/src/docker# go run main.go
sh-5.1# ls /proc
1 120 15 1765 225 2309231 2318013 2330980 234 3 419 797 843 95 cpuinfo iomem locks scsi uptime
10 121 16 1766 2290557 2309337 2323926 2331826 235 30 420 8 844 951 crypto ioports mdstat self version
100 12477 1630162 1767 230 2309347 2324122 2332069 2359 303 421 80 845 96 devices irq meminfo slabinfo version_signature
1004 126 1655406 18 2309061 2309457 2325016 2332738 236 304 427 809 85 98 diskstats kallsyms misc softirqs vmallocinfo
101 13 1655423 1839 2309068 2309605 2325027 2332746 237 31 5 81 86 99 dma kcore modules stat vmstat
103 1319 1656492 19 2309090 2312349 2325085 2333183 24 32 6 812 87 acpi driver keys mounts swaps zoneinfo
104 1399 1659802 2 2309111 2312407 2325297 2333225 240 33 603 82 88 bootconfig dynamic_debug key-users mtrr sys
105 14 166 20 2309157 2312634 2325563 2333229 25 34 765 820 90 buddyinfo execdomains kmsg net sysrq-trigger
1087 1449264 1756 21 2309158 2312635 2328552 2333289 253 4 767 83 919 bus fb kpagecgroup pagetypeinfo sysvipc
11 1449266 1762 22 2309159 2317057 2328610 2333330 26 40269 782 837 92 cgroups filesystems kpagecount partitions thread-self
117 1452065 1763 223 2309160 2317141 2328614 2333331 27 417 788 84 926 cmdline fs kpageflags pressure timer_list
12 149 1764 224 2309161 2317400 233 2333332 29 418 796 842 93 consoles interrupts loadavg schedstat tty
以看到,有一大堆文件,这是因为现在查看到的其实是宿主机上的 /proc 目录。
现在把 proc 目录挂载到当前 Namespace 中来:
sh-5.1# mount -t proc proc /proc
sh-5.1# /proc
sh: /proc: Is a directory
sh-5.1# ls /proc
1 bus crypto dynamic_debug interrupts kcore kpagecount meminfo net scsi swaps timer_list vmallocinfo
5 cgroups devices execdomains iomem keys kpageflags misc pagetypeinfo self sys tty vmstat
acpi cmdline diskstats fb ioports key-users loadavg modules partitions slabinfo sysrq-trigger uptime zoneinfo
bootconfig consoles dma filesystems irq kmsg locks mounts pressure softirqs sysvipc version
buddyinfo cpuinfo driver fs kallsyms kpagecgroup mdstat mtrr schedstat stat thread-self version_signature
可以看到,少了一些文件,少的主要是数字命名的目录,因为当前 Namespace 下没有这些进程,自然就看不到对应的信息了。
此时就可以通过 ps 命令来查看了:
可以看到,在当前 Namespace 中 bash 为 1 号进程。
这就说明,当前 Mount Namespace 中的 mount 和外部是隔离的,mount 操作并没有影响到外部,Docker volume 也是利用了这个特性。
User Narespace 主要是隔离用户的用户组ID。
也就是说,一个进程的 UserID 和GroupID 在不同的 User Namespace 中可以是不同的。
比较常用的是,在宿主机上以一个非 root 用户运行创建一个 User Namespace, 然后在 User Namespace 里面却映射成 root 用户。这意味着,这个进程在 User Namespace 里面有 root 权限,但是在 User Namespace 外面却没有 root 的权限。
再次修改代码,增加 User Namespace 的 flag:
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS | syscall.CLONE_NEWUSER,
}
运行并测试:
首先在宿主机上查看一个 user 和 group:
可以看到,此时是 root 用户。
运行程序,进入新的 sh 环境,可以看到,UID 是不同的,说明 User Namespace 生效了。
Network Namespace 是用来隔离网络设备、IP 地址端口等网络栈的 Namespace。
Network Namespace 可以让每个容器拥有自己独立的(虛拟的)网络设备,而且容器内的应用可以绑定到自己的端口,每个 Namespace 内的端口都不会互相冲突。
在宿主机上搭建网桥后,就能很方便地实现容器之间的通信,而且不同容器上的应用可以使用相同的端口。
再次修改代码,增加 Network Namespace 的 flag:
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS |
syscall.CLONE_NEWUSER | syscall.CLONE_NEWNET,
}
运行并测试:
首先看一下宿主机上的网络设备:
有 lo、eth0 等10个设备。
然后运行程序:
可以发现,新的 Namespace 中只有1个设备了,说明 Network Namespace 生效了。
下一篇将会继续介绍Docker中Cgroup的核心原理。
所有Docker实战内容合集:《Docker就应该这么学》
本文所有内容都是基于「动手写Docker」此书。关注公众号,后台回复“动手写Docker”即可领取。
同时,准备了一份云原生实战大礼包送给大家,关注公众号,后台回复“云原生资料”即可领取。