乐趣区

关于java:一文理解-K8s-容器网络虚拟化

简介:本文须要读者相熟 Ethernet(以太网)的基本原理和 Linux 零碎的根本网络命令,以及 TCP/IP 协定族并理解传统的网络模型和协定包的流转原理。文中波及到 Linux 内核的具体实现时,均以内核 v4.19.215 版本为准。

作者 | 浅奕
起源 | 阿里技术公众号

本文须要读者相熟 Ethernet(以太网)的基本原理和 Linux 零碎的根本网络命令,以及 TCP/IP 协定族并理解传统的网络模型和协定包的流转原理。文中波及到 Linux 内核的具体实现时,均以内核 v4.19.215 版本为准。

一 内核网络包接管流程

1 从网卡到内核协定栈

如图[1],网络包达到 NC(Network Computer,本文指物理机)时,由 NIC(Network Interface Controller,网络接口控制器,俗称网卡)设施解决,NIC 以中断的形式向内核传递音讯。Linux 内核的中断解决分为上半部(Top Half)和下半部(Bottom Half)。上半部须要尽快解决掉和硬件相干的工作并返回,下半部由上半部激活来解决后续比拟耗时的工作。

具体到 NIC 的解决流程如下:当 NIC 收到数据时,会以 DMA 形式将数据拷贝到 Ring Buffer (接管队列) 里描述符指向的映射内存区域,拷贝实现后会触发中断告诉 CPU 进行解决。这里能够应用 ethtool -g {设施名,如 eth0} 命令查看 RX/TX(接管 / 发送)队列的大小。CPU 辨认到中断后跳转到 NIC 的中断处理函数开始执行。此时要辨别 NIC 的工作模式,在新近的非 NAPI(New API)[2]模式下,中断上半部更新相干的寄存器信息,查看接管队列并调配 sk_buff 构造指向接管到的数据,最初调用 netif_rx() 把 sk_buff 递交给内核解决。在 netif_rx() 的函数的流程中,这个调配的 sk_buff 构造被放入 input_pkt_queue 队列后,会把一个虚构设施退出 poll_list 轮询队列并触发软中断 NET_RX_SOFTIRQ 激活中断下半部。此时中断上半部就完结了,具体的解决流程能够参见 net/core/dev.c 的 netif_rx() -> netif_rx_internal() -> enqueue_to_backlog() 过程。下半部 NET_RX_SOFTIRQ 软中断对应的处理函数是 net_rx_action(),这个函数会调用设施注册的 poll() 函数进行解决。非 NAPI 的状况下这个虚构设施的 poll() 函数固定指向 process_backlog() 函数。这个函数将 sk_buff 从 input_pkt_queue 挪动到 process_queue 中,调用 __netif_receive_skb() 函数将其投递给协定栈,最初协定栈相干代码会依据协定类型调用相应的接口进行后续的解决。特地地,这里的 enqueue_to_backlog() 以及 process_backlog() 函数也用于和启用了 RPS 机制后的相干逻辑。

非 NAPI(New API)模式下每个网络包的达到都会触发一次中断解决流程,这么做升高了整体的解决能力,曾经过期了。当初大多数 NIC 都反对 NAPI 模式了。NAPI 模式下在首包触发 NIC 中断后,设施就会被退出轮询队列进行轮询操作以晋升效率,轮询过程中不会产生新的中断。为了反对 NAPI,每个 CPU 保护了一个叫 softnet_data 的构造,其中有一个 poll_list 字段搁置所有的轮询设施。此时中断上半部很简略,只须要更新 NIC 相干的寄存器信息,以及把设施退出 poll_list 轮询队列并触发软中断 NET_RX_SOFTIRQ 就完结了。中断下半部的解决仍旧是 net_rx_action() 来调用设施驱动提供的 poll() 函数。只是 poll() 此时指向的就是设施驱动提供的轮询处理函数了(而不是非 NAPI 模式下的内核函数 process_backlog())。这个设施驱动提供的轮询 poll() 函数最初也会调用 __netif_receive_skb() 函数把 sk_buff 提交给协定栈解决。

非 NAPI 模式和 NAPI 模式下的流程比照如下(其中灰色底色是设施驱动要实现的,其余都是内核本身的实现):

对于 NAPI 模式网络设备驱动的实现以及具体的 NAPI 模式的解决流程,这里提供一篇文章和其译文作为参考[3](强烈推荐)。这篇文章很具体的形容了 Intel Ethernet Controller I350 这个 NIC 设施的收包和解决细节(其姊妹篇发包处理过程和译文[4])。另外收包这里还波及到多网卡的 Bonding 模式(能够在 /proc/net/bonding/bond0 里查看模式)、网络多队列(sudo lspci -vvv 查看 Ethernet controller 的 Capabilities 信息里有 MSI-X: Enable+ Count=10 字样阐明 NIC 反对,能够在 /proc/interrupts 里查看中断绑定状况)等机制。这些本文都不再赘述,有趣味的话请参阅相干材料[5]。

2 内核协定栈网络包解决流程

前文说到 NIC 收到网络包结构出的 sk_buff 构造最终被 __netif_receive_skb() 提交给了内核协定栈解析解决。这个函数首先进行 RPS[5] 相干的解决,数据包会持续在队列里转一圈(个别开启了 RSS 的网卡不须要开启 RPS)。如果须要散发包到其余 CPU 去解决,则会应用 enqueue_to_backlog() 投递给其余 CPU 的队列,并在 process_backlog()) 中触发 IPI(Inter-Processor Interrupt,处理器间中断,于 APIC 总线上传输,并不通过 IRQ)给其余 CPU 发送告诉(net_rps_send_ipi()函数)。

最终,数据包会由 __netif_receive_skb_core() 进行下一阶段的解决。这个处理函数次要的性能有:

  • 解决 ptype_all 上所有的 packet_type->func(),典型场景是 tcpdump 等工具的抓包回调(paket_type.type 为 ETH_P_ALL,libcap 应用 AF_PACKET Address Family)
  • 解决 VLAN(Virtual Local Area Network,虚构局域网)报文 vlan_do_receive() 以及解决网桥的相干逻辑(skb->dev->rx_handler() 指向了 br_handle_frame())
  • 解决 ptype_base 上所有的 packet_type->func() , 将数据包传递给下层协定层解决,例如指向 IP 层的回调 ip_rcv() 函数

