Kubernetes中有三种网络和三种IP。

docker网络实现

想要理解k8s的网络,需要先了解docker的网络实现,用过docker基本都知道,启动docker engine后,主机的网络设备里会有一个docker0的网关,而容器默认情况下会被分配在一个以docker0为网关的虚拟子网中。

root@VM-66-197-ubuntu:/home/ubuntu# ifconfig
...
docker0   Link encap:Ethernet  HWaddr 02:42:ec:43:56:b2
          inet addr:172.17.0.1  Bcast:0.0.0.0  Mask:255.255.0.0
...
root@VM-66-197-ubuntu:/home/ubuntu# docker inspect nginx
···
"IPAddress": "172.17.0.2",
···

为了实现上述功能,docker主要用到了linux的Bridge、Network Namespace、VETH。

  • Bridge相当于是一个虚拟网桥,工作在第二层网络。也可以为它配置IP,工作在三层网络。docker0网关就是通过Bridge实现的。
  • Network Namespace是网络命名空间,通过Network Namespace可以建立一些完全隔离的网络栈。比如通过docker network create xxx就是在建立一个Network Namespace。
  • VETH是虚拟网卡的接口对,可以把两端分别接在两个不同的Network Namespace中,实现两个原本隔离的Network Namespace的通信。

所以总结起来就是:Network Namespace做了容器和宿主机的网络隔离,Bridge分别在容器和宿主机建立一个网关,然后再用VETH将容器和宿主机两个网络空间连接起来。

这就docker默认的bridge实现方式,对此,docker总结提出来CNM(container network model)理论,还在这个理论的基础上实现了libnetwork。具体docker网络的实现可以查看这里

Kubernetes网络模型

1、每个Pod拥有唯一的IP(perPodperIp模型)。

2、所有的pod都在一个可以直接连通的、扁平的网络空间中。满足以下条件:

  • 任意两个 pod 之间其实是可以直接通信的,无需经过显式地使用 NAT 来接收数据和地址的转换;
  • node 与 pod 之间是可以直接通信的,无需使用明显的地址转换;
  • pod 看到自己的 IP 跟别人看见它所用的 IP 是一样的,中间不能经过转换。

3、四大目标,其实是在设计一个 K8s 的系统为外部世界提供服务的时候,从网络的角度要想清楚,外部世界如何一步一步连接到容器内部的应用?

  • 外部世界和 service 之间是怎么通信的?就是有一个互联网或者是公司外部的一个用户,怎么用到 service?service 特指 K8s 里面的服务概念。
  • service 如何与它后端的 pod 通讯?
  • pod 和 pod 之间调用是怎么做到通信的?
  • 最后就是 pod 内部容器与容器之间的通信?

最终要达到目标,就是外部世界可以连接到最里面,对容器提供服务。

Pod Network

pod内网络

Kubernetes的一个Pod中包含有多个容器,这些容器共享一个Network Namespace,更具体的说,是共享一个Network Namespace中的一个IP。创建Pod时,首先会生成一个pause容器,然后其他容器会共享pause容器的网络。

root@kube-2:~# docker ps
CONTAINER ID        IMAGE                                            COMMAND                  CREATED             STATUS              PORTS               NAMES
d2dbb9e288e2        mirrorgooglecontainers/pause-amd64:3.0           "/pause"                 3 weeks ago         Up 3 weeks                              k8s_POD_frontend-647d9fdddf-n4x9w_default_9f275ea8-4853-11e8-8c42-fa163e4a07e5_0
cf1bfff28238        nginx                                            "nginx -g 'daemon of…"   3 weeks ago         Up 3 weeks                              k8s_nginx-demo_frontend-647d9fdddf-n4x9w_default_9f275ea8-4853-11e8-8c42-fa163e4a07e5_0

root@kube-2:~# docker inspect cf1bf
...
            "NetworkMode": "container:d2dbb9e288e26231759e28e8d4816862c6c57d4d2822a259bee7fcc9a2fd0b20",
...

可以看出,在这个Pod中,nginx容器通过”NetworkMode”: “container:d2db…“与pause容器共享了网络,我们可以想到docker我网络的contaienr模式。这时候,相同容器之间的访问只需要用localhost+端口的形式,就像他们是部署在同一台物理机的不同进程一样,可以使用本地IPC进行通信。

pod间通信

节点内通信

在每个Kubernetes节点上,都有一个根(root)命名空间(root是作为基准,而不是超级用户)–root netns。这个命令空间就是我们node节点的网络,包含这个物理网卡eth0。

类似的,每个Pod都有其自身的netns,通过一个虚拟的以太网对连接到root netns。这基本上就是一个管道对,一端在root netns内,另一端在Pod的nens内。

我们把Pod端的网络接口叫 eth0,这样Pod就不需要知道底层主机,它认为它拥有自己的根网络设备。另一端命名成比如 vethxxx。你可以用ifconfig 或者 ip a 命令列出你的节点上的所有这些接口。

节点上的所有Pod都会完成这个过程。这些Pod要相互通信,就要用到linux的以太网桥 cbr0 了。Docker使用了类似的网桥,称为docker0。

所以一个网络数据包要由pod1到pod2。

  • 它由pod1中netns的eth0网口离开,通过vethxxx进入root netns。
  • 然后被传到cbr0,cbr0使用ARP请求,说“谁拥有这个IP”,从而发现目标地址。
  • vethyyy说它有这个IP,因此网桥就知道了往哪里转发这个包。
  • 数据包到达vethyyy,跨过管道对,到达pod2的netns。

这就是同一节点内容器间通信的流程。当然也可以用其它方式,但是无疑这是最简单的方式,同时也是Docker单机采用的方式,可以看出只是把container换成了pod。

节点间通信

正如我前面提到,Pod也需要跨节点可达。Kubernetes不关心如何实现。我们可以使用L2(ARP跨节点),L3(IP路由跨节点,就像云提供商的路由表),Overlay网络,或者甚至信鸽。无所谓,只要流量能到达另一个节点的期望Pod就好。每个节点都为Pod IPs分配了唯一的CIDR块(一段IP地址范围),因此每个Pod都拥有唯一的IP,不会和其它节点上的Pod冲突。

