k8s基础应用二:linux的namespace和cgroups机制

1,148 阅读11分钟

云计算的核心技术是虚拟化,而虚拟化的基础就是隔离和资源限制。对公共的计算、存储、网络资源进行分组(租户),不同组之间进行隔离,在非明确授权的情况下,不同组无法看到对方的资源情况,资源的标识也可以在不同组中独立分配、可以重复,然后对每个组能使用的公共资源进行限制,从而实现公共资源同时被不同的租户使用,每个租户都认为自己独占一部分资源的目的。linux通过在内核中引入namespace和cgroups机制,实现了资源的隔离和限制。

1 cgroups

linux的cgroups(control group)机制可以实现对进程或进程组能够使用的cpu、内存、网络、硬盘等硬件资源进行限制。我们可以创建一个cgroup,规定该cgroup能使用的cpu、内存、硬盘等资源的数量,然后将需要限制的进程或进程组添加到该cgroup中,那么这些进程和进程组能够使用的最大资源总量就是由该cgroup规定的。

对于开启了cgroup功能的系统,我们查看一下系统的文件系统挂载情况,可以发现有一个文件系统挂载在/sys/fs/cgroup目录下,如果还没有文件系统挂载在该目录下,我们也可以手动挂载一个:

$ sudo su -
# df
Filesystem      1K-blocks       Used  Available Use% Mounted on
devtmpfs         65663968          0   65663968   0% /dev
tmpfs            65675528      10244   65665284   1% /dev/shm
tmpfs            65675528    4311396   61364132   7% /run
tmpfs            65675528          0   65675528   0% /sys/fs/cgroup

查看cgroup更详细一点的挂载信息:

# mount -t cgroup
cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd)
cgroup on /sys/fs/cgroup/net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_prio)
cgroup on /sys/fs/cgroup/tos_cgroup type cgroup (rw,nosuid,nodev,noexec,relatime,tos_cgroup)
cgroup on /sys/fs/cgroup/net_cls type cgroup (rw,nosuid,nodev,noexec,relatime,net_cls)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)
cgroup on /sys/fs/cgroup/tcp_throt type cgroup (rw,nosuid,nodev,noexec,relatime,tcp_throt)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)
cgroup on /sys/fs/cgroup/cpu type cgroup (rw,nosuid,nodev,noexec,relatime,cpu)
cgroup on /sys/fs/cgroup/cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpuacct)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)
cgroup on /sys/fs/cgroup/rdma type cgroup (rw,nosuid,nodev,noexec,relatime,rdma)

# ls /sys/fs/cgroup/
blkio  cpu  cpuacct  cpuset  devices  freezer  hugetlb  memory  net_cls  net_prio  perf_event  pids  rdma  systemd  tcp_throt  tos_cgroup

/sys/fs/cgroup目录下有一系列的子目录,每一个目录代表cgroup的一个子系统(Subsystem),代表了对某一项资源的cgroup配置。这些子系统管理的目标资源和功能为:

Subsystem功能简述
memorymemory使用量限制,当进程申请的内存超过该限额时,会异常结束
blkio磁盘IO使用量限制
cpucpu使用量限制
cpuacctcpu使用量统计数据
cpusetcpu绑定,指定属于该cgroup的进程运行在指定的cpu核上
devices允许或禁止 cgroup 中的任务访问设备。
freezer暂停/恢复 cgroup 中的任务。
hugetlb限制使用的内存页数量。
net_cls使用等级识别符(classid)标记网络数据包,这让 Linux 流量控制器(tc 指令)可以识别来自特定 cgroup 任务的数据包,并进行网络限制。
net_prio设置网络流量(netowork traffic)的优先级。
perf_event允许使用 perf 工具来监控 cgroup。
pidspid数量限制

1.1 cpu限制

如果我们想为某个Subsystem创建一个cgroup,可以在相应Subsystem目录下创建一个子目录,比如创建一个限制cpu使用量的cgroup:

# cd /sys/fs/cgroup
# cd cpu
# mkdir cpu-test-cgroup
# cd cpu-test-cgroup
# ls
cgroup.clone_children  cgroup.procs  cpu.bt_shares  cpu.cfs_burst_us  cpu.cfs_period_us  cpu.cfs_quota_us  cpu.offline  cpu.rt_period_us  cpu.rt_runtime_us  cpu.shares  cpu.stat  notify_on_release  tasks

可以发现,系统在创建的cpu-test-cgroup目录下自动创建了一些文件。如果我们想限制cgroup使用2个cpu,可以这样做:

