k8s底层网络原理
K8s核心组件原理
1.Master
Master节点负责整个集群的控制和管理,所有的控制命令都是发给它,上面运行着一组关键进程:
kube-apiserver:提供了HTTP REST接口,是k8s所有资源增删改查等操作的唯一入口,也是集群控制的入口。
kube-controller-manager:所有资源的自动化控制中心。当集群状态与期望不同时,kcm会努力让集群恢复期望状态,比如:当一个pod死掉,kcm会努力新建一个pod来恢复对应replicas set期望的状态。
kube-scheduler:负责Pod的调度
2.kubelet原理
每个Node都会启动一个kubelet,主要作用有:
(1)Node管理
注册节点信息;
通过
cAdvisor
监控容器和节点的资源;定期向
Master(实际上是apiserver)
汇报本节点资源消耗情况
(2)Pod管理
所以非通过apiserver方式创建的Pod叫Static Pod,这里我们讨论的都是通过apiserver创建的普通Pod。kubelet通过apiserver监听etcd,所有针对Pod的操作都会被监听到,如果其中有涉及到本节点的Pod,则按照要求进行创建、修改、删除等操作。
(3)容器健康检查
kubelet通过两类探针检查容器的状态:
LivenessProbe:判断一个容器是否健康,如果不健康则会删除这个容器,并按照restartPolicy看是否重启这个容器。实现的方式有ExecAction(在容器内部执行一个命令)、TCPSocketAction(如果端口可以被访问,则健康)、HttpGetAction(如果返回200则健康)。
ReadinessProbe:用于判断容器是否启动完全。如果返回的是失败,则Endpoint Controller会将这个Pod的Endpoint从Service的Endpoint列表中删除。也就是,不会有请求转发给它
kube-proxy的原理
kube-proxy 实际上并不起一个 proxy 的作用,而是 watch 变更并更新 iptables
每个Node上都运行着一个kube-proxy进程,它在本地建立一个SocketServer接收和转发请求,可以看作是Service的透明代理和负载均衡器,负载均衡策略模式是Round Robin
。也可以设置会话保持,策略使用的是“ClientIP”,将同一个ClientIP的请求转发同一个Endpoint上。
Service的Cluster IP和NodePort等概念都是kube-proxy服务通过Iptables的NAT转换实现,Iptables机制针对的是kube-proxy监听的端口,所以每个Node上都要有kube-proxy。
二、需要解决的网络问题
根据以上的一些要求,需要解决的问题
Docker容器和Docker容器之间的网络
Pod与Pod之间的网络
Pod与Service之间的网络
Internet与Service之间的网络
1.容器和容器之间的网络
pod有多个容器,它们之间怎么通信?
pod中每个docker容器和pod在一个网络命名空间内,所以ip和端口等等网络配置,都和pod一样,主要通过一种机制就是,docker的一种网络模式,container
container模式指定新创建的Docker容器和已经存在的一个容器共享一个网络命名空间,而不是和宿主机共享。新创建的Docker容器不会创建自己的网卡,配置自己的 IP,而是和一个指定的容器共享 IP、端口范围等
每个Pod容器有有一个pause容器其有独立的网络命名空间,在Pod内启动Docker容器时候使用 –net=container就可以让当前Docker容器加入到Pod容器拥有的网络命名空间(pause容器)
在k8s网络,docker并没有使用默认网络bridge 通过docker network inspect bridge可以查看bridge网络
上面bridge里"Containers": {}, 原因是目前没有正在运行的container
2.pod与pod之间的网络
pod与pod之间的网络:首先pod自身拥有一个IP地址,不同pod之间直接使用IP地址进行通信即可
同一台node节点上pod和pod通信
疑问:那么不同pod之间,也就是不同网络命名空间之间如何进行通信(现在还是,同一台node节点上)
解决:
使用linux虚拟以太网设备或者说是由两个虚拟接口组成的veth对使不同的网络命名空间链接起来,这些虚拟接口分布在多个网络命名空间上(这里是指多个Pod上)。
简单说veth对就是一个成对的端口,所有从这对端口一端进入的数据包,都将从另一端出来。
为了让多个Pod的网络命名空间链接起来,我们可以让veth对的一端链接到root网络命名空间(宿主机的),另一端链接到Pod的网络命名空间。
嗯,那么继续。
还需要用到一个Linux以太网桥,它是一个虚拟的二层网络设备,目的就是把多个以太网段连接起来,它维护一个转发表,通过查看每个设备mac地址决定转发,还是丢弃数据
pod1-->pod2(同一台node上),pod1通过自身eth0网卡发送数据,eth0连接着veth0,网桥把veth0和veth1组成了一个以太网,然后数据到达veth0之后,网桥通过转发表,发送给veth1,veth1直接把数据传给pod2的eth0。
不同node节点上pod和pod通信
CIDR的介绍:
CIDR(Classless Inter-Domain Routing,无类域间路由选择)它消除了传统的A类、B类和C类地址以及划分子网的概念,因而可以更加有效地分配IPv4的地址空间。它可以将好几个IP网络结合在一起,使用一种无类别的域际路由选择算法,使它们合并成一条路由从而较少路由表中的路由条目减轻Internet路由器的负担。
看图,接着往下捋。
k8s集群中,每个node节点都会被分配一个CIDR块,(把网络前缀都相同的连续地址组成的地址组称为CIDR地址块)用来给node上的pod分配IP地址,另外还需要把pod的ip和所在nodeip进行关联
比如node1上pod1和node2上的pod4进行通信
首先pod1上网卡eth0将数据发送给已经管理到root命名空间的veth0上,被虚拟网桥收到,查看自己转发表之后,并没有pod4的mac地址。
就会把包转发到默认路由,(root命名空间的eth0上,也就是已经到了node节点的往卡上)通过eth0,发送到网络中。
寻址转发后包来到了node2,首先被root命名空间的eth0设备接受,查看目标地址是发往pod4的,交给虚拟网桥路由到veth1,最终传给pod4的eth0上。
3.pod与service之间的网络
pod的ip地址是不持久的,当集群中pod的规模缩减或者pod故障或者node故障重启后,新的pod的ip就可能与之前的不一样的。所以k8s中衍生出来Service来解决这个问题。
Service管理了多个Pods,每个Service有一个虚拟的ip,要访问service管理的Pod上的服务只需要访问你这个虚拟ip就可以了,这个虚拟ip是固定的,当service下的pod规模改变、故障重启、node重启时候,对使用service的用户来说是无感知的,因为他们使用的service的ip没有变。
当数据包到达Service虚拟ip后,数据包会被通过k8s给该servcie自动创建的负载均衡器路由到背后的pod容器。
在k8s里,iptables规则是由kube-proxy配置,kube-proxy监视APIserver的更改,因为集群中所有service(iptables)更改都会发送到APIserver上,所以每台kubelet-proxy监视APIserver,当对service或pod虚拟IP进行修改时,kube-proxy就会在本地更新,以便正确发送给后端pod
pod到service包的流转:
数据包从pod1所在eth0离开,通过veth对的另一端veth0传给网桥cbr0,网桥找不到service的ip对应的mac,交给了默认路由,到达了root命名空间的eth0
root命名空间的eth0接受数据包之前会经过iptables进行过滤,iptables接受数据包后使用kube-proxy在node上配置的规则响应service,然后数据包的目的ip重写为service后端指定的pod的ip了
service到pod包的流转
收到包的pod会回应数据包到源pod,源ip是发送方ip,目标IP是接收方,数据包进行回复时经过iptables,iptables使用内核机制conntrack记住它之前做的选择,又将数据包源ip重新为service的ip,目标ip不变,然后原路返回至pod1的eth0
4.Internet与service之间的网络
将k8s集群服务暴露给互联网上用户使用,有两个问题;(1)从k8s的service访问Internet,以及(2)从Internet访问k8s的service.
根据参考文章,通过Internet网关,node可以将流量路由到Internet,但是pod具有自己的IP地址,Internet王冠上的NAT转换并不适用。参考方案:就是node主机通过iptables的nat来解决
node到internet包的流转
数据包源自pod1网络命名空间,通过veth对连接到root网络命名空间,紧接着,转发表里没有IP对应的mac,会发送到默认路由,到达root网络命名空间的eth0
那么在到达root网络明明空间之前,iptables会修改数据包,现在数据包源ip是pod1的,继续传输会被Internet网关拒绝掉,因为网关NAT仅转发node的ip,解决方案:使iptables执行源NAT更改数据包源ip,让数据包看起来是来自于node而不是pod
iptables修改完源ip之后,数据包离开node,根据转发规则发给Internet网关,Internet网关执行另一个NAT,内网ip转为公网ip,在Internet上传输。
数据包回应时,也是按照:Internet网关需要将公网IP转换为私有ip,到达目标node节点,再通过iptables修改目标ip并且最终传送到pod的eth0虚拟网桥。
Internet到k8s的流量
让Internet流量进入k8s集群,这特定于配置的网络,可以在网络堆栈的不同层来实现:
(1) NodePort
(2)Service LoadBalancer
(3)Ingress控制器。
这里只介绍第三种,如果想看详细的,文章开始有一个链接
第七层流量入口:Ingress Controller
通过一个公开的ip地址来公开多个服务,第7层网络流量入口是在网络堆栈的HTTP / HTTPS协议范围内运行,并建立在service之上。
工作:客户端现针对www.1234.com执行dns解析,DNS服务器返回ingress控制器的ip,客户端拿到ip后,向ingress控制器发送http的get请求,将域名加在host头部发送。控制器接收到请求后,从host头部就知道了该访问哪一个服务,通过与该service关联的endpoint对象查询podIP地址,将请求进行转发
第7层负载均衡器的一个好处是它们具有HTTP感知能力,因此它们了解URL和路径。 这允许您按URL路径细分服务流量。 它们通常还在HTTP请求的X-Forwarded-For标头中提供原始客户端的IP地址。
3 Docker0网桥和flannel网络方案
在介绍Ingress和service这两个组件之前,我们先简单了解一下k8s节点之间的底层网络原理及典型的flannel-VXLAN方案。后面的章节,默认在节点之间的传输,都会有docker0网桥和flannel插件的功劳。(有资料提到K8S采用cni0网桥替代了docker0网桥,两者的原理是一样的,我搭建的环境里只有docker0网桥,所以我们按docker0来分析)
大家注意到没有,每个pod具备不同的Ip(这里指k8s集群内可访问的虚拟ip),不同node下的pod甚至在不同的网段。那么问题来了,集群内不同IP、不同网段的节点是怎么实现通讯的呢?这样归功于docker0和flannel.1这两个虚拟网络设备,我们先ifconfig查看一下:
部署flannel和docker后,会在宿主机上创建上述两个网络设备。接下来我们通过一个示意图来了解这两个设备的工作: • K8s在每个宿主机(node)上创建了cni0网桥(这篇文档对应的集群环境采用的是docker0网桥,原理一样):容器的网关,实际指向的是这个网桥。 • Flannel则在每个宿主机上创建了一个VTEP(虚拟隧道端点)设备flannel.1。
现在我们来分析下docker0和flannel.1是怎么实现跨主机通讯的(由node1的business-manager:172.30.76.7发往node2的data-product:172.30.9.2):
上图是node1的路由表:第2行表示凡是发往172.30.0.0/16网段的包均交给node1-flannel.1设备处理;第3行表示凡是发往172.30.76.0/8网段的包均交给node1-docker0网桥处理。
于是business- manager的请求,首先到达node1-docker0网桥,目的地址是172.30.9.2,只能匹配第2条规则,请求被交给node1-flannel.1设备。
node1-flannel.1又如何处理呢?请看下图,展示的是flannel.1的ARP表:
node1-flannel.1的ARP表记录的是ip和对应节点上的flannel.1设备mac的映射。于是发往172.30.9.2匹配到了上述第1条规则,需要发往mac地址为96:8f:2d:49:c5:31的设备。
这时候node1-flannel.1设备又扮演一个网桥的角色,上图为node1上查询出的桥接规则,96:8f:2d:49:c5:31的目的ip对应于192.168.0.22,这正是我们这个例子里node2的宿主机Ip。于是这个请求被转发给了node2。
不难理解,node2也有一个像第1步那样的路由表,于是来自node1-business-manager:172.30.76.7的请求最终经node2-docker0送达node2-data-product:172.30.9.2。
• 随着node和pod加入和退出集群,flannel进程会从ETCD感知相应的变化,并及时更新上面的规则。 • 现在我们已实现通过ip访问pod,但pod的ip随着k8s调度会变化,不可能隔三差五的去人工更新每个ip配置吧,这就需要service这个组件了,请看下一章。
4 Service和DNS
4.1 service
pod的ip不是固定的,而且同一服务的多个pod需要有负载均衡,这正是创建service的目的。 Service是由kube-proxy组件和iptables来共同实现的。 分析service原理前,大家可以先带上这个问题:service的ip为什么ping不通? OK,我们现在直接上图,随便一个node的iptables(内容比较丰富,我随便截了几段,下文会挑几个重要的规则展开分析):
现在可以回答service ip ping不通的问题了,因为service不是真实存在的(没有挂接具体的网络设备),而是由上图这些iptables规则组成的一个虚拟的服务。 • Iptables是linux内核提供给用户的可配置的网络层防火墙规则,内核在解析网络层ip数据包时,会加入相应的检查点,匹配iptables定义的规则。
• 我们还是看第3章的例子,business-manager要访问data-product,于是往service-data-product的ip和port(10.254.116.224:50051)发送请求。每个service对象被创建时,k8s均会分配一个集群内唯一的ip给它,并且该ip伴随service的生命周期不会变化,这就解决了本节开篇的Pod ip不固定的问题。
• KUBE-SERVICES:Iptables表里存在上面这条规则,表示发往10.254.116.224:50051的数据包,跳转到KUBE-SVC-45TXGSBX3LGQQRTB规则。
• KUBE-SVC-xxx:这条规则,实际上是一条规则链,data-product我建了3个pod,所以这条规则链对应的正是这3个pod。这里是service负载均衡的关键实现,第1条规则表示采用随机模式,有1/3(33%)的概率跳转到KUBE-SEP-DGXT5Z3WOYVLBGRM;第2条规则的概率是1/2(50%);第3条则直接跳转。这里有个需要注意的地方,iptables是顺序往下匹配的,所以多节点随机算法,概率是递增的,以data-product为例,我配置了3个Pod,就有3条规则,第1条被选中的概率为1/3,第2条则为1/2,最后1条没得挑了,概率配置为1或直接跳转。
• KUBE-SEP-xxx:假设随机到第2条KUBE-SEP-P6GCAAVN4MLBXK7I,这里又是两条规则。第1条是给转发的数据包加标签Mark,目的是在集群多入口的场景下,保证数据包从哪进来的就从哪个node返回给客户端,详细原理就不展开说了。同时这里还涉及到一个技术点,经过service转发的数据包,pod只能追查到转发的service所在的Node,如果有场景需要Pod明确知道外部client的源Ip,可以借用service的spec.externalTrafficPolicy=local字段实现。 • KUBE-SEP-xxx:第2条规则就很简单了,数据包转发给172.30.76.5:50051,这里已经拿到pod的ip和port,可以通过第3章的docker0和flannel.1网络进行通信了。
上面是基于iptables的service方案,存在一个风险,当pod数量很大,几百、几千时,遍历iptables将会是性能瓶颈。IPVS虚拟网卡技术在大量级的pod场景下表现比iptables优秀(运维的同事反馈11版本的k8s,官方已默认采用IPVS)。这不属于本文档的目的,不展开说。 本节开头我们提到service是由kube-proxy和iptables共同实现的,所以Kube-proxy所扮演的角色就不难想象了,kube-proxy负责感知集群的变化,及时更新service的规则。
最后,我们还面临着一个小问题,上面的过程是基于服务的VIP的访问服务的,通过服务名的方式访问又是怎么实现的呢,请看下一节:DNS
4.2 DNS
本来写这个文档没想到要有DNS这一章节的,但集群搭建好之后发现通过服务名无法访问服务,通过VIP却可以,才想起来集群还需要额外搭个DNS组件。
DNS组件是跑在kube-system命名空间下的一个pod,监听着集群ip:10.254.0.2:53。通过这个Ip:port(创建kubelet时指定DNS的ip)即可获取到集群内部的DNS解析服务。 现在我们随便进入一个pod里,可以看到dns的信息已被k8s写入。同时我们ping一个service:
当然是ping不通的,但vip已经被解析出来了。 Kubenetes可以为pod提供稳定的DNS名字,且这个名字可通过pod名和service名拼接出来,以上面的data-product为例,该服务的完整域名是[服务名].[命名空间].svc.[集群名称]。相应的,每个pod也有类似规则的域名。
5 外部访问集群
5.1 外部访问service
Service代理的是集群内部的ip和端口,出了集群这个ip:port就没什么意义了。所以如何在集群外部访问到service呢?
方式一:配置service的type=NodePort,此方式下k8s会给service做端口映射。这种方式是最常用的,我们DEV环境下很多service做了端口映射,可以通过宿主机Ip加映射出去的端口号直接访问服务。这种方式的原理简单,kube-proxy只需要在iptables里增加一条规则,将外部端口的包导向第4章的service规则去处理即可。(下一节要讲的ingress,正是这种方式的一种更细致的实现) 方式二:type=LoadBalancer,适用于公有云提供的K8s环境,此时K8s使用一个叫作CloudProvider的转接层与公有云的API交互,并由公有云API来实现负载均衡。 方式三:type=ExternalName,这个方式的用法我还没搞清楚。
按前面章节的套路,这里我们依然会面临一个小问题,把外部需要访问的服务大量的通过端口映射方式暴露出去,势必给端口的管理带来麻烦。所以,接下来我们看看ingress是怎么作为集群的入口,帮我们管理后端服务的。
5.2 ingress
4.2章节,在集群内部我们实现了通过域名(服务名)获取具体的服务vip,从而免去了管理Vip烦恼。那么从外部访问集群的服务,又如何实现通过域名的方式呢?后端的服务有很多,我们也需要一个全局的负载均衡器来管理后面服务。这就是ingress。
• 使用ingress,我们除了要创建ingress对象以外,还需要安装一个ingress-controller,这里我们选择最常用的nginx-ingress-controller。如上所示,安装之后,会增加一个ingress-nginx命名空间,运行着nginx-ingress-controller容器。
• 当Ingress对象被创建时,nginx-ingress-controller会在这个nginx容器内部生成一个配置文件/etc/nginx/nginx.conf(内容比较丰富,上图我截了一小段,可以看到data-product.default的主要配置),并用这个文件启动nginx服务。当ingress对象被更新时,nginx-ingress-controller会实现nginx服务的动态更新。
• Nginx服务的功能:随便找一个ingress文件查看,rules字段包含一组域名、路径、后端服务名、服务端口的映射,这就是个反向代理的配置文件。当前我们用nginx做反向代理,以及将请求负载给后端的service。加上证书,nginx还可以解析https,给后端依然是http明文通信
现在又面临一个小问题了,这个nginx服务居然运行在容器里,参考5.1章节,这个服务外部还是访问不了啊?所以安装nginx-ingress-controller时还需要创建一个服务,将这个pod里的nginx服务监听的80和443端口暴露出去。
上面这个服务,正是ingress-nginx的SVC,它向外暴露的端口(NodePort)是30799和31522,对应的endpoints正是nginx容器里的nginx服务监听的两个端口80和433。这个ingress-service加上ingress-nginx容器,共同组成了ingress。所以广义上,ingress提供的是集群入口服务,是一个虚拟的概念。不考虑具体的功能的话,business层以NodePort方式运作时,就可以看作business层就是data层的ingress。
现在我们可以用business-manager.default:30799/api/v1/product/list来发起请求。
六-补充说明 集群内通信
Master-Node 之间的通信可以分为如下两类:
Cluster to Master
得益于这些措施,默认情况下,从集群(节点以及节点上运行的 Pod)访问 master 的连接是安全的,因此,可以通过不受信的网络或公网连接 Kubernetes 集群
Master to Cluster
从 master(apiserver)到Cluster存在着两条主要的通信路径:
apiserver 访问集群中每个节点上的 kubelet 进程
使用 apiserver 的 proxy 功能,从 apiserver 访问集群中的任意节点、Pod、Service
apiserver 在如下情况下访问 kubelet:
抓取 Pod 的日志
通过
kubectl exec -it
指令(或 kuboard 的终端界面)获得容器的命令行终端提供
kubectl port-forward
功能
这些连接的访问端点是 kubelet 的 HTTPS 端口。默认情况下,apiserver 不校验 kubelet 的 HTTPS 证书,这种情况下,连接可能会收到 man-in-the-middle 攻击,因此该连接如果在不受信网络或者公网上运行时,是 不安全 的。
如果要校验 kubelet 的 HTTPS 证书,可以通过 --kubelet-certificate-authority
参数为 apiserver 提供校验 kubelet 证书的根证书。
从 apiserver 到 节点/Pod/Service 的连接使用的是 HTTP 连接,没有进行身份认证,也没有进行加密传输。您也可以通过增加 https
作为 节点/Pod/Service 请求 URL 的前缀,但是 HTTPS 证书并不会被校验,也无需客户端身份认证,因此该连接是无法保证一致性的。目前,此类连接如果运行在非受信网络或公网上时,是 不安全 的
附 扩展实战
原理分析的再多再深入,最终还是希望能够为我们的工作提供一些帮助。所以下面的篇幅我记录了在分析过程中看到或是想到的可能有助于我们实际工作的思路,限于精力有限,这些思路我暂时还没有完整验证过,同学们有兴趣的话可以参与进来。
附A 用service实现DB的管理
当前DB的ip和端口是配置在每个应用的configmap里的,如果出现DB切换、迁移等因素导致IP或端口变更,我们需要挨个去修改每个应用的config。 K8s支持指定service的endpoints为一个特定的点,比如可以指定为DB的IP和端口。这样我们可以创建两个service:service-DB-read,和service-DB-write。由service来管理DB的IP和PORT,变更只需要修改这两个service的config即可。由4.1章节的分析我们知道,应用访问上述两个service,数据包会被转发给endpoints也就是真正的db。
请见下图,Endpoints指向集群外部数据库的service-mysql:
应用层通过访问service-mysql,流量最终会到达endpoints也就是集群外部的真实数据库的ip:port。细心的同学应该能想到,这玩意可以用于简单的数据库负载均衡,比如有多个读库的情况下,我们只需要让service-mysql的endpoints指向这几个读库,流量即能被负载均衡到各个库。
附B 用NetworkPolicy实现访问权限隔离
以DB为例,当前集群的DB对所有pod开放,那有没有办法限制访问权限呢,比如只允许data层访问。回想第4.1章节service的本质是iptables规则,那么就有可能通过iptables实现更细致的规则,比如DB的访问权限管理。这就是k8s的NetworkPolicy,支持以pod的标签的形式制定相应的iptables规则。目前flannel网络插件不支持NetworkPolicy,flannel + Calico插件可以实现。
附C 用secret对象管理账户密码
附D kubectl logs [pod name]的日志在哪?
每个pod的日志在宿主机的/var/log/pods/下可以找到,这里的日志文件实际上是链接到了docker管理的每个容器的日志文件上
这也就能解释,当一个Pod里有多个容器时,为什么kubectl logs [pod name]会报错。因为日志文件实际是按容器为单位管理的。下面举个更明显的例子:
参考文章:
链接:https://www.jianshu.com/p/e9cf221fa6ab
Last updated