一个数据包要从pod1到达pod4(在不同的节点上)

  • 它由pod1中netns的eth0网口离开,通过vethxxx进入root netns。
  • 然后被传到cbr0,cbr0通过发送ARP请求来找到目标地址。
  • 本节点上没有Pod拥有pod4的IP地址,因此数据包由cbr0 传到 主网络接口 eth0.
  • 数据包的源地址为pod1,目标地址为pod4,它以这种方式离开node1进入电缆。
  • 路由表有每个节点的CIDR块的路由设定,它把数据包路由到CIDR块包含pod4的IP的节点。
  • 因此数据包到达了node2的主网络接口eth0。现在即使pod4不是eth0的IP,数据包也仍然能转发到cbr0,因为节点配置了IP forwarding enabled。节点的路由表寻找任意能匹配pod4 IP的路由。它发现了 cbr0 是这个节点的CIDR块的目标地址。你可以用route -n命令列出该节点的路由表,它会显示cbr0的路由。
  • 网桥接收了数据包,发送ARP请求,发现目标IP属于vethyyy。
  • 数据包跨过管道对到达pod4。

Node network

其实就是整个k8s集群node之间的网络,也就是基本的物理机网络,只要各个node之间能相互通信就好。

Service Network

service网络主要是kube-proxy来实现构建的,主要是Pod 和 Service 间通信,外部和 Service 间通信,这些都可以使用kube-proxy的各种模式来解决,具体可以看这里

service network比较特殊,每个新创建的service会被分配一个service IP,比如在集群中设置环境变量export SERVICE_CLUSTER_IP_RANGE=${SERVICE_CLUSTER_IP_RANGE:-192.168.3.0/24},并在kube-apiserver的启动参数–service-cluster-ip-range使用这个环境变量,这个IP的分配范围是192.168.3.0/24。不过这个IP并不“真实”,更像一个“占位符”并且只有入口流量。

在 Service 创建的请求中,可以通过设置 spec.clusterIP 字段来指定自己的集群 IP 地址。 比如,希望替换一个已经已存在的 DNS 条目,或者遗留系统已经配置了一个固定的 IP 且很难重新配置。用户选择的 IP 地址必须合法,并且这个 IP 地址在 service-cluster-ip-range CIDR 范围内,这对 API Server 来说是通过一个标识来指定的。 如果 IP 地址不合法,API Server 会返回 HTTP 状态码 422,表示值不合法。

Kubernetes网络开源组件

service network的实现依靠kube-proxy,node network依靠的是物理组网,pod network就需要插件来完成了,目前比较通用的就是CNI规范的插件。其实主要是解决两个问题:

  • 为容器分配IP地址
  • 不同容器之间的互通

基本概念

第2层网络:OSI(Open Systems Interconnections,开放系统互连)网络模型的“数据链路”层。第2层网络会处理网络上两个相邻节点之间的帧传递。第2层网络的一个值得注意的示例是以太网,其中MAC表示为子层。

第3层网络:OSI网络模型的“网络”层。第3层网络的主要关注点,是在第2层连接之上的主机之间路由数据包。IPv4、IPv6和ICMP是第3层网络协议的示例。

IPAM:IP地址管理;这个IP地址管理并不是容器所特有的,传统的网络比如说DHCP其实也是一种IPAM,到了容器时代我们谈IPAM,主流的两种方法: 基于CIDR的IP地址段分配地或者精确为每一个容器分配IP。但总之一旦形成一个容器主机集群之后,上面的容器都要给它分配一个全局唯一的IP地址,这就涉及到IPAM的话题。

Overlay:在现有二层或三层网络之上再构建起来一个独立的网络,这个网络通常会有自己独立的IP地址空间、交换或者路由的实现。

IPSesc:一个点对点的一个加密通信协议,一般会用到Overlay网络的数据通道里。

vxLAN:由VMware、Cisco、RedHat等联合提出的这么一个解决方案,这个解决方案最主要是解决VLAN支持虚拟网络数量(4096)过少的问题。因为在公有云上每一个租户都有不同的VPC,4096明显不够用。就有了vxLAN,它可以支持1600万个虚拟网络,基本上公有云是够用的。

网桥Bridge: 连接两个对等网络之间的网络设备,但在今天的语境里指的是Linux Bridge,就是大名鼎鼎的Docker0这个网桥。

BGP: 代表“边界网关协议”,主干网自治网络的路由协议,今天有了互联网,互联网由很多小的自治网络构成的,自治网络之间的三层路由是由BGP实现的。

SDN、Openflow: 软件定义网络里面的一个术语,比如说我们经常听到的流表、控制平面,或者转发平面都是Openflow里的术语。

容器网络模型

容器网络发展到现在,形成了两大阵营,就是Docker的CNM和Google、CoreOS、Kuberenetes主导的CNI。首先明确一点,CNM和CNI并不是网络实现,他们是网络规范和网络体系,从研发的角度他们就是一堆接口,你底层是用Flannel也好、用Calico也好,他们并不关心,CNM和CNI关心的是网络管理的问题。

CNM

CNM(Docker LibnetworkContainer Network Model)

Docker Libnetwork的优势就是原生,而且和Docker容器生命周期结合紧密;缺点也可以理解为是原生,被Docker“绑架”。

Docker Swarm overlay
Macvlan & IP networkdrivers
Calico
Contiv
Weave

目前因为docker很少单独使用,使用CMN规范的也比较少,但是很多都是相通的。

CNI

CNI的优势是兼容其他容器技术(e.g. rkt)及上层编排系统(Kubernetes & Mesos),而且社区活跃势头迅猛,Kubernetes加上CoreOS主推,缺点是非Docker原生。

Kubernetes
Weave
Macvlan
Calico
Flannel
Contiv
Mesos CNI

Kubernetes 在处理网络上,没有选择自己再独立创造一个,而是选择了其中的 CNI作为了自己的网络插件。(至于为什么不选择 CNM,可以看看这篇官方的解释:Why Kubernetes doesn’t use libnetwork)。不使用 CNM 最关键的一点,是 k8s 考虑到CNM 在一定程度上和 container runtime 联系相对比较紧密,不好解耦。 有了 k8s 这种巨无霸的选择之后,后来的很多项目都在 CNM 和 CNI 之间选择了 CNI。

CNI目前已经获得了众多开源项目的青睐,比如 K8S、Memos、Cloud Foundry。同时被Cloud Native Computing Foundation所认可。CNCF 背后有众多的科技大亨,所以可以预见,CNI 将会成为未来容器网络的标准,所以很有必要详细研究了解CNI

容器网络方案

容器网络方案基本就是只有下面几种解决方式,后面的组件实现都是基于这些思路来进行的。

隧道方案(Overlay Networking)

隧道方案在IaaS层的网络中应用也比较多,大家共识是随着节点规模的增长复杂度会提升,而且出了网络问题跟踪起来比较麻烦,大规模集群情况下这是需要考虑的一个点。

Weave:UDP广播,本机建立新的BR,通过PCAP互通
Open vSwitch(OVS):基于VxLan和GRE协议,但是性能方面损失比较严重
Flannel:UDP广播,VxLan
Racher:IPsec

