kube-proxy进程源码分析

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/hahachenchen789/article/details/87869793

kube-proxy是运行在Minion节点上的另外一个重要的守护进程,你可以把它当做一个HAProxy,它充当了kubernetes中service的负载均衡器和服务代理的角色,下面我们分别对其启动过程、关键代码分析及设计总结等发面进行深入分析和讲解。

进程启动过程

kube-proxy进程的入口类源码位置如下:

cmd/kube-proxy/proxy.go

入口main()函数的逻辑如下:

上述代码构造了一个ProxyServer,然后调用它的run 方法启动运行。首先我们看看NewProxyServer的代码:

在上述代码中,ProxyServer绑定本地所有IP(0.0.0.0)对外提供代理服务,而提供健康检查的HTTP Server默认绑定本地的环回IP,说明后者仅用于在本节点上访问,如果需要开发管理系统进行远程管理,则可以设置参数healthz-bind-address为0.0.0.0来达到目的。另外,从代码中看,proxyserver还有一个重要属性可以调整:portrange(对应命令行参数为proxy-port-range),它用来限定proxyserver使用哪些本地端口作为代理端口,默认是随机选择。

proxyserver的run方法流程如下:

1.设置本进程的OOM参数OOMScoreAdj,保证系统OOM时,kube-proxy不会首先被系统删除,这是因为kube-proxy与kubelet进程一样,比节点上的Pod进程更为重要。

扫描二维码关注公众号,回复: 5806257 查看本文章

2.让自己的进程运行在指定的Linux container中,这个container的名字来自proxyServer.resourcecontainer,如上所述,默认为/kube-proxy,比较重要的一点是这个container具备所有设备的访问权。

3.创建serviceconfig与endpointsconfig,它们与之前kubelet中的podconfig的作用和实现机制有点像,分别负责监听和拉取API server上service与service endpoints的信息,并通知给注册到它们上的listener接口进行处理。

4.创建一个round-robin轮询机制的load balancer,它用来实现service的负载均衡转发逻辑,它也是前面创建的endpointsconfig的一个listener。

5.创建一个proxier,它负责建立和维护service的本地代理socket,它也是前面创建的serviceconfig的一个listener。

6.创建一个config.SourceAPI,并启动两个协程,通过kubernetes client来拉取kubernetes API Server上的Service与Endpoint数据,然后分别写入之前定义的serviceconfig与endpointsconfig的channel上,从而触发整个流程的驱动。

7.本地绑定健康检查的HTTP server提供服务。

8.进入proxier的syncLoop方法里,该方法周期性检查iptables是否设置正常,服务的portal是否正常开启,以及清除load balancer上的过期会话。

从启动流程看,kube-proxy进程的参数比较少,它所做的事情也比较单一,没有kubelet进程那么复杂,在下一节我们会深入分析其关键代码。

关键代码分析

从上一节kube-proxy的启动流程来看,它和kubelet有相似的地方,即都会从kubernetes API Server拉取相关的资源数据并在本地节点上完成“深加工”,其拉取资源的做法,第一眼看上去与kubelet相似,但实际上有稍微不同的实现思路,这说明作者另有其人。

由于serviceconfig与endpointsconfig实现机制完全一样,只不过拉取的资源不同,所以我们这里仅对前者做深入分析,首先从serviceConfig结构体开始:

serviceConfig也使用了mux,它是一个多channel的多路合并器,之前kubelet的podconfig也用到了它,下面是serviceconfig的构造函数:

从上述代码看,store是serviceStore的一个实例,它作为config.Mux的merge接口的实现,负责处理config.mux的channel上收到的serviceupdate消息并更新store的内部变量services,后者是一个Map,存放了最新同步到本地的api.Service资源,是service的全量数据,下面是merge方法的逻辑:

serviceStore同时是config.Accessor接口的一个实现,MergedState接口方法返回之前merge最新的service全量数据。

上述方法在哪被用到了呢?就在之前提到的NewServiceConfig方法里:

一个协程监听serviceStore的updates(channel),在收到事件后就调用上述MergedState方法,将当前最新的Service数组通知注册到bcaster上的所有listener进行处理。下面分别给出了watchForUpdates及Broadcaster的Notify方法的源码:

上述逻辑的精巧之处在于,当serviceConfig完成merge调用后,为了及时通知Listener进行处理,就产生一个空事件并写入updates这个channel中,另外监听此channel的协程就及时得到通知,触发listener的回调动作,serviceconfig这里注册的Listener是proxy.Proxier对象。我们以后会继续分析它的回调函数onUpdate是如何使用service数据的。

接下来,我们看看serviceUpdate事件是怎么生成并传递到serviceConfig的channel上的。在kube-proxy启动流程中有调用config.NewSourceAPI函数,其内部生成了一个servicesReflector对象:

其中services这个channel是用来写入serviceUpdate事件的,它是serviceconfig的channel方法所创建并返回的channel,它写入数据会后被一个协程立即转发到ServiceConfig的Channel里,下面这段代码完整揭示了上述逻辑:

servicesReflector中的watcher用来从API Server上拉取Service数据。在config.NewSourceAPI的方法里,启动了一个协程周期性地调用watcher的list和watch方法获取数据,然后转换成ServiceUpdate事件,写入Channel中,下面是关键代码:

在上面的代码中,初始化时资源版本变量resourceVersion为空,于是会执行Service的全量拉取动作(watcher.List),之后Watch资源会开始发生变化(watcher.watch)并将Watch的结果也转换为对应的ServiceUpdate事件并写入channel中。另外当拉取数据的调用发生异常时,resourceVersion恢复为空,导致重新进行全量资源的拉取动作。这种自修复能力的编程设计足以见证谷歌大神们的深厚编程功力。

接下里才进入本节的重点,即服务代理的实现机制分析。首先,我们从代码中的load balance组件说起。下面是kube-proxy中定义的Load Balancer接口:

LoadBalancer有3个接口,其中NextEndpoint方法用于给访问指定service的新客户端请求分配一个可用的Endpoint地址;NewService用来添加一个新服务到负载均衡器上;CleanStaleStickySessions则用来清理过期的Session会话。目前kube-proxy只实现了一个基于round-robin算法的负载均衡器,它就是proxy.LoadBalancerRR组件。

LoadBalancerRR采用了affinityState这个结构体来保存当前客户端的会话信息,然后在affinityPolicy里用一个Map来记录所有活动的客户端会话,这是它实现session亲和性的负载均衡调度的基础。

balancerState用来记录一个service的所有endpoint(数组)、当前所使用的endpoint的index,以及对应的所有活动的客户端对话(affinityPolicy)。其定义如下:

有了上面的认识,再看LoadBalancerRR的构造函数就简单多了,它内部用一个map记录每个服务的balancerState状态,当然初始化时还是空的:

LoadBalancerRR的NewService方法代码很简单,就是在它的services里增加一个记录项,用户端的会话超时时间ttMinutes默认为3小时,下面是相关代码:

我们在前面提到过ServiceConfig同步并监听API Server上的api.Service的数据变化,然后调用Listener(proxy.Proxier是ServiceConfig唯一注册的Listener)的OnUpdate接口完成通知。而上述NewService就是在proxy.Proxier的OnUpdate方法里被调用的,从而实现了service的自动添加到LoadBalancer的机制。

我们再来看LoadBalancerRR的NextEndpoint方法,它实现了经典的round-robin负载均衡算法。NextEndpoint方法首先判断当前服务是否有保持会话(sessionAffinity)的要求,如果有,则看当前请求是否有连接可用:

如果服务无须会话保持、新建会话及会话过期,则采用round-robin算法得到下一个可用的服务端口,如果服务有会话保持需求,则保存当前的会话状态:

接下来我们看看service的endpoint信息是如何添加到LoadBalancerRR上的?答案很简单,类似我们之前分析过的serviceconfig。kube-proxy也设计了一个endpointsConfig来拉取和监听API Server上的服务的Endpoint信息,并调用LoadBalancerRR的Onupdate接口完成通知,在这个方法里,loadBalancerRR完成了服务访问端口的添加和同步逻辑。

我们先来看看api.Endpoints的定义:

一个EndpointAddress与EndpointPort对象可以组成一个服务访问地址,而子EndpointSubset对象里则定义了两个单独的EndpointAddress与EndpointPort数组而不是服务访问地址的一个列表。初看这样的定义可能会觉得奇怪,为什么没有设计一个Endpoint结构?这里的深层次原因在于,service的Endpoint信息来源于两个独立的实体:Pod与Service,前者负责提供IP地址即EndpointAddress,而后者负责提供Port即EndpointPort,由于在一个pod上可以运行多个service,而一个service也通常跨越多个Pod,于是就产生了一个笛卡尔乘积的Endpoint列表,这就是EndpointSubset的设计灵感。

LoadBalancerRR的OnUpdate方法里循环对每个api.Endpoints进行处理,先把它转化为一个Map,Map的Key是EndpointPort的Name属性(代表一个service的访问端口);而value则是hostPortPair的一个数组,hostPortPair其实就是之前缺失的Endpoint结构体,包括一个IP地址与端口属性,即某个服务在一个Pod上的对应访问端口。

下一步,针对portsToEndpoints进行循环处理,对于每个记录,判断是否已经在service中存在,并作出相应的更新或跳过的逻辑,最后删除那些已经不在集合中的端口,完成整个同步逻辑。下面是相关代码:

LoadBalancerRR的代码总体来说还是比较简单的,它主要被kube-proxy中的关键组件proxy.Proxier所使用,后者用到的主要数据结构为proxy.serviceInfo,它定义和保存了一个Service的代理过程中的必要参数和对象,下面是其定义:

其中各个属性解释如下:

portal:用于存放服务的portal地址,即service的ClusterIP地址和端口。

protocol:服务的协议,目前是TCP和UDP。

socket、proxyPort:socket是Proxier在本机上为该服务打开的代理socket:proxyPort则是这个代理socket的监听端口。

timeout:目前只用于UDP的service,表示服务连接的超时时间。

nodePort:该服务定义的NodePort。

loadBalancerStatus:在cloud环境下,如果存在由cloud服务提供者提供的负载均衡器(软件或者硬件)用于kubernetes service的负载均衡,则这里存放那些负载均衡器的IP。

sessionAffinityType:该服务的负载均衡调度是否保持会话。

stickyMaxAgeMinutes:即前面说的session过期时间

deprecatePublicIPs:已过期、废弃的服务的public IP地址。

理解了serviceInfo,我们再来看Proxier的数据结构:

Proxier用一个Map维护了每个服务的serviceInfo信息,同时为了快速查询和检测服务端口是否有冲突,比如定义了两个一样端口的服务,又设计了一个portMap,其Key为服务的端口信息(portMapKey由port和protocol组合而成),value为servicePortName。Proxier的listenIP为proxier监听的本节点IP,它在这个IP上接收请求并做转发代理。由于每个服务的proxySocket在本节点监听的Port端口默认是系统随机分配的,所以使用PortAllocator来分配这个端口。另外,service的portal与NodePort是通过Linux防火墙机制实现的,因此这里引用了iptables的组件来完成相关操作。

要理解proxier中使用Iptables的方式,首先我们要弄明白kubernetes中service访问的一些网络细节。先来看看下图

这是一个外部应用通过Nodeport(TCP: //NodeIP:NodePort)来访问service时的网络流量示意图,访问流量进入节点网卡eth0后,到达iptables的PREROUTING链,通过KUBE-NODEPORT-CONTAINER这个NAT规则被转发到kube-proxy进程上该service对应的proxy端口,然后由kube-proxy进程进程负载均衡并且将流量转发到Service所在container的本地端口。

弄明白proxier中关于iptables的事情之后,我们来研究分析下proxier如何在OnUpdate方法里为每个service建立起对应的Proxy并完成同步工作。首先,在OnUpdate方法里创建一个map来标识当前所有alive的service,key为servicePortName,然后对OnUpdate参数里的service数组进行循环,判断每个service是否需要进行新建、变更或者删除操作,对于需要新建或变更的service,先用PortAllocator获取一个新的未用的本地代理端口,然后调用addServiceOnPort方法创建一个ProxySocket用于实现此服务的代理,接着调用openPortal方法添加iptables里的NAT映射规则,最后调用LoadBalancer的NewService方法把该服务添加到负载均衡器上。OnUpdate方法的最后一段逻辑是处理已经被删除的Service,对于每个要被删除的Service,先删除Iptables中相关的NAT规则,然后关闭对应的proxySocket,最后释放ProxySocket占用的监听端口,并把该端口还给PortAllocator。

从上面分析中,我们看到addServiceOnPort是Proxier的核心方法之一,下面是该方法的源码:

在上述代码中,先创建一个ProxySocket,然后创建一个serviceInfo并添加到Proxier的serviceMap中,最后启动一个协程调用ProxySocket的ProxyLoop方法,使得ProxySocket进入Listen状态,开始接收并转发客户端请求。

kube-proxy中的ProxySocket有两个实现,其中一个是tcpProxySocket,另外一个是udpProxySocket,两者的工作原理都一样,它们的工作流程就是为每个客户端socket请求创建一个岛Service的后端Socket连接,并且打通这两个socket,即把客户端socket发来的数据复制到对应的后端socket,然后把后端socket上服务响应的数据写入客户端socket上。

以tcpProxySocket为例,我们先看看它是如何完成service后端连接创建过程的:


在上述方法里,首先调用loadBalancer.NextEndpoint方法获取服务的下一个可用Endpoint地址,然后调用标准网络库中的方法建立到此地址的连接,如果连接失败,则会重新尝试,间隔时间指数增加(参见endpointDialTimeout的值)。

在后端service的连接建立以后,proxyTCP方法就会启动两个协程,通过调用Go标准库io里的Copy方法把输入流的数据写入输出流,从而完成前后端连接的数据转发功能。此外,proxyTCP方法会阻塞,知道前后端两个连接的数据流都关闭才会返回,下面是源码:

猜你喜欢

转载自blog.csdn.net/hahachenchen789/article/details/87869793