Docker进阶与实战
上QQ阅读APP看书,第一时间看更新

2.4.3 各个Namespace介绍

1.UTS Namespace

UTS Namespace用于对主机名和域名进行隔离,也就是uname系统调用使用的结构体struct utsname里的nodename和domainname这两个字段,UTS这个名字也是由此而来的。

那么,为什么要使用UTS Namespace做隔离?这是因为主机名可以用来代替IP地址,因此,也就可以使用主机名在网络上访问某台机器了,如果不做隔离,这个机制在容器里就会出问题。

调用之前的程序后,在Namespace终端执行以下命令:

# hostname container
# hostname
container

这里已经改变了主机名,现在通过host终端来看看host的主机名:

# hostname
linux-host

可以看到,host的主机名并没有变化,这就是Namespace所起的作用。

2.IPC Namespace

IPC是Inter-Process Communication的简写,也就是进程间通信。Linux提供了很多种进程间通信的机制,IPC Namespace针对的是SystemV IPC和Posix消息队列。这些IPC机制都会用到标识符,例如用标识符来区别不同的消息队列,然后两个进程通过标识符找到对应的消息队列进行通信等。

IPC Namespace能做到的事情是,使相同的标识符在两个Namespace中代表不同的消息队列,这样也就使得两个Namespace中的进程不能通过IPC进程通信了。

举个例子,在namespace终端创建了一个消息队列:

# ipcmk -Q
Message queue id: 65536
# ipcs -q
------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages
0x0ec037c7 65536      root       644        0            

这个消息队列的标识符是65536,现在在host终端看一下:

# ipcs -q
------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages

在这里看不到任何消息队列,IPC隔离的效果达到了。

3.PID Namespace

PID Namespace用于隔离进程PID号,这样一来,不同的Namespace里的进程PID号就可以是一样的了。

当创建一个PID Namespace时,第一个进程的PID号是1,也就是init进程。init进程有一些特殊之处,例如init进程需要负责回收所有孤儿进程的资源。另外,发送给init进程的任何信号都会被屏蔽,即使发送的是SIGKILL信号,也就是说,在容器内无法“杀死”init进程。

但是当用ps命令查看系统的进程时,会发现竟然可以看到host的所有进程:

# ps ax
    PID TTY      STAT   TIME COMMAND
      1 ?        Ss     0:24 init [5]
      2 ?        S      0:06 [kthreadd]
      3 ?        S      1:37 [ksoftirqd/0]
      5 ?        S<     0:00 [kworker/0:0H]
      7 ?        S      0:16 [kworker/u33:0]
...
7585 pts/0       S+     0:00 sleep 1000

这是因为ps命令是从procfs读取信息的,而procfs并没有得到隔离。虽然能看到这些进程,但由于它们其实是在另一个PID Namespace中,因此无法向这些进程发送信号:

# kill -9 7585
-bash: kill: (7585) - No such process

4.Mount Namespace

Mount Namespace用来隔离文件系统挂载点,每个进程能看到的文件系统都记录在/proc/$$/mounts里。在创建了一个新的Mount Namespace后,进程系统对文件系统挂载/卸载的动作就不会影响到其他Namespace。

之前看到,创建PID Namespace后,由于procfs没有改变,因此通过ps命令看到的仍然是host的进程树,其实可以通过在这个PID Namespace里挂载procfs来解决这个问题,如下:

# mount t proc none /proc
# ps ax
    PID TTY      STAT   TIME COMMAND
        1 pts/2    S+     0:00 newns
        3 pts/2    R+     0:00 ps ax

但此时由于文件系统挂载点没有隔离,因此host看到的procfs也会是这个新的procfs,这样在host上就会出问题:

# ps ax
Error, do this: mount -t proc none /proc

可如果同时使用Mount Namespace和PID Namespace,新的Namespace里的进程和host上的进程将会看到各自的procfs,故而也就不存在上面的问题了。

5.Network Namespace

这个Namespace会对网络相关的系统资源进行隔离,每个Network Namespace都有自己的网络设备、IP地址、路由表、/proc/net目录、端口号等。网络隔离的必要性是很明显的,举一个例子,在没有隔离的情况下,如果两个不同的容器都想运行同一个Web应用,而这个应用又需要使用80端口,那就会有冲突了。

新创建的Network Namespace会有一个loopback设备,除此之外不会有任何其他网络设备,因此用户需要在这里面做自己的网络配置。IP工具已经支持Network Namespace,可以通过它来为新的Network Namespace配置网络功能。首先创建Network Namespace:

# ip netns add new_ns

使用“ip netns exec”命令可以对特定的Namespace执行网络管理:

# ip netns exec new_ns ip link list
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

看到确实只有loopback这个网络接口,并且它还因处于DOWN状态而不可用:

# ip netns exec new_ns ping 127.0.0.1
connect: Network is unreachable

通过以下命令可以启用loopback网络接口:

# ip netns exec new-ns ip link set dev lo up
# ip netns exec new-ns ping 127.0.0.1
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.053 ms
...

最后可以这样删除Namespace:

# ip netns delete new_ns

容器的网络配置是一个很大的话题,后面有专门的章节讲解,因此这里暂不展开。

6.User Namespace

User Namespace用来隔离用户和组ID,也就是说一个进程在Namespace里的用户和组ID与它在host里的ID可以不一样,这样说可能读者还不理解有什么实际的用处。User Namespace最有用的地方在于,host的普通用户进程在容器里可以是0号用户,也就是root用户。这样,进程在容器内可以做各种特权操作,但是它的特权被限定在容器内,离开了这个容器它就只有普通用户的权限了。

注意 容器内的这类root用户,实际上还是有很多特权操作不能执行,基本上如果这个特权操作会影响到其他容器或者host,就不会被允许。

在host上,可以看到我们是lizf用户。

$ id
uid=1000(lizf) gid=100(users) groups=100(users)

现在创建新的User Namespace,看看又是什么情况?

$ new-userns
$ id
uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup)

可以看到,用户名和组名都变了,变成65534,不再是原来的1000和100。

接下来的问题是,怎么设定Namespace和host的UID的映射关系?方法是在创建新的Namespace后,设置这个Namespace里进程的/proc/<PID>/uid_map。在Namespace终端看到的是这样的:

$ id
uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup)
$ echo $$
17074
$ cat /proc/17074/uid_map
$

可以看到uid_map是空的,也就是还没有UID的映射。这可以在host终端上通过root用户设置,如下。

# echo "0 1000 65536" > /proc/17074/uid_map

上面命令表示要将[1000,66536]的UID在Namespace里映射成[0,65536]。再切回到Namespace终端看看:

$ id
uid=0(root) gid=65534(nogroup) 65534(nogroup)

可以看到,我们成功地将lizf用户映射成容器里的root用户了。对于gid,也可以做类似的操作。

至此,关于Namespace和Cgroup的知识就讲解完了,可以看到,Namespace和Cgroup的使用是很灵活的,同时这里面又有不少需要注意的地方,因此直接操作Namespace和Cgroup并不是很容易。正是因为这些原因,Docker通过Libcontainer来处理这些底层的事情。这样一来,Docker只需要简单地调用Libcontainer的API,就能将完整的容器搭建起来。而作为Docker的用户,就更不用操心这些事情了,而只需要学习Docker的使用手册,就能通过一两条简单的Docker命令启动容器。