底层网络(Underlay Networking)

underlay的一般是从3层或者2层实现隔离和跨主机容器互通的,出了问题也很容易排查。

  • 二层主要是mac的识别,一般二层交换机只要在一个lan中都能通信,使用vlan的话需要在同一个vlan中,如果跨网段进行通信就需要网关,网关其实就是一个中转的三层交换机,在二层的基础上有路由功能
  • 三层主要是ip的路由,我们一般使用三层交换机作为网关,先和网关进行通信,在和目的进行通信

常用的产品

Calico:基于BGP协议的路由方案,支持很细致的ACL控制,对混合云亲和度比较高,是一个三层的实现方案。
Macvlan:从逻辑和Kernel层来看隔离性和性能最优的方案,基于二层隔离,所以需要二层路由器支持,大多数云服务商不支持,所以混合云上比较难以实现。

组件

其实网络插件解决的是pod network的问题,都是基于CMN或者CNI的管理规范来开发的网络插件。

kubenet

kubenet 是一个基于 CNI bridge 的网络插件,它为每个容器建立一对 veth pair 并连接到 cbr0 网桥上。kubenet 在 bridge 插件的基础上拓展了很多功能,包括

  • 使用 host-local IPAM 插件为容器分配 IP 地址, 并定期释放已分配但未使用的 IP 地址
  • 设置 sysctl net.bridge.bridge-nf-call-iptables = 1
  • 为 Pod IP 创建 SNAT 规则
    • -A POSTROUTING ! -d 10.0.0.0/8 -m comment –comment “kubenet: SNAT for outbound traffic from cluster” -m addrtype ! –dst-type LOCAL -j MASQUERADE
  • 开启网桥的 hairpin 和 promisc 模式,允许 Pod 访问它自己所在的 Service IP(即通过 NAT 后再访问 Pod 自己)
    • -A OUTPUT -j KUBE-DEDUP
    • -A KUBE-DEDUP -p IPv4 -s a:58:a:f4:2:1 -o veth+ –ip-src 10.244.2.1 -j ACCEPT
    • -A KUBE-DEDUP -p IPv4 -s a:58:a:f4:2:1 -o veth+ –ip-src 10.244.2.0/24 -j DROP
  • HostPort 管理以及设置端口映射
  • Traffic shaping,支持通过 kubernetes.io/ingress-bandwidth 和 kubernetes.io/egress-bandwidth 等 Annotation 设置 Pod 网络带宽限制

未来 kubenet 插件会迁移到标准的 CNI 插件。

flannel

Flannel是CoreOS团队针对Kubernetes设计的一个网络规划服务,简单来说,它的功能是让集群中的不同节点主机创建的Docker容器都具有全集群唯一的虚拟IP地址。

在默认的Docker配置中,每个节点上的Docker服务会分别负责所在节点容器的IP分配。这样导致的一个问题是,不同节点上容器可能获得相同的内外IP地址。并使这些容器之间能够之间通过IP地址相互找到,也就是相互ping通。

Flannel的设计目的就是为集群中的所有节点重新规划IP地址的使用规则,从而使得不同节点上的容器能够获得“同属一个内网”且”不重复的”IP地址,并让属于不同节点上的容器能够直接通过内网IP通信。

Flannel实质上是一种“覆盖网络(overlaynetwork)”,也就是将TCP数据包装在另一种网络包里面进行路由转发和通信,目前已经支持udp、vxlan、host-gw、aws-vpc、gce和alloc路由等数据转发方式,默认的节点间数据通信方式是UDP转发。

flannel网络模型

1、互补冲突的ip

  • flannel利用Kubernetes API或者etcd用于存储整个集群的网络配置,根据配置记录集群使用的网段。
  • flannel在每个主机中运行flanneld作为agent,它会为所在主机从集群的网络地址空间中,获取一个小的网段subnet,本主机内所有容器的IP地址都将从中分配。

我们可以查看/run/flannel/subnet.env文件来查看对于的IP分配,在不同的主机上进行分配。

在flannel network中,每个pod都会被分配唯一的ip地址,且每个K8s node的subnet各不重叠,没有交集,如下:

2、pod间互相访问

  • flanneld将本主机获取的subnet以及用于主机间通信的Public IP通过etcd存储起来,需要时发送给相应模块。
  • flannel利用各种backend mechanism,例如udp,vxlan等等,跨主机转发容器间的网络流量,完成容器间的跨主机通信。

flannel架构

Flannel 项目本身只是一个框架,真正为我们提供容器网络功能的,是 Flannel 的后端实现,flannnel主要支持的后端实现

  • UDP
  • VXLAN
  • host-gw
UDP

UDP 模式,是 Flannel 项目最早支持的一种方式,却也是性能最差的一种方式。所以,这个模式目前已经被弃用。

可以看到,Flannel UDP 模式提供的其实是一个三层的 Overlay 网络,即:它首先对发出端的 IP 包进行 UDP 封装,然后在接收端进行解封装拿到原始的 IP 包,进而把这个 IP 包转发给目标容器。这就好比,Flannel 在不同宿主机上的两个容器之间打通了一条“隧道”,使得这两个容器可以直接使用 IP 地址进行通信,而无需关心容器和宿主机的分布情况。

  • 首先容器发出包,因为该封包的目的地不在本主机subnet内,因此封包会首先通过网桥转发到主机中。
  • 在主机上经过路由匹配,进入网卡flannel0。(需要注意的是flannel0是一个tun设备,它是一种工作在三层的虚拟网络设备,而flanneld是一个proxy,它会监听flannel0并转发流量。)
  • 当封包进入flannel0时,flanneld就可以从flanne0中将封包读出,由于flanne0是三层设备,所以读出的封包仅仅包含IP层的报头及其负载。
  • 最后flanneld会将获取的封包作为负载数据,通过udp socket发往目的主机。
  • 在目的主机的flanneld会监听Public IP所在的设备,从中读取udp封包的负载,并将其放入flannel0设备内。
  • 容器网络封包到达目的主机,之后就可以通过网桥转发到目的容器了。

优点:Pod能够跨网段访问

缺点:隔离性不够,udp不能隔离两个网段,性能差。

实例

我有两台宿主机。

  • 宿主机 Node 1 上有一个容器 container-1,它的 IP 地址是 100.96.1.2,对应的 docker0 网桥的地址是:100.96.1.1/24。
  • 宿主机 Node 2 上有一个容器 container-2,它的 IP 地址是 100.96.2.3,对应的 docker0 网桥的地址是:100.96.2.1/24。

