Linux提供了强大的数据包处理和管理能力,开发人员依赖这些系统级别的能力创建防火墙,记录流量,路由数据包以及实现负载均衡功能。Kubernetes在POD之间的连接性,POD和NODE之间的连通性,以及Kubernetes服务功能上重度依赖于这些数据包处理能力,因此咱们(下)这篇文章的核心是详细的剖析Kubernetes平台使用最多的三个操作系统网络工具,包括iptables,IPVS和eBPF。
iptables可以说是Linux操作系统管理最常使用的工具都不为过,已经在Linux操作系统上存在很多年了,系统管理员可以使用iptables来创建防火墙规则,收集数据包处理日志,修改和重新路由数据包等,iptales的背后实现使用了Netfiler,允许我们通过配置iptables rule来处理数据包。iptables rules虽然说功能强大,但是非常不适合手动编辑,幸运的是有很多工具可以用来管理iptables rules,比如我们可以用ufw或指责firewalld来管理防火墙rules。
在Kubernetes环境中,工作节点上的iptables rules主要由kubelet和kube-proxy两个组件创建和管理,因此理解iptables是理解POD的访问流量如何被接入的基础。具体来说,iptables有三个核心的概念:tables,chains和rules。这三个概念相互关联,形成一种层级结构,tables包含chains,chains包含rules。
由于iptables包含了范围众多的功能,因此设计人员按照tables将这些不同的功能进行分类,咱们在使用和运维Kubernetes集群最常见的有三个tables类型:Filter(包含了和防火墙相关的rules),NAT(包含了和网络地址转化相关的rules),Mangle(包含非NAT,数据包修改的rules),并且iptables会按特定的顺序来执行tables,咱们稍后会详细介绍。
Chains中包含一组rules,当数据包packet在一个chain中被处理的时候,这些rules会被按顺序逐个处理。Chains隶属于table,并且chains中的rule按Netfiler定义的hooks规则进行组织。具体来说,iptables中有5个内建的顶层chains,这个五个chains和Netfiler的hooks一一对应,因此当我们要给chain中insert一个新的rule,具体要插入的chain取决于我们希望这个rule在什么情况下以及什么时候被执行。
最后一个概念是rule,规则rule由condition条件和action动作组合而成,比如如果packet的目标端口号是22(条件condition),那么就丢弃这个数据包(action动作)。因此我们通常说iptables用来处理机器接收到的数据包,本质上是包含在chains和tables中的rules来具体对流经的数据包进行评估(是否符合特定的条件)和处理(action)。
坦白讲iptables设计的这种table-chain-rule(target)的结构和执行规则非常复杂,咱们上边描述的内容只是大体的把这几个核心组件是什么,以及它们之间是如何进行协作进行了说明。笔者反复强调过理解iptables是理解容器网络,特别是Kubernetes网络机制的核心,因此咱们接下来就对这三个概念进行更深入的介绍。
iptables中的table本质上是用来将不容的功能进行分类管理,因此每个table只负责特定类型的action,用大白话说就是每个table都有特定的功能范围,并且多个table之间不应该有相同功能的重复。iptables默认包含了五种类型的tables,详细介绍如下边的清单:
- Filter表,用来处理数据包的acceptance或者rejection
- NAT表,用来对数据包的源地址或者目的地址进行修改
- Mangle表,用来对数据包进行通用的操作,比如修改packet的headers等
- Raw表,用来对数据包在进行connection tracking或者其他表的rule处理之前进行修改操作
- Security表,专门用来支持SELinux
iptables按照Raw,Mangle,NAT,Filter的顺序来执行tables中定义的rules,但是由于chain的存在,因此严格意义上来讲,这是一个二维的执行顺序,在table维度上按Raw,Mangle,NAT,Filter这样的顺序,而在另外一个维度上按chain的顺序执行。虽然在Linux系统管理领域,大家都会说tables contains chains,实际上这句话稍微有点歧义,因为执行的顺序是先chains,然后才是tablestate)。Conntrack会将受到的每个数据包和特定的连接关联起来,如果没有Conntrack提供的连接追踪能力,内核收到的数据流会显得非常混乱,Conntrack是操作系统的防火墙和NAT功能模块的基石。
举个例子,某个特定的packet会按照 Raw PREROUTONG,Mangle PREROUTONG,NAT PREROUTONG先执行完PREROUTING chain,然后是INPUT或者FORWARD chain,依次类推。
iptables中chains的概念大家可以简单理解为包含一组rules的链表,当数据包被处理的时候,chain的rule会按先后顺序对数据包进行处理,如果数据包符合某个rule定义的“终结”动作(比如drop),那么数据包的处理就结束了,直至到chain的最后一个rule。
iptables中定义的顶层(top-level)chain有五个,他们和Netfiler中定义的hooks钩子函数想呼应:PREROUTING,INPUT,NAT,OUTPUT和POSTROUTING。Netfiler hooks和chains的映射对应关系如下所示:
- PREROUTING对应的Netfiler hooks是NF_IP_PRE_ROUTING
- INPUT对应的Netfiler hooks是NF_IP_LOCAL_IN
- NAT对应的Netfiler hooks是NF_IP_FORWARD
- OUTPUT对应的Netfiler hooks是NF_IP_LOCAL_OUT
- POSTROUTING对应的Netfiler hooks是NF_IP_POST_ROUTING
上边文字描述的五个chain和Netfiler钩子函数的映射通过下图会理解的更加清楚:
对于每个进入机器的数据包(packet)来说,只有特定的chain执行路径,咱们通过一个三台机器组成的场景来说明,假设我们有三台机器,IP地址分别是10.0.0.1,10.0.0.2和10.0.0.3。从机器10.0.0.1的角度,会有以下几种数据包在iptables chain中路由的路径:
- 场景1:数据包从外部发送到机器10.0.0.1,发送数据的机器为10.0.0.2,执行的tables为PREROUTING,INPUT
- 场景2:数据包从外部发送到机器10.0.0.1,但是目的地址不是机器10.0.0.1,发送数据的机器为10.0.0.2,目的机器为10.0.0.3,执行的tables为PREROUTING,NAT,POSTROUTING
- 场景3:数据包从本地机器的进程发送给本机上的进程,源地址和目的地址都是127.0.0.1,执行的tables为OUTPUT,POSTROUTING,PREROUTING,INPUT
- 场景4:数据包从本地放给外部机器,发送数据的机器为10.0.0.1,目的地址为10.0.0.2,执行的tables为OUTPUT,POSTROUTING
咱们前边介绍iptables的tables执行顺序为RAW,MANGLE,NAT,Filter,对于大部分chains来说,一般并不包含所有五个tables,但是table的执行顺讯不变。举个例子,Raw tables主要用来对进入iptables的数据包进行修改操作,因此Raw tables中只包含PREROUTING和OUTPUT这两个chains,这和Netfiler的packet处理流程一致。具体tables中包含chains的详细清单如下图所示:
在Linux操作系统上,我们可以通过命令iptables -L -t来查看iptables包含的内容,以下是笔者的minikube集群上的虚拟机机返回的结果:
$ sudo iptables -L -t filter
Chain INPUT (policy ACCEPT)
target prot opt source destination
KUBE-SERVICES all -- anywhere anywhere ctstate NEW /* kubernetes service portals */
KUBE-EXTERNAL-SERVICES all -- anywhere anywhere ctstate NEW /* kubernetes externally-visible service portals */
KUBE-FIREWALL all -- anywhere anywhere
Chain FORWARD (policy ACCEPT)
target prot opt source destination
KUBE-FORWARD all -- anywhere anywhere /* kubernetes forwarding rules */
KUBE-SERVICES all -- anywhere anywhere ctstate NEW /* kubernetes service portals */
DOCKER-USER all -- anywhere anywhere
DOCKER-ISOLATION-STAGE-1 all -- anywhere anywhere
ACCEPT all -- anywhere anywhere ctstate RELATED,ESTABLISHED
DOCKER all -- anywhere anywhere
ACCEPT all -- anywhere anywhere
ACCEPT all -- anywhere anywhere
(省略若干)
注:对NAT来说,分为DNAT和SNAT,其中DNAT作用于chain PREROUTING或者OUTPUT;而SNAT作用于chain INPUT或者POSTROUTING。
我们继续通过例子来说明让读者加深理解,假设有inbound的数据包目的地址是本机,那么table和chain的执行顺序看起来如下:
1.PREROUTING
a.Raw
b.Mangle
c.NAT
2.INPUT
a.Mangle
b.NAT
c.Filter
最后,如果我们把前边所有和Netfiler hooks,tables和chains介绍的信息放在一张表里,结果就是packet在iptables的处理流程,如下图所示:
上图中的圆圈代表的是iptables rule,并且rule隶属于table和chain。对于特定的chain,iptables会按固定的顺序来触发table/chain中的rule,咱们以本地发往外部机器的数据包为例,执行的顺序为:
1. Raw/OUTPUT
2. Mangle/OUTPUT
3. NAT/OUTPUT
4. Filter/OUTPUT
5. Mangle/POSTROUTING
6. NAT/POSTROUTING
最后详细说明一下规则Rule,咱们前边说过由两部分组成,condition和action。condition部分其实描述是packet的特征,当有match这个condition时,action会被执行。如果packet不matchcondition,iptables会继续评估后续的table/chain中的rule,直到最后一个rule。基于condition对packet是否匹配的检查包括但不限于packt的目的地址是否和条件中ip地址一致等,下图展示了一些通用的条件类型:
action部分有两种类型,terminating类型和nonterminating类型,其中terminating类型会“终止”iptables继续对数据包的处理,因为terminating类型的action已经完成了数据包的最终处理。而nonterminating类型的action在处理完后,后续iptables的chain定义的rule会被继续处理。具体来说,ACCEPT,DROP,REJECT和RETURN都是terninating类型action。咱们来看看几个具体的iptables命令,来把前边的内容串在一起看看,如下图所示:
最后咱们再重复一次iptables几个关键概念的关系,rule隶属于table和chain,并且rule在table和chain中的类型决定了rule的执行时机。
注:iptables中的数据在机器重启后会丢失,不过iptables提供了iptables-save以及iptables-restore工具来应对这种场景。运维人员可以在机器重启之前通过命令iptables-save来手动的备份机器上的iptables配置,在机器重启后,通过工具iptables-restore来回复数据。这也是很多底层的自动化资源扩展机制背后的工作原理。
对于Kubernetes来说,使用iptables提供的masquerade连接的功能,让POD使用Node的IP地址,能够在集群外部的网络上进行传输,即便是我们都知道POD有自己的私有IP地址。MASQUERADE类型的action和SNAT稍微不同,我们使用SNAT的时候需要通过参数--source-address来指定具体的IP地址,而MASQUERADE不需要,它会使用指定网络接口的IP地址来修改source地址。不过正是由于MASQUERADE提供的这种动态性,它的性能会比SNAT差很多。下边是一条使用MASQUERADE类型的命令:
$iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
上边这条命令的含义是,在数据被离开机器的时候(POSTROUTING),用eth0以太网接口的ip地址替换数据包的源地址。另外iptables还可以在连接级进行负载均衡,大白话说就是连接上数据的扇出,咱们在介绍Kukbernetes的Service具体实现原理的文章中,关于Service如何将收到的服务请求负载到多个后端POD就是原理,并且iptables还提供了随机性,避免所有的服务请求都被DNAT到第一个POD上(也叫DNAT target)。
$ iptables -t nat -A OUTPUT -p tcp --dport 80 -d $FRONT_IP -m statistic \
--mode random --probability 0.5 -j DNAT --to-destination $BACKEND1_IP:80
$ iptables -t nat -A OUTPUT -p tcp --dport 80 -d $FRONT_IP \ -j DNAT --to-destination $BACKEND2_IP:80
如上是笔者本地minikube集群上的一个有两个replica的服务,从配置信息中可以看到,请求有50%的几率被DNAT到第一个POD($BACKEND1_IP)上,如果第一个目标target没有被选中,那么请求会默认被第二个POD($BACKEND2_IP)处理。这种n分之一的目标比率配置显得有点呆板,因为如果有3个pod,那么每个pod就有三分之一的比率被选中来处理服务请求。眼尖的读者应该能看出使用DNAT这种扇出模式的负载均衡存在一些问题。首先因为没有反馈机制(iptables根本不知道每台后台pod的处理情况和状态),因此在长连接的情况下,即便是我们紧急给服务增加了处理POD,很多已经和服务建立连接并且连接没有失效的情况下,负载不会被均匀的分配到新增的处理POD上。
因此在很多长连接的场景下,笔者建议大家采用客户端负载均衡会更加实用来规避这个问题,因为客户端会定期去API Server上获取服务可用的IP地址清单,然后定期强制客户端重新连接来解决长连接的问题。虽说iptables已经被广泛适用了很多年,但是由于iptables本身的工作原理无法支撑大规模集群的网络处理场景,因此特别是在负载流量负载均衡的场景中,替代方案被开发出来,这就是我们接下来要讲IPVS机制。
IPVS也叫IP Virtual Server,IP虚拟服务器机制,是Linux系统上的L4层连接负载均衡机制,下图可以帮助大家理解IPVS在整个Linux操作系统处理数据包的位置:
如咱们在前边介绍DNAT机制所述,iptables对L4层负载均衡的支持考DNAT rules和weights(百分比率),而IPVS原生就支持多种模式的负载均衡机制,因此就负载均衡这个场景来说,IPVS的性能要优于iptabes,当然也取决于IPVS的配置和流量模型。IPVS支持的负载均衡模式如下图所示:
另外IPVS支持网络连上的接数据包以下几种forwarding模式:
- NAT重新源和目的IP地址
- DR将IP数据包封装进IP数据包来在主机网络中传递
- 使用IP通道技术(IP tunneling)来直接把L2数据包的目标MAC地址更新为选中机器的MAC地址
iptables的DNAT机制提供的负载均衡功能最被大家诟病的原因有三个:
- 随着集群节点的增多,比如5000个节点的集群,kube-proxy和itables就会变成整个集群的瓶颈。比如说NodePort类型的服务,如果我们在集群上有2000个NodePort类型的服务,每个服务有10个POD实例,那么每个节点上的iptables中大概有20000个记录,这会占用大量的内核资源
- 如果我们在这样的集群上增加一个rule,测试结果显示,如果有5000个服务,需要11分钟,如果有20000个服务,就需要大概5个小时
- 客户访问服务的延迟会增加,因为每个数据包都需要遍历整个iptables直到terminating rule
而IPVS提供的多种模式有效的解决了iptables带来的约束。我们可以通过ipvsadm工具来管理节点上的IPVS配置,比如我们可以通过如下几个命令来配置ipvs,
# ipvsadm -A -t 1.1.1.1:80 -s lc
# ipvsadm -a -t 1.1.1.1:80 -r 2.2.2.2 -m -w 100
# ipvsadm -a -t 1.1.1.1:80 -r 3.3.3.3 -m -w 100
然后我们可以通过IPVS -L来罗列机器上的虚拟服务器,如下是笔者机器上的输出结果,
# ipvsadm -L
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn InActConn
TCP 1.1.1.1.80:http lc
-> 2.2.2.2:http Masq 100 0 0
-> 3.3.3.3:http Masq 100 0 0
最后我们来看看eBPF机制,eBPF也叫扩展伯克利数据包过滤器,允许我们在内核运行特殊的应用程序而不是在内核和用户态来回传递数据包,咱们前边介绍的Netfiler和iptables就是后者,在内核和用户态之间来回传递数据包。
读者看到eBPF这个名字,肯定能猜到有BPF机制,没错BPF是eBPF之前内核处理数据包的机制,主要被用来对网络数据包进行分析。比如大家熟知的tcpdump工具,当我们使用tcpdump来抓取特定的数据包来进行分析,背后使用的就是BPF提供的内核功能。特别是当我们在tcpdump命令后边指定了过滤条件,这个条件会被传递到BPF,BPF接着会使用过滤器把符合条件的数据包抓取并返回给运行在用户空间的应用程序tcpdump,并显示在控制台。
➜ data sudo tcpdump -i en0 arp -vvv
tcpdump: listening on en0, link-type EN10MB (Ethernet), capture size 262144 bytes
上边是笔者在本地通过tcpdump来抓arp请求,在有多台机器的局域网内,这个命令会返回所有的arp请求信息,因此返回的数据会非常非常多。
eBPF是对BPF功能的扩充,并且eBPF代码可以直接调用操作系统的接口以及监控系统调用,而不需要通过运行在用户空间的应用程序使用操作系统提供的钩子函数来实现。这种直接在内核运行特定条件数据包的方式性能有很大的提升,因此很适合作为网络工具的技术实现手段。eBPF除了提供数据包过滤之外,还提供了如下罗列的其他功能:
- Kprobes模块提供了动态追踪内核模块的能力
- Uprobes提供了用户态追踪的能力
- Tracepoints提供了静态的内核模块追踪能力
- perf_events提供时序内核运行监控数据
- XDP提供了在驱动层面的数据包抓取能力,大家要注意的是XDP可以深入到内核之下,在驱动层工作
如果我们以tcpdump为例,看看eBPF具体是如何工作的,如下图所示:
假设我们运行tcpdump -i any命令,这个命令会被pcap_compile模块编译成BPF程序,接着内核会使用这个编译好的BPF程序来过滤所有网络接口的所有的数据包,这个过滤条件就是-i any。tcpdump不是直接从内核读取满足条件的数据,而是通过eBPF map结构,map是一个key-value类型的数据结构,来协助内核和BPF程序之间进行数据交换,在我们的例子中就是tcpdump。
文章的最后我们来总结一下使用eBPF带来的好处(特别是在Kubernetes场景下):
- 性能的提升。使用iptables在有20000个NodePort类型服务的集群中,更新一条rule最坏需要5个小时,这肯定是不能接受的,因此使用IPVS的哈希表会更加有效
- 指标追踪,通过使用BPF,我们可以收集pod和容器级别的网络运行信息。特别是eBPF提供的cgroups级别的资源使用监控能力,能够让我们做耕细粒度的控制。
- 监控kubectl exec,我们可以通过编写特定的代码来记录集群汇总kubeclt exec执行命令,并持久化来支持后续的审计和分析工作
- 安全提升,我们可以通过eBPF来限制容器实例可执行的系统调用,以及向Falco这样的运行时安全工具来提升集群的整体安全性。
另外Kuberntes集群中使用eBPF最好的例子就是Cilum,一种遵守CNI规范的网络插件,并提供取代默认kube-prox提供的服务能力。具体来说,Cilium会直接解析请求并把数据包直接路由到内核,绕过iptables,效率得到的极大的提升,特别是application级别的数据包路由场景。
好了,咱们这边文章的内容就这么多了,在详细介绍容器网络之前,我们下篇文章先介绍几个常用的网络工具,敬请期待!