截至目前,数据包仍旧在数据链路层的解决流程中。这里温习下 OSI 七层模型与 TCP/IP 五层模型:

在网络分层模型里,后一层即为前一层的数据局部,称之为载荷(Payload)。一个残缺的 TCP/IP 应用层数据包的格局如下[6]:

__netif_receive_skb_core() 的解决逻辑中须要关注的是网桥和接下来 IP 层以及 TCP/UDP 层的解决。首先看 IP 层,__netif_receive_skb_core() 调用 deliver_skb(),后者调用具体协定的 .func() 接口。对于 IP 协定,这里指向的是 ip_rcv() 函数。这个函数做了一些统计和查看之后,就把包转给了 Netfilter [7]框架并指定了函数 ip_rcv_finish() 进行后续的解决(如果包没被 Netfilter 抛弃)。通过路由子系统查看解决后,如果包是属于本机的,那么会调用 ip_local_deliver() 将数据包持续往下层协定转发。这个函数相似之前的逻辑,仍旧是呈递给 Netfilter 框架并指定函数 ip_local_deliver_finish() 进行后续的解决,这个函数最终会检查和抉择对应的下层协定接口进行解决。

常见的下层协定比方 TCP 或者 UDP 协定的流程不在本文探讨的范畴内,仅 TCP 的流程所须要的篇幅足以超过本文所有的内容。这里给出 TCP 协定(v4)的入口函数 tcp_v4_rcv() 以及 UDP 协定的入口函数 udp_rcv() 作为指引自行钻研,也能够浏览其余的材料进行进一步的理解[9]。

3 Netfilter/iptables 与 NAT(网络地址转换)

对于 Netfilter 框架须要略微着重的强调一下,因为后文要提到的网络策略和很多服务透出的实现都要应用 Netfilter 提供的机制。

Netfilter 是内核的包过滤框架(Packet Filtering Framework)的实现。简略说就是在协定栈的各个档次的包处理函数中内置了很多的 Hook 点来反对在这些点注册回调函数。

图片来自 Wikimedia,能够点开参考文献 [8] 查看大图(svg 矢量图,能够调大网页显示百分比持续放大)。

Linux 上最罕用的防火墙 iptables 即是基于 Netfilter 来实现的(nftables 是新一代的防火墙)。iptables 基于表和链(Tables and Chains)的概念来组织规定。留神这里不要被“防火墙”这个词误导了,iptables 所能做的不仅仅是对包的过滤(Filter Table),还反对对包进行网络地址转换(NAT Table)以及批改包的字段(Mangle Table)。在网络虚拟化里,用的最多的便是 NAT 地址转换性能。通常此类性能个别在网关网络设备或是负载平衡设施中很常见。当 NC 须要在外部进行网络相干的虚拟化时,也是一个相似网关以及负载平衡设施了。

在设置 iptables 的 NAT 规定前,还须要关上内核的包转发性能 echo “1” > /proc/sys/net/ipv4/ip_forward 才能够。另外倡议也关上 echo “1” /proc/sys/net/bridge/bridge-nf-call-iptables 开关(可能须要 modprobe br_netfilter)。bridge-nf-call-iptables 从下面的源码剖析就能了解,网桥的转发解决是在 Netfilter 规定之前的。所以默认状况下二层网桥的转发是不会受到三层 iptables 的限度的,然而很多虚拟化网络的实现须要 Netfilter 规定失效,所以内核也反对了让网桥的转发逻辑也调用一下 Netfilter 的规定。这个个性默认状况不开启,所以须要查看开关。至于具体的 iptables 命令,能够参考这篇文章和其译文 [10] 进行理解,本文不再探讨。

这里强调下,Netfilter 的逻辑运行在内核软中断上下文里。如果 Netfilter 增加了很多规定,必然会造成肯定的 CPU 开销。下文在提到虚拟化网络的性能升高时,很大一部分开销便是源自这里。

二 虚构网络设备

在传统的网络认知里,网络就是由带有一个或多个 NIC 的一组 NC 应用硬件介质和 switch(交换机)、Router(路由器)所组成的一个通信汇合(图片来自 [11],下同):

网络虚拟化作为 SDN(Software Defined Network,软件定义网络)的一种实现,无非就是虚构出 vNIC(虚构网卡)、vSwitch(虚构交换机)、vRouter(虚构路由器)等设施,配置相应的数据包流转规定而已。其对外的接口必然也是合乎其所在的物理网络协议标准的,比方 Ethernet 和 TCP/IP 协定族。

随着 Linux 网络虚拟化技术的演进,有了若干种虚拟化网络设备,在虚拟机和虚构容器网络中失去了宽泛的利用。典型的有 Tap/Tun/Veth、Bridge 等:

  • Tap/Tun 是 Linux 内核实现的一对虚构网络设备,Tap/Tun 别离工作在二层 / 三层。Linux 内核通过 Tap/Tun 设施和绑定该设施的用户空间之间替换数据。基于 Tap 驱动即可实现虚拟机 vNIC 的性能,Tun 设施做一些其余的转发性能。
  • Veth 设施总是成对创立(Veth Pair),一个设施收到内核发送的数据后,会发送到另一个设施下来,能够把 Veth Pair 能够设想成一对用网线连接起来的 vNIC 设施。
  • Bridge 是工作在二层的虚构网桥。这是虚构设施,尽管叫网桥,但其实相似 vSwitch 的设计。当 Bridge 配合 Veth 设施应用时,能够将 Veth 设施的一端绑定到一个 Bridge 上,相当于实在环境把一个 NIC 接入一个交换机里。

虚拟机和容器的网络在传输流程上有些区别,前者比方 KVM 个别是应用 Tap 设施将虚拟机的 vNIC 和宿主机的网桥 Bridge 连接起来。而容器的 Bridge 网络模式是将不同 Namespace 里的 Veth Pair 连贯网桥 Bridge 来实现通信(其余形式下文探讨)。