我们现在的任务,就是让 container-1 访问 container-2。

这种情况下,container-1 容器里的进程发起的 IP 包,其源地址就是 100.96.1.2,目的地址就是 100.96.2.3。由于目的地址 100.96.2.3 并不在 Node 1 的 docker0 网桥的网段里,所以这个 IP 包会被交给默认路由规则,通过容器的网关进入 docker0 网桥(如果是同一台宿主机上的容器间通信,走的是直连规则),从而出现在宿主机上。

这时候,这个 IP 包的下一个目的地,就取决于宿主机上的路由规则了。此时,Flannel 已经在宿主机上创建出了一系列的路由规则,以 Node 1 为例,如下所示:

# 在Node 1上
$ ip route
default via 10.168.0.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
10.168.0.0/24 dev eth0  proto kernel  scope link  src 10.168.0.2

可以看到,由于我们的 IP 包的目的地址是 100.96.2.3,它匹配不到本机 docker0 网桥对应的 100.96.1.0/24 网段,只能匹配到第二条、也就是 100.96.0.0/16 对应的这条路由规则,从而进入到一个叫作 flannel0 的设备中。

flannel0是一个 TUN 设备(Tunnel 设备)。在 Linux 中,TUN 设备是一种工作在三层(Network Layer)的虚拟网络设备。TUN 设备的功能非常简单,即:在操作系统内核和用户应用程序之间传递 IP 包。

像上面提到的情况,当操作系统将一个 IP 包发送给 flannel0 设备之后,flannel0 就会把这个 IP 包,交给创建这个设备的应用程序,也就是 Flannel 进程。这是一个从内核态(Linux 操作系统)向用户态(Flannel 进程)的流动方向。

所以,当 IP 包从容器经过 docker0 出现在宿主机,然后又根据路由表进入 flannel0 设备后,宿主机上的 flanneld 进程(Flannel 项目在每个宿主机上的主进程),就会收到这个 IP 包。然后,flanneld 看到了这个 IP 包的目的地址,是 100.96.2.3,就把它发送给了 Node 2 宿主机。

flanneld 进程在处理由 flannel0 传入的 IP 包时,就可以根据目的 IP 的地址(比如 100.96.2.3),匹配到对应的子网(比如 100.96.2.0/24),从 Etcd 中找到这个子网对应的宿主机的 IP 地址是 10.168.0.3。

在由 Flannel 管理的容器网络里,一台宿主机上的所有容器,都属于该宿主机被分配的一个“子网”。在我们的例子中,Node 1 的子网是 100.96.1.0/24,container-1 的 IP 地址是 100.96.1.2。Node 2 的子网是 100.96.2.0/24,container-2 的 IP 地址是 100.96.2.3。而这些子网与宿主机的对应关系,正是保存在 Etcd 当中,如下所示:

$ 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

Docker Daemon 启动时配置如下所示的 bip 参数即可配置对应的子网

$ FLANNEL_SUBNET=100.96.1.1/24
$ dockerd --bip=$FLANNEL_SUBNET ...

而对于 flanneld 来说,只要 Node 1 和 Node 2 是互通的,那么 flanneld 作为 Node 1 上的一个普通进程,就一定可以通过上述 IP 地址(10.168.0.3)访问到 Node 2,这没有任何问题。

所以说,flanneld 在收到 container-1 发给 container-2 的 IP 包之后,就会把这个 IP 包直接封装在一个 UDP 包里,然后发送给 Node 2。不难理解,这个 UDP 包的源地址,就是 flanneld 所在的 Node 1 的地址,而目的地址,则是 container-2 所在的宿主机 Node 2 的地址,其实就是宿主机之间的通信。

每台宿主机上的 flanneld,都监听着一个 8285 端口,所以 flanneld 只要把 UDP 包发往 Node 2 的 8285 端口即可。

到Node 2上就是反向的处理,由于这个 IP 包的目的地址是 100.96.2.3,它跟第三条、也就是 100.96.2.0/24 网段对应的路由规则匹配更加精确。所以,Linux 内核就会按照这条路由规则,把这个 IP 包转发给 docker0 网桥,然后到容器。

# 在Node 2上
$ ip route
default via 10.168.0.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
10.168.0.0/24 dev eth0  proto kernel  scope link  src 10.168.0.3

性能问题

实际上,相比于两台宿主机之间的直接通信,基于 Flannel UDP 模式的容器通信多了一个额外的步骤,即 flanneld 的处理过程。而这个过程,由于使用到了 flannel0 这个 TUN 设备,仅在发出 IP 包的过程中,就需要经过三次用户态与内核态之间的数据拷贝,如下所示:

我们可以看到:

  • 用户态的容器进程发出的 IP 包经过 docker0 网桥进入内核态;
  • IP 包根据路由表进入 TUN(flannel0)设备,从而回到用户态的 flanneld 进程;
  • flanneld 进行 UDP 封包之后重新进入内核态,将 UDP 包通过宿主机的 eth0 发出去。

此外,我们还可以看到,Flannel 进行 UDP 封装(Encapsulation)和解封装(Decapsulation)的过程,也都是在用户态完成的。在 Linux 操作系统中,上述这些上下文切换和用户态操作的代价其实是比较高的,这也正是造成 Flannel UDP 模式性能不好的主要原因。所以说,我们在进行系统级编程的时候,有一个非常重要的优化原则,就是要减少用户态到内核态的切换次数,并且把核心的处理逻辑都放在内核态进行。这也是为什么,Flannel 后来支持的VXLAN 模式,逐渐成为了主流的容器网络方案的原因。

VXLAN

VXLAN,即 Virtual Extensible LAN(虚拟可扩展局域网),是 Linux 内核本身就支持的一种网络虚似化技术。所以说,VXLAN 可以完全在内核态实现上述封装和解封装的工作,从而通过与前面相似的“隧道”机制,构建出覆盖网络(Overlay Network)。

VXLAN 的覆盖网络的设计思想是:在现有的三层网络之上,“覆盖”一层虚拟的、由内核 VXLAN 模块负责维护的二层网络,使得连接在这个 VXLAN 二层网络上的“主机”(虚拟机或者容器都可以)之间,可以像在同一个局域网(LAN)里那样自由通信。当然,实际上,这些“主机”可能分布在不同的宿主机上,甚至是分布在不同的物理机房里。

而为了能够在二层网络上打通“隧道”,VXLAN 会在宿主机上设置一个特殊的网络设备作为“隧道”的两端。这个设备就叫作 VTEP,即:VXLAN Tunnel End Point(虚拟隧道端点)。