# echo 100000 > cpu.cfs_period_us
# echo 200000 > cpu.cfs_quota_us

上面的意思是:在100ms(cpu.cfs_period_us = 100000)时间周期内,该cgroup能使用200ms(cpu.cfs_quota_us = 200000)cpu。这个配置有意义的不是它们的绝对值,而是相对值。所以,同样是限制使用2个cpu,也可以这样配置:

# echo 50000 > cpu.cfs_period_us
# echo 100000 > cpu.cfs_quota_us

限制cgroup只能使用0.5个cpu,可以这样配置:

# echo 100000 > cpu.cfs_period_us
# echo 50000 > cpu.cfs_quota_us

要将相应的进程添加到指定的cgroup,将进程PID值添加到tasks文件中即可:

# echo 44971 >> tasks
# echo 43247 >> tasks

查看进程添加的cgroup:

# cat /proc/44971/cgroup 
16:rdma:/
15:freezer:/
14:devices:/user.slice
13:memory:/user.slice
12:hugetlb:/
11:perf_event:/
10:pids:/user.slice
9:cpuacct:/user.slice
8:cpu:/cpu-test-cgroup
7:blkio:/user.slice
6:tcp_throt:/
5:cpuset:/
4:net_cls:/
3:tos_cgroup:/
2:net_prio:/
1:name=systemd:/user.slice/user-0.slice/session-7885.scope

查看进程的挂载信息,也能看到cgroup的踪影:

# cat /proc/44971/mountinfo 
20 73 0:20 / /sys rw,nosuid,nodev,noexec,relatime - sysfs sysfs rw
21 73 0:4 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw
22 73 0:6 / /dev rw,nosuid - devtmpfs devtmpfs rw,size=65663968k,nr_inodes=16415992,mode=755
23 20 0:7 / /sys/kernel/security rw,nosuid,nodev,noexec,relatime - securityfs securityfs rw
24 22 0:21 / /dev/shm rw,nosuid,nodev - tmpfs tmpfs rw
25 22 0:22 / /dev/pts rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=000
26 73 0:23 / /run rw,nosuid,nodev - tmpfs tmpfs rw,mode=755
27 20 0:24 / /sys/fs/cgroup ro,nosuid,nodev,noexec - tmpfs tmpfs ro,mode=755
28 27 0:25 / /sys/fs/cgroup/systemd rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd
29 20 0:26 / /sys/fs/pstore rw,nosuid,nodev,noexec,relatime - pstore pstore rw
30 27 0:27 / /sys/fs/cgroup/net_prio rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,net_prio
31 27 0:28 / /sys/fs/cgroup/tos_cgroup rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,tos_cgroup
32 27 0:29 / /sys/fs/cgroup/net_cls rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,net_cls
33 27 0:30 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,cpuset
34 27 0:31 / /sys/fs/cgroup/tcp_throt rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,tcp_throt
35 27 0:32 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,blkio
36 27 0:33 / /sys/fs/cgroup/cpu rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,cpu
37 27 0:34 / /sys/fs/cgroup/cpuacct rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,cpuacct
38 27 0:35 / /sys/fs/cgroup/pids rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,pids
39 27 0:36 / /sys/fs/cgroup/perf_event rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,perf_event
40 27 0:37 / /sys/fs/cgroup/hugetlb rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,hugetlb
41 27 0:38 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,memory
42 27 0:39 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,devices
43 27 0:40 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,freezer
44 27 0:41 / /sys/fs/cgroup/rdma rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,rdma
71 20 0:19 / /sys/kernel/config rw,relatime - configfs configfs rw
73 0 8:2 / / rw,relatime - ext2 /dev/sda2 rw,errors=continue,user_xattr,acl
18 21 0:18 / /proc/sys/fs/binfmt_misc rw,relatime - autofs systemd-1 rw,fd=32,pgrp=1,timeout=0,minproto=5,maxproto=5,direct
45 20 0:8 / /sys/kernel/debug rw,relatime - debugfs debugfs rw
46 22 0:17 / /dev/mqueue rw,relatime - mqueue mqueue rw
47 22 0:42 / /dev/hugepages rw,relatime - hugetlbfs hugetlbfs rw,pagesize=2M
48 18 0:43 / /proc/sys/fs/binfmt_misc rw,relatime - binfmt_misc binfmt_misc rw
85 73 8:8 / /home rw,relatime - ext4 /dev/sda8 rw,data=ordered
86 73 8:6 / /has rw,relatime - ext2 /dev/sda6 rw,errors=continue,user_xattr,acl
90 85 8:33 / /home/disk2 rw,relatime - ext4 /dev/sdc1 rw,data=ordered
92 85 259:1 / /home/disk4 rw,relatime - ext4 /dev/nvme0n1p1 rw,data=ordered
89 85 8:49 / /home/disk3 rw,relatime - ext4 /dev/sdd1 rw,data=ordered
91 85 8:17 / /home/disk1 rw,relatime - ext4 /dev/sdb1 rw,data=ordered
97 73 8:5 / /matrix rw,relatime - ext2 /dev/sda5 rw,errors=continue,user_xattr,acl
99 73 8:7 / /tmp rw,relatime - ext2 /dev/sda7 rw,errors=continue,user_xattr,acl
101 73 8:3 / /var rw,relatime - ext2 /dev/sda3 rw,errors=continue,user_xattr,acl
103 73 8:4 / /noah rw,relatime - ext2 /dev/sda4 rw,errors=continue,user_xattr,acl
203 101 0:45 / /var/lib/nfs/rpc_pipefs rw,relatime - rpc_pipefs sunrpc rw
253 103 0:46 / /noah/download rw,relatime - tmpfs none rw,size=819200k,mode=755
254 103 0:47 / /noah/modules rw,relatime - tmpfs none rw,size=819200k,mode=755
255 103 0:48 / /noah/tmp rw,relatime - tmpfs none rw,size=409600k,mode=755
256 103 0:49 / /noah/bin rw,relatime - tmpfs none rw,size=102400k,mode=755
257 26 0:50 / /run/user/0 rw,nosuid,nodev,relatime - tmpfs tmpfs rw,size=13135108k,mode=700
313 101 8:3 /lib/docker /var/lib/docker rw,relatime shared:102 - ext2 /dev/sda3 rw,errors=continue,user_xattr,acl
312 26 0:52 / /run/user/249958 rw,nosuid,nodev,relatime - tmpfs tmpfs rw,size=13135108k,mode=700,uid=249958,gid=100000
314 313 0:53 / /var/lib/docker/overlay2/22e70d61fe729589a13c59eb6fe0b0421b2b9072d0a21240e0665ba968c809cc/merged rw,relatime shared:103 - overlay overlay rw,lowerdir=/var/lib/docker/overlay2/l/XPPLHB3PH6IG7VH4HH4EHWCICD:/var/lib/docker/overlay2/l/DP3PCF7E7YAHQEXZCTYMJW3UPJ,upperdir=/var/lib/docker/overlay2/22e70d61fe729589a13c59eb6fe0b0421b2b9072d0a21240e0665ba968c809cc/diff,workdir=/var/lib/docker/overlay2/22e70d61fe729589a13c59eb6fe0b0421b2b9072d0a21240e0665ba968c809cc/work,index=off
400 26 0:3 net:[4026534085] /run/docker/netns/53974fb2bfb2 rw - nsfs nsfs rw