Linux Bridge 配合桥接或者 NAT 模式很容易能够实现同主机或跨主机的虚拟机 / 容器之间通信,而且 Bridge 自身也反对 VLAN 的配置,能够实现一些三层交换机的能力。然而很多厂商都在研发性能更丰盛的虚构交换机,风行的有 Cisco Nexus 1000V、VMware Virtual Switch 以及宽泛应用的开源的 Open vSwitch[12] 等。利用 vSwitch,能够构建出反对更多封装协定、更高级的虚构网络:

1 Linux Bridge + Veth Pair 转发

VRF(Virtual Routing and Forwarding,虚构路由转发)在网络畛域中是个很常见的术语。上世纪九十年代开始,很多二层交换机上就能创立出 4K 的 VLAN 播送域了。4K 是因为 VLAN 标签的格局遵循 802.1q 规范,其中定义的 VLAN ID 是 12 位的缘故(802.1q in 802.1q 能够做到 4094*4094 个,0 和 4095 保留)。现在 VRF 概念被引入三层,单个物理设施上也能够有多个虚构路由 / 转发实例了。Linux 的 VRF 实现了对三层的网络协议栈的虚构,而 Network Namespace(以下简称 netns)虚构了整个网络栈。一个 netns 的网络栈包含:网卡(Network Interface)、回环设施(Loopback Device)、路由表(Routing Table)和 iptables 规定。本文应用 netns 进行演示(毕竟在探讨容器),下文应用 ip[14] 命令创立和治理 netns 以及 Veth Pair 设施。

创立、查看、删除 Network Namespace

# 创立名为 qianyi-test-1 和 add qianyi-test-2 的命名 netns,能够在 /var/run/netns/ 下查看
ip netns add qianyi-test-1
ip netns add qianyi-test-2

# 查看所有的 Network Namespace
ip netns list

# 删除 Network Namespace
ip netns del qianyi-test-1
ip netns del qianyi-test-2

执行后果如图(删除先不执行):

有趣味的话能够应用 strace 命令跟踪这个创立过程看看 ip 命令是怎么创立的(strace ip netns add qianyi-test-1)。

在 netns 中执行命令

# 在 qianyi-test-1 这个 netns 中执行 ip addr 命令(甚至能够间接执行 bash 命令失去一个 shell)# nsenter 这个命令也很好用,能够 man nsenter 理解
ip netns exec qianyi-test-1 ip addr

这个新创建的 netns 里家徒四壁,只有一个孤单的 lo 网卡,还是 DOWN 状态的。上面开启它:

开启 lo 网卡,这个很重要

ip netns exec qianyi-test-1 ip link set dev lo up
ip netns exec qianyi-test-2 ip link set dev lo up

状态变成了 UNKOWN,这是失常的。这个状态是驱动提供的,然而 lo 的驱动没有做这个事件。

创立 Veth Pair 设施

# 别离创立 2 对名为 veth-1-a/veth-1-b 和 veth-2-a/veth-2-b 的 Veth Pair 设施
ip link add veth-1-a type veth peer name veth-1-b
ip link add veth-2-a type veth peer name veth-2-b

应用 ip addr 命令能够查看:

8-9,10-11 便是下面创立进去的 2 对 Veth Pair 设施,此时它们都没有调配 IP 地址且是 DOWN 状态。

将 Veth Pair 设施退出 netns

# 将 veth-1-a 设施退出 qianyi-test-1 这个 netns
ip link set veth-1-a netns qianyi-test-1

# 将 veth-1-b 设施退出 qianyi-test-2 这个 netns
ip link set veth-1-b netns qianyi-test-2

# 为设施增加 IP 地址 / 子网掩码并开启
ip netns exec qianyi-test-1 ip addr add 10.0.0.101/24 dev veth-1-a
ip netns exec qianyi-test-1 ip link set dev veth-1-a up

ip netns exec qianyi-test-2 ip addr add 10.0.0.102/24 dev veth-1-b
ip netns exec qianyi-test-2 ip link set dev veth-1-b up

此时咱们别离在两个 netns 中执行 ip addr 命令,即可看到设施曾经存在,且路由表(route 或 ip route 命令)也被默认创立了:

这里操作和查看设施都必须采纳 ip netns exec {…} 的形式进行,如果命令很多,也能够把执行的命令换成 bash,这样能够不便的在这个 shell 里对该 netns 进行操作。

当初通过 veth-1-a/veth-1-b 这对 Veth Pair 联通了 qianyi-test-1 和 qianyi-test-2 这两个 netns,这两个 netns 之间就能够通过这两个 IP 地址互相拜访了。

ping 的同时在 101 上抓包的后果如下:

能够很分明的看到,eth-1-a(10.0.0.101)先通过 ARP(Address Resolution Protocol,地址解析协定)询问 10.0.0.102 的 MAC 地址。失去回应后,就以 ICMP (Internet Control Message Protocol,Internet 报文控制协议) request 和 reply 了,这也正是 ping 应用的协定。

ARP 解析的缓存信息也能够通过 arp 命令查看:

此时的网络连接模式是这样的:

这种连贯模式,就很像是事实中把两个带有 NIC 的设施用网线连接起来,而后配置了雷同网段的 IP 后彼此就能够通信了。那如果超过一个设施须要建设互联呢?事实中就须要交换机等网络设备了。还记得前文中说的 Linux 自带的 Bridge 么?接下来就应用 Bridge 机制建设网络。

进行上面的试验前须要把 veth-1-a/veth-1-b 这对 Veth Pair 从 qianyi-test-1 和 qianyi-test-2 挪动回宿主机的 netns 里,复原初始的环境。

# 宿主机的 netns id 是 1(有些零碎可能不是,请查问相干零碎的文档)ip netns exec qianyi-test-1 ip link set veth-1-a netns 1
ip netns exec qianyi-test-2 ip link set veth-1-b netns 1

创立 Linux Bridge 并配置网络