而 VTEP 设备的作用,其实跟前面的 flanneld 进程非常相似。只不过,它进行封装和解封装的对象,是二层数据帧(Ethernet frame);而且这个工作的执行流程,全部是在内核里完成的(因为 VXLAN 本身就是 Linux 内核中的一个模块)。

每台宿主机上名叫 flannel.1 的设备,就是 VXLAN 所需的 VTEP 设备,它既有 IP 地址,也有 MAC 地址。

优点:效率高

缺点:隔离差

实例

上图,我们的 container-1 的 IP 地址是 10.1.15.2,要访问的 container-2 的 IP 地址是 10.1.16.3。

与前面 UDP 模式的流程类似,当 container-1 发出请求之后,这个目的地址是 10.1.16.3 的 IP 包,会先出现在 docker0 网桥,然后被路由到本机 flannel.1 设备进行处理。

为了能够将“原始 IP 包”封装并且发送到正确的宿主机,VXLAN 就需要找到这条“隧道”的出口,即:目的宿主机的 VTEP 设备。

而这个设备的信息,正是每台宿主机上的 flanneld 进程负责维护的。比如,当 Node 2 启动并加入 Flannel 网络之后,在 Node 1(以及所有其他节点)上,flanneld 就会添加一条如下所示的路由规则:

$ route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
...
10.1.16.0       10.1.16.0       255.255.255.0   UG    0      0        0 flannel.1

这条规则的意思是:凡是发往 10.1.16.0/24 网段的 IP 包,都需要经过 flannel.1 设备发出,并且,它最后被发往的网关地址是:10.1.16.0。10.1.16.0 正是 Node 2 上的 VTEP 设备(也就是 flannel.1 设备)的 IP 地址。

“源 VTEP 设备”收到“原始 IP 包”后,就要想办法把“原始 IP 包”加上一个目的 MAC 地址,封装成一个二层数据帧,然后发送给“目的 VTEP 设备”(当然,这么做还是因为这个 IP 包的目的地址不是本机)。

根据前面的路由记录,我们已经知道了“目的 VTEP 设备”的 IP 地址。而要根据三层 IP 地址查询对应的二层 MAC 地址,这正是 ARP(Address Resolution Protocol )表的功能。而这里要用到的 ARP 记录,也是 flanneld 进程在 Node 2 节点启动时,自动添加在 Node 1 上的。我们可以通过 ip 命令看到它,如下所示:

# 在Node 1上
$ ip neigh show dev flannel.1
10.1.16.0 lladdr 5e:f8:4f:00:e3:37 PERMANENT

这条记录的意思非常明确,即:IP 地址 10.1.16.0,对应的 MAC 地址是 5e:f8:4f:00:e3:37。

可以看到,最新版本的 Flannel 并不依赖 L3 MISS 事件和 ARP 学习,而会在每台节点启动时把它的 VTEP 设备对应的 ARP 记录,直接下放到其他每台宿主机上。

有了这个“目的 VTEP 设备”的 MAC 地址,Linux 内核就可以开始二层封包工作了。这个二层帧的格式,如下所示:

可以看到,Linux 内核会把“目的 VTEP 设备”的 MAC 地址,填写在图中的 Inner Ethernet Header 字段,得到一个二层数据帧。需要注意的是,上述封包过程只是加一个二层头,不会改变“原始 IP 包”的内容。所以图中的 Inner IP Header 字段,依然是 container-2 的 IP 地址,即 10.1.16.3。

但是,上面提到的这些 VTEP 设备的 MAC 地址,对于宿主机网络来说并没有什么实际意义。所以上面封装出来的这个数据帧,并不能在我们的宿主机二层网络里传输。为了方便叙述,我们把它称为“内部数据帧”(Inner Ethernet Frame)。所以接下来,Linux 内核还需要再把“内部数据帧”进一步封装成为宿主机网络里的一个普通的数据帧,好让它“载着”“内部数据帧”,通过宿主机的 eth0 网卡进行传输。我们把这次要封装出来的、宿主机对应的数据帧称为“外部数据帧”(Outer Ethernet Frame)。为了实现这个“搭便车”的机制,Linux 内核会在“内部数据帧”前面,加上一个特殊的 VXLAN 头,用来表示这个“乘客”实际上是一个 VXLAN 要使用的数据帧。而这个 VXLAN 头里有一个重要的标志叫作 VNI,它是 VTEP 设备识别某个数据帧是不是应该归自己处理的重要标识。而在 Flannel 中,VNI 的默认值是 1,这也是为何,宿主机上的 VTEP 设备都叫作 flannel.1 的原因,这里的“1”,其实就是 VNI 的值。

然后,Linux 内核会把这个数据帧封装进一个 UDP 包里发出去。所以,跟 UDP 模式类似,在宿主机看来,它会以为自己的 flannel.1 设备只是在向另外一台宿主机的 flannel.1 设备,发起了一次普通的 UDP 链接。它哪里会知道,这个 UDP 包里面,其实是一个完整的二层数据帧。

不过,不要忘了,一个 flannel.1 设备只知道另一端的 flannel.1 设备的 MAC 地址,却不知道对应的宿主机地址是什么。也就是说,这个 UDP 包该发给哪台宿主机呢?在这种场景下,flannel.1 设备实际上要扮演一个“网桥”的角色,在二层网络进行 UDP 包的转发。而在 Linux 内核里面,“网桥”设备进行转发的依据,来自于一个叫作 FDB(Forwarding Database)的转发数据库。不难想到,这个 flannel.1“网桥”对应的 FDB 信息,也是 flanneld 进程负责维护的。它的内容可以通过 bridge fdb 命令查看到,如下所示:

# 在Node 1上,使用“目的VTEP设备”的MAC地址进行查询
$ bridge fdb show flannel.1 | grep 5e:f8:4f:00:e3:37
5e:f8:4f:00:e3:37 dev flannel.1 dst 10.168.0.3 self permanent

可以看到,在上面这条 FDB 记录里,指定了这样一条规则,即:发往我们前面提到的“目的 VTEP 设备”(MAC 地址是 5e:f8:4f:00:e3:37)的二层数据帧,应该通过 flannel.1 设备,发往 IP 地址为 10.168.0.3 的主机。显然,这台主机正是 Node 2,UDP 包要发往的目的地就找到了。所以接下来的流程,就是一个正常的、宿主机网络上的封包工作。

