介绍
使用过k8s集群的同学都知道,k8s中的网络问题排查一直是一个难点,很多时候面对出现的网络问题都无从下手。这篇文章就是针对k8s的网络,尽量用简洁易懂的方式介绍了k8s网络的底层原理,在能让大家对k8s的网络有个整体的认识的同时,也介绍了针对各个流程的排查问题的方式,让大家面对k8s网络问题时能做到心中有数,可以针对不同的环节使用不同的方式进行排查,从而做到有的放矢。
首先,本文会使用由顶向下的方式介绍k8s网络实现,首先介绍个整体的流程,然后针对每部分进行详细介绍,从而达到在把握整体流程的情况下,也能了解具体细节。需要注意的是,这里默认介绍的cni实现是flannel的vxlan模式。
准备知识
在开始介绍流程之前,需要先介绍几个基础组件,大家只要了解各个组件的作用就可以,至于更详细的信息可以自己后续再进一步深入了解。
- linux网络设备:linux的网络设备负责转发网络包,可以通过ip a或者ifconfig等命令查看,常见的有以下几种: 1.1 物理网卡,用于连接物理网络,如eth0,不同主机之间的流量都要走这个网络设备 1.2 虚拟以太网对(veth pairs),了解docker的同学可能都很熟悉,这个是用于连接容器和宿主机的接口,这个网络设备总是成对出现,一个在容器内,另一个可以注册到桥接设备上,一个网络包从一个设备中进入,就会从另一个设备上出来,可以看成是一根连接容器和主机的虚拟网线 1.3 桥接设备,如cni0,docker0,也常叫网桥。
-
Veth Pair设备和网桥的关系:网桥一般一般用于充当veth pairs的网关,每个容器内会有一个veth设备,另一个对端的veth设备就会注册到主机上的网桥设备上。当把访问容器的网络包发给网桥时,他就会找到容器对应ip的对端veth设备并把网络包从这一端输入,网络包就会出现在容器内的veth设备上了
-
路由表:路由表是存储在网络设备中的数据转发决策表,用于控制网络包的流向,内核或者设备会根据路由表将网络包转发给对应的设备,可以通过route -n 或者ip route show来查看,下文中的路由表专指linux内核中存储的路由表数据。
- 路由表和网络设备的关系:路由表确定了网络包流向哪个网络设备,网络设备实现了具体细节,把网络包送到目的地。路由表决定去哪里,网络设备负责怎么走
- iptable:iptable是linux内核中Netfilter框架的配置工具,可以用于数据包过滤,网络地址转换(NAT),数据包修改等。
- iptable和路由表的关系:iptable是内核提供给用户用于修改网络包转发逻辑的一个接口,可以认为iptable和路由表一起构成了网络包的转发规则,路由表决定将网络包发给哪个设备,而iptable则是在查看路由表之前执行的用户配置逻辑,可以将网络包进行修改,达到影响网络包流向的效果。
总体流程
假设现在我们在pod a中,需要通过b服务的service ip,向b服务发起请求,现在就将这个流程分为四个步骤
- 在pod a中使用service ip访问一个服务,请求从pod的容器来到宿主机上
- 请求的service ip被解析为服务b的一个pod的ip
- 请求被发送到pod b所在的宿主机上
- 请求被发送到pod b容器中
请求从容器来到宿主机
这个步骤比较简单,可以参考docker的实现,首先介绍下docker的流程: 在启动容器时,会在生成一对veth pairs,然后其中一端安装在容器中,作为eth0网卡,然后另一端veth设备会注册到docker0网桥上。这样,当容器内发起请求时,因为容器内的路由默认会使用eth0网卡,也就是veth设备进行处理,网络包就会从veth的另一端出来,也就来到了宿主机的docker0网络设备上。
k8s的实现也是差不多的,只不过docker0这个网桥不会被使用,而是创建了一个cni0网桥,用于处理宿主机和容器之间的交互。此时网络包的目标ip还是service ip,因此这一步骤的问题排查,就可以围绕cni0网络设备和service ip展开。
问题排查
通过tcpdump指定cni0网卡,抓取service ip来确认网络包是否来到宿主机上。比如: tcpdump host 10.201.56.42 -i cni0 -nn -n -vvv
请求被解析为一个具体的pod ip
请求的网络包从容器发送到cni0网卡后,他发现不是自己处理的网络包,就会将网络包交给内核,通过路由表确认网络包需要交给哪个网络设备。在使用路由表判断之前,会执行一些iptable的钩子函数,而service ip到pod ip的解析就是在iptable的规则中实现的。
这里简单介绍下iptable,他是由一些规则组成的,内核会在不同的阶段执行不同的规则集合,就类似java中的代理类,可以在执行被代理方法之前执行一部分逻辑,也可以在执行被代理方法之后执行另外一部分逻辑。 iptable就是如此,他被分为了不同的称为表的规则集合,执行的时机和作用不同,有的在路由表之前,有的在之后。
而实现k8s中的service ip转变为pod ip的逻辑,就是在iptable中的nat表里面的规则配置的。 nat表包含一些规则,如果规则经过网络包匹配到了他的一些条件,就会根据规则的动作修改网络包。而k8s正式通过iptable的规则,将service ip转换为随机的某个pod ip,实现了简单的均衡负载功能。
示例:
比如容器中访问的service ip是 10.201.56.42,首先通过iptables -t nat -L -n -v |grep -C 3 10.201.56.42 命令检查是否有规则匹配,如下图所示,会发现有个KUBE-SVC-K33ANIACEBTJHVD3字符,这个字符其实是iptable中的名为chain(链)的规则的名称,第一次他作为nat表中的一个规则,配置了如果ip是10.201.56.42,那就会匹配到这条规则,然后跳转到KUBE-SVC-K33ANIACEBTJHVD3这个链进行处理。
再看下面这个链的规则,会发现他有个 statistic mode random probability 0.50000000000 字符,这个意思就是执行到这个规则时,有一半的几率会匹配,这是因为我的这个服务有两个实例,所以这个概率就是0.5,决定了这次请求会解析为哪个pod实例的ip。
我们假设匹配中了KUBE-SEP-U2CN6GROI4QX6PV3 这个规则,我们通过iptables -t nat -L -n -v |grep -C 3 KUBE-SEP-U2CN6GROI4QX6PV3查看下详细信息,就会发现有个DNAT的规则,并且有tcp to:10.200.1.145:20116 这个字符,他就是将网络包的目标ip改为了10.200.1.145 这个pod ip,如果没有匹配中,走另一个规则,那就会改成另外一个pod实例的ip了。这里就完成了service ip到pod ip的转变。
kube-proxy的作用
上面说了service ip是通过iptable的规则来转为具体一个实例的pod ip的,那么这些iptable规则又是哪里来的呢? 其实这些规则就是k8s的 kube-proxy 这个组件来负责维护的,这个是k8s的kube-system 命名空间中的一个 deamonSet 组件,会在每个机器上部署一个实例,他会通过api server监听k8s中pod的变化,每当pod有改动,他就会将对应的变化同步到当前机器的iptable规则中,比如某个service增加了一个pod实例,那所有机器上的kube-proxy组件就会更新iptable规则,把新启动的pod的ip加入到均衡负载的iptable规则中。
问题排查
如果出现通过service ip访问不通的问题,就可以使用这里介绍的方式进行初步确认,确保service ip能正常的转为pod ip。
- 首先获取到要排查问题的service ip,如10.201.56.42
- 通过在任意一台k8s的node上查看iptable规则,执行iptables -t nat -L -n -v |grep -C 3 10.201.56.42查看service ip的规则。
- 在第二步中找到匹配的KUBE-SEP开头的iptable规则,通过执行命令查询转换的对应pod ip,iptables -t nat -L -n -v |grep -C 3 KUBE-SEP-U2CN6GROI4QX6PV3。
如果在第二步或第三步没发现匹配的规则,或者第三步中的pod ip和实际的不一致,那可能是kube-proxy有问题,没有将变更同步到机器上,service ip无法解析成pod ip,可以通过kubectl重启kube-proxy来尝试解决。执行命令:kubectl rollout restart ds/kube-proxy -n kube-system
请求被发送到pod b所在的宿主机上
在上一步中,网络包中的service ip被iptable转为pod ip 10.200.1.145,接下来就会通过路由表决定这个网络包要转给哪个网络设备进行处理,首先执行route -n 查看路由表。最左边的Destination就是网络包的目的地址网络段,右边的Iface就是网络设备,可以看到pod ip会匹配到10.200.1.0这个网络段,然后交给flannel.1设备处理。 而flannel就是我们k8s的cni插件,他负责连通k8s集群不同机器之间的网络。他的目标就是根据pod ip找到对应pod所在的宿主机,然后将网络包发送过去,我们来看下他是如何一步步实现这个目标的。
通过pod ip找到要发送的pod所在的宿主机
其实说来也简单,这个主要是因为kube-flannel这个k8s组件维护了一些信息,通过这些信息就能很容易通过pod ip找到pod所在的宿主机ip,这里简单介绍下。
kube-flannel程序会在每个机器上启动,然后负责维护一些信息。
- 每个k8s的新机器加入集群时,kube-flannel会在路由表中加入一条数据,将k8s新节点的ip网段的包转发到flannel.1 设备上。可以通过route -n查看:10.200.1.0 10.200.1.0 255.255.255.0 UG 0 0 0 flannel.1。 其中10.200.1.0 就是新节点的k8s ip 网段,发往这个ip的请求都会交给flannel.1设备处理。
- 新节点启动时,kube-flannel程序也会在本机的ARP表中加入k8s新节点三层ip地址对应的二层mac地址,可以通过ip neigh show dev flannel.1查看,能看到结果是 10.200.1.0 lladdr 76:15:b7:e3:89:bf PERMANENT
- 同时kube-flannel程序也会维护FDB(Forwarding Database)的转发数据库,可以通过他在Node 1上使用“目的VTEP设备”的MAC地址查询到Node2主机的ip,bridge fdb show flannel.1 | grep 76:15:b7:e3:89:bf , 能看到结果是 76:15:b7:e3:89:bf dev flannel.1 dst 172.16.1.3 self permanent,其中的172.16.1.3就是宿主机的内网ip。
也就是说,当新机器加入后,kube-flannel程序会自动维护一些信息,可以通过新机器的k8s地址网段查询到新机器上flannel.1设备的mac地址,再通过这个地址查到新机器的宿主机ip。
vxlan封包
找到宿主机的ip后,应该怎么将网络包发送过去呢,如果只是简单的将网络包的目的ip换成目标宿主机的内网ip,网络包或许能发送到目标机器上,但是丢失了pod ip,就没有意义了,因为网络包到目标机器后也没法到达目的pod的容器,也就无法完成请求处理。
因此,flannel.1在查到目的宿主机的ip后,会进行封包,在原有的网络包不变,保留目标pod ip的情况下进行封包,即在原有的网络包前面加上额外数据,让网络包通过这些额外数据能发送到目的宿主机,这时再通过目的宿主机上的flannel.1网络设备进行处理,还原出原来的网络包,这样就能将请求原封不动的发往pod了。
具体的实现逻辑是:
- 通过之前kube-flannel维护的信息,找出目的宿主机的内网ip,因为只有内网ip才能在机器外的内网环境中流转。
- 通过linux机器使用arp协议维护的信息找到内网ip对应的mac地址,这个数据是linux会自动维护的。
- 有了上述两个数据,flannel.1通过在原有的网络包前面加上 ip头和mac头,将其封装为一个普通的udp包,发往目的宿主机。只不过他设置了一个特殊的VXLAN 头,将里面的 VNI设置为1,这样目标程序可以通过检查这个参数发现这个网络包是封装的包,会将其还原为被封装的tcp包。
- flannel.1将封装后的udp包发出,因为使用的是机器的内网ip,网络包就会发送到目标宿主机上,而封装的dup端口是指定的8472,发往这个指定端口的网络包就会被目标机器上的flannel.1设备处理
问题排查
这一部分的问题排查主要围绕8472这个端口展开,如果使用pod ip访问不通,有几个可用的排查方式:
- 检查不同机器之间udp 8472端口是否放开,如果防火墙没放开端口,就会网络不通
- kube-flannel维护的信息有问题导致网络不通,这个可以先简单的重启kube-flannel组件试下,kubectl rollout restart ds/kube-fannel -n kube-system
- 在请求发出的机器和 目标pod所在的机器上使用tcpdump抓包udp 8472端口,查看是否有请求过来,如果目标机器上有收到数据,那这一阶段就没问题,否则就要进一步查看不同机器之间网络通讯是否有其他问题。tcpdump port 8472 -i any -n -nn -vvv
到达宿主机之后被转发到pod b中
网络包到达宿主机后会被flannel.1网络设备处理,通过解包还原出原来的网络包,然后交给内核进一步路由。这里就和第一步有点像,只是方向相反。过程如下:
- 网络包的目的ip是pod ip,首先过iptable,因为pod ip不会匹配到规则,这一步没有修改。
- 然后走路由表,在目的机器的宿主机上通过route -n查看,会匹配到cni0接口(pod ip是10.200.1.145)。这里解释下,其实kube-flannel在维护网段时,是会给每个机器分配一个k8s的虚拟ip网段,比如10.200.1.0 这个网段就对应一个机器,而10.200.2.0就对应另一台机器,所以其实看pod ip就大概知道他在哪个k8s的node上。而cni0接口是负责管理每个机器上的容器网络的,所以每个机器上route -n查出来的cni0设备处理的网络段都不一样,其实这个网络段代表的正是当前机器的k8s网络段。
3. cni0接口收到网络包后,就会通过他网桥上的veth设备信息找到对应的veth设备,然后将网络包发送给veth设备,请求就到达了目标pod容器中了。
问题排查
这里的问题排查主要和cni0设备有关,主要是查看网络包是否到达cni0,如果没有到达,可能是因为网络配置问题,路由到错误的网络设备了。
- 使用tcpdump抓包cni0设备,查看是否有目标pod ip 的请求进来,tcpdump -i cni0 -n -nn -vvv
- 重启kube-flannel组件,kubectl rollout restart ds/kube-fannel -n kube-system
结语
以上就是全部内容了,通过一个在pod中访问service ip的实例,介绍了各个流程中发生的变化,描述了这个请求是怎么一步一步走到目标容器上的,同时也介绍了排查问题的一些方式,希望大家看完文章之后有所收获。