# 创立一个名为 br0 的 bridge 并启动(也能够应用 brctl 命令)ip link add br0 type bridge
ip link set br0 up

# 将 veth-1-a/veth-1-b 和 veth-2-a/veth-2-b 这两对 Veth Pair 的 a 端放入 qianyi-test-1 和 qianyi-test-2
ip link set veth-1-a netns qianyi-test-1
ip link set veth-2-a netns qianyi-test-2

# 为 veth-1-a 和 veth-2-a 配置 IP 并开启
ip netns exec qianyi-test-1 ip addr add 10.0.0.101/24 dev veth-1-a
ip netns exec qianyi-test-1 ip link set dev veth-1-a up

ip netns exec qianyi-test-2 ip addr add 10.0.0.102/24 dev veth-2-a
ip netns exec qianyi-test-2 ip link set dev veth-2-a up

# 将 veth-1-a/veth-1-b 和 veth-2-a/veth-2-b 这两对 Veth Pair 的 b 端接入 br0 网桥并开启接口
ip link set veth-1-b master br0
ip link set dev veth-1-b up
ip link set veth-2-b master br0
ip link set dev veth-2-b up

执行完能够查看创立好的网桥和配置好的 IP,实际上 brctl show 命令显示的后果更易懂,能够很分明的看到 veth-1-b 和 veth-2-b 连贯在了网桥的接口上。当 Veth Pair 的一端连贯在网桥上时,就会从“网卡”进化成一个“水晶头”。

当下模式抓包的后果并没有什么区别,但网络连接模式不同:

依照这个模式,如果有更多的 Network Namespace 和 Veth Pair 的话,应用雷同的形式增加就能够程度扩大了。

然而尝试从 qianyi-test-1 中 ping 宿主机天然是不通的,因为没有任何网络规定能够拜访到宿主机的网络:

下面的截图中有个 docker0 的网桥。当机器上装置了 Docker 之后会被主动设置好这个网桥供 Docker 应用。可能你留神到了,这个名为 docker0 的网桥竟然是有 IP 地址的。事实中的网桥天然是没有 IP 的,然而 Linux Bridge 这个虚构设施是能够设置的。当 Bridge 设置 IP 之后,就能够将其设置成这个外部网络的网关(Gateway),再配合路由规定就能够实现最简略的虚构网络跨机通信了(相似事实中的三层交换机)。

上面持续给 br0 网桥创立地址并在 veth-1-a 和 veth-2-a 上设置其为默认的网关地址:

# 确认路由转发开启
echo "1" > /proc/sys/net/ipv4/ip_forward

# 为 br0 设置 IP 地址
ip addr add local 10.0.0.1/24 dev br0

# 为 veth-1-a 和 veth-2-a 设置默认网关
ip netns exec qianyi-test-1 ip route add default via 10.0.0.1
ip netns exec qianyi-test-2 ip route add default via 10.0.0.1

此时就能胜利的拜访宿主机地址了(宿主机上的路由表在 ip link set br0 up 这一步主动创立了):

网络模型进一步变成了这样:

如果此时,另一台宿主机上也存在另一个网段的网桥和若干个 netns 的话,怎么让他们互通呢?别离在两边宿主机上配置一条到目标宿主机的路由规定就好了。假如另一台宿主机的 IP 地址是 10.97.212.160,子网是 10.0.1.0/24 的话,那么须要在以后机器上加一条 10.0.1.0/24 via 10.97.212.160 的规定,10.97.212.160 上加一条 10.0.0.0/24 via 10.97.212.159 的规定就能够了(或者 iptables 配置 SNAT/DNAT 规定)。那如果有 N 台呢?那就会是个 N * N 条的规定,会很简单。这就一个简略的 Underlay 模式的容器通信计划了。毛病也很显著,要求对宿主机底层网络有修改权,且比拟难和底层网络解耦。那如果能在物理网络上构建出一个横跨所有宿主机的虚构网桥,把所有相干的 netns 外面的设施都连贯下来,不就能够解耦了么。这就是 Overlay Network(笼罩网络)计划,下文会进行论述。至于本节其余虚构网络设备的装置和配置(比方 Open vSwitch)也是比拟相似的,这里不再赘述,有趣味的话能够自行查看文档并测试。

2 Overlay 网络计划之 VXLAN

VXLAN(Virtual eXtensible Local Area Network,虚构可扩大局域网,RFC7348)[16],VLAN 的扩大协定,是由 IETF 定义的 NVO3(Network Virtualization over Layer 3)规范技术之一(其余有代表性的还有 NVGRE、STT)。然而 VXLAN 和 VLAN 想要解决的问题是不一样的。VXLAN 实质上是一种隧道封装技术,它将数据链路层(L2)的以太网帧(Ethernet frames)封装成传输层(L4)的 UDP 数据报(Datagrams),而后在网络层(L3)中传输。成果就像数据链路层(L2)的以太网帧在一个播送域中传输一样,即逾越了三层网络却感知不到三层的存在。因为是基于 UDP 封装,只有是 IP 网络路由可达就能够构建出宏大的虚构二层网络。也因为是基于高层协定再次封装,性能会比传统的网络低 20%~30% 左右(性能数据随着技术倒退会有变动,仅代表以后程度)。

这里简要介绍下 VXLAN 的 2 个重要概念:

  • VTEP(VXLAN Tunnel Endpoints,VXLAN 隧道端点),负责 VXLAN 报文的封装和解封,对下层暗藏了链路层帧的转发细节
  • VNI(VXLAN Network Identifier,VXLAN 网络标识符),代表不同的租户,属于不同 VNI 的虚构网络之间不能间接进行二层通信。

VXLAN 的报文格式如图[17]:

Linux kernel v3.7.0 版本开始反对 VXLAN 网络。但为了稳定性和其余性能,请尽量抉择 kernel v3.10.0 及之后的版本。上面咱们应用 10.97.212.159 和 11.238.151.74 这两台机器创立一个测试的 VXLAN 网络。

# 10.97.212.159 上操作

