本来以为一篇就能搞定,还是低估了自己的废话,好吧,只能通过两篇文章向大家介绍K8s核心原理。
一、 Kubernetes API Server 原理分析
1. kubernetes API Server介绍
kubernetes API server的和核心功能是提供了kubernetes各类资源对象(pod、RC 、service等)的增、删、改、查以及watch等HTTP Rest接口,是整个系统的数据总线和数据中心。有时候我们使用kubectl创建或者查看pod等资源的时候,发现没有反应,可能就是你的kube-apiservice服务异常退出导致的。
Kubernetes API server通过一个名为kube-apiservice的进程提供服务,该进程运行与master节点上。默认情况下该进程的端口是本机的8080提供restful服务。(注意如果是HTTPS,则是6443端口)。
接下来的一些操作,介绍一些如何通过rest 与kubernetes API server交互,这有便于后各k8s各个组件之间通信的理解:
[root@zy ~]# kubectl cluster-info #查看主节点信息
[root@zy ~]# curl localhost:8080/api #查看kubernetes API的版本信息
[root@zy ~]# curl localhost:8080/api #查看kubernetes API支持的所有的资源对象
当然我们也可以访问具体的资源
[root@zy ~]# curl localhost:8080/api/v1/pods
[root@zy ~]# curl localhost:8080/api/v1/services
[root@zy ~]# curl localhost:8080/api/v1/replicationcontrollers
当我们在运行kubectl get svc时会发现:
有一个以上话红框的服务,这个是什么呢,原来为了让pod中的进程能够知道kubernetes API server的访问地址,kubernetes API server本身也是一个service,名字就叫“kubernetes”,并且他的cluster IP 就是cluster IP地址池例的第一个地址,另外它服务的端口就是443。
2. Kubernetes proxy API接口
kubernetes API server还提供了一类很特殊的rest接口—proxy接口,这个结构就是代理REST请求,即kubernetes API server把收到的rest请求转发到某个node上的kubelet守护进程的rest端口上,由该kubelet进程负责相应。
举例:
masterIP:8080/api/v1/proxy/nodes/{node_name}/pods #某个节点下所有pod信息
masterIP:8080/api/v1/proxy/nodes/{node_name}/stats #某个节点内物理资源的统计信息
masterIP:8080/api/v1/proxy/nodes/{node_name}/spec #某个节点的概要信息
#接下来说一下比较重要的pod的相关接口
masterIP:8080/api/v1/proxy/namespaces/{namespace}/pods/{pod_name}/{path:*} #访问pod的某个服务接口
masterIP:8080/api/v1/proxy/namespaces/{namespace}/pods/{pod_name} #访问pod
假如这里有一个名为myweb的Tomcat的pod
我们在浏览器中输入masterIP:8080/api/v1/proxy/namespaces/{namespace}/pods/myweb就能访问到该pod的http服务了。
如果这里是一个几个web的pod组成的service的话:
masterIP:8080/api/v1/proxy/namespaces/{namespace}/services/{service_name}
就能访问到其下面的服务,当然最终会通过kube-proxy被定位到相应的pod下。
3.集群功能模块之间的通信
Kubernetes API Server作为集群的和核心,负责集群各功能模块之间的通信。集群内的各个功能模块通过API server将信息存入etcd,同样的想要获取和操作这些数据时,也是通过API Server的REST接口(GET、LIST、WATCH)来实现,从而实现各个模块之间的交互。
接下来小编通过一张图,简单介绍一下几种典型的交互场景:
- 场景一(kubelet -- API Server):每个node节点上的kubelet每个一个时间周期,就会调用一次API Server的REST接口报告自身的状态,API Server接受到这些信息后,将节点信息更新到etcd中。还有,kubelet也通过API Server的Watch接口监听Pod信息,如果监听到新的Pod副本被调用绑定到本节点,则执行pod对应的容器的创建和启动;如果监听到Pod的删除操作,则删除本节点上相应的Pod容器;如果检测到修改操作,则kubelet会相应的修改本节点的Pod的容器。
- 场景二(kube-controller-manager -- API Server):kube-controller-manager中的Node Controller模块通过API Server模块提供的WATCH接口,实时监控Node信息。并做相应的处理。
- 场景三(scheduler -- API Server):当Scheduler通过API Server的Watch接口监听到新建Pod副本的信息后,它会检索所有符合该Pod要求的Node列表,开始执行Pod调度逻辑,调度成功后将Pod绑定到目标节点上。
介绍了上面的场景,大家不免会想到,这里多的功能模块都会频繁的使用API Server,而且API Server这个服务也是如此的重要,长时间的压力工作,会不会容器挂掉。问得好,k8s为了缓解集群各模块对API Server的访问压力,各模块之间都采用了缓存机制。各个模块定时的从API Server获取制定资源对象信息,并缓存到本地,这样各个功能模块先从本地获取资源对象信息,本地没有时再访问API Server。
二、 Controller Manager 原理的分析
介绍:Controller Manager作为集群内部管理控制中心,负责集群内的Node、Pod副本、EndPoint、命名空间、服务账号、资源定额等管理。当某个Node意外宕机了,Controller Manager会及时发现此故障并执行自动修复流程,确保集群始终处于预期的工作状态。
由上图所示,Controller Manager中包含很多个controller,每一种controller都负责一种具体的控制流程,而Controller Manager正是这些controller的核心管理者。一般来说,智能系统和自动系统都被称为一个“操纵系统”的机构来不断修正系统的工作状态。在kubernetes集群中,每个controller都有这样一个“操纵系统”,他们通过API Server提供的接口实时监控整个集群里的每一个资源对象的当前状态,当发生各种故障导致系统状态发生变化,这些controller会尝试将系统从“现有装态”修正到“期望状态”。
接下来小编会介绍一些比较重要的Controller。
1. Replication Controller
在介绍replication controller时小编要强调一点的是,千万不要把资源对象的那个RC和这个replication controller弄混淆了,我们这里介绍的replication controller是副本的控制器,RC只是一个资源对象,上层是replication controlle管理各个RC。(这里我们统一的将replication controller叫副本的控制器,资源对象RC叫RC)。
副本的控制器的核心作用是确保任何使用集群中的一个RC所关联的Pod副本数量保持预设的值。如果发现Pod副本数超过预设的值,则副本的控制器会销毁一些Pod的副本,反之则创建一些新的Pod的副本以达到目标值。值得注意的是只有当Pod的重启策略是always时,副本的控制器才会管理该pod的操作。通常情况下,pod对象被成功的创建之后不会消失,唯一例外的是当pod处于success或者failed状态的时间过长(超时时间可以设定),该pod会被系统自动回收,管理该pod的副本的控制器将在其他的工作节点上重新创建、启动该pod。
RC中的Pod模板就像一个模具,模具制作出来的东西一旦离开模具,二者将毫无关系,一旦pod创建,无论模板如何变化都不会影响到已经创建的pod,并且删除一个RC 不会影响它所创建出来的Pod,当然如果想在RC控制下,删除所有的Pod,需要将RC中设置的pod的副本数该为0,这样才会自动删除所有的Pod。
replication controller(副本的控制器)的职责:
- 确保当前管理的Pod的数量为预设值
- 通过调用RC的spec.replicas属性实现系统扩容和缩容
- 通过改变RC中的Pod的模板中的image,来实现系统的滚动升级
replication controller(副本的控制器)的使用场景 :
- 重新调度:无论是否有节点宕机,还是pod意外死亡,RC都可以保证自己所管理的正在运行Pod的数量为预设值
- 弹性伸缩:实现集群的扩容和缩容(根据集群的可用资源和负载压力)
- 滚动升级:应用服务升级新的版本,并且保证整个升级过程,应用服务仍可对外提供服务。
2. Node Controller
Kubelet进程在启动时会通过API Server注册自身的节点信息,并定时的向API Server汇报状态信息,API Server在接受到这些信息后,将这些信息更新到etcd中。Etcd中存储的节点信息包括:节点的健康状态、节点资源、节点名称、节点地址信息、操作系统版本、docker版本、kubelet版本等。而节点的健康状态有三种:就绪(True)、未就绪(False)、未知(Unknown)。
接下来小编通过图来介绍Node Controller的核心工作流程:
具体步骤
- 如果controller manager在启动时设置了--cluster-cidr,那么为每一个没有设置spec.PodCIDR的节点生成一个CIDR地址,并用该地址设置节点的spec.PodCIDR属性。
- 逐个读取节点信息,此时node controller中有一个nodestatusMap,里面存储了信息,与新发送过来的节点信息做比较,并更新nodestatusMap中的节点信息。Kubelet发送过来的节点信息,有三种情况:未发送、发送但节点信息未变化、发送并且节点信息变化。此时node controller根据发送的节点信息,更新nodestatusMap,如果判断出在某段时间内没有接受到某个节点的信息,则设置节点状态为“未知”。
- 最后,将未就绪状态的节点加入到待删除队列中,待删除后,通过API Server将etcd中该节点的信息删除。如果节点为就绪状态,那么就向etcd中同步该节点信息。
3. ResourceQuota controller
Kubernetes提供了资源配额管理(resourceQuota controller)这里高级功能,资源配置管理确保了指定的资源对象在任何时候都不会超量占用系统物理资源,避免了由于某些业务进程的设计或者实现的缺陷导致整个系统运行紊乱设置意外宕机,对整个集群的稳定性有着至关重要的作用。
目前kubernetes支持如下三个层次的资源配额管理:
- 容器级别:对CPU 和 memory的限制
- Pod级别:可以对一个pod内所有容器的可用资源进行限制
- Namespace级别:为namespace(多租户)级别的资源限制,其中限制的资源包括:
△ Pod数量
△ RC数量
△ Service数量
△ ResourceQuota数量
△ Secret数量
△ 可持有的PV数量
Kubernetes的配额管理是通过admission control(准入控制)来控制的。admission control当前提供了两种方式的配额约束,分别是limitRanger和resourceQuota。其中limitRanger作用于pod和容器上。ResourceQuota作用于namespace上,用于限定一个namespace里的各类资源的使用总额。
Kubernetes的配额管理是通过admission control(准入控制)来控制的。admission control当前提供了两种方式的配额约束,分别是limitRanger和resourceQuota。其中limitRanger作用于pod和容器上。ResourceQuota作用于namespace上,用于限定一个namespace里的各类资源的使用总额。
从上图中,我们可以看出,大概有三条路线,resourceQuota controller在这三条路线中都起着重要的作用:
- 如果用户在定义pod时同时声明了limitranger,则用户通过API Server请求创建或者修改资源对象,这是admission control会计算当前配额的使用情况,不符合约束的则创建失败。(一、三)
- 对于定义了resource Quota的namespace,resourceQuota controller会定期统计和生成该namespace下的各类对象资源使用总量,统计结果包括:pod、service、RC、secret和PV等对象的实例个数,以及该namespace下所有的container实例所使用的资源量(CPU,memory),然后会将这些结果写入到etcd中,写入的内容为资源对象名称、配额制、使用值,然后admission control会根据统计结果判断是否超额,以确保相关namespace下的资源配置总量不会超过resource Quota的限定值。(二、三)
4. Namespace controller
用户通过API Server可以创建新的namespace并保存在etcd中,namespace controller定时通过API Server读取这些namespace信息。如果namespace被API标记为优雅删除(通过设置删除周期),则将该namespace的状态设置为“terminating”并保存到etcd中。同时namespace controller删除该namespace下的serviceAccount,RC,pod,secret,PV,listRange,resourceQuota和event等资源对象。
当namespace的状态为“terminating”后,由admission controller的namespaceLifecycle插件来阻止为该namespace创建新的资源。同时在namespace controller删除完该namespace中的所有资源对象后,namespace controller对该namespace 执行finalize操作,删除namespace的spec.finallizers域中的信息。
当然这里有一种特殊情况,当个namespace controller发现namespace设置了删除周期,并且该namespace 的spec.finalizers域值为空,那么namespace controller将通过API Server删除该namespace 的资源。
5. service controller 与 endpoint controller
上图所示了service和endpoint与pod的关系,endpoints表示一个service对应的所有的pod副本的访问地址,而endpoints controller就是负责生成和维护所有endpoints对象的控制器。
它负责监听service和对应的pod副本的变化,如果检测到service被删除,则删除和该service同名的endpoints对象。如果检测到新的service被创建或者修改,则根据该service的信息获取到相关的pod列表,然后创建或者更新service对应的endpoints对象。如果检测到pod的事件,则更新它对应service的endpoints对象(增加或者删除或者修改对应的endpoint条目)。
kubernetes scheduler 在整个系统中承担了“承上启下”的作用,“承上”是指它负责接收controller manager创建的新的pod,为其安排一个落脚的“家”,“启下”是指安置工作完成以后,目标node上的kubelet服务进程接管后继工作,负责pod生命周期中的“下半生”。
我们都知道将service和pod通过label关联之后,我们访问service的clusterIP对应的服务,就能通过kube-proxy将路由转发到对应的后端的endpoint(pod IP +port)上,最终访问到容器中的服务,实现了service的负载均衡功能。
那么接下来说一说service controller的作用,它其实是属于kubernetes与外部的云平台之间的一个接口控制器。Service controller监听service的变化,如果是一个loadBalancer类型的service,则service controller确保外部的云平台上该service对应的loadbalance实例被相应的创建、删除以及更新路由转发表(根据endpoint的条目)。
三、 Scheduler 原理分析
1. 介绍
kubernetes scheduler 在整个系统中承担了“承上启下”的作用,“承上”是指它负责接收controller manager创建的新的pod,为其安排一个落脚的“家”,“启下”是指安置工作完成以后,目标node上的kubelet服务进程接管后继工作,负责pod生命周期中的“下半生”。
具体的来说,kubernetes scheduler的作用就是将待调度的pod(新建的、补足副本而创建的)按照特定的调度算法和调度策略绑定到集群中某个合适的node上,并将绑定信息写入到etcd中。整个调度过程分为三个对象,分别是:待调度的pod列表、可有的合适的node列表、调度算法和策略。一句话就是通过合适的调度算法和策略,将待调度的pod列表中的pod在合适的node上创建并启动。
接下来小编通过一幅图简单介绍一下scheduler的工作流程:
有图可知:
遍历所有目标node,筛选出符合要求的候选节点。为此,kubernetes内置了多种预选策略
确定优先节点,在第1步的基础上,采用优选策略,计算出每一个节点候选的积分,积分最高者胜出
最后通过API Server将待调度的Pod,通知给最优node上的kubelet,将其创建并运行
2. scheduler预选策略
在scheduler中可用的预选算有很多:NoDiskconflict、PodFitsResources、PodSelectorMatches、PodFitsHost、CheckNodeLabelPresence、CheckServiceAffinity、PodFitsPorts等策略。其中的5个默认的预选策略:PodFitsPorts、PodFitsResources、NoDiskconflict、PodSelectorMatches、PodFitsHost每个节点只有通过这5个预选策略后,才能初步被选中,进入下一个流程。
下面小编介绍几个常用的预选策略:
(1) NoDiskconflict
判断备选pod的gcePersistentDisk或者AWSElasticBlockStore和备选的节点中已存在的pod是否存在冲突具体检测过程如下:
- 首先,读取备选pod的所有的volume信息,对每一个volume执行一下步骤的冲突检测
- 如果该volume是gcePersistentDisk,则将volume和备选节点上的所有pod的每个volume进行比较,如果发现相同的gcePersistentDisk,则返回false,表明磁盘冲突,检测结束,反馈给调度器该备选节点不合适作为备选的pod,如果volume是AWSElasticBlockStore,则将volume和备选节点上的所有pod的每个volume进行比较,如果发现相同的AWSElasticBlockStore,则返回false,表明磁盘冲突,检测结束,反馈给调度器该备选节点不合适作为备选的pod
- 最终,检查备选pod的所有的volume均为发现冲突,则返回true,表明不存在磁盘冲突,反馈给调度器该备选节点合适备选pod
(2) podFistResources
判断备选节点资源是否满足备选pod的需求,检测过程如下:
- 计算备选pod和节点中已存在的pod的所有容器的需求资源(CPU 和内存)的总和
- 获得备选节点的状态信息,其中包括节点的资源信息
- 如果备选pod和节点中已存在pod的所有容器的需求资源(CPU和内存)的总和超出了备选节点拥有的资源,则返回false,表明备选节点不适合备选pod,否则返回true,表明备选节点适合备选pod
(3) PodSelectorMatches
判断备选节点是否包含备选pod的标签选择器指定的标签:
- 如果pod没有指定spec.nodeSelector标签选择器,则返回true
- 如果获得备选节点的标签信息,判断节点是否包含备选pod的标签选择器所指的标签,如果包含返回true,不包含返回false
(4) PodFitsHost
判断备选pod的spec.nodeName域所指定的节点名称和备选节点的名称是否一致,如果一致返回true,否则返回false。
(5) PodFitsPorts
判断备选pod所用的端口列表汇中的端口是否在备选节点中被占用,如果被占用,则返回false,否则返回true。
3 .scheduler优选策略
Scheduler中的优选策略有:leastRequestedPriority、CalculateNodeLabelPriority和BalancedResourceAllocation等。每个节点通过优先策略时都会算出一个得分,计算各项得分,最终选出得分值最大的节点作为优选结果。
小编接下来就给大家介绍一下一些常用的优选策略:
(1) leastRequestedPriority
该策略用于从备选节点列表中选出资源消耗最小的节点:
- 计算出所有备选节点上运行的pod和备选pod的CPU占用量
- 计算出所有备选节点上运行的pod和备选pod的memory占用量
- 根据特定的算法,计算每个节点的得分
(2) CalculateNodeLabelPriority
如果用户在配置中指定了该策略,则scheduler会通过registerCustomPriorityFunction方法注册该策略。该策略用于判断策略列出的标签在备选节点中存在时,是否选择该备选节点。如果备选节点的标签在优选策略的标签列表中且优选策略的presence值为true,或者备选节点的标签不在优选策略的标签列表中且优选策略的presence值为false,则备选节点score=10,否则等于0。
(3) BalancedResourceAllocation
该优选策略用于从备选节点列表中选出各项资源使用率最均衡的节点:
- 计算出所有备选节点上运行的pod和备选pod的CPU占用量
- 计算出所有备选节点上运行的pod和备选pod的memory占用量
- 根据特定的算法,计算每个节点的得分
四、 Kubelet 运行机制分析
在kubernetes集群中,每个node上都会启动一个kubelet服务进程。该进程用于处理master节点下发到本节点的任务,管理Pod以及Pod中的容器。每个kubelet进程会在API Server上注册节点信息,定期向master节点汇报节点资源的使用情况,并通过cAdvisor监控容器和节点的资源。
1. 节点管理
节点通过设置kubelet的启动参数“--register-node”来决定是否向API Server注册自己。如果该参数为true,那么kubelet将试着通过API Server注册自己。在自注册时,kubelet启动时还包括以下参数:
-api-servers:API Server的位置
--kubeconfing:kubeconfig文件,用于访问API Server的安全配置文件
--cloud-provider:云服务商地址,仅用于共有云环境
如果没有选择自注册模式,用户需要手动去配置node的资源信息,同时告知ndoe上的kubelet API Server的位置。Kubelet在启动时通过API Server注册节点信息,并定时向API Server发送节点新消息,API Server在接受到这些消息之后,将这些信息写入etcd中。通过kubelet的启动参数“--node-status-update-frequency”设置kubelet每个多长时间向API Server报告节点状态,默认为10s。
2. pod管理
kubelet通过以下几种方式获取自身node上所要运行的pod清单:
文件:kubelet启动参数“--config”指定的配置文件目录下的文件(默认为“/etc/Kubernetes/manifests”)通过--file-check-frequency设置检查该文件的时间间隔,默认为20s
HTTP端点:通过“--manifest-url”参数设置。通过“--http-check-frequency”设置检查该HTTP端点数据的时间间隔,默认为20s。
API Server:kubelet通过API server监听etcd目录,同步pod列表
注意:这里static pod,不是被API Server创建的,而是被kubelet创建,之前文章中提到了静态的pod是在kubelet的配置文件中编写,并且总在kubelet所在node上运行。
Kubelet监听etcd,所有针对pod的操作将会被kubelet监听到。如果是新的绑定到本节点的pod,则按照pod清单的要求创建pod,如果是删除pod,则kubelet通过docker client去删除pod中的容器,并删除该pod。
具体的针对创建和修改pod任务,流程为:
- 为该pod创建一个目录
- 从API Server读取该pod清单
- 为该pod挂载外部volume
- 下载pod用到的secret
- 检查已经运行在节点中的pod,如果该pod没有容器或者Pause容器没有启动,则先停止pod里的所有容器的进程。如果pod中有需要删除的容器,则删除这些容器
- 检查已经运行在节点中的pod,如果该pod没有容器或者Pause容器没有启动,则先停止pod里的所有容器的进程。如果pod中有需要删除的容器,则删除这些容器
- 为pod中的每个容器做如下操作
△ 为容器计算一个hash值,然后用容器的名字去查询docker容器的hash值。若查找到容器,且两者得到hash不同,则停止docker中的容器的进程,并且停止与之关联pause容器的进程;若两个相同,则不做任何处理
△ 如果容器被停止了,且容器没有指定restartPolicy(重启策略),则不做任何处理
△调用docker client 下载容器镜像,调用docker client 运行容器
3. 容器的健康检查
Pod通过两类探针来检查容器的健康状态。一个是livenessProbe探针,用于判断容器是否健康,告诉kubelet一个容器什么时候处于不健康状态,如果livenessProbe探针探测到容器不健康,则kubelet将删除该容器,并根据容器的重启策略做相应的处理;如果一个容器不包含livenessProbe探针,那么kubelet认为livenessProbe探针的返回值永远为“success”。另一个探针为ReadinessProbe,用于判断容器是否启动完成,且准备接受请求。如果ReadinessProbe探针检测到失败,则pod的状态将被修改,endpoint controller将从service的endpoints中删除包含该容器所在pod的IP地址的endpoint条目。
Kubelet定期调用容器中的livenessProbe探针来诊断容器的健康状态。livenessProbe包括以下三种实现方式:
- Execaction:在容器内执行一个命令,如果该命令的退出状态码为0,表示容器健康
- TCPSocketAction:通过容器的IP地址和端口执行一个TCP检查,如果端口能被访问,则表明该容器正常
- TCPSocketAction:通过容器的IP地址和端口执行一个TCP检查,如果端口能被访问,则表明该容器正常
具体的配置小编之前的文章中有详细说明:https://blog.51cto.com/14048416/2396640
五、 Kube-proxy运行机制分析
1. 概念介绍
介绍kube-proxy,不得不说service,这里小编先带大家回顾一下service,由于pod每次创建时它的IP地址是不固定的,为了访问方便以及负载均衡,这里引入了service的概念,service在创建后有一个clusterIP,这个IP是固定的,通过labelselector与后端的pod关联,这样我们如果想访问后端的应用服务,只需要通过service的clusterIP,然后就会将请求转发到后端的pod上,即使一个反向代理,又是一个负载均衡。
但是在很多情况下service只是一个概念,而真正将service的作用落实的这是背后的kube-proxy服务进程。那么接下来就具体的介绍kube-proxy。
在kubernetes集群中的每一个node上都有一个kube-proxy进程,这个进程可以看做service的透明代理兼负载均衡,其核心功能就是将到某个service的访问请求转发到后端的多个pod实例上。对每一个TCP类型的kubernetes service,kube-proxy都会在本地node上建立一个socketserver来负责接收请求,然后均匀发送到后端的某个pod的端口上,这个过程默认采用round robin负载均衡算法。另外,kubernetes也提供通过修改service的service.spec.sessionAffinity参数的值来实现会话保持特性的定向发送,如果设置的值为“clientIP”,那么则将来来自同一个clientIP的请求都转发到同一个后端的pod上。
此外,service的clusterIP和nodePort等概念是kube-proxy服务通过Iptables的NAT转换实现的,kube-proxy在运行过程中动态创建于service相关的Iptable规则,这些规则实现了clusterIP以及nodePort的请求流量重定向到kube-proxy进程上对应的服务的代理端口的功能。由于Iptable机制针对的是本地的kube-proxy端口,所有每一个node上都要运行kube-proxy组件,这样一来,在kubernetes集群内部,我们可以在任意node上发起对service的访问。由此看来,由于kube-proxy的作用,在service的调用过程中客户端无序关心后端有几个pod,中间过程的通信,负载均衡以及故障恢复都是透明。
2. 后端的pod选择
目前kube-proxy的负载均衡只支持round robin算法。round robin算法按照成员列表逐个选取成员,如果一轮循环结束,便从头开始下一轮循环,如此循环往复。Kube-proxy的负载均衡器在round robin算法得到基础上还支持session保持。如果service在定义中指定了session保持,则kube-proxy接受请求时会从本地内存中查找是否存在来自该请求IP的affinitystate对象,如果存在该对象,且session没有超时,则kube-proxy将请求转向该affinitystate所指向的后端的pod。如果本地存在没有来自该请求IP的affinitystate对象,则按照round robin算法算法为该请求挑选一个endpoint,并创建一个affinitystate对象,记录请求的IP和指向的endpoint。后面请求就会“黏连”到这个创建好的affinitystate对象上,这就实现了客户端IP会话保持的功能。
3. kube-proxy实现细节
kube-proxy通过查询和监听API Server中service与endpoint的变换,为每一个service都建立一个“服务代理对象“,并自动同步。服务代理对相关是kube-proxy程序内部的一种数据结构,它包括一个用于监听此务请求的socketServer, socketServer的端口是随机指定的是本地一个空闲端口。此外,kube-proxy内部也创建了一个负载均衡器—loadBalancer, loadBalancer上保存了service到对应的后端endpoint列表的动态路由转发表,而具体的路由选择则取决于round robin算法和service的session会话保持。
针对发生变化的service列表,kube-proxy会逐个处理,下面是具体的处理流程:
- 如果service没有设置集群IP,这不做任何处理,否则,获取该service的所有端口定义列表
- 逐个读取服务端口定义列表中的端口信息,根据端口名称、service名称和namespace判断本地是否已经存在对应的服务代理对象,如果不存在则创建,如果存在并且service端口被修改过,则先删除Iptables中和该service端口相关的规则,关闭服务代理对象,然后走新建流程并为该service创建相关的Iptables规则
- 更新负载均衡组件中对应service的转发地址列表,对于新建的service,确定转发时的会话保持策略
- 对于已删除的service则进行清理
接下来小编通过一个具体的案例,实际的给大家介绍一下kube-proxy的原理:
#首先创建一个service:
apiVersion: v1
kind: Service
metadata:
labels:
name: mysql
role: service
name: mysql-service
spec:
ports:
- port: 3306
targetPort: 3306
nodePort: 30964
type: NodePort
selector:
mysql-service: "true"
mysql-service对应的nodePort暴露出来的端口为30964,对应的cluster IP(10.254.162.44)的端口为3306,进一步对应于后端的pod的端口为3306。这里的暴露出来的30964也就是为mysql-service服务创建的代理对象在本地的端口,在ndoe上访问该端口,则会将路由转发到service上。
mysql-service后端代理了两个pod,ip分别是192.168.125.129和192.168.125.131。先来看一下iptables。
[root@localhost ~]# iptables -S -t nat
首先如果是通过node的30964端口访问,则会进入到以下链:
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/mysql-service:" -m tcp --dport 30964 -j KUBE-MARK-MASQ
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/mysql-service:" -m tcp --dport 30964 -j KUBE-SVC-67RL4FN6JRUPOJYM
然后进一步跳转到KUBE-SVC-67RL4FN6JRUPOJYM的链
-A KUBE-SVC-67RL4FN6JRUPOJYM -m comment --comment "default/mysql-service:" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-ID6YWIT3F6WNZ47P
-A KUBE-SVC-67RL4FN6JRUPOJYM -m comment --comment "default/mysql-service:" -j KUBE-SEP-IN2YML2VIFH5RO2T
这里利用了iptables的--probability的特性,使连接有50%的概率进入到KUBE-SEP-ID6YWIT3F6WNZ47P链,50%的概率进入到KUBE-SEP-IN2YML2VIFH5RO2T链。
KUBE-SEP-ID6YWIT3F6WNZ47P的链的具体作用就是将请求通过DNAT发送到192.168.125.129的3306端口。
-A KUBE-SEP-ID6YWIT3F6WNZ47P -s 192.168.125.129/32 -m comment --comment "default/mysql-service:" -j KUBE-MARK-MASQ
-A KUBE-SEP-ID6YWIT3F6WNZ47P -p tcp -m comment --comment "default/mysql-service:" -m tcp -j DNAT --to-destination 192.168.125.129:3306
同理KUBE-SEP-IN2YML2VIFH5RO2T的作用是通过DNAT发送到192.168.125.131的3306端口。
-A KUBE-SEP-IN2YML2VIFH5RO2T -s 192.168.125.131/32 -m comment --comment "default/mysql-service:" -j KUBE-MARK-MASQ
-A KUBE-SEP-IN2YML2VIFH5RO2T -p tcp -m comment --comment "default/mysql-service:" -m tcp -j DNAT --to-destination 192.168.125.131:3306
总的来说就是:在创建service时,如果不指定nodePort则为其创建代理对象时代理对象再本地监听一个随机的空闲端口,如果设置了nodePort则以nodePort为本地代理对象的端口。客户端在访问本地代理对象的端口后此时会根据iptables转发规则,将请求转发到service的clusterIP+port上,然后根据负载均衡策略指定的转发规则,将请求再次转发到后端的endpoint的target Port上,最终访问到具体pod中容器的应用服务,然后将响应返回。
核心机制第二篇,共享存储:https://blog.51cto.com/14048416/2412207
文章内容参考至《kubernetes权威指南》