当前位置: 首页 > 科技观察

深入理解Kubernetes容器网络_0

时间:2023-03-17 10:23:36 科技观察

需要在访问容器网络时满足以下基本原则:Pod无论运行在任何节点的哪个位置,都可以直接相互通信,不需要进行NAT地址转换。Node和Pod可以相互通信,Pod可以不受限制地访问任何网络。Pod有独立的网络栈,Pod看到的地址应该和外界看到的是一样的,同一个Pod中的所有容器共享同一个网络栈。Linux容器的网络堆栈在其自己的网络命名空间中是隔离的。NetworkNamespace包括:NetworkInterface、LookbackDevice、RoutingTable和iptablesrules。对于服务流程来说,这些搭建了它发起请求的基础环境和对应的环境。要实现一个容器网络,离不开以下Linux网络功能:网络命名空间:将独立的网络协议栈隔离到不同的命令空间,相互之间不能通信VethPair:Veth设备对的引入是为了实现网络命名空间中的通信总是以两个虚拟网卡(vethpeers)的形式成对出现。而且,一端发送的数据总能在另一端收到Iptables/Netfilter:Netfilter负责执行内核中的各种钩子规则(过滤、修改、丢弃等),运行在内核中;Iptables模式是运行在用户态的进程,负责协助维护内核中Netfilter的各种规则表;通过两者的配合,实现了整个Linux网络协议栈中灵活的报文处理机制。网桥:网桥是一个二层网络虚拟设备,类似于交换机,主要作用是通过学习到的Mac地址将数据帧转发到网桥的不同端口。路由: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:0溢出:0帧:0TX数据包:0错误:0丢弃:0溢出:0载体:0冲突:0txqueuelen:0RX字节:1172(1.1KiB)TX字节:0(0.0B)loLinkencap:LocalLoopbackinetaddr:127.0.0.1Mask:255.0.0.0UPLOOPBACKRUNNINGMTU:65536Metric:1RXpackets:0errors:0dropped:0overruns:0frame:0TXpackets:0errors:0dropped:0overruns:0carrier:0collisions:0txqueuelen:1000RXbytes:0(0.0B)TXbytes:0(0.0B)/#route-nKernelIProutingtableDestinationGatewayGenmaskFlagsMetricRef使用Iface0.0.0.0172.17.0.10.0.0.0UG000eth0172.17.0.00.0.0.0255.255.0.0U000eth0可以看到有一个eth0网卡,是veth的一端peer虚拟网卡然后使用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:93d2prefixlen64scopeid0x22ether02:42:6a:46:93:d2txqueuelen0(以太网)RX数据包0字节0(0.0B)RX错误0丢弃0溢出0帧0TX数据包8字节656(656.0B)TX错误0dropped0overruns0carrier0collisions0eth0:flags=4163mtu1500inet10.100.0.2netmask255.255.255.0broadcast10.100.0.255inet6fe80::5400:2ff:fea3:4b44prefixlen24scopeidether56:00:02:a3:4b:44txqueuelen1000(以太网)RX数据包7788093字节9899954680(9.2GiB)RX错误0丢弃0overruns0TX帧5512037字节9512685850(8.8GiB)TX错误0dropped0overruns0carrier0collisions0lo:flags=73mtu65536inet127.0.0.1netmask255.0.0.0inet6::1prefixlen128scopeid0x10looptxqueuelenX1000(LocalLooppacks)字节2592(2.5KiB)RXerrors0dropped0overruns0frame0TXpackets32bytes2592(2.5KiB)TXerrors0dropped0overruns0carrier0collisionsmveth20b3dac:flags=4163ether32:e2:9c:45:03:29txqueuelen0(以太网)RX数据包0字节0(0.0B)RX错误0丢弃0溢出0帧0TX数据包8bytes656(656.0B)TXerrors0dropped0overruns0carrier0collisions0我们可以看到容器对应的Vethpeer的另一端是宿主机上名为veth20b3dac的虚拟网卡,可以通过brctl查看网桥信息,可以看到这个网卡在docker0上#brctlshowdocker08000.02426a4693d2noveth20b3dac然后我们启动另外一个容器,是否可以从第一个容器ping到第二个容器。dockerrun-d--namec2-ithub.pri.ibanyu.com/devops/alpine:v3.8/bin/shdockerexec-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.142ms64bytesfrom172.17.0.3:seq=3ttl=64time=0.169ms64bytesfrom172.17.0.3:seq=4ttl=64time=0.194ms^C---172.17.0.3pingstatistics---发送了5个包,接收了5个包,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地址相互访问的。为了解决这个问题,社区中出现了很多网络解决方案。同时,为了更好地控制网络访问,Kubernetes推出了容器网络的API接口CNI。它是在Kubernetes中调用网络实现的标准接口。Kubelet通过这个API调用不同的网络插件来实现不同的网络配置。实现该接口的CNI插件实现了一系列的CNIAPI接口。目前有Flannel、Calico、Kube-OVN、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)等,Underlay网络是底层网络,负责互连。容器网络和宿主网络仍然属于不同的网段,但是处于同一网络层,处于同一位置。全网三层互通,不受第二层的限制,但需要高度依赖底层网络的实现支持。实现的插件有Calico(BGP)等。再来看看flannelHost-gw,路由模式的一种实现:如图可以看出,当node1上的container-1要向node2上的container2发送数据时,会匹配如下路由表规则:10.244.1.0/24via10.168.0.3deveth0表示去目标网段的IP10.244.1.0/24数据包需要通过本机的eth0发送到下一跳IP地址10.168.0.3(node2)本机,到达10.168.0.3后通过路由表转发到CNI网桥,然后进入container2。上面可以看到host-gw的工作原理。实际上,每个Node节点上配置到每个Pod网段的下一跳就是Pod网段所在的Node节点IP,Pod网段与Node节点IP的映射关系,以及Flannel存储在etcd或者Kubernetes中.Flannel只需要观察这些数据的变化就可以动态更新路由表。这种网络方式最大的好处是避免了额外打包和解包带来的网络性能损失。缺点我们也可以看到,当容器IP报文通过下一跳出去时,必须通过二层通信封装成数据帧发送到下一跳。如果不在同一个二层局域网,那么必须交给三层网关,网关此时并不知道目标容器网络(也可以在每个网关上静态配置Pod网段路由).所以flannelhost-gw必须要求集群主机二层互通。为了解决二层互通的局限性,Calico提供的网络方案可以更好的实现。Calico的大三层网络模型和Flannel提供的类似,也会在每台主机上添加如下格式的路由规则:via<网关的IP地址>deveth0where网关的IP地址无法访问具有不同的含义。如果主机在二层可达,则为目的容器所在主机的IP地址。如果是在第三层不同的局域网是本地主机的网关IP(交换机或路由器地址)。与Flannel通过存储在Kubernetes或etcd中的数据维护本地路由信息的方式不同,Calico通过BGP动态路由协议将路由信息分发到整个集群。BGP全称BorderGatewayProtocol,Linxu原生支持,专门用于大型数据中心不同自治系统之间传递路由信息。只要记住,简单理解BGP其实就是一种在大规模网络中实现节点路由信息同步共享的协议。BGP协议可以替代Flannel维护主机路由表的功能。Calico主要由三部分组成:CalicoCNI插件:主要负责与kubernetes对接,进行kubelet调用。Felix:负责维护宿主机上的路由规则,FIB转发信息库等。BIRD:负责分发路由规则,类似于路由器。Confd:配置管理组件。另外,Calico与flannelhost-gw不同的是,它不创建桥接设备,而是通过路由表维护各个Pod之间的通信,如下图:可以看Calico的CNI插件一个vethpairdevice会为每个容器设置,然后另一端连接到主机网络空间。由于没有网桥,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维护网络的默认模式是节点到节点的网格。在这种模式下,每台主机的BGP客户端会与集群中所有节点的BGP客户端进行通信和交换路由。这样,随着节点数量N的增加,连接数会增加N次方,这会给集群网络本身带来巨大的压力。所以这种模式推荐的集群规模一般在50个节点左右,超过50个节点推荐另一种RR(RouterReflector)模式。在这种模式下,Calico可以指定几个节点作为RR,它们负责与所有节点的BGP客户端。建立通信学习集群的所有路由,其他节点只需要与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可以看到虽然下一个是仍然是Node的IP地址,但是export设备是tunnel0,这是一个IP隧道设备,主要由Linux内核的IPIP驱动实现。容器的IP包会直接封装成host网络的IP包,这样到达node2后,原始容器IP包会被IPIP驱动解包,然后发送给vethpair设备到达目标容器通过路由规则。以上虽然可以解决非二层网络通信,但是仍然会因为打包解包导致性能下降。如果Calico能够让宿主机之间的路由设备也学习到容器路由规则,那么它们就可以在三层之间直接通信。例如,在路由器中添加如下路由表:10.92.203.0/24via10.100.1.2devinterface1并在node1中添加如下路由表:10.92.203.0/24via10.100.1.1devtunnel0那么发送的IP包node1上的container是根据本地路由表发送给10.100.1.1网关路由器,然后路由器收到IP包查看目的IP,通过本地路由表找到下一跳地址发送给node2,最终到达目的容器。我们可以基于底层网络来实现这个方案。只要底层支持BGP网络,我们就可以和我们的RR节点建立EBGP关系,在集群中交换路由信息。Kube-OVN实现的网络模型Flannel,很多网络的实现都是一个Node,一个子网。这种子网模型非常不灵活且难以扩展。Kube-OVN采用一个Namespace和一个子网的模型,子网可以跨节点,更符合用户的期望和管理。每个子网对应OVN中的一个虚拟交换机。LB、DNS、ACL等流量规则也应用到虚拟交换机上,方便我们以后做更细粒度的权限控制,比如实现VPC、多租户等功能。所有的虚拟交换机目前都连接到一个全局的虚拟路由器,可以保证默认容器网络的互通性,未来的隔离也可以很容易地在路由层面进行控制。此外,还有一个特殊的Node子网,它会为每个主机添加一个OVS网卡。这个网络主要是把Node连接到容器网络上,让宿主机和容器之间的网络可以互通。需要注意的是,这里的虚拟交换机和虚拟路由器是逻辑上的,在实现中通过流表分布在所有节点上,所以不存在单点问题。网关负责访问集群外的网络。目前有两种实现方式,一种是分布式的,每台主机都可以作为Ood在其上运行的一个出站节点。另一个是中心化的。可以在一个Namespace中配置一个gateway节点作为当前Namespace中的Pod出网的网关。这样所有出站流量都使用特定的IPnat出去,方便外部审计和防火墙控制。当然网关节点也可以不做nat,让容器IP直接暴露给外网,实现内外网直连。以上是Kubernetes常用的几种网络方案。在公有云场景下,一般使用云厂商或者使用flannelhost-gw会更方便。在私有物理机房环境中,Kube-OVN和Calico项目比较适合。请根据您的实际场景选择合适的网络方案。