# 创立名为 qianyi-test-1 和 add qianyi-test-2 的命名 netns,能够在 /var/run/netns/ 下查看
ip netns add qianyi-test-1
ip netns add qianyi-test-2

# 开启 lo 网卡,这个很重要
ip netns exec qianyi-test-1 ip link set dev lo up
ip netns exec qianyi-test-2 ip link set dev lo up

# 创立一个名为 br0 的 bridge 并启动(也能够应用 brctl 命令)ip link add br0 type bridge
ip link set br0 up

# 别离创立 2 对名为 veth-1-a/veth-1-b 和 veth-2-a/veth-2-b 的 Veth Pair 设施
ip link add veth-1-a type veth peer name veth-1-b
ip link add veth-2-a type veth peer name veth-2-b

# 将 veth-1-a/veth-1-b 和 veth-2-a/veth-2-b 这两对 Veth Pair 的 a 端放入 qianyi-test-1 和 qianyi-test-2
ip link set veth-1-a netns qianyi-test-1
ip link set veth-2-a netns qianyi-test-2

# 为 veth-1-a 和 veth-2-a 配置 IP 并开启
ip netns exec qianyi-test-1 ip addr add 10.0.0.101/24 dev veth-1-a
ip netns exec qianyi-test-1 ip link set dev veth-1-a up

ip netns exec qianyi-test-2 ip addr add 10.0.0.102/24 dev veth-2-a
ip netns exec qianyi-test-2 ip link set dev veth-2-a up

# 将 veth-1-a/veth-1-b 和 veth-2-a/veth-2-b 这两对 Veth Pair 的 b 端接入 br0 网桥并开启接口
ip link set veth-1-b master br0
ip link set dev veth-1-b up
ip link set veth-2-b master br0
ip link set dev veth-2-b up

# 11.238.151.74 上操作

# 创立名为 qianyi-test-3 和 add qianyi-test-4 的命名 netns,能够在 /var/run/netns/ 下查看
ip netns add qianyi-test-3
ip netns add qianyi-test-4

# 开启 lo 网卡,这个很重要
ip netns exec qianyi-test-3 ip link set dev lo up
ip netns exec qianyi-test-4 ip link set dev lo up

# 创立一个名为 br0 的 bridge 并启动(也能够应用 brctl 命令)ip link add br0 type bridge
ip link set br0 up

# 别离创立 2 对名为 veth-3-a/veth-3-b 和 veth-4-a/veth-4-b 的 Veth Pair 设施
ip link add veth-3-a type veth peer name veth-3-b
ip link add veth-4-a type veth peer name veth-4-b

# 将 veth-3-a/veth-3-b 和 veth-4-a/veth-4-b 这两对 Veth Pair 的 a 端放入 qianyi-test-3 和 qianyi-test-4
ip link set veth-3-a netns qianyi-test-3
ip link set veth-4-a netns qianyi-test-4

# 为 veth-3-a 和 veth-4-a 配置 IP 并开启
ip netns exec qianyi-test-3 ip addr add 10.0.0.103/24 dev veth-3-a
ip netns exec qianyi-test-3 ip link set dev veth-3-a up

ip netns exec qianyi-test-4 ip addr add 10.0.0.104/24 dev veth-4-a
ip netns exec qianyi-test-4 ip link set dev veth-4-a up

# 将 veth-3-a/veth-3-b 和 veth-4-a/veth-4-b 这两对 Veth Pair 的 b 端接入 br0 网桥并开启接口
ip link set veth-3-b master br0
ip link set dev veth-3-b up
ip link set veth-4-b master br0
ip link set dev veth-4-b up

这一长串的命令和之前的步骤完全一致,构建了一个如下图所示的网络环境:

这个环境里,10.0.0.101 和 10.0.0.102 是通的,10.0.0.103 和 10.0.0.104 也是通的,然而显然 10.0.0.101/10.0.0.102 和 10.0.0.103/10.0.0.104 是无奈通信的。

接下来配置 VXLAN 环境买通这四个 netns 环境:

# 10.97.212.159 上操作(本机有多个地址时能够用 local 10.97.212.159 指定)ip link add vxlan1 type vxlan id 1 remote 11.238.151.74 dstport 9527 dev bond0
ip link set vxlan1 master br0
ip link set vxlan1 up

# 11.238.151.74 上操作(本机有多个地址时能够用 local 11.238.151.74 指定)ip link add vxlan2 type vxlan id 1 remote 10.97.212.159 dstport 9527 dev bond0
ip link set vxlan2 master br0
ip link set vxlan2 up

应用 brctl show br0 命令能够看到两个 VXLAN 设施都连贯下来了:

而后从 10.0.0.101 上就能够 ping 通 10.0.0.103 了:

在 10.0.0.101 上抓的包来看,就像是二层互通一样:

间接查看 arp 缓存,也是和二层互通截然不同:

应用 arp -d 10.0.0.103 删掉这个缓存我的项目,在宿主机上从新抓包并保留文件,而后用 WireShark 关上看看(因为下面设置的不是 VXLAN 默认端口 4789,还须要设置 WireShark 把抓到的 UDP 解析为 VXLAN 协定):

咱们失去了预期的后果。此时的网络架构如图所示:

那么问题来了,这里应用 UDP 协定能实现牢靠通信吗?当然能够,可靠性不是这一层思考的事件,而是里层被包裹的协定须要思考的。残缺的通信原理其实也并不简单,两台机器上各自有 VTEP(VXLAN Tunnel Endpoints,VXLAN 隧道端点)设施,监听着 9527 端口上发送的 UDP 数据包。在收到数据包后拆解通过 Bridge 传递给指定的设施。那 VETP 这个虚构设施怎么晓得相似 10.0.0.3 这样的地址要发送给哪台机器上的 VETP 设施呢?这可是虚构的二层网络,底层网络上可不意识这个地址。事实上在 Linux Bridge 上保护着一个名为 FDB(Forwarding Database entry)的二层转发表,用于保留远端虚拟机 / 容器的 MAC 地址、远端 VTEP 的 IP,以及 VNI 的映射关系,能够通过 bridge fdb 命令来对 FDB 表进行操作:

# 新增条目
bridge fdb add <remote_host_mac_addr> dev <vxlan_interface> dst <remote_host_ip_addr>

# 删除条目
bridge fdb del <remote_host_mac_addr> dev <vxlan_interface>

# 替换条目
bridge fdb replace <remote_host_mac_addr> dev <vxlan_interface> dst <remote_host_ip_addr>

# 显示条目
bridge fdb show

下面这个简略的试验就 2 台机器,应用了命令的形式间接指定了彼此的 VTEP 地址,当 fdb 表查不到信息时发给对方就行了,这是最简略的互联模式。大规模 VXLAN 网络下,就须要思考如何发现网络中其余的 VETP 地址了。解决这个问题个别有 2 种形式:一是应用组播 / 多播(IGMP, Internet Group Management Protocol),把节点组成一个虚构的整体,包不分明发给谁的话就播送给整个组了(上述试验中的创立 VETH 设施的命令批改为组播 / 多播地址比方 224.1.1.1 就行,remote 关键字也要改成 group,具体请参阅其余材料);二是通过内部的分布式控制中心来收集 FDB 信息并分发给同一个 VXLAN 网络的所有节点。组播 / 多播受限于底层网络的反对状况和大规模下的的性能问题,比方很多云网络上不肯定容许这么做。所以下文在探讨和钻研 K8s 的网络计划时会看到很多网络插件的实现采纳的都是相似后者的实现形式。

这节就介绍到这里了。当然 Overlay 网络计划也不止 VXLAN 这一种形式,只是目前很多支流的计划都采纳了这种形式。其余的 Overlay 模式看上去目迷五色,其实说白了,无非就是 L2 over L4,L2 over L3,L3 over L3 等等各种包装形式罢了,懂了基本原理之后都没什么大不了的。网络虚拟化的设施和机制也有很多[18],细说的话一天都说不完,然而根本的网络原理把握之后,无非是各种协定包的流转罢了。

三 K8s 的网络虚拟化实现

1 K8s 的网络模型

每一个 Pod 都有它本人的 IP 地址,这就意味着你不须要显式地在每个 Pod 之间创立链接,你简直不须要解决容器端口到主机端口之间的映射。这将创立一个洁净的、向后兼容的模型,在这个模型里,从端口调配、命名、服务发现、负载平衡、利用配置和迁徙的角度来看,Pod 能够被视作虚拟机或者物理主机。

Kubernetes 对所有网络设施的施行,都须要满足以下的根本要求(除非有设置一些特定的网络分段策略):

  • 节点上的 Pod 能够不通过 NAT 和其余任何节点上的 Pod 通信
  • 节点上的代理(比方:零碎守护过程、kubelet)能够和节点上的所有 Pod 通信

备注:仅针对那些反对 Pods 在主机网络中运行的平台(比方:Linux):

  • 那些运行在节点的主机网络里的 Pod 能够不通过 NAT 和所有节点上的 Pod 通信

这个模型不仅不简单,而且还和 Kubernetes 的实现便宜的从虚拟机向容器迁徙的初衷相兼容,如果你的工作开始是在虚拟机中运行的,你的虚拟机有一个 IP,这样就能够和其余的虚拟机进行通信,这是基本相同的模型。

Kubernetes 的 IP 地址存在于 Pod 范畴内 – 容器共享它们的网络命名空间 – 包含它们的 IP 地址和 MAC 地址。这就意味着 Pod 内的容器都能够通过 localhost 达到各个端口。这也意味着 Pod 内的容器都须要互相协调端口的应用,然而这和虚拟机中的过程仿佛没有什么不同,这也被称为“一个 Pod 一个 IP”模型。

这几段话援用自 K8s 的官网文档[19],简略概括下就是一个 Pod 一个独立的 IP 地址,所有的 Pod 之间能够不通过 NAT 通信。这个模型把一个 Pod 的网络环境近似等同于一个 VM 的网络环境。

2 K8s 的支流网络插件实现原理

K8s 中的网络是通过插件形式实现的,其网络插件有 2 种类型:

  • CNI 插件:恪守 CNI(Container Network Interface,容器网络接口)标准,其设计上并重互操作性
  • Kubenet 插件:应用 bridge 和 host-local CNI 插件实现了根本的 cbr0

图片来自[20],本文只关注 CNI 接口插件。支流的 K8s 网络插件有这些[21],本文选出 github star 数在千以上的几个项目分析下:

  • Flannel:https://github.com/flannel-io…
  • Calico:https://github.com/projectcal…
  • Cilium:https://github.com/cilium/cilium

Flannel

CNI 是由 CoreOS 提出的标准,那就先看下 CoreOS 本人的 Flannel 我的项目的设计。Flannel 会在每台机宿主机上部署一个名为 flanneld 的代理过程,网段相干的数据应用 Kubernetes API/Etcd 存储。Flannel 我的项目自身是一个框架,真正为咱们提供容器网络性能的,是 Flannel 的后端实现。

目前的 Flannel 有上面几种后端实现:VXLAN、host-gw、UDP 以及阿里云和其余大厂的反对后端(云厂商都是实验性反对),还有诸如 IPIP、IPSec 等一些隧道通信的实验性反对。依照官网文档的倡议是优先应用 VXLAN 模式,host-gw 举荐给经验丰富且想要进一步晋升性能的用户(云环境通常不能用,起因前面说),UDP 是 Flannel 最早反对的一种性能比拟差的计划,基本上曾经弃用了。

下文别离对这三种模式进行剖析。

1)VXLAN

应用 Linux 内核 VXLAN 封装数据包的形式和原理上文曾经介绍过了,Flannel 创立了一个名为 flannel.1 的 VETH 设施。因为 flanneld 过程的存在,所以注册和更新新的 VETH 设施关系的工作就依附这个过程了。所以如同也没什么须要说的了,就是每个新的 K8s Node 都会创立 flanneld 这个 DeamonSet 模式的守护过程。而后注册、更新新的 VETH 设施就变得十分天然了,全局的数据天然也是保留在 Etcd 里了。这种形式图曾经画过了,无非是设施名字不一样(VETH 叫 flannel.1,网桥叫 cni0)而已。

