【云原生•容器】容器技术的秘密武器:90%的人不了解的Namespace魔法,必收藏!
概述
容器技术、虚拟化技术都能做到资源层面上的隔离和限制,对于容器技术而言,它实现资源层面上的限制和隔离,依赖于 Linux 内核所提供的 namespace 和 cgroup 技术。容器技术核心:Linux内核提供 namespace 完成隔离,cgroup完成资源限制,再通过 unionFS 技术实现基于镜像构建容器根文件系统,namespace + cgroup + unionFS构成了容器技术三大底座。
今天先来分析下namespace,它是Linux内核提供的一种环境资源隔离机制,用来让运行在同一个操作系统上的进程互相不会干扰;使用场景可以用来隔离运行在同一个宿主机上的容器,让这些容器之间不能访问彼此的资源。从这里也可以看出容器和虚拟化技术的本质区别:容器技术本质上是系统上通过进程资源隔离,而虚拟化通过引入中间转换层屏蔽底层差异,模拟出一个新环境,实现与平台无关,达到与外界隔离目的。
namespace资源隔离有两个作用:
- 第一是可以充分地利用系统的资源,也就是说在同一台宿主机上可以运行多个用户的容器,并且它们之间互不干扰,彼此之间感知不到对方存在;
- 第二是保证了安全性,因为不同用户之间不能访问对方的资源。
这种隔离机制和 chroot 很类似,chroot 是把某个目录修改为根目录,从而无法访问外部的内容,namespace 正是受到这种思想的影响产生的,所以,第一个namespace类型就是mount namespace,后续又提供了uts namespace、ipc namespace、pid namespace、networknamespace、user namespace等类型隔离机制,如下:
Docker 中主要支持的前6种 namespace 机制,因为后面两种出现较晚,对内核版本要求较高。通过Mount隔离文件系统挂载点、通过UTS隔离主机名、通过PID实现独立进程管理、通过IPC实现进程间通信协议隔离、通过Net实现独立网络协议栈、最后通过User实现用户权限隔离,这些基本涵盖了一个小型操作系统的运行要素,包括文件系统、主机名、进程号、进程间通信、网络、用户权限等。
容器案例实战
上面介绍了namespace用于资源隔离,下面通过实战案例更加直观清晰理解namespace概念。
下面案例主要用到unshare指令,它是Linux系统提供可以从父进程中脱离,用新命名空间运行用户程序的工具,下面我们就用该工具创建一个在UTS、PID、IPC、Mount、Net、User等6种类型namespace和当前宿主机环境隔离的容器环境,这样在容器环境中各种操作并不会影响当前宿主机及宿主机上其它容器。
1、打开shell窗口,简称1号shell,创建容器环境:
[root@docker02 ~]# unshare --uts --net --user --pid --ipc --fork --map-root-user --mount-proc /bin/bash
❝
新建namespace一般需要root权限,user namespace比较例外,可以使用普通用户创建,后续再单独讲解。
2、在宿主机上重新打开一个shell窗口,简称2号shell,通过lsns指令可以查看到当前主机上创建的namespace信息:
从上面可以看到unshare进程运行在user、mnt、uts、ipc、net等资源隔离的环境中,上面明明指定了6种资源namespace类型,为什么缺少pid namespace呢?
其中有个pid=1752进程运行在pid namespace下,通过ps查看pid=1752就是unshare子进程,即上面运行unshare进程,根据--fork参数创建1个/bin/bash子进程。子进程如果没有特殊指定,会保持和父进程相同的namespace,所以/bin/bash和unshare进程一样运行在和宿主机隔离的namespace下。其中有个例外,unshare进程并没有运行在独立的pid namespace,而只有/bin/bash子进程运行在pid namespace下。 这是因为:如果unshare进程也运行在pid namespace下,那容器的1号进程就是unshare进程,而不是这里指定的用户进程/bin/bash,在1号shell窗口下ps aux查看容器里的进程信息,可以看到pid=1进程是/bin/bash用户进程,而不是unshare进程:
UTS Namespace
UTS(UNIX Time-sharing System) Namespace 主要提供了主机名(nodename)和域名(domainname)的隔离,这样每个Docker容器就可以拥有独立的主机名和域名,在网络上就可以被视为一个独立的节点,而非宿主机上的一个进程。
1、切换到上面1号shell,在容器环境里将主机名称修改为uts-ns:
1、查看主机名,和宿主机一致:
[root@docker02 ~]# hostname
docker02
2、将主机名修改为uts-ns:
[root@docker02 ~]# hostname -b uts-ns
3、修改后查看主机名,已经生效
[root@docker02 ~]# hostname
uts-ns
2、切换到2号shell,查看宿主机的主机名还是docker02,并未被影响到:
[root@docker02 ~]# hostname
docker02
3、我们还可以使用nsenter指令进入某个namespace里,重新在宿主机上打开1个shell,查询进程pid,然后进入到该进程的uts namespace里,使用hostname发现主机名已修改为nts-ns:
查看unshare进程pid=1750,使用nsenter指令,-t参数指定如要进入哪个进程的namespace下,-u指定进入该进程的uts namespace,还可以通过其它参数进入到不同类型的namespace里。
PID Namespace
uts namespace让容器拥有独立的主机名等信息,可以被视为一个独立的节点基本要素,接下来讲的pid namespace用于进行进程隔离。你想一下:假如没有进程隔离,你在容器里使用ps -ef讲宿主机上所有进程信息都查看的到,那是不是很不友好,也不安全呢?
在容器里主进程一般pid=1,同时容器外其它进程使用ps -ef查看不到,但是在容器外,使用ps -ef会发现同样进程却有不同的PID,这就是PID Namespace做的事情。注意:父pid namespace中可以看到子pid namespace中的进程,并可以通过信号等方式对子节点中的进程产生影响;反过来,子pid namespace却不能看到父pid namespace中的任何内容。因此:父命名空间可以知道每个子命名空间的运行状态,而子命名空间之间是相互隔离的!
❝
1、内核为所有的PID namespace维护了一个树状结构,最顶层的是系统初始时创建的,被称为root namespace,它创建的心PID namespace被称为child namespace(树的子节点)。
2、通过这种方式,不同的PID namespace会形成一个层级体系,所属的父节点可以看到子节点中的进程,并可以通过信号等方式对子节点中的进程产生影响;反过来,子节点却不能看到父节点PID namespace中的任何内容。
3、A和B空间均存在PID为1的init进程,子namespace的进程映射至父namespace的进程上,因此:父namespace可以知道每个子namespace的运行状态,而namespace之间是相互隔离的!
在每个pid namespace下,pid编号又会从1开始继续,同时在这个 pid namespace 中也只能看到当前 namespace 中的进程,而看不到其它 namespace 里的进程,pid namespace 实现各个容器环境间进程资源隔离,这样类似拥有独立的主机节点更加前进一步。
下面继续上面实战案例:
1、切换到上面1号shell,在容器环境里使用ps -ef查看当前pid namespace下进程信息:
从上面可以看到pid=1进程就是上面unshare指令指定的/bin/bash用户进程,宿主机上的其它进程通过pid namespace隔离在容器环境下是看不到的。
❝
注意:通过
unshare --pid --fork --mount-proc /bin/bash方式创建pid namespace,需要带上--mount-proc参数,用于创建新pid namespace时基于当前namespace重新将proc伪文件系统挂载到/proc节点上。1、/proc文件系统是一个特殊的文件系统,通常被挂载在Linux系统的/proc目录,它不存储实际的文件,/proc中的内容是动态生成的,包括当前运行的每个进程的详细信息,例如 /proc/[pid] 目录包含特定进程的信息。
2、重新挂载 /proc 文件系统将显示该pid namespace 下的进程视图,而不是整个系统的视图,这对于隔离进程视图非常重要。否则和宿主机一致,使用ps、top等导致可以查看到宿主机上全部进程信息,因为这些指令就是从 /proc 下读取系统的进程信息。
Network Namespace
network namespace 是用来隔离网络协议栈相关,比如网络设备、IP地址、端口信息、路由表等,这样可以让容器真正成为独立节点,通过网络和其它节点进行通信交互。
下面继续上面实战案例:
1、切换到上面1号shell,在容器环境里使用ip a查看下容器环境下网卡信息:
[root@docker02 ~]# ip a
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
从上面看到,除了一个lo环回网卡外,并未有其它任何网卡设备,即和宿主机在网络设备上产生隔离。在一个个隔离的容器网络环境中,docker借助bridge网桥和veth pair虚拟网卡等设备实现容器间网络交互。
容器网络实战
网络作为容器、云原生下最为复杂的技术,下面我就通过一个案例还原最为简单的容器网络的创建流程和原理。
下面通过指令模拟下docker容器创建网络大概流程(下面步骤在宿主机打开shell操作即可):
a、给之前unshare方式创建的network namespace 起个名称,方便后续操作:
b、创建1个bridge网桥设备、配置IP地址并启动,bridge网桥类似交换机用于连接各个容器网络:
[root@docker02 ~]# ip link add docker_br0 type bridge
[root@docker02 ~]# ip addr add 192.168.1.100/24 dev docker_br0
[root@docker02 ~]# ip link set docker_br0 up
然后使用ip a指令可以查看到docker_br0网桥信息:
c、创建veth pair网络设备:
1、创建veth pair设备,该设备是成对出现,类似于网线两端:
[root@docker02 ~]# ip link add veth-ns1 type veth peer name veth-ns1-br
2、将其中veth-ns1一端"插到"之前unshare创建的network namespace里:
[root@docker02 ~]# ip link set veth-ns1 netns docker_ns1
3、将另一端veth-ns1-br"插到"刚才创建的bridge网桥设备上
[root@docker02 ~]# ip link set veth-ns1-br master docker_br0
4、设置docker_ns1命名空间下veth-ns1网卡设备IP:
[root@docker02 ~]# ip netns exec docker_ns1 ip address add 192.168.1.1/24 dev veth-ns1
5、将veth pair两端网卡都启动:
[root@docker02 ~]# ip link set veth-ns1-br up
[root@docker02 ~]# ip netns exec docker_ns1 ip link set veth-ns1 up
#注意:容器里环回网卡也要启动
[root@docker02 ~]# ip netns exec docker_ns1 ip link set lo up
上面容器网络创建完成后,下面就来验证下(下面步骤在1号shell容器里操作):
a、切换到上面1号shell,在容器环境里使用ip a再次查看下容器环境下网卡信息:
这时,在容器里就可以看到刚才"插入"进来的网卡veth-ns1,并且其IP地址为刚才配置的192.168.1.1。
b、在容器里ping 192.168.1.1(自身IP)连通正常,但是ping 192.168.1.100(docker_br0网桥地址)和ping 192.168.31.152(宿主机IP)都是不通的:
查看下容器里的路由表(见下图),路由表是空的,所以导致对外的网络不通:
c、添加一条192.168.1.0网段的网络路由:
再次测试网络,发现到192.168.1.100(docker_br0网桥设备)网络连通,但是192.168.31.152(宿主机IP)不通:
这也比较好理解,容器IP和docker_br0同属 192.168.1.0网段,并且添加了该网段的网络路由,该网段的数据流量从veth-ns1网卡发送,但是宿主机网段是192.168.31.0网段,因此还需要添加一条默认路由,将docker_br0网桥作为容器的默认网关。
d、添加默认网关路由:
再次测试网络,发现到网桥设备和宿主机网络都正常连通:
上面演示了docker容器简单网络创建的大概流程,当然这个网络还有些问题,网络作为容器最为复杂部分,后续我们还会专门分析。
总结
Linux 内核所提供的 namespace 机制是容器技术的核心支柱之一,它为容器提供了至关重要的资源隔离功能。这种机制就像是在同一个操作系统内部创建了多个独立的“小宇宙”,让每个容器都能在自己的空间内自由运行,而不必担心与其他容器发生冲突。
在本节内容中,我们将通过一系列实战案例,深入探讨uts namespace、pid namespace和network namespace这三种关键的资源隔离技术。这些技术让容器能够拥有独立的主机名、进程和网络资源,还通过实战演练让您看到容器如何逐步构建起自己的网络环境,赋予了容器网络通信的能力使它们在功能上越来越接近于独立的主机节点。在下一节中,我们将继续深入探索namespace技术,以进一步优化和完善容器的功能。