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

Kubernetes网络四种场景分析

时间:2023-03-14 00:31:26 科技观察

在实际业务场景中,业务组件之间的关系非常复杂,尤其是有了微服务的概念,应用部署的粒度更加精细灵活。为了支持业务应用组件的通信,Kubernetes网络的设计主要致力于解决以下场景:(1)紧耦合容器之间的直接通信;(2)抽象Pod之间的通信;(3))Pod与Service之间的通信;(4)集群外部和内部组件之间的通信。1、容器间通信同一个Pod中的容器(Pod中的容器不会跨主机)共享同一个网络命名空间和同一个Linux协议栈。所以对于网络上的各种操作,就像是在同一台机器上一样,甚至可以使用localhost地址来访问对方的端口。这样做的结果是简单、安全和高效,也可以降低将现有程序从物理机或虚拟机迁移到容器的难度。下图中阴影部分是运行在Node上的Pod实例。容器1和容器2共享一个网络命名空间。由于共享命名空间,它们似乎在同一台机器上运行。他们打开的端口不会有冲突,可以直接和Linux的本地IPC通信。他们只需要使用localhost就可以互相访问。容器到容器的通信2.Pod之间的通信每个Pod都有一个真实的全局IP地址。同一Node中的不同Pod可以直接使用对端Pod的IP地址进行通信,而无需使用其他发现方法。DNS、Consul或etcd等机制。Pod可能运行在同一个Node上,也可能运行在不同的Node上,因此通信也分为两种:同一Node内的Pod之间的通信和不同Node上的Pod之间的通信。1)同一个Node中的Pod之间的通信如图所示。可以看出Pod1和Pod2通过Veth连接到同一个Docker0网桥,它们的IP地址IP1和IP2是从Docker0的网段中自动获取到的,与网桥本身的IP3。另外,在Pod1和Pod2的Linux协议栈上,默认路由是Docker0的地址,也就是说所有非本地网络的数据都会默认发送到Docker0网桥,由Docker0网桥。可以直接沟通。同一Node中的Pod关系2)不同Node上的Pod之间的通信Pod的地址与Docker0在同一网段。我们知道Docker0网段和宿主机网卡是两个完全不同的IP网段,不同Node之间的通信只能通过宿主机的物理网卡进行,所以为了实现Pod之间的通信位于不同节点上的容器,需要想办法通过宿主机的IP地址进行寻址和通信。另一方面,也可以找到这些动态分配并隐藏在Docker0后面的所谓“私有”IP地址。Kubernetes会记录所有正在运行的Pod的IP分配信息,并将这些信息保存在etcd(作为Service的端点)中。这个私有IP信息对于Pod到Pod的通信也非常重要,因为我们的网络模型要求Pod到Pod使用私有IP进行通信。前面说到Kubernetes网络是扁平的,直达Pod的地址,所以这些Pod的IP规划也很重要,不能有冲突。综上所述,为了支持不同Node上的Pod之间的通信,必须满足两个条件:(1)在整个Kubernetes集群中规划Pod分配,不冲突;(2)想办法,将Pod的IP和所在Node的IP关联起来,通过这个关联,Pod之间就可以互相访问了。根据条件1的要求,我们在部署Kubernetes时需要规划好Docker0的IP地址,保证各个Node上Docker0的地址不冲突。我们可以规划好之后手动分配给每个Node,或者制定一个分配规则,让安装的程序自己分配占用。例如,Kubernetes的网络增强开源软件Flannel可以管理资源池的分配。根据条件2的要求,当发送Pod中的数据时,需要有一种机制来知道另一个Pod的IP地址链接到哪个具体的Node上。也就是说,首先找到宿主机对应的Node的IP地址,将数据发送到宿主机的网卡,然后将对应的数据传输到宿主机上具体的Docker0中。一旦数据到达主机Node,该Node内的Docker0就知道如何将数据发送到Pod。具体情况如下图所示。跨节点Pod通信图6中,IP1对应Pod1,IP2对应Pod2。Pod1访问Pod2时,首先需要从源Node的eth0发送数据,找到并到达Node2的eth0。也就是说必须先从IP3下发到IP4,再从IP4下发到IP2。3.Pod与Service的通信为了支持集群的水平扩展和高可用,Kubernetes抽象出了Service的概念。Service是对一组Pod的抽象,它会根据访问策略(LB)来访问这组Pod。当Kubernetes创建服务时,它会为服务分配一个虚拟IP地址。客户端通过访问这个虚拟IP地址来访问服务,服务负责将请求转发给后端Pod。这类似于反向代理,但与普通反向代理有一些区别:首先,它的IP地址是虚拟的,从外部访问它需要一些技巧;其次,它的部署、启动和停止都由Kubernetes自动管理。Service在很多时候只是一个概念,真正实现Service作用的是其背后的kube-proxy服务进程。Kubernetes集群的每个Node上都会运行一个kube-proxy服务进程。这个过程可以看作是Service的透明代理和负载均衡器。它的核心功能是将一个Service的访问请求转发给后端的多个Pod。在实例上。对于每一个TCP类型的KubernetesService,kube-proxy都会在本地Node上建立一个SocketServer负责接收请求,然后平均发送到后端某个Pod的端口。这个过程默认使用RoundRobin负载均衡算法。Kube-proxy和后端Pod的通信方式与标准Pod到Pod通信的方式完全相同。另外,Kubernetes还通过修改Service的service.spec.-sessionAffinity参数的值,提供了会话持久化特性的定向转发。如果设置值为“ClientIP”,则来自同一个ClientIP的所有请求将被转发到Pod上的同一个帖子。另外,Service的ClusterIP和NodePort的概念是kube-proxy通过Iptables和NAT转换来实现的。Kube-proxy在运行过程中动态创建与Service相关的Iptables规则。这些规则实现了将ClusterIP和NodePort请求流量重定向到kube-proxy进程上服务对应的代理端口的功能。由于Iptables机制针对的是本地的kube-proxy端口,如果Pod需要访问Service,则其所在的Node必须运行kube-proxy,kube-proxy组件会运行在每个KubernetesNode上。在Kubernetes集群内部,任何Node上都可以访问Service集群IP和Port,因为每个Node上的kube-proxy都为Service设置了相同的转发规则。综上所述,由于kube-proxy的作用,客户端在Service调用过程中不需要关心后端有多少个Pod,中间过程的通信、负载均衡、故障恢复都是全部透明,如下图所示。Service的负载均衡转发访问Service的请求,无论是使用ClusterIP+TargetPort的方式还是节点机IP+NodePort的方式,都会重定向到kube-proxy监听Service服务代理端口通过节点机器的Iptables规则。Kube-proxy收到服务访问请求后,会如何选择后端Pod呢?首先,目前kube-proxy的负载均衡只支持RoundRobin算法。该算法根据成员列表逐一选择成员。如果一个循环完成,则下一个循环将从头开始,依此类推。基于RoundRobin算法,Kube-proxy的负载均衡器也支持会话保持。如果Service在定义中指定了Session持久化,那么当kube-proxy收到请求时,会检查本地内存中是否存在来自请求IP的affinityState对象。如果对象存在且Session没有超时,kube-proxy会将请求重定向到这个affinityState指向的后端Pod。如果本地没有来自请求IP的affinityState对象,记录请求IP和指向的Endpoint。后续请求会粘附到创建的affinityState对象上,实现维护客户端IP会话的功能。接下来,我们深入分析kube-proxy的实现细节。kube-proxy进程为每个服务创建一个“服务代理对象”。服务代理对象是kube-proxy程序内部的一个数据结构,包括一个Socket-Server和SocketServer端口,用于监听这个服务请求。是随机选择的本地自由端口。此外,kube-proxy内部还建立了一个“负载均衡器组件”,用于实现SocketServer上接收到的连接与后端多个Pod连接之间的负载均衡和会话保持。kube-proxy通过查询和监控APIServer中Service和Endpoint的变化实现其主要功能,包括为新建的Service开启一个本地代理对象(代理对象是kube-proxy程序内部的一种数据结构,一个Service端口它是一个代理对象,包括一个监听服务请求的SocketServer),接收请求,kube-proxy会一一处理变化的Service列表。下面是具体的处理流程:(1)如果Service没有设置集群IP(ClusterIP),什么都不做,否则,获取Service的所有端口定义列表(spec.ports域)(2)读取服务端口定义列表中一个一个的Port信息,根据端口名、Service名和Namespace判断本地是否已经存在相应的服务代理对象,如果不存在则新建一个,如果存在且Service端口已经存在修改后,先删除Iptables中Service的相关规则,关闭服务代理对象,然后走新的流程,即给Service端口分配一个服务代理对象,为Service创建相关的Iptables规则。(3)更新负载均衡器组件中对应Service的转发地址表,为新建的Service确定转发时的会话保持策略。(4)清理被删除的Service。Kube-proxy与APIServer的交互过程4.ExternaltointernalaccessPod是基础资源对象,不仅会被集群内部的pod访问,也会被外部使用。Service是一组具有相同功能的Pod的抽象,是对外提供服务最合适的粒度。由于分配给ClusterIPRange池中Service对象的IP只能在内部访问,其他Pod可以无障碍访问。但是如果这个Service作为前端服务,准备为集群外的客户端提供服务,就需要对外部可见。Kubernetes支持两种类型的外部服务服务类型定义:NodePort和LoadBalancer。(1)NodePort在定义Service时指定spec.type=NodePort,并指定spec.ports.nodePort的值,系统会在Kubernetes集群中的每个Node上的host上开启一个真实的端口号。这样,可以访问Node的客户端就可以通过这个端口号访问内部的Service了。(2)LoadBalancer如果云服务商支持外部负载均衡器,可以通过spec.type=LoadBalancer定义Service,需要指定负载均衡器的IP地址。使用该类型需要指定Service的NodePort和ClusterIP。对该Service的访问请求会通过LoadBalancer转发到后端Pod。负载分配的实现依赖于云服务商提供的LoadBalancer的实现机制。(3)外部访问内部Service的原理我们从集群外部访问集群,最终落在具体的Pod上。NodePort的使用方式是开启kube-proxy,使用Iptables对service的NodePort进行规则设置,将Service的访问权交给kube-proxy,这样kube-proxy就可以访问服务中的service了与内部Pod访问服务相同的方式在后端访问一组Pod。这种模式是使用kube-proxy作为负载均衡器来处理外部对服务的访问,进而对Pod的访问。比较常用的是外置均衡器方式。一个常见的实现是使用一个针对集群中所有节点的外部负载均衡器。当网络流量发送到LoadBalancer地址时,它会识别它是服务的一部分并将其路由到适当的后端Pod。因此,从外部访问内部Pod资源有许多不同的组合。外面没有负载均衡器,直接访问内部Pod。外部没有负载均衡器,通过访问内部负载均衡器直接访问Pod。外部有负载均衡器,内部Pod直接通过外部负载均衡器访问。外面有一个负载均衡器。通过访问内部负载均衡器来访问内部Pod的情况非常少,只有在特殊情况下才需要。在实际的生产项目中,我们需要一个一个访问启动的Pod,并向其发送刷新命令。仅在这种情况下使用此方法。这就需要开发额外的程序来读取Service下的Endpoint列表,并与这些Pod一一通信。通常避免这种类型的通信,例如让每个Pod从集中式数据源中拉取命令而不是向其推送命令。因为每个Pod的启动和关闭本身就是动态的,如果依赖特定的Pod,就相当于绕过了Kubernetes的Service机制。虽然可以实现,但并不理想。第二种情况是NodePort方法。外部应用直接访问Service的NodePort,通过Kube-proxy负载均衡器访问内部Pod。第三种情况是LoadBalancer模式,因为外部的LoadBalancer是具有Kubernetes知识的负载均衡器,它会监听Service的创建,从而知道后端Pod的启停变化,所以它有能力与后端Pod通信。但是这里有一个问题需要注意,就是负载均衡器需要有一种方式可以直接和Pod通信。也就是说,外部负载均衡器需要使用与Pod到Pod相同的通信机制。第四种情况也很少用到,因为需要经过两级负载均衡设备,网络调用随机负载均衡两次后比较难追踪。在实际生产环境中排查问题时,很难跟踪网络数据的流向。(4)外部硬件负载均衡器模式在很多实际生产环境中,由于Kubernetes集群部署在私有云环境中,传统的负载均衡器是不感知Service的。其实我们只需要解决两个问题就可以把它变成一个服务感知的负载均衡器,这也是实际系统中外部访问Kubernetes集群的一种理想模式。通过编写程序监控Service的变化,根据负载均衡器的通信接口,将变化写入负载均衡器。为负载均衡器提供一种直接与Pod通信的方式。下图说明了这个过程。自定义外部负载均衡器访问Service这里提供了一个ServiceAgent来实现对Service变化的感知。Agent可以直接从etcd或通过接口调用APIServer,监听Service和Endpoint的变化,并将变化写入外部硬件负载均衡器。同时,每个Node运行带有路由发现协议的软件,负责将Node上的所有地址通过路由发现协议多播到网络中的其他主机,当然也包括硬件负载均衡器。这样硬件负载均衡器就可以知道每个Pod实例的IP地址在哪个Node上。通过以上两步,建立了一个基于硬件的外部Service-aware负载均衡器。具体案例请参考第5章实战部分。5.小结本章重点介绍了Kubernetes网络的各种场景,包括container-to-container、pod-to-pod四种场景下的不同通信方式、pod到服务和外部到内部。在设计Kubernetes容器平台时,建议根据这些通信方式和具体场景,一一对比选择合适的方案。其中需要注意的是,外部到内部的访问可以通过NodePort、LoadBalancer或者Ingress方式,需要根据具体场景具体分析。NodePort服务是公开服务的最原始方式。它将在所有节点上打开一个特定端口,发送到该端口的任何流量都将转发到该服务。这种方式有很多缺点:每个端口只能有一个服务;默认只能使用30000~32767端口;如果节点IP地址更改,则会导致问题。由于这些原因,不建议在生产中使用这种方法。如果服务可用性不是特别关心的问题,或者如果成本是特别关心的问题,则此选项是合适的。LoadBalancer是公开服务的标准方式,将启动网络负载均衡器,提供一个IP地址,将所有流量转发到服务。如果直接公开服务,这是默认方法。指定端口上的所有流量都将转发到服务,无需过滤、路由等。这意味着几乎可以发送任何类型的流量,例如HTTP、TCP、UDP、Websocket、gRPC或其他。这种方式最大的缺点是每个使用LoadBalancer暴露的服务都会得到自己的IP地址,每个服务都必须使用一个LoadBalancer,代价比较大。Ingress实际上不是服务。相反,它位于多个服务的前面,充当“智能路由器”或集群的入口点。默认的Ingress控制器将启动一个HTTP(s)负载均衡器。这将执行基于路径和基于子域的路由到后端服务。Ingress可能是公开服务最强大的方式,但也可能是最复杂的方式。如果你想在同一个IP地址下公开多个服务,并且这些服务都使用相同的L7协议,Ingress是最有用的。