2)host-gw

顾名思义,host-gw 就是把宿主机 Host 当做 Gateway 网关来解决协定包的流动。这个形式上文其实也演示过了,至于节点的变动和路由表的增删也是依附 flanneld 在做的。这个计划优缺点都很显著,最大的长处天然是性能,实打实的间接转发(性能整体比宿主机层面的通信低 10%,VXLAN 可是 20% 起步,甚至 30%)。毛病也很显著,这种形式要求宿主机之间是二层连通的,还须要对基础设施有掌控权(编辑路由表),这个在云服务环境个别较难实现,另外规模越来越大时候的路由表规模也会随之增大。这种形式原理上比较简单,图不画了。

3)UDP

每台宿主机上的 flanneld 过程会创立一个默认名为 flannel0 的 Tun 设施。Tun 设施的性能非常简单,用于在内核和用户应用程序之间传递 IP 包。内核将一个 IP 包发送给 Tun 设施之后,这个包就会交给创立这个设施的应用程序。而过程向 Tun 设施发送了一个 IP 包,那么这个 IP 包就会呈现在宿主机的网络栈中,而后依据路由表进行下一跳的解决。在由 Flannel 治理的容器网络里,一台宿主机上的所有容器都属于该宿主机被调配的一个“子网”。这个子网的范畴信息,所属的宿主机 IP 地址都保留在 Etcd 里。flanneld 过程均监听着宿主机上的 UDP 8285 端口,相互之间通过 UDP 协定包装 IP 包给目标主机实现通信。之前说过这个模式性能差,差在哪里?这个计划就是一个在应用层模仿实现的 Overlay 网络似得(像不像一个用户态实现的 VETH 设施?),数据包相比内核原生反对的 VXLAN 协定在用户态多了一次进出(flanneld 过程封包 / 拆包过程),所以性能上损失要大一些。

Calico

Calico 是个挺有意思的我的项目,根本思维是把宿主机齐全当成路由器,不应用隧道或 NAT 来实现转发,把所有二三层流量转换成三层流量,并通过宿主机上的路由配置实现包的转发。

Calico 和之前说的 Flannel 的 host-gw 模式区别是什么?首先 Calico 不应用网桥,而是通过路由规定在不同的 vNiC 间转发数据。另外路由表也不是靠 Etcd 存储和告诉更新,而是像事实环境一样间接用 BGP(Border Gateway Protocol, 边界网关协定)进行路由表数据交换。BGP 挺简单的,具体的论述这个协定有点吃力(而且我也不是很懂),在本文里只须要晓得这个协定使得路由设施能够互相发送和学习对方的路由信息来空虚本人就能够了,有趣味的话请查阅其余材料进一步理解。回到 Calico,宿主机上仍旧有个名为 Felix 的守护过程和一个名为 BIRD 的 BGP 客户端。

上文说过,Flannel 的 host-gw 模式要求宿主机二层是互通的(在一个子网),在 Calico 这里仍然有这个要求。然而 Calico 为不在一个子网的环境提供了 IPIP 模式的反对。开启这个模式之后,宿主机上会创立一个 Tun 设施以 IP 隧道(IP tunnel)的形式通信。当然用了这个模式之后,包又是 L3 over L3 的 Overlay 网络模式了,性能也和 VXLAN 模式相当。

全路由表的通信形式也没有额定组件,配置好 IP 路由转发规定后全靠内核路由模块的流转来做。IPIP 的架构图也是大同小异的,也不画了。

Cilium

eBPF-based Networking, Security, and Observability

光从这个介绍上就看进去 Cilium 散发出的那种不同凡响的气味。这个我的项目目前的 Github Star 数字快过万了,间接力压后面两位。Cilium 部署后会在宿主机上创立一个名为 cilium-agent 的守护过程,这个过程的作用就是保护和部署 eBPF 脚本来实现所有的流量转发、过滤、诊断的事件(都不依赖 Netfilter 机制,kenel > v4.19 版本)。从原理图的角度画进去的架构图很简略(配图来自 github 主页):

Cilium 除了反对根本的网络连通、隔离与服务透出之外,依靠 eBPF 所以对宿主机网络有更好的观测性和故障排查能力。这个话题也很大,本文就此收住。这里给两几篇写的很好的文章何其译文能够进一步理解 22。

3 K8s 容器内拜访隔离

上文介绍了网络插件的机制和实现原理,最终能够构建出一个二层 / 三层连通的虚构网络。默认状况下 Pod 间的任何网络拜访都是不受限的,然而外部网络中常常还是须要设置一些拜访规定(防火墙)的。

针对这个需要,K8s 形象了一个名为 NetworkPolicy 的机制来反对这个性能。网络策略通过网络插件来实现,要应用网络策略就必须应用反对 NetworkPolicy 的网络解决方案。为什么这么说?因为不是所有的网络插件都反对 NetworkPolicy 机制,比方 Flannel 就不反对。至于 NetworkPolicy 的底层原理,天然是应用 iptables 配置 netfilter 规定来实现对包的过滤的。NetworkPolicy 配置的办法和 iptables/Netfilter 的原理细节不在本文范畴内,请参阅其余材料进行理解 24。

4 K8s 容器内服务透出

在一个 K8s 集群外部,在网络插件的帮忙下,所有的容器 / 过程能够互相进行通信。然而作为服务提供方这是不够的,因为很多时候,服务的应用方不会在同一个 K8s 集群内的。那么就须要一种机制将这个集群内的服务对外透出。K8s 应用 Service 这个对象来实现这个能力的形象。Service 在 K8s 里是个很重要的对象,即便在 K8s 外部进行拜访,往往也是须要 Service 包装的(一来 Pod 地址不是永远固定的,二来总是会有负载平衡的需要)。