接下来,Node 1 上的 flannel.1 设备就可以把这个数据帧从 Node 1 的 eth0 网卡发出去。显然,这个帧会经过宿主机网络来到 Node 2 的 eth0 网卡。这时候,Node 2 的内核网络栈会发现这个数据帧里有 VXLAN Header,并且 VNI=1。所以 Linux 内核会对它进行拆包,拿到里面的内部数据帧,然后根据 VNI 的值,把它交给 Node 2 上的 flannel.1 设备。而 flannel.1 设备则会进一步拆包,取出“原始 IP 包”,最终,IP 包就进入到了 container-2 容器的 Network Namespace 里。

数据格式

我们知道,UDP 包是一个四层数据包,所以 Linux 内核会在它前面加上一个 IP 头,即原理图中的 Outer IP Header,组成一个 IP 包。并且,在这个 IP 头里,会填上前面通过 FDB 查询出来的目的主机的 IP 地址,即 Node 2 的 IP 地址 10.168.0.3。然后,Linux 内核再在这个 IP 包前面加上二层数据帧头,即原理图中的 Outer Ethernet Header,并把 Node 2 的 MAC 地址填进去。这个 MAC 地址本身,是 Node 1 的 ARP 表要学习的内容,无需 Flannel 维护。这时候,我们封装出来的“外部数据帧”的格式,如下所示:

问题

如果集群的规模变大,按着每个节点之间都会建立一对vtep,规模会越来越大,性能会是很严重的问题,可以使用vxlan网关来解决,使用分布式vxlan网关也是现在比较流行的解决方案。

host-gw

假设现在,Node 1 上的 Infra-container-1,要访问 Node 2 上的 Infra-container-2。当你设置 Flannel 使用 host-gw 模式之后,flanneld 会在宿主机上创建这样一条规则,以 Node 1 为例:

$ ip route
...
10.244.1.0/24 via 10.168.0.3 dev eth0

这条路由规则的含义是:目的 IP 地址属于 10.244.1.0/24 网段的 IP 包,应该经过本机的 eth0 设备发出去(即:dev eth0);并且,它下一跳地址(next-hop)是 10.168.0.3(即:via 10.168.0.3)。所谓下一跳地址就是:如果 IP 包从主机 A 发到主机 B,需要经过路由设备 X 的中转。那么 X 的 IP 地址就应该配置为主机 A 的下一跳地址。而从 host-gw 示意图中我们可以看到,这个下一跳地址对应的,正是我们的目的宿主机 Node 2。

一旦配置了下一跳地址,那么接下来,当 IP 包从网络层进入链路层封装成帧的时候,eth0 设备就会使用下一跳地址对应的 MAC 地址,作为该数据帧的目的 MAC 地址。显然,这个 MAC 地址,正是 Node 2 的 MAC 地址。这样,这个数据帧就会从 Node 1 通过宿主机的二层网络顺利到达 Node 2 上。而 Node 2 的内核网络栈从二层数据帧里拿到 IP 包后,会“看到”这个 IP 包的目的 IP 地址是 10.244.1.3,即 Infra-container-2 的 IP 地址。这时候,根据 Node 2 上的路由表,该目的地址会匹配到第二条路由规则(也就是 10.244.1.0 对应的路由规则),从而进入 cni0 网桥,进而进入到 Infra-container-2 当中。

host-gw 模式的工作原理,其实就是将每个 Flannel 子网(Flannel Subnet,比如:10.244.1.0/24)的“下一跳”,设置成了该子网对应的宿主机的 IP 地址。也就是说,这台“主机”(Host)会充当这条容器通信路径里的“网关”(Gateway)。这也正是“host-gw”的含义。

Flannel 子网和主机的信息,都是保存在 Etcd 当中的。flanneld 只需要 WACTH 这些数据的变化,然后实时更新路由表即可。注意:在 Kubernetes v1.7 之后,类似 Flannel、Calico 的 CNI 网络插件都是可以直接连接 Kubernetes 的 APIServer 来访问 Etcd 的,无需额外部署 Etcd 给它们使用。

Flannel host-gw 模式必须要求集群宿主机之间是二层连通的。

在这种模式下,容器通信的过程就免除了额外的封包和解包带来的性能损耗。根据实际的测试,host-gw 的性能损失大约在 10% 左右,而其他所有基于 VXLAN“隧道”机制的网络方案,性能损失都在 20%~30% 左右。

但是宿主机之间二层不连通的情况也是广泛存在的。比如,宿主机分布在了不同的子网(VLAN)里。这个时候就需要一个三层的方案,也就是我们常说的calico

总结

flannel更像是经典的桥接模式的扩展。我们知道,在桥接模式中,每台主机的容器都将使用一个默认的网段,容器与容器之间,主机与容器之间都能互相通信。要是,我们能手动配置每台主机的网段,使它们互不冲突。接着再想点办法,将目的地址为非本机容器的流量送到相应主机:如果集群的主机都在一个子网内,就搞一条路由转发过去;若是不在一个子网内,就搞一条隧道转发过去。这样以来,容器的跨网络通信问题就解决了。而flannel做的,其实就是将这些工作自动化了而已。

Weave

Weave Net是一个多主机容器网络方案,支持去中心化的控制平面,各个host上的wRouter间通过建立Full Mesh的TCP链接,并通过Gossip来同步控制信息。这种方式省去了集中式的K/V Store,能够在一定程度上减低部署的复杂性,Weave将其称为“data centric”,而非RAFT或者Paxos的“algorithm centric”。

数据平面上,Weave通过UDP封装实现L2 Overlay,封装支持两种模式:

1、运行在user space的sleeve mode:通过pcap设备在Linux bridge上截获数据包并由wRouter完成UDP封装,支持对L2 traffic进行加密,还支持Partial Connection,但是性能损失明显。

2、运行在kernal space的 fastpath mode:即通过OVS的odp封装VxLAN并完成转发,wRouter不直接参与转发,而是通过下发odp 流表的方式控制转发,这种方式可以明显地提升吞吐量,但是不支持加密等高级功能。

容器网络

  • 所有容器都连接到weave网桥
  • weave网桥通过veth pair连到内核的openvswitch模块
  • 跨主机容器通过openvswitch vxlan通信
  • policy controller通过配置iptables规则为容器设置网络策略

calico

三层网络方案得以正常工作的核心,是为每个容器的 IP 地址,找到它所对应的、“下一跳”的网关。

Calico 项目提供的网络解决方案,与 Flannel 的 host-gw 模式,几乎是完全一样的。也就是说,Calico 也会在每台宿主机上,添加一个格式如下所示的路由规则:

<目的容器IP地址段> via <网关的IP地址> dev eth0

其中,网关的 IP 地址,正是目的容器所在宿主机的 IP 地址。

