This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,350 @@
<audio id="audio" title="第29讲 | 容器网络:来去自由的日子,不买公寓去合租" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/95/18/9519c73093e8c1165c6ba4fcf7f4e118.mp3"></audio>
如果说虚拟机是买公寓容器则相当于合租有一定的隔离但是隔离性没有那么好。云计算解决了基础资源层的弹性伸缩却没有解决PaaS层应用随基础资源层弹性伸缩而带来的批量、快速部署问题。于是容器应运而生。
容器就是Container而Container的另一个意思是集装箱。其实**容器的思想就是要变成软件交付的集装箱**。集装箱的特点,一是打包,二是标准。
<img src="https://static001.geekbang.org/resource/image/a5/dc/a50157f1084c946b9e27f3b328b8d2dc.jpg" alt="">
在没有集装箱的时代假设要将货物从A运到B中间要经过三个码头、换三次船。每次都要将货物卸下船来弄得乱七八糟然后还要再搬上船重新整齐摆好。因此在没有集装箱的时候每次换船船员们都要在岸上待几天才能干完活。
有了尺寸全部都一样的集装箱以后,可以把所有的货物都打包在一起,所以每次换船的时候,一个箱子整体搬过去就行了,小时级别就能完成,船员再也不用耗费很长时间了。这是集装箱的“打包”“标准”两大特点在生活中的应用。
<img src="https://static001.geekbang.org/resource/image/50/cb/50c12f33ec178972c315e57b370dffcb.jpg" alt="">
那么容器如何对应用打包呢?
学习集装箱,首先要有个封闭的环境,将货物封装起来,让货物之间互不干扰,互相隔离,这样装货卸货才方便。
封闭的环境主要使用了两种技术,一种是**看起来是隔离的技术**,称为**namespace**,也即每个 namespace中的应用看到的是不同的 IP地址、用户空间、程号等。另一种是**用起来是隔离的技术**,称为**cgroup**,也即明明整台机器有很多的 CPU、内存而一个应用只能用其中的一部分。
有了这两项技术,就相当于我们焊好了集装箱。接下来的问题就是如何“将这个集装箱标准化”,并在哪艘船上都能运输。这里的标准首先就是**镜像**。
所谓镜像,就是将你焊好集装箱的那一刻,将集装箱的状态保存下来,就像孙悟空说:“定!”,集装箱里的状态就被定在了那一刻,然后将这一刻的状态保存成一系列文件。无论从哪里运行这个镜像,都能完整地还原当时的情况。
<img src="https://static001.geekbang.org/resource/image/ad/9d/ad59fa4271e5dd7b40588a2dfeb6f79d.jpg" alt="">
接下来我们就具体来看看,这两种网络方面的打包技术。
## 命名空间namespace
我们首先来看网络namespace。
namespace翻译过来就是命名空间。其实很多面向对象的程序设计语言里面都有命名空间这个东西。大家一起写代码难免会起相同的名词编译就会冲突。而每个功能都有自己的命名空间在不同的空间里面类名相同不会冲突。
在Linux下也是这样的很多的资源都是全局的。比如进程有全局的进程ID网络也有全局的路由表。但是当一台Linux上跑多个进程的时候如果我们觉得使用不同的路由策略这些进程可能会冲突那就需要将这个进程放在一个独立的namespace里面这样就可以独立配置网络了。
网络的namespace由ip netns命令操作。它可以创建、删除、查询namespace。
我们再来看将你们宿舍放进一台物理机的那个图。你们宿舍长的电脑是一台路由器你现在应该知道怎么实现这个路由器吧可以创建一个Router虚拟机来做这件事情但是还有一个更加简单的办法就是我在图里画的这条虚线这个就是通过namespace实现的。
<img src="https://static001.geekbang.org/resource/image/1a/1a/1a5d299c2eb5480eda93a8f8e3b3ca1a.jpg" alt="">
我们创建一个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能控制哪些资源呢它有很多子系统
<li>
CPU子系统使用调度程序为进程控制CPU的访问
</li>
<li>
cpuset如果是多核心的CPU这个子系统会为进程分配单独的CPU和内存
</li>
<li>
memory子系统设置进程的内存限制以及产生内存资源报告
</li>
<li>
blkio子系统设置限制每个块设备的输入输出控制
</li>
<li>
net_cls这个子系统使用等级识别符classid标记网络数据包可允许Linux 流量控制程序tc识别从具体cgroup中生成的数据包。
</li>
我们这里最关心的是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的时候那颗树吗
<img src="https://static001.geekbang.org/resource/image/9a/b5/9a1b8a7c0c5403a2b4b3c277545991b5.jpg" alt="">
当时我们通过这个命令设定了规则从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 &gt; /sys/fs/cgroup/net_cls/a/net_cls.classid
echo 0x00010011 &gt; /sys/fs/cgroup/net_cls/b/net_cls.classid
```
这样用户a的进程发的包会打上1:10这个标签用户b的进程发的包会打上1:11这个标签。然后TC根据这两个标签让用户a的进程的包走左边的分支用户b的进程的包走右边的分支。
## 容器网络中如何融入物理网络?
了解了容器背后的技术,接下来我们来看,容器网络究竟是如何融入物理网络的?
如果你使用docker run运行一个容器你应该能看到这样一个拓扑结构。
<img src="https://static001.geekbang.org/resource/image/20/d2/20e87bc215b9d049a4a504d775d26dd2.jpg" alt="">
是不是和虚拟机很像容器里面有张网卡容器外有张网卡容器外的网卡连到docker0网桥通过这个网桥容器直接实现相互访问。
如果你用brctl查看docker0网桥你会发现它上面连着一些网卡。其实这个网桥和[第24讲](https://time.geekbang.org/column/article/10742)咱们自己用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。
从容器内部的客户端访问外部网络中的服务器,我画了一张图。在[虚拟机](https://time.geekbang.org/column/article/10742)那一节,也有一张类似的图。
<img src="https://static001.geekbang.org/resource/image/54/93/5452971c96e8fea33c3f873860e25c93.jpg" alt="">
在宿主机上有这么一条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端口了。
<img src="https://static001.geekbang.org/resource/image/49/bb/49bb6b2a30fe76b124182980da935ebb.jpg" alt="">
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
```
如此就可以实现容器和物理网络之间的互通了。
## 小结
好了,这一节就到这里了,我们来总结一下。
<li>
容器是一种比虚拟机更加轻量级的隔离方式主要通过namespace和cgroup技术进行资源的隔离namespace用于负责看起来隔离cgroup用于负责用起来隔离。
</li>
<li>
容器网络连接到物理网络的方式和虚拟机很像通过桥接的方式实现一台物理机上的容器进行相互访问如果要访问外网最简单的方式还是通过NAT。
</li>
最后,给你留两个思考题:
<li>
容器内的网络和物理机网络可以使用NAT的方式相互访问如果这种方式用于部署应用有什么问题呢
</li>
<li>
和虚拟机一样,不同物理机上的容器需要相互通信,你知道容器是怎么做到这一点吗?
</li>
我们的专栏更新到第29讲不知你掌握得如何每节课后我留的思考题你都有没有认真思考并在留言区写下答案呢我会从**已发布的文章中选出一批认真留言的同学**,赠送学习奖励礼券和我整理的独家网络协议知识图谱。
欢迎你留言和我讨论。趣谈网络协议,我们下期见!

