gitbook/趣谈网络协议/docs/11465.md
2022-09-03 22:05:03 +08:00

16 KiB
Raw Permalink Blame History

第29讲 | 容器网络:来去自由的日子,不买公寓去合租

如果说虚拟机是买公寓容器则相当于合租有一定的隔离但是隔离性没有那么好。云计算解决了基础资源层的弹性伸缩却没有解决PaaS层应用随基础资源层弹性伸缩而带来的批量、快速部署问题。于是容器应运而生。

容器就是Container而Container的另一个意思是集装箱。其实容器的思想就是要变成软件交付的集装箱。集装箱的特点,一是打包,二是标准。

在没有集装箱的时代假设要将货物从A运到B中间要经过三个码头、换三次船。每次都要将货物卸下船来弄得乱七八糟然后还要再搬上船重新整齐摆好。因此在没有集装箱的时候每次换船船员们都要在岸上待几天才能干完活。

有了尺寸全部都一样的集装箱以后,可以把所有的货物都打包在一起,所以每次换船的时候,一个箱子整体搬过去就行了,小时级别就能完成,船员再也不用耗费很长时间了。这是集装箱的“打包”“标准”两大特点在生活中的应用。

那么容器如何对应用打包呢?

学习集装箱,首先要有个封闭的环境,将货物封装起来,让货物之间互不干扰,互相隔离,这样装货卸货才方便。

封闭的环境主要使用了两种技术,一种是看起来是隔离的技术,称为namespace,也即每个 namespace中的应用看到的是不同的 IP地址、用户空间、程号等。另一种是用起来是隔离的技术,称为cgroup,也即明明整台机器有很多的 CPU、内存而一个应用只能用其中的一部分。

有了这两项技术,就相当于我们焊好了集装箱。接下来的问题就是如何“将这个集装箱标准化”,并在哪艘船上都能运输。这里的标准首先就是镜像

所谓镜像,就是将你焊好集装箱的那一刻,将集装箱的状态保存下来,就像孙悟空说:“定!”,集装箱里的状态就被定在了那一刻,然后将这一刻的状态保存成一系列文件。无论从哪里运行这个镜像,都能完整地还原当时的情况。

接下来我们就具体来看看,这两种网络方面的打包技术。

命名空间namespace

我们首先来看网络namespace。

namespace翻译过来就是命名空间。其实很多面向对象的程序设计语言里面都有命名空间这个东西。大家一起写代码难免会起相同的名词编译就会冲突。而每个功能都有自己的命名空间在不同的空间里面类名相同不会冲突。

在Linux下也是这样的很多的资源都是全局的。比如进程有全局的进程ID网络也有全局的路由表。但是当一台Linux上跑多个进程的时候如果我们觉得使用不同的路由策略这些进程可能会冲突那就需要将这个进程放在一个独立的namespace里面这样就可以独立配置网络了。

网络的namespace由ip netns命令操作。它可以创建、删除、查询namespace。

我们再来看将你们宿舍放进一台物理机的那个图。你们宿舍长的电脑是一台路由器你现在应该知道怎么实现这个路由器吧可以创建一个Router虚拟机来做这件事情但是还有一个更加简单的办法就是我在图里画的这条虚线这个就是通过namespace实现的。

我们创建一个routerns于是一个独立的网络空间就产生了。你可以在里面尽情设置自己的规则。

ip netns add routerns

既然是路由器肯定要能转发嘛因而forward开关要打开。

ip netns exec routerns sysctl -w net.ipv4.ip_forward=1

exec的意思就是进入这个网络空间做点事情。初始化一下iptables因为这里面要配置NAT规则。

ip netns exec routerns iptables-save -c 
ip netns exec routerns iptables-restore -c

路由器需要有一张网卡连到br0上因而要创建一个网卡。

ovs-vsctl -- add-port br0 taprouter -- set Interface taprouter type=internal -- set Interface taprouter external-ids:iface-status=active -- set Interface taprouter external-ids:attached-mac=fa:16:3e:84:6e:cc

这个网络创建完了但是是在namespace外面的如何进去呢可以通过这个命令

ip link set taprouter netns routerns

要给这个网卡配置一个IP地址当然应该是虚拟机网络的网关地址。例如虚拟机私网网段为192.168.1.0/24网关的地址往往为192.168.1.1。

ip netns exec routerns ip -4 addr add 192.168.1.1/24 brd 192.168.1.255 scope global dev taprouter

为了访问外网还需要另一个网卡连在外网网桥br-ex上并且塞在namespace里面。

ovs-vsctl -- add-port br-ex taprouterex -- set Interface taprouterex type=internal -- set Interface taprouterex external-ids:iface-status=active -- set Interface taprouterex external-ids:attached-mac=fa:16:3e:68:12:c0

ip link set taprouterex netns routerns