一句话概括 Service 的原理就是:Service = kube-proxy + iptables 规定。当一个 Service 创立时,K8s 会为其调配一个 Cluster IP 地址。这个地址其实是个 VIP,并没有一个实在的网络对象存在。这个 IP 只会存在于 iptables 规定里,对这个 VIP:VPort 的拜访应用 iptables 的随机模式规定指向了一个或者多个实在存在的 Pod 地址(DNAT),这个是 Service 最根本的工作原理。那 kube-proxy 做什么?kube-proxy 监听 Pod 的变动,负责在宿主机上生成这些 NAT 规定。这个模式下 kube-proxy 不转发流量,kube-proxy 只是负责畅通管道。

K8s 官网文档比拟好的介绍了 kube-proxy 反对的多种模式和根本的原理[26]。新近的 userspace 模式基本上弃用了,下面所述的 iptables 随机规定的做法在大规模下也不举荐应用了(想想为什么)。当初最举荐的当属 IPVS 模式了,绝对于前两种在大规模下的性能更好。如果说 IPVS 这个词比拟生疏的话,LVS 这个词恐怕是咱们耳熟能详的。在这个模式下,kube-proxy 会在宿主机上创立一个名为 kube-ipvs0 的虚构网卡,而后调配 Service VIP 作为其 IP 地址。最初 kube-proxy 应用内核的 IPVS 模块为这个地址设置后端 POD 的地址(ipvsadm 命令能够查看)。其实 IPVS 在内核中的实现也是用了 Netfilter 的 NAT 机制。不同的是,IPVS 不会为每一个地址设置 NAT 规定,而是把这些规定的解决放到了内核态,保障了 iptables 规定的数量基本上恒定,比拟好的解决了之前的问题。

下面说的只是解决了负载平衡的问题,还没提到服务透出。K8s 服务透出的形式次要有 NodePort、LoadBalancer 类型的 Service(会调用 CloudProvider 在私有云上为你创立一个负载平衡服务)以及 ExternalName(kube-dns 里增加 CNAME)的形式。对于第二种类型,当 Service 繁多然而又流量很小的状况下,也能够应用 Ingress 这个 Service 的 Service 来收敛掉[27]。Ingress 目前只反对七层 HTTP(S) 转发(Service 目前只反对四层转发),从这个角度猜猜 Ingress 怎么实现的?来张图看看吧[28](当然还有很多其余的 Controller[29]):

对于这部分,本文不再进行具体论述了,无非就是 NAT,NAT 多了就想方法收敛 NAT 条目。依照常规,这里给出一篇特地好的 kube-proxy 原理论述的文章供进一步理解[30]。

四 总结

网络虚拟化是个很大的话题,很难在一篇文章中齐全讲清楚。只管这篇文章尽量想把重要的常识节点编织分明,但受限于作者自己的精力以及认知上的限度,可能存在忽略甚至错漏。如有此类问题,欢送在评论区探讨 / 斧正。参考文献里给出了很多不错的材料值得进一步去学习(局部地址受限于网络环境,可能须要特定的形式能力拜访)。

参考文献

1、TCP Implementation in Linux: A Brief Tutorial, Helali Bhuiyan, Mark McGinley, Tao Li, Malathi Veeraraghavan, University of Virginia:https://www.semanticscholar.o…
2、NAPI, linuxfoundation, https://wiki.linuxfoundation….
3、Monitoring and Tuning the Linux Networking Stack: Receiving Data, Joe Damato,译文:Linux 网络栈监控和调优:接收数据(2016):http://arthurchiao.art/blog/t…
4、Monitoring and Tuning the Linux Networking Stack: Sending Data, Joe Damato, 译文:Linux 网络栈监控和调优:发送数据(2017):http://arthurchiao.art/blog/t…
5、Scaling in the Linux Networking Stack, https://github.com/torvalds/l…
6、Understanding TCP internals step by step for Software Engineers and System Designers, Kousik Nath
7、Netfilter, https://www.netfilter.org/
8、Netfilter-packet-flow, https://upload.wikimedia.org/…
9、Analysis TCP in Linux, https://github.com/fzyz999/An…
10、NAT – Network Address Translation, 译文:NAT – 网络地址转换(2016):http://arthurchiao.art/blog/n…
11、Virtual networking in Linux, By M. Jones, IBM Developer:https://developer.ibm.com/tut…
12、Open vSwitch, http://www.openvswitch.org/
13、Linux Namespace, https://man7.org/linux/man-pa…
14、ip, https://man7.org/linux/man-pa…
15、Veth, https://man7.org/linux/man-pa…
16、VxLAN, https://en.wikipedia.org/wiki…
17、QinQ vs VLAN vs VXLAN, John, https://community.fs.com/blog…
18、Introduction to Linux interfaces for virtual networking, Hangbin Liu:https://developers.redhat.com…
19、Cluster Networking, 英文地址 https://kubernetes.io/zh/docs…
20、THE CONTAINER NETWORKING LANDSCAPE: CNI FROM COREOS AND CNM FROM DOCKER, Lee Calcote:https://thenewstack.io/contai…
21、CNI – the Container Network Interface, https://github.com/containern…
22、Making the Kubernetes Service Abstraction Scale using eBPF, [译] 利用 eBPF 撑持大规模 K8s Service (LPC, 2019):https://linuxplumbersconf.org…
23、基于 BPF/XDP 实现 K8s Service 负载平衡 (LPC, 2020)https://linuxplumbersconf.org…
24、A Deep Dive into Iptables and Netfilter Architecture, Justin Ellingwood:https://www.digitalocean.com/…
25、Iptables Tutorial 1.2.2, Oskar Andreasson:https://www.frozentux.net/ipt…
26、Virtual IPs and service proxies, 英文地址:https://kubernetes.io/docs/co…
27、Ingress, 英文地址:https://kubernetes.io/docs/co…
28、NGINX Ingress Controller, https://www.nginx.com/product…
29、Ingress Controllers, 英文地址:https://kubernetes.io/docs/co…
30、Cracking kubernetes node proxy (aka kube-proxy), [译] 深刻了解 Kubernetes 网络模型:本人实现 Kube-Proxy 的性能:https://cloudnative.to/blog/k…

原文链接
本文为阿里云原创内容,未经容许不得转载。

退出移动版