View File

@@ -0,0 +1,111 @@
<audio id="audio" title="第30讲 | 容器网络之Flannel每人一亩三分地" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/43/d9/43537d3108145697419700fac4ad6ad9.mp3"></audio>
上一节我们讲了容器网络的模型以及如何通过NAT的方式与物理网络进行互通。
每一台物理机上面安装好了Docker以后都会默认分配一个172.17.0.0/16的网段。一台机器上新创建的第一个容器一般都会给172.17.0.2这个地址,当然一台机器这样玩玩倒也没啥问题。但是容器里面是要部署应用的,就像上一节讲过的一样,它既然是集装箱,里面就需要装载货物。
如果这个应用是比较传统的单体应用自己就一个进程所有的代码逻辑都在这个进程里面上面的模式没有任何问题只要通过NAT就能访问进来。
但是因为无法解决快速迭代和高并发的问题,单体应用越来越跟不上时代发展的需要了。
你可以回想一下,无论是各种网络直播平台,还是共享单车,是不是都是很短时间内就要积累大量用户,否则就会错过风口。所以应用需要在很短的时间内快速迭代,不断调整,满足用户体验;还要在很短的时间内,具有支撑高并发请求的能力。
单体应用作为个人英雄主义的时代已经过去了。如果所有的代码都在一个工程里面,开发的时候必然存在大量冲突,上线的时候,需要开大会进行协调,一个月上线一次就很不错了。而且所有的流量都让一个进程扛,怎么也扛不住啊!
没办法,一个字:拆!拆开了,每个子模块独自变化,减少相互影响。拆开了,原来一个进程扛流量,现在多个进程一起扛。所以,微服务就是从个人英雄主义,变成集团军作战。
容器作为集装箱可以保证应用在不同的环境中快速迁移提高迭代的效率。但是如果要形成容器集团军还需要一个集团军作战的调度平台这就是Kubernetes。它可以灵活地将一个容器调度到任何一台机器上并且当某个应用扛不住的时候只要在Kubernetes上修改容器的副本数一个应用马上就能变八个而且都能提供服务。
然而集团军作战有个重要的问题就是通信。这里面包含两个问题第一个是集团军的A部队如何实时地知道B部队的位置变化第二个是两个部队之间如何相互通信。
第一个问题位置变化往往是通过一个称为注册中心的地方统一管理的这个是应用自己做的。当一个应用启动的时候将自己所在环境的IP地址和端口注册到注册中心指挥部这样其他的应用请求它的时候到指挥部问一下它在哪里就好了。当某个应用发生了变化例如一台机器挂了容器要迁移到另一台机器这个时候IP改变了应用会重新注册则其他的应用请求它的时候还是能够从指挥部得到最新的位置。
<img src="https://static001.geekbang.org/resource/image/a0/0d/a0763d50fc4e8dcec37ae25a2f6cc60d.jpeg" alt="">
接下来是如何相互通信的问题。NAT这种模式在多个主机的场景下是存在很大问题的。在物理机A上的应用A看到的IP地址是容器A的是172.17.0.2在物理机B上的应用B看到的IP地址是容器B的不巧也是172.17.0.2,当它们都注册到注册中心的时候,注册中心就是这个图里这样子。
<img src="https://static001.geekbang.org/resource/image/e2/dd/e20596506dd34122e302a7cfc8bb85dd.jpg" alt="">
这个时候应用A要访问应用B当应用A从注册中心将应用B的IP地址读出来的时候就彻底困惑了这不是自己访问自己吗
怎么解决这个问题呢一种办法是不去注册容器内的IP地址而是注册所在物理机的IP地址端口也要是物理机上映射的端口。
<img src="https://static001.geekbang.org/resource/image/8f/18/8fabf1de2a7d346856a032dbf2417b18.jpg" alt="">
这样存在的问题是应用是在容器里面的它怎么知道物理机上的IP地址和端口呢这明明是运维人员配置的除非应用配合读取容器平台的接口获得这个IP和端口。一方面大部分分布式框架都是容器诞生之前就有了它们不会适配这种场景另一方面让容器内的应用意识到容器外的环境本来就是非常不好的设计。
说好的集装箱说好的随意迁移呢难道要让集装箱内的货物意识到自己传的信息而且本来Tomcat都是监听8080端口的结果到了物理机上就不能大家都用这个端口了否则端口就冲突了因而就需要随机分配端口于是在注册中心就出现了各种各样奇怪的端口。无论是注册中心还是调用方都会觉得很奇怪而且不是默认的端口很多情况下也容易出错。
Kubernetes作为集团军作战管理平台提出指导意见说网络模型要变平但是没说怎么实现。于是业界就涌现了大量的方案Flannel就是其中之一。
对于IP冲突的问题如果每一个物理机都是网段172.17.0.0/16肯定会冲突啊但是这个网段实在太大了一台物理机上根本启动不了这么多的容器所以能不能每台物理机在这个大网段里面抠出一个小的网段每个物理机网段都不同自己看好自己的一亩三分地谁也不和谁冲突。
例如物理机A是网段172.17.8.0/24物理机B是网段172.17.9.0/24这样两台机器上启动的容器IP肯定不一样而且就看IP地址我们就一下子识别出这个容器是本机的还是远程的如果是远程的也能从网段一下子就识别出它归哪台物理机管太方便了。
接下来的问题,就是**物理机A上的容器如何访问到物理机B上的容器呢**
你是不是想到了熟悉的场景虚拟机也需要跨物理机互通往往通过Overlay的方式容器是不是也可以这样做呢
**这里我要说Flannel使用UDP实现Overlay网络的方案。**
<img src="https://static001.geekbang.org/resource/image/07/71/07217a9ee64e1970ac04de9080505871.jpeg" alt="">
在物理机A上的容器A里面能看到的容器的IP地址是172.17.8.2/24里面设置了默认的路由规则default via 172.17.8.1 dev eth0。
如果容器A要访问172.17.9.2就会发往这个默认的网关172.17.8.1。172.17.8.1就是物理机上面docker0网桥的IP地址这台物理机上的所有容器都是连接到这个网桥的。
在物理机上面查看路由策略会有这样一条172.17.0.0/24 via 172.17.0.0 dev flannel.1也就是说发往172.17.9.2的网络包会被转发到flannel.1这个网卡。
这个网卡是怎么出来的呢在每台物理机上都会跑一个flanneld进程这个进程打开一个/dev/net/tun字符设备的时候就出现了这个网卡。
你有没有想起qemu-kvm打开这个字符设备的时候物理机上也会出现一个网卡所有发到这个网卡上的网络包会被qemu-kvm接收进来变成二进制串。只不过接下来qemu-kvm会模拟一个虚拟机里面的网卡将二进制的串变成网络包发给虚拟机里面的网卡。但是flanneld不用这样做所有发到flannel.1这个网卡的包都会被flanneld进程读进去接下来flanneld要对网络包进行处理。
物理机A上的flanneld会将网络包封装在UDP包里面然后外层加上物理机A和物理机B的IP地址发送给物理机B上的flanneld。
为什么是UDP呢因为不想在flanneld之间建立两两连接而UDP没有连接的概念任何一台机器都能发给另一台。
物理机B上的flanneld收到包之后解开UDP的包将里面的网络包拿出来从物理机B的flannel.1网卡发出去。
在物理机B上有路由规则172.17.9.0/24 dev docker0 proto kernel scope link src 172.17.9.1。
将包发给docker0docker0将包转给容器B。通信成功。
上面的过程连通性没有问题,但是由于全部在用户态,所以性能差了一些。
跨物理机的连通性问题在虚拟机那里有成熟的方案就是VXLAN那**能不能Flannel也用VXLAN呢**
当然可以了。如果使用VXLAN就不需要打开一个TUN设备了而是要建立一个VXLAN的VTEP。如何建立呢可以通过netlink通知内核建立一个VTEP的网卡flannel.1。在我们讲OpenvSwitch的时候提过netlink是一种用户态和内核态通信的机制。
当网络包从物理机A上的容器A发送给物理机B上的容器B在容器A里面通过默认路由到达物理机A上的docker0网卡然后根据路由规则在物理机A上将包转发给flannel.1。这个时候flannel.1就是一个VXLAN的VTEP了它将网络包进行封装。
内部的MAC地址这样写源为物理机A的flannel.1的MAC地址目标为物理机B的flannel.1的MAC地址在外面加上VXLAN的头。
外层的IP地址这样写源为物理机A的IP地址目标为物理机B的IP地址外面加上物理机的MAC地址。
这样就能通过VXLAN将包转发到另一台机器从物理机B的flannel.1上解包变成内部的网络包通过物理机B上的路由转发到docker0然后转发到容器B里面。通信成功。
<img src="https://static001.geekbang.org/resource/image/01/79/01f86f6049eef051d48e2e235fa43d79.jpeg" alt="">
## 小结
好了,今天的内容就到这里,我来总结一下。
<li>
基于NAT的容器网络模型在微服务架构下有两个问题一个是IP重叠一个是端口冲突需要通过Overlay网络的机制保持跨节点的连通性。
</li>
<li>
Flannel是跨节点容器网络方案之一它提供的Overlay方案主要有两种方式一种是UDP在用户态封装一种是VXLAN在内核态封装而VXLAN的性能更好一些。
</li>
最后,给你留两个问题:
<li>
通过Flannel的网络模型可以实现容器与容器直接跨主机的互相访问那你知道如果容器内部访问外部的服务应该怎么融合到这个网络模型中吗
</li>
<li>
基于Overlay的网络毕竟做了一次网络虚拟化有没有更加高性能的方案呢
</li>
我们的专栏更新到第30讲不知你掌握得如何每节课后我留的思考题你都有没有认真思考并在留言区写下答案呢我会从**已发布的文章中选出一批认真留言的同学**,赠送学习奖励礼券和我整理的独家网络协议知识图谱。
欢迎你留言和我讨论。趣谈网络协议,我们下期见!