1.2 memory限制

memory有很多维度的限制项,也包括在内存使用量超出限制时的处理方式。如果我们想创建一个内存使用量限制为1G bytes的cgroup,可以如下配置:

# cd /sys/fs/cgroup/memory
# mkdir memory-test-cgroup
# cd memory-test-cgroup
# echo 1073741824 > memory.limit_in_bytes

1.3 磁盘io限制

限制磁盘/dev/sda1设备的读速率为1MB/s,可以如下配置:

# 查看磁盘的设备号
# ls -l /dev/sda1
brw-rw---- 1 root disk 8, 1 Apr 28 15:48 /dev/sda1

# cd /sys/fs/cgroup/blkio
# mkdir blkio-test-cgroup
# cd blkio-test-cgroup
# echo '8:1 1048576'  > blkio.throttle.read_bps_device

2 namespace

linux很早就支持进程chroot的功能,进程chroot后,进程看到的根目录就是chroot的目录(往往是进程的工作目录),无法查看和修改文件系统的其他目录,从而将进程的文件系统限制在chroot的范围内。linux的namespace功能可以认为是对chroot功能的升级,不仅扩展和增强了对文件系统的隔离功能,还增加了对其他资源的隔离。

linux namespace将进程使用的资源分为几大类,每类资源可以分别创建自己的namespace,将相应的资源进行隔离,也限制了进程可以使用的资源范围。这几类namespace简述如下:

namespace系统调用参数功能简述
Mount namespacesCLONE_NEWNS文件系统namespace。每个namespace有自己的文件系统视图
UTS namespacesCLONE_NEWUTSUNIX Time-Sharing namespace,主机名和域名namespace。每个namespace有自己的主机名和域名,有独立的/etc/hostame、/etc/hosts、/etc/resovle.conf文件
IPC namespacesCLONE_NEWIPC进程间通信namespace。每个namespace有自己的semophore、shm、msgqueue等进程间资源
PID namespacesCLONE_NEWPID进程和进程组namespace。每个 namespace有自己的进程树。树根进程在namespace中的PID值为1,它只是整个linux系统进程树的一个子树。
User namespacesCLONE_NEWUSER用户和用户组namespace。每个namespace有自己的用户和用户组,比如每个namespace有自己的root用户,但这个root用户只在自己的namespace中具有超级权限
Network namespacesCLONE_NEWNET网络namespace。每个namespace有自己独立的网络设备、协议栈、路由表、arp表等资源,namespace间不能直接进行网络通信

我们可以通过以下方式查看指定进程的namespace:

# ls -l /proc/44971/ns
total 0
lrwxrwxrwx 1 root root 0 Sep 29 16:10 cgroup -> cgroup:[4026531835]
lrwxrwxrwx 1 root root 0 Jun 13 19:46 ipc -> ipc:[4026531839]
lrwxrwxrwx 1 root root 0 Jun 13 19:46 mnt -> mnt:[4026531840]
lrwxrwxrwx 1 root root 0 Jun 13 19:42 net -> net:[4026532009]
lrwxrwxrwx 1 root root 0 Jun 13 19:42 pid -> pid:[4026531836]
lrwxrwxrwx 1 root root 0 Sep 29 16:10 pid_for_children -> pid:[4026531836]
lrwxrwxrwx 1 root root 0 Jun 13 19:46 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 Jun 13 19:46 uts -> uts:[4026531838]

从上面的结果可知,PID值为44971的进程,它所属的ipc namespace的id为4026531839。

2.1 Mount namespaces

我们查看一下本机的目录:

# ls /
bin  boot  dev  DoorGod  etc  has  home  lib  lib64  lost+found  matrix  mnt  noah  opt  proc  root  run  sbin  sys  tmp  usr  var
# ls /usr/
bin  etc  games  include  lib  lib64  libexec  local  sbin  share  src  tmp

然后启动一个busybox docker容器,查看一下相同目录的内容:

# docker run -it --rm busybox sh
/ # ls /
bin   dev   etc   home  proc  root  sys   tmp   usr   var
/ # ls /usr/
sbin

可见我们启动的busybox容器与宿主机具有独立的文件系统视图

2.2 UTS namespaces

我们查看一下上面启动的busybox容器的hostname:

/ # hostname
763e8f3ab1a6

可见容器的hostname与本机的hostname不一样,而且是一个比较奇怪的,看不出规律的字符串。我们再看一下容器基本信息:

# docker ps
CONTAINER ID   IMAGE     COMMAND   CREATED          STATUS          PORTS     NAMES
763e8f3ab1a6   busybox   "sh"      10 minutes ago   Up 10 minutes             zealous_hertz

原来docker容器的hostname默认就是容器的id,它与宿主机的hostname是隔离的

2.3 PID namespaces

我们继续在busybox容器里查看容器运行的进程的pid:

/ # ps aux
PID   USER     TIME  COMMAND
    1 root      0:00 sh
   11 root      0:00 ps aux

可见sh进程在容器中的PID为1,是容器的根进程,但是它在宿主机上的pid是下面这样的:

# ps -ef | grep busybox
root     29266 39096  0 16:22 pts/2    00:00:00 docker run -it --rm busybox sh

2.4 Network namespaces

2.4.1 初识network namespace

network namespace是这些命名空间中最复杂的,linux系统也提供了更多的工具来配置和管理网络命名空间。我们可以使用ip netns listip netns show命令查看系统已经创建的network namespace:

# ip netns list

表示当前系统还没有创建network namespace。不过需要注意的是,ip netns list命令查看的是/var/run/netns目录下的network namespace,而docker创建的network namespace存放在/var/run/docker/netns目录下:

# ls /var/run/docker/netns
53974fb2bfb2

可见,当前系统其实存在一个由docker创建的network namespace,它对应的文件的inode id是53974fb2bfb2

下面我们创建一个network namespace:

# ip netns add client-ns

创建后我们可以查看已创建的network namespace了:

# ip netns show
client-ns
# ls /var/run/netns
client-ns

我们创建了network namespace,尝试在新创建的network namespace中执行一些网络指令(ip netns exec命令后面跟network namespace名字,后面再跟具体的命令,表示在指定network namespace中执行对应的命令):

