kubelet
kubelet组件是Kubernetes集群工作节点上最重要的组件进程,它负责管理和维护在这台主机上运行着的所有容器。本质上,它的工作可以归结为使得pod的运行状态(status)与它的期望值(spec)一致。目前,kubelet支持docker和rkt两种容器;而社区也在尝试使用C/S架构来支持更多container runtime与Kubernetes的结合。在很多类似的项目中,这种类型的组件一般会被命名为agent,但是kubelet这个名称则明显脱胎于Borg系统的borglet,两者的角色也是类似的。接下来,还是先kubelet的启动过程
- kubelet的启动过程
(1) kubelet需要启动的主要进程是KubeletServer,它所需加载的重要属性包括kubelet本身的属性、接入的runtime容器(目前支持docker和rkt)所需的基础信息以及定义kubelet与整个集群进行交互所需的信息。在v1.2.0版本的代码中,一部分信息是KubeletServer结构体的属性,而余下的部分则存放在KubeletConfig中。社区会考虑在代码重构时将两部分信息均合并到KubeletServer里。(2) 进行如下一系列的初始化工作。
- 选取APIServerList的第一个APIServer,创建一个APIServer的客户端。Kubernetes的v1.2.0还不支持kubelet自动地对接APIServer集群的负载均衡,故当APIServerList的长度>1时,kubelet只会选择一个APIServer发送API请求。而当APIServerList的长度<1时,kubelet会报错。
- 如果上一步骤执行成功,则再创建一个APIServer的客户端用于向APIServer发送event对象。
- 初始化cloud provider。当然,如果集群的kubelet组件并没有运行在cloud provider上,该步骤将跳过。
- 创建并启动cAdvisor服务进程,返回一个cAdvisor的http客户端,IP和Port分别是localhost和CAdvisorPort的值。如果CAdvisorPort设置为0,将不启用cadvisor。
- 创建ContainerManager,为Docker daemon、kubelet等进程创建cgroups,并确保它们运行时使用的资源在限额之内。
- 对kubelet进程应用OOMScoreAdj值,即向/proc/self/oom_score_adj文件中写入OOMScoreAdj的值(默认值为-999)。OOMScoreAdj是用于描述在该进程发生内存溢出时被强行终止的可能性,分数越高,进程越有可能被杀死;其合法范围是[-1000, 1000]。换句话说,这里希望kubelet是最不容易被杀死的进程(之一)。
- 配置kubelet支持的pod配置方式,包括文件、url以及APIServer,支持多种方式一起使用
(3) 初始化工作完成后,实例化一个真正的kubelet进程。重点值得关注的有以下几点。
- 创建工作节点本地的service和node的cache,并且使用list/watch机制持续对其进行更新。
- 创建DiskSpaceManager,用以与cadvisor配合进行工作节点的磁盘管理,这与kubelet是否接受新的pod在该工作节点上运行有密切关系。
- 创建ContainerRefManager,用以记录每个container及其*对应的引用的映射关系,主要用于在pod更新或者删除时进行事件的记录。
- 创建VolumeManager,用以记录每个pod及其挂载的volume的映射关系。
- 创建OOMWatcher,用以从cadvisor中获取系统的内存溢出(Out Of Memory, OOM)事件,并对其进行记录。
- 初始化kubelet网络插件,可以指定传入一个文件夹中的plugin作为kubelet的网络插件。
- 创建LivenessManager,用以维护容器及其对应的probe结果的映射关系,用以进行pod的健康检查。
- 创建podCache来缓存pod的本地状态。
- 创建PodManager,用以存储和管理对pod的访问。值得注意的是,kubelet支持3种更新pod的方式,其中通过文件和url创建的pod是不能自动被APIServer感知的,称其为static pod。为了监控这些pod的状态,kubelet会为每个static pod在相同的namespace下创建一个同名的mirror pod,用以反应static pod的更新状态。
- 配置hairpin NAT。
- 创建container runtime,支持docker和rkt。
- 创建PLEG(pod lifecycle event generator),专门进行pod变化的监控,避免了并发的pod worker来进行轮询工作。
- 创建镜像垃圾回收对象containerGC。
- 创建imageManager管理容器镜像的生命周期,处理镜像的垃圾回收工作。
- 创建statusManager,用以向APIServer同步pod实际状态的更新。
- 创建probeManager,用作pod健康检查的探针。
- 初始化volume插件。
- 创建RuntimeCache,用以缓存pod列表。
- 创建reasonCache,用以缓存每个容器对应的最新的失败原因信息。
- 创建podWorker。每个pod将对应一个podWorker用以同步pod状态信息。
kubelet启动完成后通过事件收集器向APIServer发送一个kubelet已经启动的event,表明集群新加入了一个新的工作节点,kubelet将这一过程称为BirthCry,即“出生的啼哭”。并且开始进行容器和镜像的垃圾回收,对应的时间间隔分别为1分钟和5分钟。
(4) 根据Runonce的值选择运行仅一次kubelet进程或在后台持续运行kubelet进程,如果Runonce为true,则kubelet根据容器配置文件的内容创建pod后就退出;否则,将以goroutine的方式持续运行kubelet。另外,默认启用kubelet Server的功能,它将根据admin的配置创建HTTP Server或HTTPS Server,监听10250端口。同时,创建一个HTTP Server监听10255端口,用于heapster向kubelet收集统计信息。
在Kubernetes中真正负责容器操作的只有kubelet组件,它担负着一个工作节点上所有容器的司令官的角色。虽然整个过程看起来有些复杂与繁琐,但是本质上kubelet对工作节点上的两种更新做出相应的行为反馈,其一为pod spec的更新,其二为容器实际运行状态的更新。所以,kubelet(及上述提及的所有module)都是为了获取或同步两种更新所设计的。
kubelet与cAdvisor的交互
kubelet使用我们第4章介绍过的cAdvisor(Container Advisor)作为抓取Docker容器和宿主机资源信息的工具。运行在宿主机上的cAdvisor后台服务通过暴露一个TCP端口对外提供一套REST API,客户端可以发起形如以下的HTTP请求。
http://<hostname>:<port>/api/<version>/<request>
cAdvisor主要负责收集工作节点上的容器信息及宿主机信息,下面将一一进行介绍。
● 容器信息
获取容器信息的URL形如:/api/{api version}/containers/。绝对容器名(absolute container name)与URL的对应关系如表所示。
绝对容器名/下包含整个宿主机上所有容器(包括Docker容器)的资源信息,而绝对容器名/docker下才包含所有Docker容器的资源信息。如果想获取特定Docker容器的资源信息,绝对容器名字段需要填入/docker/{container ID}。
● 宿主机信息
类似地,还可以访问URL:/api/{api version}/machine来获取宿主机的资源信息。返回的JSON结果则包括这台机器的CPU核心数、内存总容量、磁盘容量信息等。
- kubelet垃圾回收机制
相信曾经尝试过较大规模运行Docker容器的用户一定感受过大量垃圾容器和镜像给用户和系统带来的资源浪费和操作延时。所以作为一个容器云框架,能够保证容器运行环境的干净和简单是提高容器管理性能的不二法宝,在这一点上Kubernetes和Mesos都有着很先进的设计。kubelet垃圾回收机制主要涵盖两个方面:容器回收和镜像回收。目前支持的两种容器runtime(docker及rkt)分别实现了各自的细节逻辑。此处以docker为例进行说明。
● Docker容器的垃圾回收
我们知道,停止运行的容器仍会占据系统的磁盘空间且Docker daemon没有容器垃圾回收机制,如果系统一直保留已经停止运行的容器实例,久而久之磁盘空间就会被消耗殆尽。因此定期对系统中不再使用的容器进行回收的工作责无旁贷地落到了运行在工作节点上且直接与容器打交道的kubelet肩上。Docker容器回收策略主要涉及3个因素,
(1) 获取所有可以被kubelet垃圾回收的容器。调用一次Docker客户端API获取工作节点上所有由kubelet创建的容器信息,形成一个容器列表,这些容器可能处于不同的生命周期状态,包括正在运行的和已经停止运行的。注意,需要通过命名规则来判断容器是否由kubelet创建并维护,如果忽略了这一点可能会因为擅自删除某些容器而惹恼用户。
遍历该列表,过滤出所有可回收的容器。所谓可回收的容器必须同时满足两个条件:已经停止运行;创建时间距离现在达到预设的报废时间MinAge。
过滤出所有符合条件的可回收容器后,kubelet会将这些容器以所属的pod及容器名对为单位放到一个集合(evictUnits)中,并根据pod创建时间的早晚进行排序,创建时间越早的pod对应的容器越排在前面。注意,在创建evictUnits的过程中,需要解析容器及其对应的pod名字,解析失败的容器称为unidentifiedContainers。
(2) 根据垃圾回收策略回收镜像。
首先,删除unidentifiedContainers以及被删除的pod对应的容器。这部分容器的删除不需要考虑回收策略中MaxPerPodContainer和MaxContainers。如果podMaxPerpodContainer的值大于等于0,则遍历evictUnits中所有的pod,如果某个pod内的可回收容器数量大于MaxPerpodContainer,则删除多出的容器及其日志存储目录,其中创建时间较早的容器优先被删除。如果MaxContainers的值大于等于0且evictUnits中的容器总数也大于MaxContainers,则执行以下两步。
❏ 先逐一删除pod中的容器,直到每个pod内的可回收容器数=MaxContainers/evictUnits的大小,如果删除之后某个pod内的容器数<1,则置为1,目的是为每个pod尽量至少保留一个可回收容器。❏ 如果此时可回收容器的总数还是大于MaxContainers,则按创建时间的先后顺序删除容器,较早创建的容器优先被删除。
● Docker镜像的垃圾回收
与容器的垃圾回收机制的目的一样,Docker镜像垃圾回收机制主要是为了防止长时间未使用的镜像占据大量的磁盘空间,而且过多的镜像还会拖慢很多Docker请求处理的速度(因为要load的graph太大了)。Docker镜像回收策略主要涉及3个因素
在Kubernetes中,Docker镜像的垃圾回收步骤如下所示。
(1) 首先,调用cadvisor客户端API获取工作节点的文件系统信息,包括文件系统所在磁盘设备、挂载点、磁盘空间总容量(capacity)、磁盘空间使用量(usage)和等。如果capacity为0,返回错误,并记录下InvalidDiskCapacity的事件。
(2) 如果磁盘空间使用率百分比(usage*100/capacity)大于或等于预设的使用率上限HighThresholdPercent,则触发镜像的垃圾回收服务来释放磁盘空间,否则本轮检测结束,不进行任何回收工作。至于具体回收多少磁盘空间,使用以下公式计算:
amountToFree := usage - (int64(im.policy.LowThresholdPercent) * capacity / 100)
其实就是释放超出LowThresholdPercent的那部分磁盘空间。那么kubelet会选择删除哪些镜像来释放磁盘空间呢?
首先,获取镜像信息。参考当时的时间(Time.Now())kubelet会检调用Docker客户端查询工作节点上所有的Docker镜像和容器,获取每个Docker镜像是否正被容器使用、占用的磁盘空间大小等信息,生成一个系统当前存在的镜像列表imageRecords,该列表中记录着每个镜像的最早被检测到的时间、最后使用时间(如果正被使用则使用当前时间值)和镜像大小;删除imageRecords中不存在的镜像的记录。
然后,根据镜像最后使用时间的大小进行排序,时间戳值越小即最后使用时间越早的镜像越排在前面。如果最后使用时间相同,则按照最早被检测到的时间排序,时间戳越小排在越前面。最后,删除镜像。遍历imageRecords中的所有镜像,如果该镜像的最后使用时间小于执行第一步时的时间戳,且该镜像的存在时间大于MinAge,则删除该镜像,并且将删除Docker镜像计入释放的磁盘空间值,如果释放的空间总量大于等于前面公式计算得到的amountToFree值,则本轮镜像回收工作结束。否则,则记录一条失败事件,说明释放的空间未达到预期。
这些宿主机信息包括以下几点。
❏ 工作节点IP地址。
❏ 工作节点的机器信息,包括内核版本、操作系统版本、docker版本、kubelet监听的端口、工作节点上现有的容器镜像。
❏ 工作节点的磁盘使用情况——即是否有out of disk事件。
❏ 工作节点是否Ready。在node对象的状态字段更新工作节点状态,并且更新时间戳,则node controller就可以凭这些信息是否及时来判定一个工作节点是否健康。
❏ 工作节点是否可以被调度pod。
最后,kubelet再次调用APIServer API将上述更新持久化到etcd里。
kube-proxy
Kubernetes基于service、endpoint等概念为用户提供了一种服务发现和反向代理服务,而kube-proxy正是这种服务的底层实现机制。kube-proxy支持TCP和UDP连接转发,默认情况下基于Round Robin算法将客户端流量转发到与service对应的一组后端pod。在服务发现的实现上,Kube-prxoy使用etcd的watch机制,监控集群中service和endpoint对象数据的动态变化,并且维护一个从service到endpoint的映射关系,从而保证了后端pod的IP变化不会对访问者造成影响。另外kube-proxy还支持session affinity(即会话保持或粘滞会话)。
kube-proxy主要有两种工作模式:userspace和iptables, v1.2.0版本中默认使用iptables模式,所以除非有特殊说明,否则本书均以iptables模式为主进行讲解。现在假设要创建以下这个service对象,该service暴露80端口对外提供服务,代理所有name=service-nginx的pod。
{
"kind": "Service",
"apiVersion": "v1",
"metadata": {
"name": "nginx-service",
"labels": {
"name": "service-nginx"
}
},
"spec": {
"selector": {
"name": "service-nginx"
},
"ports": [{
"port": 80
}]
}
}
创建成功后使用查看系统的service信息,可以看到新创建的service实例的cluster IP为10.0.210.167,它代理了两个pod(对应的pod ip为10.1.99.5:80和10.1.99.6:80)。
$ kubectl get services
NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes 10.0.0.1 <none> 443/TCP 31d
nginx-service 10.0.210.167 <none> 80/TCP 9s
$ kubectl get endpoints
NAME ENDPOINTS AGE
kubernetes 10.211.55.15:6443 32d
nginx-service 10.1.99.5:80,10.1.99.6:80 2h
10.0.210.167是系统从预留的service-cluster-ip-range地址段中随机选出来的,以上service和endpoint配置保证当访问10.0.210.167:80时,就能够访问到被这个service代理的后端pod。这是怎么实现的呢?实际上,在iptables工作模式下的,kube-proxy将根据Kubernetes集群中的service与pod的配置在工作节点上维护如下iptables设置,此处我们省略与Kubernetes无关的链(Chain)和规则(Rule):
1. kube-proxy的启动过程
由于职责单一,kube-proxy相比前面介绍过的组件都要简单一些,大体包括如下几个步骤。
- 新建一个ProxyServer,包括两个功能性的结构的创建,负责流量转发的proxier和负责负载均衡的endpointsHandler。这里最有趣的一点在于proxy模式的选择,默认使用iptables。iptables模式的kube-proxy要求系统本身拥有br_netfilter,且net/bridge/bridge-nf-call-iptables的值为1,以及用户为kube-proxy传入的IPTablesMasqueradeBit参数,当然,对iptables版本也都有所要求;如不满足,将退化成userspace模式。iptables模式下的proxier和endpointsHandler都由proxier担任。而事实上,该模式下的kube-proxy不再负责具体的endpoint选取工作,只进行iptables规则的更新。
userspace模式的kube-proxy会创建一个基于round-robin机制的负载均衡器作为endpoints-Handler,在proxier的创建过程中还会初始化iptables,并将旧的iptables rules清除。同步service对应的iptables规则,不同工作模式下的kube-proxy会有不同的具体工作逻辑。userspace模式的kube-proxy为每个service随机创建一个本地端口,并且对其进行监听;所有访问service的流量会经过iptables转发到本地端口,再由kube-proxy负责转发给具体的pod。iptables模式的kube-proxy则只在出现了service或者endpoint变更时进行iptables规则的创建与保存。
同步service对应的iptables规则,不同工作模式下的kube-proxy会有不同的具体工作逻辑。userspace模式的kube-proxy为每个service随机创建一个本地端口,并且对其进行监听;所有访问service的流量会经过iptables转发到本地端口,再由kube-proxy负责转发给具体的pod。iptables模式的kube-proxy则只在出现了service或者endpoint变更时进行iptables规则的创建与保存。建立对service和endpoint的监听机制。使用etcd的watch机制实时监控etcd上Kubernetes对象数据的变化,其中proxier监控service对象的变化,endpointsHandler则监控endpoint对象的变化。至此,则已经完成一个ProxyServer的创建工作。 - 运行ProxyServer。如果启用了健康检查服务功能,则运行kube-proxy的HTTP健康检查服务器,监听HealthzPort。同时,同样像kubelet一样发出birthCry(即记录一条已经创建完毕并开始运行kube-proxy的事件),并且开启同步工作。
- kube-proxy的同步工作是一个周期性的循环,同样对于不同工作模式的kube-proxy会有具体的实现逻辑。userspace模式的kube-proxy在每个循环中将检查系统定义的iptables规则/链是否存在,如果不存在则重新创建;遍历所有用户自定义的service,以service名为键值,清理Session Affinity类型不为空且超时(默认为180分钟)的客户端会话记录。iptables模式的kube-proxy则只在出现了service或者endpoint变更时进行iptables规则的创建与保存。
至此,整个启动过程就讲解完毕了。总的来说,iptables模式与userspace模式的kube-proxy相比,在效率和可靠性上都有较大的优势。前者的kube-proxy本质上只负责根据service和endpoint的更新来维护iptables规则,而转发则依赖于内核态的br_netfilter,而后者需要负责监听本地端口并完成流量转发到pod的全盘工作,可能会因为打开连接数限制等各种原因影响service的访问速度,也会在资源消耗上有着更显著的瓶颈。
2. proxier
前面已经介绍过,kube-proxy中工作的主要服务是proxier,而LoadBalancer只负责执行负载均衡算法来选择某个pod。默认情况下,proxier绑定在BindAddress上运行,并需要根据etcd上service对象的数据变化实时更新宿主机的防火墙规则/链。由于每个工作节点上都有一个kube-proxy在工作,所以无论在哪个节点上访问service的virtual IP比如11.1.1.88,都可以被转发到任意一个被代理pod上。可见,由proxier负责的维护service和iptables规则尤为重要。这个过程通过OnServiceUpdate方法实现,该方法的参数就是从etcd中获取的变更service对象列表,下面将分别分析userspace和iptables模式下的proxier的工作流程。
● userspace模式
(1) 遍历期望service对象列表,检查每个servie对象是否合法。维护了一个activeServices,用于记录service对象是否活跃。对于用户指定不为该service对象设置cluster IP的情况,则跳过后续检查。否则,在activeServices中标记该service处于活跃状态。由于可能存在多端口service,因此对Service对象的每个port,都检查该socket连接是否存在以及新旧连接是否相同;如果协议、cluster IP及其端口、nodePort、externalIPs、loadBalancerStatus以及sessionAffinityType中的任意一个不相同,则判定为新旧连接不相同。如果service与期望一致,则跳过后续检查。否则,则proxier在本地创建或者更新该service实例。如果该service存在,进行更新操作,即首先将旧的service关闭并停止,并创建新的service实例。否则,则直接进行创建工作
删除proxier维护的service状态信息表(serviceMap)中且不在activeServices记录里的service。
(2) 删除service实例的关键在于在宿主机上关闭通向旧的service的通道。对任何一个Kubernetes service(包括两个系统service)实例,kube-proxy都在其运行的宿主机上维护两条流量通道,分别对应于两条iptables链——KUBE-PORTALS-CONTAINER和KUBE-PORTALS-HOST。所以,这一步proxier就必须删除iptables的nat表中以上两个链上的与该service相关的所有规则。
说明 service状态信息表记录的数据包括:cluster IP/Port、proxyPort(kube-proxy为每个service分配的随机端口)、ProxySocket(TCP/UDP socket的抽象,一个ProxySocket就代表一个全双工的TCP/UDP连接)和Session Affinity等。
(3) 新建一个service实例。首先,根据service的协议(TCP/UDP)在本机上为其分配一个指定协议的端口。接着,启动一个goroutine监听该随机端口上的数据,并建立一条从上述端口到service endpoint的TCP/UDP连接。连接成功建立后,填充该service实例的各属性值并在service状态信息表中插入该service实例。然后,开始为这个service配置iptables,即根据该service实例的入口IP地址(包括私有和公有IP地址)、入口端口、proxier监听的IP地址、随机端口等信息,使用iptables在KUBE-PORTALS-CONTAINER和KUBE-PORTALS-HOST链上添加相应的IP数据包转发规则。最后,以service id(由service的namespace、service名称和service端口名组成)为key值,调用LB接口在本地添加一条记录Serice实例与service endpoint的映射关系。
● iptables模式
iptables模式下的proxier只负责在发现存在变更时更新iptables规则,而不再为每个service打开一个本地端口,所有流量转发到pod的工作将交由iptables来完成。OnServiceUpdate的具体工作步骤如下。
(1) 遍历期望service对象列表,检查每个service对象是否合法,并更新其维护的serviceMap,使其与期望列表保持同步(包括创建新的service、更新过时的service以及删除不再存在的service)。
(2) 更新iptables规则。注意,这个步骤通过一个名为syncProxyRules的方法完成,在这个方法中涉及了service及endpoint两部分更新对于iptables规则的调整。处于代码完整性和逻辑严密性的考虑,此处将两部分内容合并到此处进行讲解。具体步骤如下。
- 确保filter表和NAT表中"KUBE-SERVICES"链(chain)的存在,若不存在,则为其创建。
- 确保filter表和NAT表中"KUBE-SERVICES"规则(rule)的存在,若不存在,则为其创建。
- 确保nat表中"KUBE-POSTROUTING"链的存在,若不存在,则为其创建。
- 确保nat表中"KUBE-POSTROUTING"规则的存在,若不存在,则为其创建。
- 保存当前filter表,将以冒号开头的那些行(即链)存入existingFilterChains中,这是一个以iptables规则Target为键、链为值的map。
- 保存当前nat表,将以冒号开头的那些行(即链)存入existingNATChains中,这同样是一个以iptables规则Target为键、链为值的map。
- 将existingFilterChains中的"KUBE-SERVICES"链写入filterChains(一个以*filter为开头的buffer)中。
- 将existingNATChains中"KUBE-SERVICES"、“KUBE-NODEPORTS”、“KUBE-POSTROUTING”、"KUBE-MARK-MASQ"链写入natChains(一个以*nat为开头的buffer)中。
- 在natRules(一个buffer)中写入如下数据。分别用于之后创建"KUBE-POSTROUTING"和"KUBE-MARK-MASQ"规则。
在以上9个步骤中,proxier将现有的与Kubernetes相关的最基本的链分别写入了两个buffer中,即filterChains, natChains,另外还将两条iptables规则写入了natRules。 - 遍历proxier维护的serviceMap结构(保存着最新的service对象),为每个service执行如下操作。
(i) 首先获得该service对应的iptables链,命名形式为"KUBE-SVC-{hash值}“(如"KUBE-SVC-OKIBPPLEBEZLXS53”)。
(ii) 在existingNATChains中查找其是否存在,如果存在,则直接将该链写入natChains,否则在natChains写入一条新链(如:KUBE-SVC-OKIBPPLEBEZLXS53- [0:0])。
(iii) 在activeNATChains(一个以链名为键的map)中标记该链为活跃状态。
(iv) 加入clusterIP对应的iptables规则。根据proxier参数MasqueradeAll的不同(该参数用于决定是否对所有请求都进行源地址转换),在natRules中写入形如如下两条规则中的一条,前一条对应参数为true的情况。
(v) 处理externalIPs,在natRules中添加如下iptables规则。注意,如果该externalIPs是一个本地IP,则还需要将其对应的port打开。
(vi) 处理loadBalancer ingress,在natRules中添加如下iptables规则。
(vii) 处理nodePort。首先要在本地打开一个端口,然后在natRules添加如下iptables规则。
(viii) 如果一个service没有可用的后端endpoint,那么需要拒绝对其的请求。在filterRules中添加如下iptables规则。
至此,所有与service相关的iptables规则就已经全部创建完毕了。接下来,将为endpoint创建链和iptables规则。注意,下面的步骤12) 仍然处于步骤10) 中的循环里,即遍历service中。 - 遍历proxier维护的endpointsMap结构(以service为键,对应的endpoint列表为值的map),为每个endpoint执行如下操作。
(i) 获得每个endpoint对应的iptables链,命名形式为"KUBE-SEP-{hash值}"(如KUBE-SEP-XL4YDER4UGY5O2IL)。
(ii) 在existingNATChains中查找该链是否存在,如果存在,则直接将该链写入natChains,否则在natChains写入一条新链(如:KUBE-SEP-XL4YDER4UGY5O2IL - [0:0])。
(iii) 在activeNATChains中将该链标记为活跃状态。12) 首先考虑session affinity规则。为启用了该功能的service在natRules中加入如下iptables规则。 - 接下来采用load balance规则,将一个service的流量分散到各个endpoint上。
(i) 对于除了最后一个endpoint的其他endpoint,在natRules中加入如下规则。可以看到,这里出现了一个随机分配的机制,每条规则被选中的概率是1/(该service对应的endpoint数目-1)。
endpointsHandler
endpointHandler在选择后端时默认采用Round Robin算法,同时需要兼顾session affinity等要求。
● userspace模式
前面已经介绍过,当访问请求经过iptables转发至proxier之后,选择一个pod的工作就需要交给endpointsHandler。userspace模式下的endpointsHandler本质上是一个loadBalancer(LB),它不仅能够按照策略选择出一个service endpoint(后端pod),还需要能实时更新并维护service对应的endpoint实例信息。这两个过程分别对应loadBalancer的两个处理逻辑,即NextEndpoint和OnEndpointsUpdate。下面将逐一进行分析
- NextEndpoint方法
NextEndpoint方法核心调度算法是Round-Robin,每次一个请求到达,它的目的地都应该“下一个pod”。但是在LoadBalancer中,这个Round-Robin算法还能够同时考虑“Session Affinity”的因素,即如果用户指定这个service需要考虑会话亲密性,那么对于一个给定的客户端,NextEndpoint会一直返回它上一次访问到的那个pod直至会话过期 - OnEndpointsUpdate方法
由于所有被代理pod的变化最后都会反映到etcd里面对应的pod数据上,所以存储在etcd中的pod对象总可以认为是用户的期望值,代表了endpoint的“理想世界”,而LoadBalancer内存中的endpoint对象则反映了service对象与实际后端pod的“现实世界”。因此,OnEndpointsUpdate方法的作用就是用“理想世界”的endpoint对象同步“现实世界”的endpoint,这个同步的过程就是一旦etcd中的endpoint信息发生变化,那么LoadBalancer就会把endpoint列表(理想世界)加载进来,然后通过对比注册新添的endpoint到自己的service信息中,或者删除那些已经不存在的endpoint,同时更新service Affinity数据。
● iptables模式
iptables模式下的endpointsHandler本质上由proxier担任。它不再处理具体的选取service后端endpoint的工作,而只负责跟进endpoint对应的iptables规则。
OnEndpointsUpdate方法接收到etcd中endpoint对象的更新列表后,更新其维护的endpointsMap,包括更新、创建和删除其中的service和endpoint对应记录。