View File

@@ -0,0 +1,224 @@
<audio id="audio" title="第31讲 | 容器网络之Calico为高效说出善意的谎言" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/1c/98/1ca02d9aac71d9173ec414fd7ec34e98.mp3"></audio>
上一节我们讲了Flannel如何解决容器跨主机互通的问题这个解决方式其实和虚拟机的网络互通模式是差不多的都是通过隧道。但是Flannel有一个非常好的模式就是给不同的物理机设置不同网段这一点和虚拟机的Overlay的模式完全不一样。
在虚拟机的场景下,整个网段在所有的物理机之间都是可以“飘来飘去”的。网段不同,就给了我们做路由策略的可能。
## Calico网络模型的设计思路
我们看图中的两台物理机。它们的物理网卡是同一个二层网络里面的。由于两台物理机的容器网段不同,我们完全可以将两台物理机配置成为路由器,并按照容器的网段配置路由表。
<img src="https://static001.geekbang.org/resource/image/19/37/1957b75dd689127c4621b5460c356137.jpg" alt="">
例如在物理机A中我们可以这样配置要想访问网段172.17.9.0/24下一跳是192.168.100.101也即到物理机B上去。
这样在容器A中访问容器B当包到达物理机A的时候就能够匹配到这条路由规则并将包发给下一跳的路由器也即发给物理机B。在物理机B上也有路由规则要访问172.17.9.0/24从docker0的网卡进去即可。
当容器B返回结果的时候在物理机B上可以做类似的配置要想访问网段172.17.8.0/24下一跳是192.168.100.100也即到物理机A上去。
当包到达物理机B的时候能够匹配到这条路由规则将包发给下一跳的路由器也即发给物理机A。在物理机A上也有路由规则要访问172.17.8.0/24从docker0的网卡进去即可。
这就是**Calico网络的大概思路****即不走Overlay网络不引入另外的网络性能损耗而是将转发全部用三层网络的路由转发来实现**,只不过具体的实现和上面的过程稍有区别。
首先如果全部走三层的路由规则没必要每台机器都用一个docker0从而浪费了一个IP地址而是可以直接用路由转发到veth pair在物理机这一端的网卡。同样在容器内路由规则也可以这样设定把容器外面的veth pair网卡算作默认网关下一跳就是外面的物理机。
于是,整个拓扑结构就变成了这个图中的样子。
<img src="https://static001.geekbang.org/resource/image/f4/58/f4fab81e3f981827577aa7790b78dc58.jpg" alt="">
## Calico网络的转发细节
我们来看其中的一些细节。
容器A1的IP地址为172.17.8.2/32这里注意不是/24而是/32将容器A1作为一个单点的局域网了。
容器A1里面的默认路由Calico配置得比较有技巧。
```
default via 169.254.1.1 dev eth0
169.254.1.1 dev eth0 scope link
```
这个IP地址169.254.1.1是默认的网关,但是整个拓扑图中没有一张网卡是这个地址。那如何到达这个地址呢?
前面我们讲网关的原理的时候说过当一台机器要访问网关的时候首先会通过ARP获得网关的MAC地址然后将目标MAC变为网关的MAC而网关的IP地址不会在任何网络包头里面出现也就是说没有人在乎这个地址具体是什么只要能找到对应的MAC响应ARP就可以了。
ARP本地有缓存通过ip neigh命令可以查看。
```
169.254.1.1 dev eth0 lladdr ee:ee:ee:ee:ee:ee STALE
```
这个MAC地址是Calico硬塞进去的但是没有关系它能响应ARP于是发出的包的目标MAC就是这个MAC地址。
在物理机A上查看所有网卡的MAC地址的时候我们会发现veth1就是这个MAC地址。所以容器A1里发出的网络包第一跳就是这个veth1这个网卡也就到达了物理机A这个路由器。
在物理机A上有三条路由规则分别是去两个本机的容器的路由以及去172.17.9.0/24下一跳为物理机B。
```
172.17.8.2 dev veth1 scope link
172.17.8.3 dev veth2 scope link
172.17.9.0/24 via 192.168.100.101 dev eth0 proto bird onlink
```
同理物理机B上也有三条路由规则分别是去两个本机的容器的路由以及去172.17.8.0/24下一跳为物理机A。
```
172.17.9.2 dev veth1 scope link
172.17.9.3 dev veth2 scope link
172.17.8.0/24 via 192.168.100.100 dev eth0 proto bird onlink
```
如果你觉得这些规则过于复杂,我将刚才的拓扑图转换为这个更加容易理解的图。
<img src="https://static001.geekbang.org/resource/image/5f/47/5f23071c1e1b17cc46f1cb8955084247.jpg" alt="">
在这里,物理机化身为路由器,通过路由器上的路由规则,将包转发到目的地。在这个过程中,没有隧道封装解封装,仅仅是单纯的路由转发,性能会好很多。但是,这种模式也有很多问题。
## Calico的架构
### 路由配置组件Felix
如果只有两台机器,每台机器只有两个容器,而且保持不变。我手动配置一下,倒也没啥问题。但是如果容器不断地创建、删除,节点不断地加入、退出,情况就会变得非常复杂。
<img src="https://static001.geekbang.org/resource/image/f2/31/f29027cca71f3dfbba8c2f1a35c29331.jpg" alt="">
就像图中,有三台物理机,两两之间都需要配置路由,每台物理机上对外的路由就有两条。如果有六台物理机,则每台物理机上对外的路由就有五条。新加入一个节点,需要通知每一台物理机添加一条路由。
这还是在物理机之间一台物理机上每创建一个容器也需要多配置一条指向这个容器的路由。如此复杂肯定不能手动配置需要每台物理机上有一个agent当创建和删除容器的时候自动做这件事情。这个agent在Calico中称为Felix。
### 路由广播组件BGP Speaker
当Felix配置了路由之后接下来的问题就是如何将路由信息也即将“如何到达我这个节点访问我这个节点上的容器”这些信息广播出去。
能想起来吗这其实就是路由协议啊路由协议就是将“我能到哪里如何能到我”的信息广播给全网传出去从而客户端可以一跳一跳地访问目标地址的。路由协议有很多种Calico使用的是BGP协议。
在Calico中每个Node上运行一个软件BIRD作为BGP的客户端或者叫作BGP Speaker将“如何到达我这个Node访问我这个Node上的容器”的路由信息广播出去。所有Node上的BGP Speaker 都互相建立连接,就形成了全互连的情况,这样每当路由有所变化的时候,所有节点就都能够收到了。
### 安全策略组件
Calico中还实现了灵活配置网络策略Network Policy可以灵活配置两个容器通或者不通。这个怎么实现呢
<img src="https://static001.geekbang.org/resource/image/dd/f2/ddae28956780cc3e45fde76ae96701f2.jpg" alt="">
虚拟机中的安全组是用iptables实现的。Calico中也是用iptables实现的。这个图里的内容是iptables在内核处理网络包的过程中可以嵌入的处理点。Calico也是在这些点上设置相应的规则。
<img src="https://static001.geekbang.org/resource/image/8f/3f/8f2be6638615fc5501c460d1206bff3f.jpg" alt="">
当网络包进入物理机上的时候进入PREOUTING规则这里面有一个规则是cali-fip-dnat这是实现浮动IPFloating IP的场景主要将外网的IP地址dnat作为容器内的IP地址。在虚拟机场景下路由器的网络namespace里面有一个外网网卡上也设置过这样一个DNAT规则。
接下来可以根据路由判断,是到本地的,还是要转发出去的。
如果是本地的走INPUT规则里面有个规则是cali-wl-to-hostwl的意思是workload也即容器也即这是用来判断从容器发到物理机的网络包是否符合规则的。这里面内嵌一个规则cali-from-wl-dispatch也是匹配从容器来的包。如果有两个容器则会有两个容器网卡这里面内嵌有详细的规则“cali-fw-cali网卡1”和“cali-fw-cali网卡2”fw就是from workload也就是匹配从容器1来的网络包和从容器2来的网络包。
如果是转发出去的走FORWARD规则里面有个规则cali-FORWARD。这里面分两种情况一种是从容器里面发出来转发到外面的另一种是从外面发进来转发到容器里面的。
第一种情况匹配的规则仍然是cali-from-wl-dispatch也即from workload。第二种情况匹配的规则是cali-to-wl-dispatch也即to workload。如果有两个容器则会有两个容器网卡在这里面内嵌有详细的规则“cali-tw-cali网卡1”和“cali-tw-cali网卡2”tw就是to workload也就是匹配发往容器1的网络包和发送到容器2的网络包。
接下来是匹配OUTPUT规则里面有cali-OUTPUT。接下来是POSTROUTING规则里面有一个规则是cali-fip-snat也即发出去的时候将容器网络IP转换为浮动IP地址。在虚拟机场景下路由器的网络namespace里面有一个外网网卡上也设置过这样一个SNAT规则。
至此为止Calico的所有组件基本凑齐。来看看我汇总的图。
<img src="https://static001.geekbang.org/resource/image/71/07/71f22fd9e8336c7e10c8ff7bd276af07.jpg" alt="">
## 全连接复杂性与规模问题
这里面还存在问题就是BGP全连接的复杂性问题。
你看刚才的例子里只有六个节点BGP的互连已经如此复杂如果节点数据再多这种全互连的模式肯定不行到时候都成蜘蛛网了。于是多出了一个组件BGP Route Reflector它也是用BIRD实现的。有了它BGP Speaker就不用全互连了而是都直连它它负责将全网的路由信息广播出去。
可是问题来了规模大了大家都连它它受得了吗这个BGP Router Reflector会不会成为瓶颈呢
所以肯定不能让一个BGP Router Reflector管理所有的路由分发而是应该有多个BGP Router Reflector每个BGP Router Reflector管一部分。
多大算一部分呢咱们讲述数据中心的时候说服务器都是放在机架上的每个机架上最顶端有个TOR交换机。那将机架上的机器连在一起这样一个机架是不是可以作为一个单元让一个BGP Router Reflector来管理呢如果要跨机架如何进行通信呢这就需要BGP Router Reflector也直接进行路由交换。它们之间的交换和一个机架之间的交换有什么关系吗
有没有觉得在这个场景下一个机架就像一个数据中心可以把它设置为一个AS而BGP Router Reflector有点儿像数据中心的边界路由器。在一个AS内部也即服务器和BGP Router Reflector之间使用的是数据中心内部的路由协议iBGPBGP Router Reflector之间使用的是数据中心之间的路由协议eBGP。
<img src="https://static001.geekbang.org/resource/image/47/a0/474cb05d5536f11d75baeb6332d788a0.jpg" alt="">
这个图中一个机架上有多台机器每台机器上面启动多个容器每台机器上都有可以到达这些容器的路由。每台机器上都启动一个BGP Speaker然后将这些路由规则上报到这个Rack上接入交换机的BGP Route Reflector将这些路由通过iBGP协议告知到接入交换机的三层路由功能。
在接入交换机之间也建立BGP连接相互告知路由因而一个Rack里面的路由可以告知另一个Rack。有多个核心或者汇聚交换机将接入交换机连接起来如果核心和汇聚起二层互通的作用则接入和接入之间之间交换路由即可。如果核心和汇聚交换机起三层路由的作用则路由需要通过核心或者汇聚交换机进行告知。
## 跨网段访问问题
上面的Calico模式还有一个问题就是跨网段问题这里的跨网段是指物理机跨网段。
前面我们说的那些逻辑成立的条件是我们假设物理机可以作为路由器进行使用。例如物理机A要告诉物理机B你要访问172.17.8.0/24下一跳是我192.168.100.100同理物理机B要告诉物理机A你要访问172.17.9.0/24下一跳是我192.168.100.101。
之所以能够这样是因为物理机A和物理机B是同一个网段的是连接在同一个交换机上的。那如果物理机A和物理机B不是在同一个网段呢
<img src="https://static001.geekbang.org/resource/image/58/89/58bb1d0965c383b1eaac06946998f089.jpg" alt=""><br>
<img src="https://static001.geekbang.org/resource/image/1b/37/1b4514ed6d0e952a9d14f55yy36c0937.jpg" alt="">
例如物理机A的网段是192.168.100.100/24物理机B的网段是192.168.200.101/24这样两台机器就不能通过二层交换机连接起来了需要在中间放一台路由器做一次路由转发才能跨网段访问。
本来物理机A要告诉物理机B你要访问172.17.8.0/24下一跳是我192.168.100.100的,但是中间多了一台路由器,下一跳不是我了,而是中间的这台路由器了,这台路由器的再下一跳,才是我。这样之前的逻辑就不成立了。
我们看刚才那张图的下半部分。物理机B上的容器要访问物理机A上的容器第一跳就是物理机BIP为192.168.200.101第二跳是中间的物理路由器右面的网口IP为192.168.200.1第三跳才是物理机AIP为192.168.100.100。
这是咱们通过拓扑图看到的关键问题是在系统中物理机A如何告诉物理机B怎么让它才能到我这里物理机A根本不可能知道从物理机B出来之后的下一跳是谁况且现在只是中间隔着一个路由器这种简单的情况如果隔着多个路由器呢谁能把这一串的路径告诉物理机B呢
我们能想到的第一种方式是让中间所有的路由器都来适配Calico。本来它们互相告知路由只互相告知物理机的现在还要告知容器的网段。这在大部分情况下是不可能的。
第二种方式还是在物理机A和物理机B之间打一个隧道这个隧道有两个端点在端点上进行封装将容器的IP作为乘客协议放在隧道里面而物理主机的IP放在外面作为承载协议。这样不管外层的IP通过传统的物理网络走多少跳到达目标物理机从隧道两端看起来物理机A的下一跳就是物理机B这样前面的逻辑才能成立。
这就是Calico的**IPIP模式**。使用了IPIP模式之后在物理机A上我们能看到这样的路由表
```
172.17.8.2 dev veth1 scope link
172.17.8.3 dev veth2 scope link
172.17.9.0/24 via 192.168.200.101 dev tun0 proto bird onlink
```
这和原来模式的区别在于下一跳不再是同一个网段的物理机B了IP为192.168.200.101并且不是从eth0跳而是建立一个隧道的端点tun0从这里才是下一跳。
如果我们在容器A1里面的172.17.8.2去ping容器B1里面的172.17.9.2首先会到物理机A。在物理机A上根据上面的规则会转发给tun0并在这里对包做封装
<li>
内层源IP为172.17.8.2
</li>
<li>
内层目标IP为172.17.9.2
</li>
<li>
外层源IP为192.168.100.100
</li>
<li>
外层目标IP为192.168.200.101。
</li>
将这个包从eth0发出去在物理网络上会使用外层的IP进行路由最终到达物理机B。在物理机B上tun0会解封装将内层的源IP和目标IP拿出来转发给相应的容器。
## 小结
好了,这一节就到这里,我们来总结一下。
<li>
Calico推荐使用物理机作为路由器的模式这种模式没有虚拟化开销性能比较高。
</li>
<li>
Calico的主要组件包括路由、iptables的配置组件Felix、路由广播组件BGP Speaker以及大规模场景下的BGP Route Reflector。
</li>
<li>
为解决跨网段的问题Calico还有一种IPIP模式也即通过打隧道的方式从隧道端点来看将本来不是邻居的两台机器变成相邻的机器。
</li>
最后,给你留两个思考题:
<li>
将Calico部署在公有云上的时候经常会选择使用IPIP模式你知道这是为什么吗
</li>
<li>
容器是用来部署微服务的,微服务之间的通信,除了网络要互通,还需要高效地传输信息,例如下单的商品、价格、数量、支付的钱等等,这些要通过什么样的协议呢?
</li>
我们的专栏更新到第31讲不知你掌握得如何每节课后我留的思考题你都有没有认真思考并在留言区写下答案呢我会从**已发布的文章中选出一批认真留言的同学**,赠送学习奖励礼券和我整理的独家网络协议知识图谱。
欢迎你留言和我讨论。趣谈网络协议,我们下期见!