我们还需要为这个网卡分配一个地址这个地址应该和物理外网网络在一个网段。假设物理外网为16.158.1.0/24可以分配一个外网地址16.158.1.100/24。

ip netns exec routerns ip -4 addr add 16.158.1.100/24 brd 16.158.1.255 scope global dev taprouterex

接下来,既然是路由器,就需要配置路由表,路由表是这样的:

ip netns exec routerns route -n
Kernel IP routing table
Destination   Gateway     Genmask     Flags Metric Ref  Use Iface
0.0.0.0     16.158.1.1  0.0.0.0     UG  0   0    0 taprouterex
192.168.1.0    0.0.0.0     255.255.255.0  U   0   0    0 taprouter
16.158.1.0  0.0.0.0     255.255.255.0  U   0   0    0 taprouterex

路由表中的默认路由是去物理外网的去192.168.1.0/24也即虚拟机私网走下面的网卡去16.158.1.0/24也即物理外网走上面的网卡。

我们在前面的章节讲过如果要在虚拟机里面提供服务提供给外网的客户端访问客户端需要访问外网IP3会在外网网口NAT称为虚拟机私网IP。这个NAT规则要在这个namespace里面配置。

ip netns exec routerns iptables -t nat -nvL
Chain PREROUTING
target  prot opt  in  out  source  destination
DNAT  all  --  *  *  0.0.0.0/0 16.158.1.103 to:192.168.1.3
Chain POSTROUTING
target  prot opt  in  out  source   destination
SNAT  all  --  *  *  192.168.1.3  0.0.0.0/0 to:16.158.1.103

这里面有两个规则一个是SNAT将虚拟机的私网IP 192.168.1.3 NAT成物理外网IP 16.158.1.103。一个是DNAT将物理外网IP 16.158.1.103 NAT成虚拟机私网IP 192.168.1.3。

至此为止基于网络namespace的路由器实现完毕。

机制网络cgroup

我们再来看打包的另一个机制网络cgroup。

cgroup全称control groups是Linux内核提供的一种可以限制、隔离进程使用的资源机制。

cgroup能控制哪些资源呢它有很多子系统

  • CPU子系统使用调度程序为进程控制CPU的访问

  • cpuset如果是多核心的CPU这个子系统会为进程分配单独的CPU和内存

  • memory子系统设置进程的内存限制以及产生内存资源报告

  • blkio子系统设置限制每个块设备的输入输出控制

  • net_cls这个子系统使用等级识别符classid标记网络数据包可允许Linux 流量控制程序tc识别从具体cgroup中生成的数据包。

我们这里最关心的是net_cls它可以和前面讲过的TC关联起来。

cgroup提供了一个虚拟文件系统作为进行分组管理和各子系统设置的用户接口。要使用cgroup必须挂载cgroup文件系统一般情况下都是挂载到/sys/fs/cgroup目录下。

所以首先我们要挂载一个net_cls的文件系统。

mkdir /sys/fs/cgroup/net_cls
mount -t cgroup -onet_cls net_cls /sys/fs/cgroup/net_cls

接下来我们要配置TC了。还记得咱们实验TC的时候那颗树吗

当时我们通过这个命令设定了规则从1.2.3.4来的发送给port 80的包从1:10走其他从1.2.3.4发送来的包从1:11走其他的走默认。

tc filter add dev eth0 protocol ip parent 1:0 prio 1 u32 match ip src 1.2.3.4 match ip dport 80 0xffff flowid 1:10
tc filter add dev eth0 protocol ip parent 1:0 prio 1 u32 match ip src 1.2.3.4 flowid 1:11

这里是根据源IP来设定的现在有了cgroup我们按照cgroup再来设定规则。

tc filter add dev eth0 protocol ip parent 1:0 prio 1 handle 1: cgroup

假设我们有两个用户a和b要对它们进行带宽限制。

首先我们要创建两个net_cls。

mkdir /sys/fs/cgroup/net_cls/a   
mkdir /sys/fs/cgroup/net_cls/b

假设用户a启动的进程ID为12345把它放在net_cls/a/tasks文件中。同样假设用户b启动的进程ID为12346把它放在net_cls/b/tasks文件中。

net_cls/a目录下面还有一个文件net_cls.classid我们放flowid 1:10。net_cls/b目录下面也创建一个文件net_cls.classid我们放flowid 1:11。

这个数字怎么放呢要转换成一个0xAAAABBBB的值AAAA对应class中冒号前面的数字而BBBB对应后面的数字。

echo 0x00010010 > /sys/fs/cgroup/net_cls/a/net_cls.classid    
echo 0x00010011 > /sys/fs/cgroup/net_cls/b/net_cls.classid

这样用户a的进程发的包会打上1:10这个标签用户b的进程发的包会打上1:11这个标签。然后TC根据这两个标签让用户a的进程的包走左边的分支用户b的进程的包走右边的分支。

