引: 之前写过一篇文章介绍如何管理 Linux 设备上的 bridge(网桥)和 docker bridge, 今天我们来看看 k8s 的网络模型.
我们先来看图示例, 下面则个是 k8s 的网络模型图.
k8s 的网络模型
我们知道, 在 k8s 里面最小的管理单元是 pod, 一个主机可以跑多个 pod, 一个 pod 里面可以跑多个容器.
如上面所示, 一个 pod 里面所有的容器共享一个网络命名空间(network namespace), 所以, pod 里面的容器之间通信, 可以直接通过 localhost 来完成, pod 里面的容器之间通过 localhost + 端口的方式来通信(这和应用程序在宿主机的通信方式是一样的).
那么 pod 和 pod 之间的通信呢? 通常来说, 我们给应用程序定死端口会给应用程序水平扩展带来很多不便, 所以 k8s 不会使用定死端口这样的方法, 而是采用其他方法来解决 pod 之间寻址的问题
每个 pod 都会有一个自己的 ip, 可以将 Pod 像 VM 或物理主机一样对待. 这样 pod 和 pod 之间的通信就不需要像容器一样, 通过内外端口映射来通信了, 这样就避免了端口冲突的问题.
特殊的情况下(比如运维做网络检测或者程序调试), 可以在 pod 所在的宿主机想向 pod 的 ip + 端口发起请求, 这些请求会转发到 pod 的端口, 但是 pod 本身它自己是不知道端口的存在的.
因此, k8s 的网络遵循以下原则:
一个节点的 pod 和其他节点的 pod 通信不需要通过做网络地址转换(NAT)
一个节点上所有的 agent 控制程序 (如 deamon 和 kubelet) 可以和这个节点上的 pod 通信
节点主机网络中的 Pod 可以与其他所有节点上的所有 Pod 通信, 而无需 NAT
把上面这个 pod 替换成容器也是成立的, 因为 pod 里面的容器和 pod 共享网络.
基本上的原则就是, k8s 的里面的 pod 可以自由的和集群里面的任何其他 pod 通信(即使他们是部署在不同的宿主机), 而且 pod 直接的通信是直接使用 pod 自己的 ip 来通信, 他们不知道宿主机的 ip, 所以, 对于 pod 之间来说, 宿主机的网络信息是透明的, 好像不存在一样.
然后, 定了这几个原则之后, 具体的实现 k8s 的这个网络模型有好多种实现, 我们这里介绍的是 Flannel, 是其中最简单的一种实现.
Flannel 实现 pod 之间的通信, 是通过一种覆盖网络(overlay network), 把数据包封装在另外一个网络来做转发, 这个覆盖网络可以给每一个 pod 分配一个独立的 ip 地址, 使他们看起来都是一台具有独立 ip 的物理主机一样.
下面这个就是 k8s 用覆盖网络来实现的一个例子:
flannel 覆盖网络
可以看到有 3 个 node, 在多个 node 上建立一个覆盖网络, 子网网段是 100.95.0.0/16, 然后, 最终到容器级别, 每个容器在这个网段里面获取到一个独立的 ip. 而宿主机所在的局域网络的网段是 172.20.32.0/19
看这两个网段, 就知道, fannel 给这个集群创建了一个更大的网络给 pod 使用, 可以容纳的主机数量达到 65535(2^16)个.
对于每个宿主机, fannel 给每个了一个小一点的网络 100.96.x.0/24, 提供给每个这个宿主机的每一个 pod 使用, 也就是说, 每一个宿主机可以有 256(2^8)个 pod.docker 默认的网桥 docker0 用的就是这个网络, 也就是所有的 docker 通过 docker0 来使用这个网络. 即使说, 对于容器来说, 都是通过 docker0 这个桥来通信, 和我们平常单机的容器是一样的(如果你不给创建的容器指定网络的话, 默认用的是 docker0, 参考我以前写的关于 docker bridge 的文章)
那么, 对于同一个 host 里面的容器通信, 我们上面说了是通过这个台宿主机的里面的 docker0 这个网桥来通信. 那对于跨宿主机, 也即是两个宿主机之间的容器是怎么通信的呢? fannel 使用了宿主机操作系统的 kernel route 和 UDP(这是其中一种实现)包封装来完成. 下图演示了这个通信过程:
fannel 网络中跨宿主机的容器通信
如图所示, 100.96.1.2(container-1) 要和 100.96.2.3(container-2)通信, 两个容器分别处于不同的宿主机.
假设有一个包是从 100.96.1.2 发出去给 100.96.2.3, 它会先经过 docker0, 因为 docker0 这个桥是所有容器的网关. 然后这个包会经过 route table 处理, 转发出去到局域网 172.20.32.0/19. 而这个 route table 的对应处理这类包的规则又是从哪里来的呢? 它们是由 fannel 的一个守护程序 flanneld 创建的.
每一台宿主机都会跑一个 flannel 的 deamon 的进程, 这个进程的程序会往宿主机的 route table 里面写入特定的路由规则, 这个规则大概是这样的.
Node1 的 route table
- admin@ip-172-20-33-102:~$ ip route
- default via 172.20.32.1 dev eth0
- 100.96.0.0/16 dev flannel0 proto kernel scope link src 100.96.1.0
- 100.96.1.0/24 dev docker0 proto kernel scope link src 100.96.1.1
- 172.20.32.0/19 dev eth0 proto kernel scope link src 172.20.33.102
图例的数据包发出去的目标地址是 100.96.2.3, 它属于网段 100.96.0.0/16, 这个目标地址命中第二条规则, 也就是这个包会发到 flannel0 这个设备(dev), 这 flannel0 是一个 TUN 设备. 是在内核里面的一个虚拟网络设备(虚拟网卡).
在内核 (kernel) 里面, 有两种虚拟网卡设备, 分别是 TUN 和 TAP, 其中 TAP 处理的是第二层 (数据链路层) 的帧, 而 TUN 处理的是第三层 (网络层) 的 ip 包.
应用程序可以绑定到 TUN 和 TAP 设备, 内核会把数据通过 TUN 或者 TAP 设备发送给这些程序, 反过来, 应用程序也可以通过 TUN 和 TAP 向内核写入数据, 进而由内核的路由处理这些发出去的数据包.
那么上面这个 flannel0 就是一个这样的 TUN 设备. 这个设备连到的是一个 flannel 的守护进程程序 flanneld
而这个 flanneld 是干嘛的呢? 它可以接受所有发往 flannel0 这个设备的数据包, 然后做数据封装处理, 它的封装的逻辑也很简单, 就是根据目标地址, 找到这个这地址对应的在整个 flannel 网络里面对应物理 ip 和端口 (这里是 Node2 对应的物理 ip), 然后增加一个包头, 增加的包头里面目标地址为这个实际的物理 ip 和端口(当然源地址也改成了局域网络的 ip), 将原来的数据包嵌入在新的数据包中, 然后再把这个封装后的包扔回去给内核, 内核根据目标地址去路由规则匹配规则, 发现目标地址 ip 是 172.20.54.98, 端口是 8285. 根据 ip 匹配不到任何特定的规则, 就用第一条 default(默认) 的规则, 通过 eth0 这个物理网卡, 把数据包发给局域网(这里是 UDP 广播出去)
当 Node2 的收到这个包后, 然后根据端口 8285 发现他的目标地址原来是发给 flanneld 的, 然后就直接交给 flanneld 这程序, flanneld 收到包后, 把包头去掉, 发现原来目标地址是 100.96.2.3, 然后就交换 flannel0,flannel0 把这个解开后的原包交给内核, 内核发现它的目标地址是 100.96.2.3, 应该交给 docker0 来处理.(图例里面画的是直接由 flannel0 交给 docker0, 没有图示出内核, 实际上 flannel0 是一个 TUN 设备, 是跑在内核的, 数据经过它后可以交给内核, 由内核根据路由决定进一步怎么 forward)
以上就是这个通信的过程, 那么这里有一个问题: flanneld 是怎么知道 100.96.2.3 对应的目标地址是 172.20.54.98:8285 的呢?
这是因为 flanneld 维护了一个映射关系, 它没创造一个虚拟的容器 ip(分配给容器新 ip 的时候), 它就知道这个容器的 ip 实际上是在哪台宿主机上, 然后把这个映射关系存储起来, 在 k8s 里面 flanneld 存储的这个映射关系放在 etd 上, 这就是为什么 flanneld 为什么知道这个怎么去封装这些包了, 下面就是 etcd 里面的数据的:
- admin@ip-172-20-33-102:~$ etcdctl ls /coreos.com/network/subnets
- /coreos.com/network/subnets/100.96.1.0-24
- /coreos.com/network/subnets/100.96.2.0-24
- /coreos.com/network/subnets/100.96.3.0-24
- admin@ip-172-20-33-102:~$ etcdctl get /coreos.com/network/subnets/100.96.2.0-24
- {
- "PublicIP":"172.20.54.98"
- }
看上面这个数据, etcd 里面存储的 100.96.2.0-24 这个网段的容器是放在 172.20.54.98 这台宿主机上的.
那么还有一个问题, 端口 8285 又是怎么知道的?
这个很简单, flanneld 的默认监听的端口就是这个 8285 端口, flanneld 启动的时候, 就监听了 UDP 端口 8285. 所以发给 Node2:8285 的所有 UDP 数据包会, flanneld 这个进程会直接处理, 如何去掉包头就还原出来原来的包了, 还原后交给 TUN 设备 flannel0, 由 flannel0 交给内核, 内核根据 Node2 的路由规则交给 docker0(Node2 的路由规则和 node1 是基本上一样的, 除了第三位的网段标识不一样, 一个是 100.96.1 一个是 100.92.2):
- admin@ip-172-20-54-98:~$ ip route
- default via 172.20.32.1 dev eth0
- 100.96.0.0/16 dev flannel0 proto kernel scope link src 100.96.2.0
- 100.96.2.0/24 dev docker0 proto kernel scope link src 100.96.2.1
- 172.20.32.0/19 dev eth0 proto kernel scope link src 172.20.54.98
看 Node2 的这个规则, flannld 去掉包头解出来的原包的目标 ip 是 100.96.2.3, 由 flannel0 交回去给 kennel,kennel 发现命中第三条规则, 所以会把这个包叫给 docker0, 继而就进入了 docker0 这个桥的子网了, 接下去就是 docker 的事情了, 参考以前写的文章.
最后一个问题, 怎么配置 docker 去使用 100.96.x.0/24 这个子网呢, 如果是手工创建容器的话, 这个也是非常简单的, 参考以前写的关于 docker bridge 的这篇文章, 但是在 k8s 里面, 是通过配置来实现的:
flanneld 会把子网信息写到一个配置文件 / run/flannel/subnet.env 里
- admin@ip-172-20-33-102:~$ cat /run/flannel/subnet.env
- FLANNEL_NETWORK=100.96.0.0/16
- FLANNEL_SUBNET=100.96.1.1/24
- FLANNEL_MTU=8973
- FLANNEL_IPMASQ=true
docker 会使用这个配置的环境变了来作为它的 bridge 的配置
dockerd --bip=$FLANNEL_SUBNET --mtu=$FLANNEL_MTU
以上, 就是 k8s 如何使用 flannel 网络来跨机器通信的原理, 总体来讲, 由于 flanneld 这个守护神干了所有的脏活累活(其实已经是 k8s 的网络实现里面最简单的一种了), 使得 pod 和容器能够连接另外一个 pod 或者容器变得非常简单, 就像连一个大局域网里面任意以太主机一样, 他们只需要知道对方的虚拟 ip 就可以直接通信了, 不需要做
NAT 等复杂的规则处理.
那么性能怎么样?
新版本的 flannel 不推荐在生产环境使用 UDP 的包封装这种实现. 只用它来做测试和调试用, 因为它的性能表现和其他的实现比差一些.
flannel0 利用的 TUN 设备做包封装原理
看上面这个图解, 一个 upd 包需要来回在用户空间 (user space) 和内核空间 (kennel space) 复制 3 次, 这会大大增加网络开销.
官方的文档里面可以看到其他的包转发实现方式, 可以进一步阅读, 其中 host-gw 的性能比较好, 它是在第二层去做数据包处理.
来源: http://www.jianshu.com/p/2f91907b2aba