# hostname -i
10.131.21.15
# ip netns exec client-ns ping -c 2 127.0.0.1
connect: Network is unreachable
# ip netns exec client-ns ping -c 2 10.131.21.15
connect: Network is unreachable

可见在client-ns网络命名空间中不仅宿主机的ip无法ping通,连loopback口都无法ping通,查看一下client-ns网络命名空间的接口和路由表内容:

# ip netns exec client-ns ip addr
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
# ip netns exec client-ns ip route

可见在client-ns网络命名空间中,只有一个loopback接口,但是状态为down,路由表没有任何表项

# ip netns exec client ip link set lo up
# ip netns exec client ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
# ip netns exec client ip route
# ip netns exec client ping -c 2 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.025 ms
64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.033 ms
# ip netns exec client ping -c 2 10.131.21.15
connect: Network is unreachable

将client-ns网络命名空间的loopback接口设置为up后,loopback接口可以ping通了,但是宿主机还是无法ping通,说明client-ns网络命名空间与宿主机的default网络命名空间还是隔离的

2.4.2 veth

既然两个不同的网络命名空间的网络是相互隔离的,那么网络命名空间之间如何实现相互通信呢?可以通过vethVirtual Ethernet),翻译过来就是虚拟以太网。不过这个虚拟以太网比较特殊,它只能连接且必须连接两个虚拟网络设备(或者称为虚拟网卡、虚拟网络接口),所以veth又叫做veth pair。同时,以太网的另一个特性:一个网络接口发出的网络报文,其他的接口都能接收到这个网络报文,被虚拟以太网保留了下来。这样,veth相当于一根网线连接了两个网卡,一个网卡发出的网络报文一定被另一个网卡接收到,反方向亦然。

下面我们创建一个veth,这个veth两端的虚拟网络接口名字分别为veth-client、veth-server:

# ip link add veth-client type veth peer name veth-server

这样创建的veth的两端虚拟网络接口都还属于宿主机的default网络命名空间,查看一下:

# ip link
46: veth-server@veth-client: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether a6:b4:ce:a1:39:dd brd ff:ff:ff:ff:ff:ff
47: veth-client@veth-server: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether d6:5a:93:1e:10:88 brd ff:ff:ff:ff:ff:ff

一些常用的网络工具也可以查看创建的虚拟网络设备的信息了:

# ls /sys/class/net/
docker0  erspan0  eth0  eth1  gre0  gretap0  lo  natgre  srv6br  srv6br0  srv6br1  veth12f63d7  veth-client  veth-server  virbr0  virbr0-nic  vnet0  vnet1  xgbe0  xgbe1

# ls /sys/class/net/veth-client
addr_assign_type  addr_len   carrier          dev_id    dormant  flags              ifalias  iflink     mtu               netdev_group  phys_port_id    phys_switch_id  queues  statistics  tx_queue_len  uevent
address           broadcast  carrier_changes  dev_port  duplex   gro_flush_timeout  ifindex  link_mode  name_assign_type  operstate     phys_port_name  proto_down      speed   subsystem   type

# 查看详细信息
# ip -d link show veth-client
47: veth-client@veth-server: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether d6:5a:93:1e:10:88 brd ff:ff:ff:ff:ff:ff promiscuity 0 
    veth addrgenmode eui64 numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535

# 系统虚拟设备中能看到它们    
# ls /sys/devices/virtual/net
docker0  erspan0  gre0  gretap0  lo  natgre  srv6br  srv6br0  srv6br1  veth12f63d7  veth-client  veth-server  virbr0  virbr0-nic  vnet0  vnet1

# ethtool -i veth-client
driver: veth
version: 1.0
firmware-version: 
expansion-rom-version: 
bus-info: 
supports-statistics: yes
supports-test: no
supports-eeprom-access: no
supports-register-dump: no
supports-priv-flags: no

根据ethtool -i命令的driver是veth可以判断当前虚拟网络设备是一个veth网络设备,如果想知道该veth设备的对端设备是哪一个,可以通过下面的脚本获取:

# peer_ifindex=$(ethtool -S veth-client | awk '/peer_ifindex/ {print $2}')
# ip link | awk -v idx="$peer_ifindex" -F: '$1==idx {print $2}'
 veth-server@veth-client

为veth的两个虚拟网络设备配置IP地址:

# ip addr add 172.18.0.11/16 dev veth-client
# ip link set veth-client up
# ip addr add 172.18.0.12/16 dev veth-server
# ip link set veth-server up