不同于 Flannel 通过 Etcd 和宿主机上的 flanneld 来维护路由信息的做法,Calico 项目使用了BGP(BGP 的全称是 Border Gateway Protocol,即:边界网关协议。它是一个 Linux 内核原生就支持的、专门用在大规模数据中心里维护不同的“自治系统”之间路由信息的、无中心的路由协议。)来自动地在整个集群中分发路由信息。

BGP

BGP 的全称是 Border Gateway Protocol,即:边界网关协议。它是一个 Linux 内核原生就支持的、专门用在大规模数据中心里维护不同的“自治系统”之间路由信息的、无中心的路由协议。

我们有两个自治系统(Autonomous System,简称为 AS):AS 1 和 AS 2。而所谓的一个自治系统,指的是一个组织管辖下的所有 IP 网络和路由器的全体。你可以把它想象成一个小公司里的所有主机和路由器。在正常情况下,自治系统之间不会有任何“来往”。但是,如果这样两个自治系统里的主机,要通过 IP 地址直接进行通信,我们就必须使用路由器把这两个自治系统连接起来。

AS 1 里面的主机 10.10.0.2,要访问 AS 2 里面的主机 172.17.0.3 的话。它发出的 IP 包,就会先到达自治系统 AS 1 上的路由器 Router 1。而在此时,Router 1 的路由表里,有这样一条规则,即:目的地址是 172.17.0.2 包,应该经过 Router 1 的 C 接口,发往网关 Router 2(即:自治系统 AS 2 上的路由器)。所以 IP 包就会到达 Router 2 上,然后经过 Router 2 的路由表,从 B 接口出来到达目的主机 172.17.0.3。但是反过来,如果主机 172.17.0.3 要访问 10.10.0.2,那么这个 IP 包,在到达 Router 2 之后,就不知道该去哪儿了。因为在 Router 2 的路由表里,并没有关于 AS 1 自治系统的任何路由规则。所以这时候,网络管理员就应该给 Router 2 也添加一条路由规则,比如:目标地址是 10.10.0.2 的 IP 包,应该经过 Router 2 的 C 接口,发往网关 Router 1。

把自治系统连接在一起的路由器,我们就把它形象地称为:边界网关

但是网络拓扑结构非常复杂,每个自治系统都有成千上万个主机、无数个路由器,甚至是由多个公司、多个网络提供商、多个自治系统组成的复合自治系统,如果还要依靠人工来对边界网关的路由表进行配置和维护,那是绝对不现实的。

在使用了 BGP 之后,你可以认为,在每个边界网关上都会运行着一个小程序,它们会将各自的路由表信息,通过 TCP 传输给其他的边界网关。而其他边界网关上的这个小程序,则会对收到的这些数据进行分析,然后将需要的信息添加到自己的路由表里。这就是BGP,就是在大规模网络中实现节点路由信息共享的一种协议。

架构

Calico网络模型主要工作组件:

  • Felix:运行在每一台 Host 的 agent 进程,一般用deamonset的模式部署在node上,主要负责网络接口管理和监听、在宿主机上插入路由规则、ARP 管理、ACL 管理和同步、状态上报等。
    • Felix会监听ECTD中心的存储,从它获取事件,比如说用户在这台机器上加了一个IP,或者是创建了一个容器等。用户创建pod后,Felix负责将其网卡、IP、MAC都设置好,然后在内核的路由表里面写一条,注明这个IP应该到这张网卡。同样如果用户制定了隔离策略,Felix同样会将该策略创建到ACL中,以实现隔离。
  • etcd:分布式键值存储,主要负责网络元数据一致性,确保Calico网络状态的准确性,可以与kubernetes共用;
  • BGP Client(BIRD):Calico 为每一台 Host 部署一个 BGP Client,使用 BIRD 实现,BIRD 是一个单独的持续发展的项目,实现了众多动态路由协议比如 BGP、OSPF、RIP 等。在 Calico 的角色是监听 Host 上由 Felix 注入的路由信息,然后通过 BGP 协议广播告诉剩余 Host 节点,然后其他的node节点就会指定这个网段的ip的下一跳就是这个主机ip,从而实现网络互通。
    • BIRD是一个标准的路由程序,它会从内核里面获取哪一些IP的路由发生了变化,然后通过标准BGP的路由协议扩散到整个其他的宿主机上,让外界都知道这个IP在这里,你们路由的时候得到这里来。
  • BGP Route Reflector:在大型网络规模中,如果仅仅使用 BGP client 形成 mesh 全网互联的方案就会导致规模限制,因为所有节点之间俩俩互联,需要 N^2 个连接,为了解决这个规模问题,可以采用 BGP 的 Router Reflector 的方法,使所有 BGP Client 仅与特定 RR 节点互联并做路由同步,从而大大减少连接数。
  • CNI插件:二进制文件放在每个node上对应的目录下,给kubelet调用。

实现

由于Calico是一种纯三层的实现,因此可以避免与二层方案相关的数据包封装的操作,中间没有任何的NAT,没有任何的overlay,所以它的转发效率可能是所有方案中最高的,因为它的包直接走原生TCP/IP的协议栈,它的隔离也因为这个栈而变得好做。因为TCP/IP的协议栈提供了一整套的防火墙的规则,所以它可以通过IPTABLES的规则达到比较复杂的隔离逻辑。

其中的绿色实线标出的路径,就是一个 IP 包从 Node 1 上的 Container 1,到达 Node 2 上的 Container 4 的完整路径。可以看到,Calico 的 CNI 插件会为每个容器设置一个 Veth Pair 设备,然后把其中的一端放置在宿主机上(它的名字以 cali 前缀开头)。此外,由于 Calico 没有使用 CNI 的网桥模式,Calico 的 CNI 插件还需要在宿主机上为每个容器的 Veth Pair 设备配置一条路由规则,用于接收传入的 IP 包。比如,宿主机 Node 2 上的 Container 4 对应的路由规则,如下所示:

10.233.2.3 dev cali5863f3 scope link

即:发往 10.233.2.3 的 IP 包,应该进入 cali5863f3 设备。基于上述原因,Calico 项目在宿主机上设置的路由规则,肯定要比 Flannel 项目多得多。

有了这样的 Veth Pair 设备之后,容器发出的 IP 包就会经过 Veth Pair 设备出现在宿主机上。然后,宿主机网络栈就会根据路由规则的下一跳 IP 地址,把它们转发给正确的网关。接下来的流程就跟 Flannel host-gw 模式完全一致了。

最核心的“下一跳”路由规则,就是由 Calico 的 Felix 进程负责维护的。这些路由规则信息,则是通过 BGP Client 也就是 BIRD 组件,使用 BGP 协议传输而来的。

BGP 协议传输的消息,你可以简单地理解为如下格式:

[BGP消息]
我是宿主机192.168.1.3
10.233.2.0/24网段的容器都在我这里
这些容器的下一跳地址是我

Calico 项目实际上将集群里的所有节点,都当作是边界路由器来处理,它们一起组成了一个全连通的网络,互相之间通过 BGP 协议交换路由规则。这些节点,我们称为 BGP Peer。

Calico 维护的网络在默认配置下,是一个被称为“Node-to-Node Mesh”的模式。这时候,每台宿主机上的 BGP Client 都需要跟其他所有节点的 BGP Client 进行通信以便交换路由信息。但是,随着节点数量 N 的增加,这些连接的数量就会以 N²的规模快速增长,从而给集群本身的网络带来巨大的压力。所以,Node-to-Node Mesh 模式一般推荐用在少于 100 个节点的集群里。而在更大规模的集群中,你需要用到的是一个叫作 Route Reflector 的模式。在这种模式下,Calico 会指定一个或者几个专门的节点,来负责跟所有节点建立 BGP 连接从而学习到全局的路由规则。而其他节点,只需要跟这几个专门的节点交换路由信息,就可以获得整个集群的路由规则信息了。这些专门的节点,就是所谓的 Route Reflector 节点,它们实际上扮演了“中间代理”的角色,从而把 BGP 连接的规模控制在 N 的数量级上。

calico也是要求所有的node是二层相通的,如果不通,就算设置了对应主机的ip为网关,也是不通的,但是我们有办法解决这个问题,就是使用ipip模式,开通overlay通道。

IPIP

在 Calico 的 IPIP 模式下,Felix 进程在 Node 1 上添加的路由规则,会稍微不同,如下所示:

10.233.2.0/24 via 192.168.2.2 tunl0

可以看到,尽管这条规则的下一跳地址仍然是 Node 2 的 IP 地址,但这一次,要负责将 IP 包发出去的设备,变成了 tunl0。注意,是 T-U-N-L-0,而不是 Flannel UDP 模式使用的 T-U-N-0(tun0),这两种设备的功能是完全不一样的。Calico 使用的这个 tunl0 设备,是一个 IP 隧道(IP tunnel)设备。

IP 包进入 IP 隧道设备之后,就会被 Linux 内核的 IPIP 驱动接管。IPIP 驱动会将这个 IP 包直接封装在一个宿主机网络的 IP 包中,如下所示:

其中,经过封装后的新的 IP 包的目的地址(图 5 中的 Outer IP Header 部分),正是原 IP 包的下一跳地址,即 Node 2 的 IP 地址:192.168.2.2。而原 IP 包本身,则会被直接封装成新 IP 包的 Payload。这样,原先从容器到 Node 2 的 IP 包,就被伪装成了一个从 Node 1 到 Node 2 的 IP 包。由于宿主机之间已经使用路由器配置了三层转发,也就是设置了宿主机之间的“下一跳”。所以这个 IP 包在离开 Node 1 之后,就可以经过路由器,最终“跳”到 Node 2 上。这时,Node 2 的网络内核栈会使用 IPIP 驱动进行解包,从而拿到原始的 IP 包。然后,原始 IP 包就会经过路由规则和 Veth Pair 设备到达目的容器内部。

当 Calico 使用 IPIP 模式的时候,集群的网络性能会因为额外的封包和解包工作而下降。在实际测试中,Calico IPIP 模式与 Flannel VXLAN 模式的性能大致相当。所以,在实际使用时,如非硬性需求,我建议你将所有宿主机节点放在一个子网里,避免使用 IPIP。

为了解决减少ipip这种模式,我们在私有云上可以使用将宿主机和宿主机网关都发展为BGP Peer 来解决,当然这个在公有云上基本是接触不到对应的网关的。

这种方案下,Node 1 和 Node 2 就需要主动跟宿主机网关 Router 1 和 Router 2 建立 BGP 连接。从而将类似于 10.233.2.0/24 这样的路由信息同步到网关上去。需要注意的是,这种方式下,Calico 要求宿主机网关必须支持一种叫作 Dynamic Neighbors 的 BGP 配置方式。这是因为,在常规的路由器 BGP 配置里,运维人员必须明确给出所有 BGP Peer 的 IP 地址。考虑到 Kubernetes 集群可能会有成百上千个宿主机,而且还会动态地添加和删除节点,这时候再手动管理路由器的 BGP 配置就非常麻烦了。而 Dynamic Neighbors 则允许你给路由器配置一个网段,然后路由器就会自动跟该网段里的主机建立起 BGP Peer 关系。

当然,在大规模集群中,Calico 本身就推荐使用 Route Reflector 节点的方式进行组网。所以,这里负责跟宿主机网关进行沟通的独立组件,直接由 Route Reflector 兼任即可。更重要的是,这种情况下网关的 BGP Peer 个数是有限并且固定的。所以我们就可以直接把这些独立组件配置成路由器的 BGP Peer,而无需 Dynamic Neighbors 的支持。当然,这些独立组件的工作原理也很简单:它们只需要 WATCH Etcd 里的宿主机和对应网段的变化信息,然后把这些信息通过 BGP 协议分发给网关即可。

contiv

Contiv是思科开源的容器网络方案,是一个用于跨虚拟机、裸机、公有云或私有云的异构容器部署的开源容器网络架构,并与主流容器编排系统集成。Contiv最主要的优势是直接提供了多租户网络,并支持L2(VLAN), L3(BGP), Overlay (VXLAN)以及思科自家的ACI。

  • 图中是多个Kubernetes Master架构。
  • 图中所示Netmaster、Netplugin、Auth_proxy都是以容器形式存在的,网络链接方式通过Contianer link方式到pause容器。
  • Auth_proxy作为认证代理,提供基于RBAC认证,通过认证后,将请求转发给Netmaster。
  • 在Contiv原生版本中,并不支持多Netmaster存在,我们多配置文件做了些修改,使Netmaster和Netplugin都是以DaemonSet资源形式存在。
  • Contiv开源版本中默认使用的OVS Driver,我们也做了改造,使其支持多网络驱动,比如SR-IOV,DPDK。
  • 最后Kubernetes和Contiv的数据都会写到ectd分布式存储中。

contiv支持多种网络模式

  • L2(VLAN)
  • L3(BGP)
  • Overlay(VXLAN)
  • Cisco SDN Solution(ACI)

其他

还有很多的网络插件,基本思路都是基于overlay的隧道,或者基于underlay的路由,基本上插件都能支持这两个功能,只是在那个功能上做的更加好,并且还带有其他的辅助功能。其实个人觉得核心解决的问题就是如何进行跨主机网络路由。