在Kubernetes中,要保证容器之间的网络互通,网络很重要。而Kubernetes本身并没有实现容器网络本身,而是通过插件的方式自由接入。访问容器网络需要满足以下基本原则:Pod无论运行在哪个节点上,都可以直接相互通信,不需要进行NAT地址转换。Node和Pod可以相互通信,Pod可以不受限制地访问任何网络。一个pod有一个独立的网络栈。pod看到的地址应该和外界看到的地址一致,同一个pod中的所有容器共享同一个网络栈。Linux容器的网络堆栈在其自己的网络命名空间中是隔离的。NetworkNamespace包括:NetworkInterface、LookbackDevice、RoutingTable和iptablesrules。对于服务流程来说,这些搭建了它发起请求的基础环境和对应的环境。要实现容器网络,离不开以下Linux网络功能:网络命名空间:将独立的网络协议栈隔离到不同的命令空间,相互之间不能通信。VethPair:Veth设备对的引入是为了实现不同网络命名空间之间的通信,总是以两个虚拟网卡(vethpeer)的形式成对出现。而且,从一端发送的数据总是可以在另一端接收到。iptables/Netfilter:Netfilter负责执行各种hooking规则(过滤、修改、丢弃等)规则表;通过两者的配合,实现了整个Linux网络协议栈中灵活的数据包处理机制。网桥:网桥是一种二层网络虚拟设备,类似于交换机。帧被转发到网桥的不同端口。路由:Linux系统包含了完整的路由功能。IP层在处理数据传输或转发时,会使用路由表来决定将数据发送到哪里。基于以上基础,如何与宿主机的容器时间通信呢?我们可以简单的理解为两台主机,通过网线连接起来。如果多台主机需要通信,我们可以通过交换机相互通信。在Linux中,我们可以通过网桥转发数据。在容器中,上述实现是通过docker0网桥实现的,任何连接到docker0的容器都可以通过它进行通信。为了让容器连接到docker0网桥,我们还需要一个类似网线的虚拟设备VethPair来连接容器和网桥。我们启动一个容器:dockerrun-d--namec1hub.pri.ibanyu.com/devops/alpine:v3.8/bin/sh然后查看网卡设备:dockerexec-itc1/bin/sh/#ifconfigeth0Linkencap:EthernetHWaddr02:42:AC:11:00:02inetaddr:172.17.0.2Bcast:172.17.255.255Mask:255.255.0.0UPBROADCASTRUNNINGMULTICASTMTU:1500Metric:1RXpackets:14errors:0dropped:0overruns:0frame:0TXpackets:0errors:0dropped:0lixlenques:0liquelens:0overruns:0:0RXbytes:1172(1.1KiB)TXbytes:0(0.0B)loLinkencap:LocalLoopbackinetaddr:127.0.0.1Mask:255.0.0.0UPLOOPBACKRUNNINGMTU:65536Metric:1RXpackets:0errors:0dropped:0overruns:0frame:0TXpackets:0errors:0rioverdropederrors:0liover0txqueuelen:1000RXbytes:0(0.0B)TXbytes:0(0.0B)/#route-nKernelIProutingtableDestinationGatewayGenmaskFlagsMetricRefUseIface0.0.0.0172.17.0.10.0.0.0UG000eth0172.17.0.00.0.0.0255.255.0.0U000eth0可以看到其中有一张eth0的网卡,这是vethpeer一端的虚拟网卡。然后使用route-n查看容器中的路由表,eth0也是默认的路由出口。所有到172.17.0.0/16网段的请求都会通过eth0出去。让我们看看Veth节点的另一端。让我们检查主机的网络设备:ifconfigdocker0:flags=4163mtu1500inet172.17.0.1netmask255.255.0.0broadcast172.17.255.255inet6fe80::42:6aff:fe46:93d2prefixlen64scopeid>0etherx20<4:0206a:46:93:d2txqueuelen0(以太网)RXpackets0bytes0(0.0B)RXerrors0dropped0overruns0frame0TXpackets8bytes656(656.0B)TXerrors0dropped0overruns0carrier0collisions0eth0:flags=4163mtu15.2.2masinetk20255.255.0broadcast10.100.0.255inet6fe80::5400:2ff:fea3:4b44prefixlen64scopeid0x20ether56:00:02:a3:4b:44txqueuelen1000(Ethernet)RXpackets7788093bytes9899954680(9.2GiB)RXerrors0dropped0overruns0frame0TXpackets5512037bytes9512685850(8.8GiB)TXerrors0dropped0overruns0carrier0collisions0lo:flags=73mtu65536inet127.0.0.1netmask255.0.0.0inet6::1prefixlen128scopeid0x10looptxqueuelen1000(LocalLoopback)RXpackets32bytes2592(2.5KiB)RXerrors0dropped0overruns0frame0TXpackets32bytes2592(2.5KiB)TXerrors0dropped0overruns0carrier0collisions0veth20b3dac:flags=4163mtu1500inet6fe80::30e2:9cff:fe45:329prefixlen64scopeid0x20ether32:e2:9c:45:03:29txqueuelen0(Ethernet)RXpackets0bytes0(0.0B)RXerrors0dropped0overruns0frame0TXpackets8bytes656(656.0B)TXerrors0dropped0overruns0carrier0collisions0我们可以看到容器对应的Vethpeer的另一端是宿主机上一个名为veth20b3dac的虚拟网卡,通过brctl查看网桥信息可以看出这个网卡是ondocker0#brctlshow.docker02426a4693d2noveth20b3dac然后我们启动另外一个容器,是否可以从第一个容器ping到第二个容器。$dockerrun-d--namec2-ithub.pri.ibanyu.com/devops/alpine:v3.8/bin/sh$dockerexec-itc1/bin/sh/#ping172.17.0.3PING172.17.0.3(172.17.0.3):56databytes64bytesfrom172.17.0.3:seq=0ttl=64time=0.291ms64bytesfrom172.17.0.3:seq=1ttl=64time=0.129ms64bytesfrom172.17.0.3:seq=2ttl=64time=0.142ms64bytesfrom17.032.172.169times17.0.3:seq=4ttl=64time=0.194ms^C---172.17.0.3pingstatistics---5packetstransmitted,5packetsreceived,0%packetlossround-tripmin/avg/max=0.129/0.185/0.291ms可以看出ping成功,原理是当我们ping目标IP172.17.0.3时,会匹配我们路由表的第二条规则,网关是0.0.0.0,表示是直连路由,通过二层转发到目的地。要通过二层网络到达172.17.0.3,我们需要知道它的Mac地址。这时候第一个容器需要发送ARP广播,??通过IP地址找到Mac。此时Vethpeer的另一段是docker0网桥,它会广播给所有与其相连的vethpeer虚拟网卡,然后正确的虚拟网卡收到ARP报文后响应,然后桥将返回到第一个容器。以上是不同容器通过docker0与宿主机的通信,如下图所示:默认情况下,受网络命名空间限制的容器进程本质上是通过Veth对端设备和主机桥实现不同网络命名空间的数据交换..同样,当你在宿主机上访问容器的IP地址时,请求的数据包会根据路由规则首先到达docker0网桥,然后转发到对应的VethPair设备,最终出现在容器中。跨主机网络通信在Docker的默认配置下,不同主机上的容器是不可能通过IP地址相互访问的。为了解决这个问题,社区中出现了很多网络解决方案。同时,为了更好地控制网络访问,K8s引入了CNI,即容器网络的API接口。是K8s中标准的网络实现接口。Kubelet通过这个API调用不同的网络插件来实现不同的网络配置。实现该接口的CNI插件实现了一系列的CNIAPI接口。目前有flannel、calico、weave、contiv等。其实CNI容器网络通信过程和之前的基础网络是一样的,只是CNI维护了一个单独的bridge而不是docker0。这个网桥的名字叫做:CNI网桥,它在主机上的设备名默认是:cni0。cni的设计思路是:Kubernetes启动Infra容器后,可以直接调用CNI网络插件,为Infra容器的NetworkNamespace配置预期的网络栈。CNI插件三种网络实现模式:overlay模式是基于隧道技术,整个容器网络独立于主机网络,当容器跨主机通信时,整个容器网络被封装到底层网络中,之后再解封装reachingthetargetmachine传递给目标容器。不依赖于底层网络的实现。实现的插件有flannel(UDP、vxlan)、calico(IPIP)等,在三层路由模式下,容器和宿主机也属于无法互通的网段。它们容器之间的互通主要是基于路由表,主机之间不需要建立隧道包。.但是,限制必须在与第二层相同的局域网内。实现的插件有flannel(host-gw)、calico(BGP)等,underlaynetwork即底层网络,负责互连。容器网络和宿主网络仍然属于不同的网段,但是处于同一网络层,处于同一位置。全网三层互通,二层无限制,但需要高度依赖底层网络的实现支持。实现的插件有calico(BGP)等,我们再来看一下flannelHost-gw,路由模式的一种实现:如图可以看出,当node1上的container-1要发送数据时到node2上的container2,会匹配如下路由表规则:10.244.1.0/24via10.168.0.3deveth0表示去往目标网段10.244.1.0/24的IP包,需要经过的下一跳ip地址本地机器eth0是10.168.0.3(node2)。到达10.168.0.3后,通过路由表转发cni网桥,然后进入container2。上面可以看到host-gw的工作原理,其实每个node节点上配置的到每个pod网段的下一跳就是pod网段所在的node节点IP,pod网段和pod网段的映射关系node节点IP,flannel保存在etcd或者k8s中。Flannel只需要观察这些数据的变化就可以动态更新路由表。这种网络方式最大的好处是避免了额外打包和解包带来的网络性能损失。缺点我们也可以看到,当容器ip包通过下一跳出去的时候,必须通过二层通信封装成数据帧发送到下一跳。如果不在同一个二层局域网,那么必须交给三层网关,此时网关不知道目标容器网络(也可以在每个静态配置pod网段路由网关)。所以flannelhost-gw必须要求集群主机二层互通。为了解决二层互通的局限性,可以更好的实现calico提供的网络方案。Calico的大三层网络模式和flannel提供的类似,也会给每台主机添加如下格式的路由规则:<目标容器IP网段>via<网关IP地址>deveth0其中IP地址为网关无法访问具有不同的含义。如果主机在二层可达,则为目的容器所在主机的IP地址。如果在第三层不同局域网是本地主机的网关IP(交换机或路由器地址)。与flannel通过k8s或etcd中存储的数据维护本地路由信息不同,calico通过BGP动态路由协议分发整个集群的路由信息??。BGP的全称是BorderGatewayProtocol,linxu原生支持,专门用于大型数据中心不同自治系统之间传递路由信息。只要记住,简单理解BGP其实就是一种在大规模网络中实现节点路由信息同步共享的协议。BGP协议可以替代flannel维护主机路由表的功能。calico主要由三部分组成:calicocni插件:主要负责与kubernetes对接,进行kubelet调用。felix:负责维护宿主机上的路由规则,FIB转发信息库等。BIRD:负责分发路由规则,类似于路由器。confd:配置管理组件。另外,calico与flannelhost-gw不同的是,它不创建bridge设备,而是通过路由表维护各个pod的通信,如下图:可以看到calico的cni插件每个容器都会设置一个vethpair设备,然后另一端连接到宿主机网络空间。由于没有网桥,cni插件还需要为主机上每个容器的vethpair设备配置路由规则,用于接收传入的IP包,路由规则如下:10.92.77.163devcali93a8a799fe1scopelink上面的意思是发送到10.92.77.163的IP数据包应该发送到cali93a8a799fe1设备,然后到达容器的另一部分。有了这样一个vethpair设备,容器发送的IP包就会通过vethpair设备到达宿主机,然后宿主机根据路径上有规则的下一个地址,发送到正确的网关(10.100.1.3),然后reachthetarget主机到达目标容器。10.92.160.0/23via10.106.65.2devbond0protobird这些路由规则由felix维护和配置,而路由信息由calicobird组件基于BGP分发。Calico实际上将集群中的所有节点都视为边界路由器。它们形成一个完全互连的网络,并通过BGP相互交换路由。这些节点称为BGP对等点。需要注意的是,calico维护网络默认的模式是node-to-nodemesh。在这种模式下,每台主机的BGP客户端会与集群中所有节点的BGP客户端进行通信和交换路由。这样,随着节点数量N的增加,连接数会增加N次方,这会给集群网络本身带来巨大的压力。所以这种模式推荐的集群规模一般在50个节点左右,超过50个节点推荐另一种RR(RouterReflector)模式。在这种模式下,calico可以指定若干个节点作为RR,它们与所有节点一起负责BGPclient。建立通信学习集群的所有路由,其他节点只需要与RR节点交换路由即可。这大大减少了连接数。同时,为了集群网络的稳定性,建议RR>=2。以上工作原理还是二层通信。当我们有两台主机时,一台是10.100.0.2/24,节点上的容器网络是10.92.204.0/24;另一个是10.100.1.2/24,节点上的容器网络是10.92.203.0/24。这时候两台机器因为不在同一个二层,所以需要三层路由通信。Calico会在节点上生成如下路由表:10.92.203.0/23via10.100.1.2deveth0protobird这时候问题就来了,因为10.100.1.2和我们的10.100.0.2不在同一个子网,所以无法在第二层。之后,您需要使用CalicoIPIP模式。当主机不在同一个二层网络时,会在发送前用overlay网络进行封装。如下图:以IPIP方式进行非二层通信时,calico会在node节点添加如下路由规则:10.92.203.0/24via10.100.1.2devtunnel0可以看到虽然下一个还是那个节点IP地址,出口设备为tunnel0,是IP隧道设备,主要由Linux内核的IPIP驱动实现。容器的IP包会直接封装成host网络的IP包,这样到达node2后,原始容器IP包会被IPIP驱动解包,然后发送给vethpair设备到达目标容器通过路由规则。以上虽然可以解决非二层网络通信,但是仍然会因为打包解包导致性能下降。如果calico能让宿主机之间的router设备也学习容器路由规则,那么它们就可以在第三层直接通信了。例如在router中添加如下路由表:10.92.203.0/24via10.100.1.2devinterface1,在node1中添加如下路由表:10.92.203.0/24via10.100.1.1devtunnel0那么node1上容器发送的IP包是根据本地路由表发送到10.100.1.1网关路由器,然后路由器收到IP包检查目的IP,通过本地路由表找到下一跳地址发送给node2,最终到达目的地容器。我们可以基于底层网络来实现这个方案。只要底层支持BGP网络,我们就可以和我们的RR节点建立EBGP关系,在集群中交换路由信息。以上是kubernetes常用的几种网络方案。在公有云场景下,一般使用云厂商或者使用flannelhost-gw更简单。在私有物理机房环境中,Calico项目更适合。请根据您的实际场景选择合适的网络方案。