查看设备状态,路由表内容,并测试网络连通性:

# ip link list | grep veth
46: veth-server@veth-client: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000
47: veth-client@veth-server: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000

# 路由表中添加了两条直连路由
# ip route | grep veth
172.18.0.0/16 dev veth-client proto kernel scope link src 172.18.0.11 
172.18.0.0/16 dev veth-server proto kernel scope link src 172.18.0.12

# ping -c 2 172.18.0.11
PING 172.18.0.11 (172.18.0.11) 56(84) bytes of data.
64 bytes from 172.18.0.11: icmp_seq=1 ttl=64 time=0.038 ms
64 bytes from 172.18.0.11: icmp_seq=2 ttl=64 time=0.050 ms

# ping -c 2 172.18.0.12
PING 172.18.0.12 (172.18.0.12) 56(84) bytes of data.
64 bytes from 172.18.0.12: icmp_seq=1 ttl=64 time=0.034 ms
64 bytes from 172.18.0.12: icmp_seq=2 ttl=64 time=0.043 ms

既然veth是让两个网络命名空间能够进行通信的,将veth的两个虚拟网络接口放到不同的网络命名空间中,才比较有意义。现在将其中一个网络接口放到client-ns网络命名空间中:

# ip link set veth-client netns client-ns

再次查看宿主机和client-ns网络命名空间的情况:

# ip addr | grep veth
46: veth-server@if47: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state LOWERLAYERDOWN group default qlen 1000
    inet 172.18.0.12/16 scope global veth-server
    
# ip netns exec client-ns ip addr | grep veth
47: veth-client@if46: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000

宿主机的default网络命名空间已经没有了veth-client虚拟网络设备,且veth-server的展示名字由veth-server@veth-client改为veth-server@if47,47正是veth-client接口的索引号。veth-client配置的ip地址没有了,状态也变为down

重新为veth-client接口配置ip地址,并让接口up:

# ip netns exec client-ns ip addr add 172.18.0.11/16 dev veth-client
# ip netns exec client-ns ip link set veth-client up

继续进行网络连通性测试:

# 宿主机ping网络命令空间中的veth接口
# ping -c 2 172.18.0.11
PING 172.18.0.11 (172.18.0.11) 56(84) bytes of data.
64 bytes from 172.18.0.11: icmp_seq=1 ttl=64 time=0.048 ms
64 bytes from 172.18.0.11: icmp_seq=2 ttl=64 time=0.033 ms

# 网络命名空间中ping宿主机的veth接口
# ip netns exec client-ns ping -c 2 172.18.0.12
PING 172.18.0.12 (172.18.0.12) 56(84) bytes of data.
64 bytes from 172.18.0.12: icmp_seq=1 ttl=64 time=0.067 ms
64 bytes from 172.18.0.12: icmp_seq=2 ttl=64 time=0.053 ms

# 网络命名空间中ping宿主机的其它接口
# ip netns exec client-ns ping -c 2 10.131.21.15
connect: Network is unreachable

查看client-ns中的路由表:

# ip netns exec client-ns ip route
172.18.0.0/16 dev veth-client proto kernel scope link src 172.18.0.11

只有一条路由,所以能ping通veth-client和veth-server,但是无法ping通主机上的其他接口

现在将veth的veth-server虚拟接口也放入一个网络命名空间server-ns,让veth连接两个网络命名空间。

# ip netns add server-ns
# ip link set veth-server netns server-ns

# ip netns exec server-ns ip addr add 172.18.0.12/16 dev veth-server
# 记住,loopback接口一定要up
# ip netns exec server-ns ip link set lo up
# ip netns exec server-ns ip link set veth-server up

这时client-ns和server-ns两个网络命名空间能够进行通信了:

# ip netns exec client-ns ping -c 1 172.18.0.12
PING 172.18.0.12 (172.18.0.12) 56(84) bytes of data.
64 bytes from 172.18.0.12: icmp_seq=1 ttl=64 time=0.067 ms

# ip netns exec server-ns ping -c 1 172.18.0.11
PING 172.18.0.11 (172.18.0.11) 56(84) bytes of data.
64 bytes from 172.18.0.11: icmp_seq=1 ttl=64 time=0.067 ms

2.4.3 bridge

linux系统中往往存在很多network namespace,如果有通信需求的network namespace两两间通过veth进行连接,veth数量会非常多,管理起来非常麻烦。这种情况下,一般创建一个或多个bridge,network namespace的虚拟网络接口都连接到bridge,network namespace间、network namespace与宿主机、network namespace与宿主机以外的网络都通过bridge相互联通

