kubelet
kubelet组件是Kubernetes集群工作节点上最重要的组件进程,它负责管理和维护在这台主机上运行着的所有容器。本质上,它的工作可以归结为使得pod的运行状态(status)与它的期望值(spec)一致。
kubelet的启动过程
- (1) kubelet需要启动的主要进程是KubeletServer,它所需加载的重要属性包括kubelet本身的属性、接入的runtime容器所需的基础信息以及定义kubelet与整个集群进行交互所需的信息。
- (2) 进行如下一系列的初始化工作。
1、选取APIServerList的第一个APIServer,创建一个APIServer的客户端。
2、如果上一步骤执行成功,则再创建一个APIServer的客户端用于向APIServer发送event对象。
3、初始化cloud provider。当然,如果集群的kubelet组件并没有运行在cloud provider上,该步骤将跳过。
4、创建并启动cAdvisor服务进程,返回一个cAdvisor的http客户端,IP和Port分别是localhost和CAdvisorPort的值。如果CAdvisorPort设置为0,将不启用cadvisor。
5、创建ContainerManager,为Docker daemon, kubelet等进程创建cgroups,并确保它们运行时使用的资源在限额之内。
6、对kubelet进程应用OOMScoreAdj值,即向/proc/self/oom_score_adj文件中写人OOMScoreAdj的值(默认值为-999 )。 OOMScoreAdj是用于描述在该进程发生内存溢出时被强行终止的可能性,分数越高,进程越有可能被杀死;其合法范围是-1000, 1000。换句话说,这里希望kubelet是最不容易被杀死的进程(之一)。
7、配置kubelet支持的pod配置方式,包括文件、url以及APIServer,支持多种方式一起使用。
- (3) 初始化工作完成后,实例化一个真正的kubelet进程。重点值得关注的有以下几点:
1、创建工作节点本地的service和node的cache,并且使用list/watch机制持续对其进行更新。
2、创建DiskSpaceManager,用以与cadvisor配合进行工作节点的磁盘管理,这与kubelet是否接受新的pod在该工作节点上运行有密切关系。
3、创建ContainerRefManager,用以记录每个container及其对应的引用的映射关系,主要用于在pod更新或者删除时进行事件的记录。
4、创建VolumeManager,用以记录每个pod及其挂载的volume的映射关系。
5、创建OOMWatcher,用以从cadvisor中获取系统的内存溢出(Out Of Memory , OOM)事件,并对其进行记录。
6、初始化kubelet网络插件,可以指定传入一个文件夹中的plugin作为kubelet的网络插件。
7、创建LivenessManager,用以维护容器及其对应的probe结果的映射关系,用以进行pod的健康检查。
8、创建podCache来缓存pod的本地状态。
9、创建PodManager,用以存储和管理对pod的访问。值得注意的是,kubelet支持3种更新pod的方式,其中通过文件和url创建的pod是不能自动被APIServer感知的,称其为static pod。为了监控这些pod的状态,kubelet会为每个static pod在相同的namespace下创建一个同名的mirror pod,用以反应static pod的更新状态。
10、配置hairpin NAT。
11、创建container runtime,支持docker和rkt。
12、创建PLEG ( pod lifecycle event generator )。为了严密监控容器运行情况,kubelet在过去采用了为每个pod启动一个goroutine来进行周期性轮询的方法,即使在pod的spec没有变化的情况下依旧如此。这种做法会消耗大量的CPU资源,在性能上不尽如人意。为了改变这个现状,Kubernetes在v 1.2.0中引入了PLEG,专门进行pod变化的监控,避免了并发的pod worker来进行轮询工作。
13、创建镜像垃圾回收对象containerGC。
14、创建imageManager理容器镜像的生命周期,处理镜像的垃圾回收工作。
15、创建statusManager,用以向APIServer同步pod实际状态的更新。
16、创建probeManager,用作pod健康检查的探针。
17、初始化volume插件。
18、创建RuntimeCache,用以缓存pod列表。
19、创建reasonCache,用以缓存每个容器对应的最新的失败原因信息。
20、创建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收集统计信息。
kubelet与cAdvisor的交互
cAdvisor主要负责收集工作节点上的容器信息及宿主机信息,下面将一一进行介绍:
- 容器信息
获取容器信息的URL形如:/api/{api version}/containers/<absolute container name>。绝
对容器名(absolute containere)与URL的对应关系如表所示。
绝对容器名/下包含整个宿主机上所有容器(包括Docker容器)的资源信息,而绝对容器名/docker下才包含所有Docker容器的资源信息。如果想获取特定Docker容器的资源信息,绝对容器名字段需要填入/docker/{container ID}。
- 宿主机信息
类似地,还可以访问URL: /api/{api version}/machine来获取宿主机的资源信息。要获取当前宿主机的资源信息。
kubelet垃圾回收机制
垃圾回收机制主要涵盖两个方面:容器回收和镜像回收。此处以docker这种容器runtime为例进行说明。
- Docker容器的垃圾回收
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镜像回收策略主要涉及3个因素,如表所示:
在Kubernetes中,Docker镜像的垃圾回收步骤如下所示:
(1) 首先,调用cadvisor客户端API获取工作节点的文件系统信息,包括文件系统所在磁盘设备、挂载点、磁盘空间总容量(capacity)、磁盘空间使用量(usage)和等。如果capacity为0,返回错误,并记录下InvalidDiskCapacity的事件。
(2) 如果磁盘空间使用率百分比(usage*1oo/capacity)大于或等于预设的使用率上限HighThresholdPercent,则触发镜像的垃圾回收服务来释放磁盘空间,否则本轮检测结束,不进行任何回收工作。至于具体回收多少磁盘空间,使用以下公式计算:
代码语言:txt复制 amountToFree := usage-(int64(im.policy.LowThresholdPercent)*capacity/100)
其实就是释放超出Low下hresholdPercent的那部分磁盘空间。
那么kubelet会选择删除哪些镜像来释放磁盘空间呢?
首先,获取镜像信息。参考当时的时间(Time.Now())kubelet会调用Docker客户端查询工作节点上所有的Docker镜像和容器,获取每个Docker镜像是否正被容器使用、占用的磁盘空间大小等信息,生成一个系统当前存在的镜像列表imageRecords,该列表中记录着每个镜像的最早被检测到的时间、最后使用时间(如果正被使用则使用当前时间值)和镜像大小;删除imageRecords中不存在的镜像的记录。
然后,根据镜像最后使用时间的大小进行排序,时间戳值越小即最后使用时间越早的镜像越排在前面。如果最后使用时间相同,则按照最早被检测到的时间排序,时间戳越小排在越前面。
最后,删除镜像。遍历imageRecords中的所有镜像,如果该镜像的最后使用时间小于执行第一步时的时间戳,且该镜像的存在时间大于MinAge,则删除该镜像,并且将删除Docker镜像计入释放的磁盘空间值,如果释放的空间总量大于等于前面公式计算得到的amountToFree值,则本轮镜像回收工作结束。否则,则记录一条失败事件,说明释放的空间未达到预期。
kubelet如何同步工作节点状态
首先,kubelet调用APIServer API向etcd获取包含当前工作节点状态信息的node对象,查询的键值就是kubelet所在工作节点的主机名。
然后,调用cAdvisor客户端API获取当前工作节点的宿主机信息,更新前面步骤获取到的node对象。
这些宿主机信息包括以下几点:
- 工作节点IP地址。
- 工作节点的机器信息,包括内核版本、操作系统版本、docker版本、kubelet监听的端口、
- 工作节点上现有的容器镜像。
- 工作节点的磁盘使用情况—即是否有out of disk事件。
- 工作节点是否Ready。在node对象的状态字段更新工作节点状态,并且更新时间戳,则node controller就可以凭这些信息是否及时来判定一个工作节点是否健康。
- 工作节点是否可以被调度podo
最后,kubelet再次调用APIServer API将上述更新持久化到etcd里。
kube-proxy
Kubernetes基于service、endpoint等概念为用户提供了一种服务发现和反向代理服务,而kube-proxy就是这种服务的底层实现机制。kube-proxy支持TCP和UDP连接转发,默认情况下基于Round Robin算法将客户端流量转发到与service对应的一组后端pod。在服务发现的实现上,Kube-proxy使用etcd的watch机制,监控集群中service和endpoint对象数据的动态变化,并且维护一个从service到endpoint的映射关系,从而保证了后端pod的IP变化不会对访问者造成影响。另外kube-proxy还支持session affinity(即会话保持或粘滞会话)。
下面我们以iptables模式对kube-proxy进行解读。
kube-proxy的启动过程
(1) 新建一个ProxyServer,包括两个功能性的结构的创建,负责流量转发的proxier和负责负载均衡的endpointsHandler。
(2) 运行ProxyServer。如果启用了健康检查服务功能,则运行kube-proxy的HTTP健康检查服务器,监听HealthzPort。同时,同样像kubelet一样发出birthCry(即记录一条已经创建完毕并开始运行kube-proxy的事件),并且开启同步工作。
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)中且不在。ctiveServices记录里的service。
(2) 删除service实例的关键在于在宿主机上关闭通向旧的service的通道。对任何一个Kubernetes service(包括两个系统service)实例,kube-proxy都在其运行的宿主机上维护两条流量通道,分别对应于两条iptables链---------KUBE-PORTALS-CONTAINER和KUBE-PORTALS一HOST。所以,这一步proxier就必须删除iptables的nat表中以上两个链上的与该service相关的所有规则。
(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接口在本地添加一条记录Service实例与service endpoint的映射关系。
- iptables模式
iptables式下的proxier只负责在发现存在变更时更新iptables规则,而不再为每个service打开一个本地端口,所有流量转发到pod的工作将交由iptables来完成。OnServiceUpdate的具体工作步骤如下:
(1) 遍历期望service对象列表,检查每个service对象是否合法,并更新其维护的serviceMap,使其与期望列表保持同步(包括创建新的service、更新过时的service以及删除不再存在的service)。
(2) 更新iptables规则。注意,这个步骤通过一个名为syncProxyRules的方法完成,在这个方法中涉及了service及endpoint两部分更新对于iptables规则的调整。处于代码完整性和逻辑严密性的考虑,此处将两部分内容合并到此处进行讲解。具体步骤如下:
1、确保filter和NAT表中”KUBE-SERVICES”链(chain)的存在,若不存在,则为其创建。
代码语言:txt复制 iptables -t filter -N KUBE-SERVICESiptables -t nat -N KUBE-SERVICES
2、确保filter表和NAT表中”KUBE-SERV工CES”规则(rule)的存在,若不存在,则为其创建。
代码语言:txt复制 iptables -I OUTPUT -t filter -m comment --comment "kubernetes service portals" -j KUBE-SERVICESiptables -I OUTPUT -t nat -m comment --comment "kubernetes service portals" -j KOBE-SERVICESiptables -I PREROUTING -t nat -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
3、确保nat表中”KUBE-POSTROUTING”链的存在,若不存在,则为其创建。
代码语言:txt复制 iptables -t nat -N KUBE-POSTROUTING
4、确保nat.中”KOBE-POSTROUTING”规则的存在,若不存在,则为其创建。
代码语言:txt复制 iptables -I POSTROUTING -t nat -m comment --comment "kubernetes postrouting rules" -j KUBE-POSTROUTING
5、保存当前filter,将以冒号开头的那些行(即链)存入existingFilterChains中,这是一个以iptables规则Target为键、链为值的map.
代码语言:txt复制 iptables-save -t filter
6、保存当前nat表,将以冒号开头的那些行(即链)存入existingNATChains中,这同样是一个以iptables规则Target为键、链为值的map。
代码语言:txt复制 iptables-save -t nat
7、将existingFilterChains中的‘'KUBE-SERVICES”链写入filterChains(一个以*filter为开头的buffer)中。*
8、将existingNATChains中”KUBE-SERVICES"、"KUBE-NODEPORTS"、"KUBE-POSTROUTING"、"KUBE-MARK-MASO”链写入natChains(一个以*nat为开头的buffer)中。
9、在natRules(一个buffer)中写入如下数据。分别用于之后创建’'KUBE-POSTROUTING'’和"KUBE-MARK-MASO"规则。
代码语言:txt复制 A KUBE-POSTROUTING -m comment "kubenetes service traffic requiring SNAT" -m mark --mark ${masqueradeMark} -j MASQUERADE
A KUBE-MARK-MASQ -j MARK --set-xmark ${masquerademark}
10、遍历proxier维护的serviceMap结构(保存着最新的service对象),为每个service执行如下操作:
- 首先获得该service对应的iptables链,命名形式为‘'KUBE-SVC-{hash值}”(如”KUBE-SVC-OKIBPPLEBEZLXS53")。
- 在existingNATChains中查找其是否存在,如果存在,则直接将该链写入natChains,否则在natChains写入一条新链(如:KUBE-SVC-OKIBPPLEBEZLXS53 -0:0)。
- 在activeNATChains(一个以链名为键的map)中标记该链为活跃状态。
- 加入clusterIP对应的iptables规则。根据proxier参数MasqueradeAll的不同(该参数用于决定是否对所有请求都进行源地址转换),在natRules中写入形如如下两条规则中的一条,前一条对应参数为true的情况。
-A KUBE-SERVICES -m comment --comment "${svcName} cluster IP" -m ${protocol} -p ${protocol) -d ${cluster-ip}/32 --dport ${port} -j ${masqueradeMark}
-A KUBE-SERVICES -m comment --comment "${svcName} cluster IP" -m ${protocol} -p ${protocol} -d ${cluster-ip}/32 --dport ${port} -j KUBE-SVC-{hash值}
- 处理externalIPs,在natRules中添加如下iptables规则。注意,如果该externalIPs是一个本地IP,则还需要将其对应的port打开。
-A KUBE-SERVICES -m comment --comment "${svcName} external IP" -m ${protocol} -p ${protocol} -d ${external-ip}/32 --dport ${port} -j ${masqueradeMark}
-A KUBE-SERVICES -m comment --comment "${svcName}
external IP" -m ${protocol} -p ${protocol} -d ${external-ip}/32 -dport ${port} -m physdev ! --physdev-is-in -m addrtype ! --src-type LOCAL -j KUBE-SVC-{hash值}
-A KUBE-SERVICES -m comment --comment "${svcName} external IP" -m ${protocol} -p ${protocol} -d ${external-ip}/32 --dport ${port} -m addrtype --dst-type LOCAL -j KUBE-SVC-{hash值}
- 处理loadBalancer ingress,在natRules中添加如下iptables规则。
-A KUBE-SERVICES -m comment "${svcName} loadbalancer IP" -m ${protocol} -p ${protocol} -d ${ingress-ip}/32 --dport ${port} -j ${masqueradeMark}
-A KUBE-SERVICES -m comment --comment "${svcName} loadbalancer IP" -m ${protocol} -p ${protocol} -d ${ingress-ip}/32 --dport ${port} -j KUBE-SVC-{hash值}
- 处理nodePort。首先要在本地打开一个端口,然后在natRules添加如下iptables规则。
-A KUBE_NODEPORTS -m comment --commetn "${svcName}" -m ${protocol} -p ${protocol} -d ${ingress-ip}/32 --dport ${port} -j ${masqueradeMark}
-A KUBE-NODEPORTS -m comment --comment "${svcName}" -m ${protocol} -p ${protocol} -d ${ingress-ip}/32 --dport ${port} -j KUBE-SVC-{hash值}
- 如果一个service没有可用的后端endpoint,那么需要拒绝对其的请求。在filterRules中添加如下iptables规则。
-A KUBE_SERVICES -m comment --comment "${svcName} has no endpoints" -m ${protocol} -p ${protocol} -d ${cluster-ip}/32 --dport ${port} -j REJECT
至此,所有与service相关的iptables规则就已经全部创建完毕了。接下来,将为endpoint创建链和iptables规则。注意,下面的步骤12仍然处于步骤10中的循环里,即遍历service中。
11、遍历proxier维护的endpointsMap结构(以service为键,对应的endpoint列表为值的map),为每个endpoint执行如下操作:
- 获得每个endpoint对应的iptables链,命名形式为”KUBE-SEP-{hash值}”(如KUBE-SEP-XL4YDER4UGY502IL)。
- 在existingNATChains中查找该链是否存在,如果存在,则直接将该链写入natChains,否则在natChains写入一条新链(如:KUBE-SEP-XL4YDER4UGY502IL -0:0)。
- 在activeNATChains中将该链标记为活跃状态。
12、首先考虑session affinity规则。为启用了该功能的service在natRules中加入如下iptables规则。
代码语言:txt复制 -A KUBE-SVC-{hash值} -m comment --comment ${svcName} -m recent --name KUBE-SEP-{hash值} --rcheck --seconds${stickyMaxAgeSeconds} --reap -j KUBE-SEP-{hash值}
13、接下来采用load balance规则,将一个service的流量分散到各个endpoint上。
- 对于除了最后一个endpoint的其他endpoint,在natRules中加入如下规则。可以看到,这里出现了一个随机分配的机制,每条规则被选中的概率是1/(该service对应的endpoint数目-1)。
-A KUBE-SVC-{hash值} -m comment --comment ${svcName} -m statistic --mode random --probability 1.0/(${endpoint-number}-1) -j KUBE-SEP-{hash值}
- 对于最后一个endpoint,在natRules中加入如下规则,说明在此前各条均没有匹配到iptables规则的情况下,则一定从这个endpoint来接收访问该service的请求。
-A KUBE-SVC-{hash值} -m comment --comment ${svcName} -j KUBE-SEP-{hash值}
- 创建导向endpoint的iptables规则,在natRules中加入如下iptables规则,进行源地址解析。
-A KUBE-SEP-{hash值} -m comment --comment ${svcName} -s ${endpoint(pod)-ip} -j ${masqueradeMark}
- 进行目的地址解析。在natRules中加入如下iptables规则。如果该service有session affnity规则,加入第一条iptables规则,否则加入第二条。
-A KUBE-SEP-{hash值} -m comment --comment ${svcName} -m recent --name KUBE-SEP-{hash值} --set -m ${protocol} -p ${protocol} -j DNAT --to-destination ${endpoints(pod)-ip}
-A KUBE-SEP-{hash值} -m comment --comment ${svcName} -m ${protocol} -p ${protocol} -j DNAT --to-destination ${endpoint(pod)-ip}
至此,所有导向endpoint的iptables规则也基本创建完毕了。
14、清除existingNATChains中不处于活跃状态的service和endpoint对应的链。并且在natChains写入这些链,同时在natRules写入-X ${chain},使得可以安全地删除这些链。
15、在natRules中写入最后一条iptables规则,用于访问”KUBE-SERVICES”的流量接入到"KUBE-NODEPORTS"。
代码语言:txt复制 -A KUBE-SERVICES -m comment --comment "kubernetes service nodeports;NOTE: this must be the last rule in this chain" -m addrtype --dst-type LOCAL -j KUBE-NODEPORTS
16、最后,为filterRules和natRules写入COMMIT,并且将其拼接起来,并通过iptables-restore将其导入到iptables中,完成根据service和endpoint的更新而同步iptables规则的任务。
17、处理不需要再占用端口的释放。
18、删除nat表中旧的源地址转换的iptables规则。
代码语言:txt复制 iptables -t nat -D POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -m mark --mark 0x4d415351 -j KUBE-MARK-MASQ
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直至会话过期。这个具体的工作流程如下所示。
(1) 根据请求中提供的service id(由namespace, service名和service端口号组成),查找该service代理的pod端点列表(一个ip:port形式的字符串链表)、当前的endpoint的索引值和该service的Session Affinity(SA)属性等。
(2) Session Affinity有两种类型:None和ClientIP,如果SA的类型是ClientIP,则来自同一个客户端IP的请求在一段时间内都将重定向到同一个后端pod,这样也就简洁做到了访问的会话粘性(Session Sticky ), SA的最长保活时间决定了这个时间段的长度,默认值是180分钟;如果SA的类型为空(None),则不进行任何会话记录。
(3) 假如这个service不需要SA功能,或者上述SA已经超时了,那么IoadBalance啥直接将当前的endpoint索引值 1,再对endpoint列表长度取余作为下一个可用endpoint的索引值。
(4) 当然,如果是由于SA超时引起的步骤(3), LoadBalancer还会为步骤(3)中最终被访问的那个pod建立Session Affinity实例并设置时间戳,这样下次这个ClientIP来的请求就一定会继续落在这个pod上。
- OnEndpointsUpdate
知道了LoadBalancer如何选择一个“合适”的pod,再来看一下它如何保证它所知道的被代理pod列表总是最“准确”的。
由于所有被代理pod的变化最后都会反映到etcd里面对应的pod数据上,所以存储在etcd中的pod对象总可以认为是用户的期望值,代表了endpoint的“理想世界”,而LoadBalancer内存中的endpoint对象则反映了service对象与实际后端pod的“现实世界”。因此,OnEndpointsUpdate方法的作用就是用“理想世界”的endpoint对象同步“现实世界”的endpoint,这个同步的过程就是一旦etcd中的endpoint信息发生变化,那么LoadBalancer就会把endpoint列表(理想世界)加载进来,然后通过对比注册新添的endpoint到自己的service信息中,或者删除那些已经不存在的endpoint ,同时更新service Affnity数据。
- iptables模式
正如上文所述,iptables式下的endpointsHandle体质上由proxier担任。它不再处理具体的选取service后端endpoint的工作,而只负责跟进endpoint对应的iptables规则。
- OnEndpointsUpdate方法
接收到etcd中en即oint对象的更新列表后,更新其维护的endpointsMap,包括更新、创建和删除其中的service和endpoint对应记录。
其后的关键步骤syncProxyRules已经在上面展开,此处不再赘述。
核心组件协作流程
至此,Kubernetes中主要的组件我们都有了大致的了解。接下来,我们梳理一下在Kubernetes的全局视图下,当执行一些指令时这些组件之间是如何协作的,这样的流程解析对于读者将来对Kubernetes进行调试、排错和二次开发都是非常有帮助的。
创建pod
如图所示,当客户端发起一个创建pod的请求后,kubectl向APIServer的/pods端点发送一个HTTP POST请求,请求的内容即客户端提供的pod资源配置文件。
APIServer收到该REST API请求后会进行一系列的验证操作,包括用户认证、授权和资源配额控制等。验证通过后,APIServer调用etcd的存储接口在后台数据库中创建一个pod对象。
scheduler使用APIServer的API,定期从etcd获取/监测系统中可用的工作节点列表和待调度pod,并使用调度策略为pod选择一个运行的工作节点,这个过程也就是绑定(bind)。
绑定成功后,scheduler会调用APIServer的API在etcd中创建一个 binding对象,描述在一个工作节点上绑定运行的所有pod信息。同时kubelet会监听APIServer上pod的更新,如果发现有pod更新信息,则会自动在podWorker的同步周期中更新对应的pod。
这正是Kubernetes实现中“一切皆资源”的体现,即所有实体对象,消息等都是作为etcd里保存起来的一种资源来对待,其他所有组件间协作都通过基于APIServer的数据交换,组件间一种松耦合的状态。
创建replication controller
如图所示,当客户端发起一个创建replication controller的请求后,kubectl向APIServer的/controllers端点发送一个HTTP POST请求,请求的内容即客户端提供的replication controller资源配置文件。
与创建pod类似,APIServer收到该REST API请求后会进行一系列的验证操作。验证通过后,APIServer调用etcd的存储接口在后台数据库中创建一个replication controller对象。
controller manager会定期调用APIServer的API获取期望replication controller对象列表。再遍历期望RC对象列表,对每个RC,调用APIServer的API获取对应的pod集的实际状态信息。然后,同步replication controller的pod期望值与pod的实际状态值,创建指定副本数的pod。
创建service
如图所示,当客户端发起一个创建service的请求后,kubectl向APIServer的/services端点发送一个HTTP POST请求,请求的内容即客户端提供的service资源配置文件。
同样,APIServer收到该REST API请求后会进行一系列的验证操作。验证通过后,APIServer调用etcd的存储接口在后台数据库中创建一个service对象。
kube-proxy会定期调用APIServer的API获取期望service对象列表,然后再遍历期望service对象列表。对每个service,调用APIServer的API获取对应的pod集的信息,并从pod信息列表中提取pod IP和容器端口号封装成endpoint对象,然后调用APIServer的API在etcd中创建该对象。
- userspace kube-proxy
对每个新建的service, kube-proxy会为其在本地分配一个随机端口号,并相应地创建一个ProxySocket,随后使用iptablesl具在宿主机上建立一条从ServiceProxy到ProxySocket的链路。同时,kube-prxoy后台启动一个协程监听ProxySocket上的数据并根据endpoint实例的信息(例如IP,port和session affinity属性等)将来自客户端的请求转发给相应的service后端pod。
- iptables kube-proxy
对于每个新建的service,kube-proxy会为其创建对应的iptables。来自客户端的请求将由内核态iptables负责转发给service后端pod完成。
最后,kube-proxy会定期调用APIServer的API获取期望service和endpoint列表并与本地的service和endpoint实例同步。