容器网络中如何融入物理网络?

了解了容器背后的技术,接下来我们来看,容器网络究竟是如何融入物理网络的?

如果你使用docker run运行一个容器你应该能看到这样一个拓扑结构。

是不是和虚拟机很像容器里面有张网卡容器外有张网卡容器外的网卡连到docker0网桥通过这个网桥容器直接实现相互访问。

如果你用brctl查看docker0网桥你会发现它上面连着一些网卡。其实这个网桥和第24讲咱们自己用brctl创建的网桥没什么两样。

那连接容器和网桥的那个网卡和虚拟机一样吗在虚拟机场景下有一个虚拟化软件通过TUN/TAP设备虚拟一个网卡给虚拟机但是容器场景下并没有虚拟化软件这该怎么办呢

在Linux下可以创建一对veth pair的网卡从一边发送包另一边就能收到。

我们首先通过这个命令创建这么一对。

ip link add name veth1 mtu 1500 type veth peer name veth2 mtu 1500

其中一边可以打到docker0网桥上。

ip link set veth1 master testbr    
ip link set veth1 up

那另一端如何放到容器里呢?

一个容器的启动会对应一个namespace我们要先找到这个namespace。对于docker来讲pid就是namespace的名字可以通过这个命令获取。

docker inspect '--format={{ .State.Pid }}' test

假设结果为12065这个就是namespace名字。

默认Docker创建的网络namespace不在默认路径下 ip netns看不到所以需要ln软链接一下。链接完毕以后我们就可以通过ip netns命令操作了。

rm -f /var/run/netns/12065    
ln -s /proc/12065/ns/net /var/run/netns/12065

然后我们就可以将另一端veth2塞到namespace里面。

ip link set veth2 netns 12065

然后,将容器内的网卡重命名。

ip netns exec 12065 ip link set veth2 name eth0

然后给容器内网卡设置ip地址。

ip netns exec 12065 ip addr add 172.17.0.2/16 dev eth0    
ip netns exec 12065 ip link set eth0 up

一台机器内部容器的互相访问没有问题了,那如何访问外网呢?

你先想想看有没有思路就是虚拟机里面的桥接模式和NAT模式。Docker默认使用NAT模式。NAT模式分为SNAT和DNAT如果是容器内部访问外部就需要通过SNAT。

从容器内部的客户端访问外部网络中的服务器,我画了一张图。在虚拟机那一节,也有一张类似的图。

在宿主机上有这么一条iptables规则

-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE

所有从容器内部发出来的包都要做地址伪装将源IP地址转换为物理网卡的IP地址。如果有多个容器所有的容器共享一个外网的IP地址但是在conntrack表中记录下这个出去的连接。

当服务器返回结果的时候到达物理机会根据conntrack表中的规则取出原来的私网IP通过DNAT将地址转换为私网IP地址通过网桥docker0实现对内的访问。

如果在容器内部属于一个服务例如部署一个网站提供给外部进行访问需要通过Docker的端口映射技术将容器内部的端口映射到物理机上来。

例如容器内部监听80端口可以通Docker run命令中的参数-p 10080:80将物理机上的10080端口和容器的80端口映射起来 当外部的客户端访问这个网站的时候通过访问物理机的10080端口就能访问到容器内的80端口了。

Docker有两种方式一种是通过一个进程docker-proxy的方式监听10080转换为80端口。

/usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 10080 -container-ip 172.17.0.2 -container-port 80

另外一种方式是通过DNAT方式,在-A PREROUTING阶段加一个规则将到端口10080的DNAT称为容器的私有网络。

-A DOCKER -p tcp -m tcp --dport 10080 -j DNAT --to-destination 172.17.0.2:80

如此就可以实现容器和物理网络之间的互通了。

小结

好了,这一节就到这里了,我们来总结一下。

  • 容器是一种比虚拟机更加轻量级的隔离方式主要通过namespace和cgroup技术进行资源的隔离namespace用于负责看起来隔离cgroup用于负责用起来隔离。

  • 容器网络连接到物理网络的方式和虚拟机很像通过桥接的方式实现一台物理机上的容器进行相互访问如果要访问外网最简单的方式还是通过NAT。

最后,给你留两个思考题:

  1. 容器内的网络和物理机网络可以使用NAT的方式相互访问如果这种方式用于部署应用有什么问题呢

  2. 和虚拟机一样,不同物理机上的容器需要相互通信,你知道容器是怎么做到这一点吗?

我们的专栏更新到第29讲不知你掌握得如何每节课后我留的思考题你都有没有认真思考并在留言区写下答案呢我会从已发布的文章中选出一批认真留言的同学,赠送学习奖励礼券和我整理的独家网络协议知识图谱。

欢迎你留言和我讨论。趣谈网络协议,我们下期见!