2.4.3.1 清理创建的network namespace和veth

先清理上面步骤创建的网络组件:

# 删除veth
# ip link delete veth-server type veth peer name veth-client

# 删除network namespace
# ip netns delete client-ns
# ip netns delete server-ns

2.4.3.2 network namespace通过bridge进行通信

我们先创建一个网桥br0

# 使用ip link命令创建网桥br0
# ip link add br0 type bridge

# 也可以使用brctl命令创建网桥br0
# brctl addbr br0

创建两个网络命名空间ns1和ns2

# ip netns add ns1
# ip netns add ns2

然后创建两个veth,分别连接ns1和br0,ns2和br0:

# ip link add veth-br0-ns1 type veth peer name veth-ns1
# ip link set veth-ns1 netns ns1

# 将虚拟网络接口veth-br0-ns1连到br0上
# ip link set veth-br0-ns1 master br0
# 也可以使用brctl命令将虚拟网络接口veth-br0-ns1连到br0上
# brctl addif veth-br0-ns1

# ip link add veth-br0-ns2 type veth peer name veth-ns2
# ip link set veth-ns2 netns ns2
# ip link set veth-br0-ns1 master br0

为虚拟网络接口和网桥br0配置同一个网段的ip地址,并使它们状态为up:

# ns1
# ip netns exec ns1 ip addr add 172.18.0.11/16 dev veth-ns1
# 不要忘了让loopback口up
# ip netns exec ns1 ip link set lo up
# ip netns exec ns1 ip link set veth-ns1 up

# ns2
# ip netns exec ns2 ip addr add 172.18.0.12/16 dev veth-ns2
# ip netns exec ns2 ip link set lo up
# ip netns exec ns2 ip link set veth-ns2 up

# br0
# ip addr add 172.18.0.1/16 dev br0
# ip link set veth-br0-ns1 up
# ip link set veth-br0-ns2 up
# ip link set br0 up

创建后查看新增的网桥信息,它有两个虚拟网络接口:

$ brctl show br0
bridge name     bridge id               STP enabled     interfaces
br0             8000.2e459b328c05       no              veth-br0-ns1
                                                        veth-br0-ns2

查看宿主机上的网络设备信息,veth-br0-ns1和veth-br0-ns2接口的master都为br0:

11: veth-br0-ns1@if10: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br0 state UP group default qlen 1000
    link/ether 56:c3:b1:70:04:15 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet6 fe80::54c3:b1ff:fe70:415/64 scope link 
       valid_lft forever preferred_lft forever
13: veth-br0-ns2@if12: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br0 state UP group default qlen 1000
    link/ether 2e:45:9b:32:8c:05 brd ff:ff:ff:ff:ff:ff link-netnsid 1
    inet6 fe80::2c45:9bff:fe32:8c05/64 scope link 
       valid_lft forever preferred_lft forever
14: br0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 2e:45:9b:32:8c:05 brd ff:ff:ff:ff:ff:ff
    inet 172.18.0.1/16 scope global br0
       valid_lft forever preferred_lft forever
    inet6 fe80::2c45:9bff:fe32:8c05/64 scope link 
       valid_lft forever preferred_lft forever

这时可以相互ping通了,但是网络命名空间还不能ping通宿主机接口

# 宿主机ping网络命名空间中的接口
# ping -c 1 172.18.0.11
PING 172.18.0.11 (172.18.0.11) 56(84) bytes of data.
64 bytes from 172.18.0.11: icmp_seq=1 ttl=64 time=0.049 ms

# 网络命名空间ping网桥
# ip netns exec ns1 ping -c 1 172.18.0.1
PING 172.18.0.1 (172.18.0.1) 56(84) bytes of data.
64 bytes from 172.18.0.1: icmp_seq=1 ttl=64 time=0.049 ms

# 网络命名空间互ping
# ip netns exec ns1 ping -c 1 172.18.0.12
PING 172.18.0.12 (172.18.0.12) 56(84) bytes of data.
64 bytes from 172.18.0.12: icmp_seq=1 ttl=64 time=0.049 ms

# ip netns exec ns1 ping -c 1 10.131.21.15
connect: Network is unreachable

查看网桥br0上的mac转发表,表明网桥br0学习到了ns1和ns2网络命名空间中接口的mac地址:

# brctl showmacs br0
port no mac addr                is local?       ageing timer
  2     2e:45:9b:32:8c:05       yes                0.00
  2     2e:45:9b:32:8c:05       yes                0.00
  1     56:c3:2e:2a:61:a6       no                 5.68
  1     56:c3:b1:70:04:15       yes                0.00
  1     56:c3:b1:70:04:15       yes                0.00
  2     86:f5:13:3c:d8:06       no                11.53

2.4.3.3 network namespace通过route(多个bridge)进行通信

当两个network namespace属于不同的网段时,它们会连接到不同的bridge,这时在它们相互通信时,需要查路由表,所以需要先允许linux系统进行ip路由转发:

# cat /proc/sys/net/ipv4/ip_forward
0
# echo 1 > /proc/sys/net/ipv4/ip_forward
# cat /proc/sys/net/ipv4/ip_forward
1

然后我们创建一个新的网桥br1,一个新的网络命名空间ns3,一个新的veth连接br1和ns3,相关配置如下:

# ip netns add ns3
# ip link add br1 type bridge
# ip link add veth-br1-ns3 type veth peer name veth-ns3
# ip link set veth-br1-ns3 master br1
# ip link set veth-ns3 netns ns3
# ip netns exec ns3 ip addr add 172.19.0.11/16 dev veth-ns3
# ip netns exec ns3 ip link set lo up
# ip netns exec ns3 ip link set veth-ns3 up
# ip addr add 172.19.0.1/16 dev br1
# ip link set veth-br1-ns3 up
# ip link set br1 up

这时ns1内ping ns3的接口仍然ping不同:

# ip netns exec ns1 ping -c 1 172.19.0.11
connect: Network is unreachable

查看ns1内的路由表,只有一条主机路由,没有到172.19.0.11的路由:

# ip netns exec ns1 ip route
172.18.0.0/16 dev veth-ns1 proto kernel scope link src 172.18.0.11

为所有的网络命名空间ns1、ns2、ns3添加默认路由,下一跳为它们连接的网桥地址:

# ip netns exec ns1 ip route add default via 172.18.0.1
# ip netns exec ns2 ip route add default via 172.18.0.1
# ip netns exec ns3 ip route add default via 172.19.0.1

# ip netns exec ns1 ip route
default via 172.18.0.1 dev veth-client 
172.18.0.0/16 dev veth-ns1 proto kernel scope link src 172.18.0.11

这时相互间能ping通了:

# ip netns exec ns3 ping -c 1 172.18.0.11
PING 172.18.0.11 (172.18.0.11) 56(84) bytes of data.
64 bytes from 172.18.0.11: icmp_seq=1 ttl=63 time=0.085 ms

# ip netns exec ns1 ping -c 1 172.19.0.11
PING 172.19.0.11 (172.19.0.11) 56(84) bytes of data.
64 bytes from 172.19.0.11: icmp_seq=1 ttl=63 time=0.078 ms

2.4.3.4 network namespace通过bridge与宿主机进行通信

通过上面的配置,网络命名空间已经配置了默认路由,网络命名空间已经可以ping通宿主机的接口了

# ip netns exec ns1 ping -c 2 10.131.21.15
PING 10.131.21.15 (10.131.21.15) 56(84) bytes of data.
64 bytes from 10.131.21.15: icmp_seq=1 ttl=64 time=0.049 ms
64 bytes from 10.131.21.15: icmp_seq=2 ttl=64 time=0.048 ms

2.4.3.5 network namespace通过bridge与宿主机外部网络进行通信

在网络命名空间配置了默认路由后,已经可以ping通宿主机的对外连接物理网卡了,这时网络命名空间中ping宿主机外部的ip地址的报文已经可以到达外部目的主机了,但是因为linux并没有运行路由协议,外部网络还没有linux网络命名空间中的虚拟接口ip路由信息,所以它的响应报文无法发送给linux网络空间中的虚拟主机。我们可以在宿主机上配置一个SNAT功能,在网络命名空间发出的报文离开对外连接物理网卡时将网络命名空间空间的ip地址修改为对外物理网卡的地址,报文返回时,再恢复为网络命名空间空间中的ip地址

# iptables -t nat -A POSTROUTING -s 172.18.0.0/16 -o eth0 -j MASQUERADE
# iptables -t nat -A POSTROUTING -s 172.19.0.0/16 -o eth0 -j MASQUERADE

这样就可以ping通网络中其他设备了(假设该eth0接口可以访问域名:webrelay.weiyun.com):

# ip netns exec ns1 ping -c 1 webrelay.weiyun.com
PING webrelay.weiyun.com (10.27.5.56) 56(84) bytes of data.
64 bytes from 10.27.5.56 (10.27.5.56): icmp_seq=1 ttl=54 time=51.0 ms