说明:Kubernetes 1.32预计2025年下半年正式发布,本文撰写时版本尚未正式发布,部分特性基于官方预告和Alpha/Beta版本,最终表现以正式发布版本为准。
用Kubernetes已经有好几年了。
从最开始的只会用kubectl apply,到后来能写YAML、排错、调优,我以为自己已经很懂K8s了。但直到最近,因为要做一次深度的技术分享,我逼着自己去看源码、看设计文档、看底层实现,才发现自己以前对K8s的理解,很多都停留在表面。
K8s看起来很复杂,概念很多,组件很多,但如果你深入到它的底层机制,会发现它的设计其实非常优雅。每个组件都有明确的职责,每个机制都有清晰的逻辑,整个系统就像一台精密的机器,有条不紊地运行着。
这篇文章,我想分享一下我对K8s 1.32底层机制的理解。从架构设计、核心组件到调度机制、网络模型、存储系统,深入剖析Kubernetes是如何工作的。
如果你也在用K8s,但对它的底层机制一知半解,希望这篇文章能帮你建立一个更清晰、更深入的理解。
先说明一下,我主要基于K8s 1.32的源码和官方文档来写,不同版本的实现可能会有差异。我不是K8s的核心开发者,只是一个有一定经验的使用者,我的理解可能有不对的地方,欢迎交流。
K8s的整体架构
在深入细节之前,先看看K8s的整体架构。
K8s采用的是主从架构,分为控制平面(Control Plane)和数据平面(Data Plane)。
控制平面是K8s的大脑,负责整个集群的管理和决策。它包括以下几个核心组件:
第一,kube-apiserver。这是K8s的入口,所有的操作都要经过它。它提供RESTful API,负责认证、授权、请求校验等。不管是kubectl,还是其他组件,都是通过调用apiserver来和集群交互的。
第二,etcd。这是K8s的数据库,所有的集群状态都存在etcd里。比如,有哪些Pod、哪些Service、哪些ConfigMap,这些信息都存在etcd中。etcd是一个分布式的键值存储,保证数据的一致性和高可用。
第三,kube-scheduler。这是K8s的调度器,负责决定Pod应该运行在哪个节点上。当你创建一个Pod的时候,scheduler会根据Pod的资源需求、节点的状态、各种调度策略,选择一个最合适的节点,然后把Pod绑定到那个节点上。
第四,kube-controller-manager。这是K8s的控制器管理器,里面运行着各种控制器。比如,Node Controller负责节点的管理,Replication Controller负责Pod副本的管理,Endpoint Controller负责Service和Pod的关联。每个控制器都在不断地监控集群状态,把当前状态调整到期望状态。
第五,cloud-controller-manager。这是和云服务商对接的控制器,负责管理云服务商提供的资源,比如负载均衡器、存储卷、路由等。如果你用的是公有云的K8s服务,这个组件会帮你自动创建和管理云资源。
数据平面是K8s的四肢,负责实际运行工作负载。每个节点上都有以下组件:
第一,kubelet。这是每个节点上的代理,负责管理这个节点上的Pod。它从apiserver获取分配到这个节点的Pod信息,然后调用容器运行时(比如containerd)来创建和运行容器。同时,它还负责监控容器的状态,上报节点和Pod的资源使用情况。
第二,kube-proxy。这是每个节点上的网络代理,负责实现Service的网络规则。它会在节点上设置iptables或者IPVS规则,让访问Service的请求能正确地转发到后端的Pod。
第三,容器运行时。这是真正运行容器的组件,比如containerd、CRI-O等。它负责拉取镜像、创建容器、管理容器的生命周期。
除了这些核心组件,K8s还有很多附加组件,比如CoreDNS(集群内的DNS服务)、Ingress Controller(入口流量管理)、Metrics Server(资源指标收集)等。这些附加组件不是必须的,但通常都会安装。
这个架构的核心设计思想是"声明式API"和"控制器模式"。你不需要告诉K8s怎么做,只需要告诉它你想要什么状态(比如,我要运行3个Nginx副本)。然后,K8s的各个控制器会不断地工作,把当前状态调整到你期望的状态。如果某个Pod挂了,控制器会自动创建一个新的;如果节点挂了,控制器会把这个节点上的Pod调度到其他节点上。
这种设计,让K8s具有了自愈能力,也让它能管理大规模的集群。
API Server:集群的入口
kube-apiserver是K8s最重要的组件,没有之一。所有的操作,不管是来自用户的,还是来自其他组件的,都要经过apiserver。
apiserver的主要职责包括:
第一,REST API。apiserver提供了一组RESTful API,用来操作K8s的各种资源,比如Pod、Service、Deployment等。你用kubectl的时候,本质上就是在调用这些API。
第二,认证和授权。apiserver会对每个请求进行认证,确认你是谁;然后进行授权,确认你有没有权限做这个操作。K8s支持多种认证方式,比如客户端证书、Token、用户名密码等;授权主要用RBAC(基于角色的访问控制)。
第三,请求校验。apiserver会校验请求的合法性。比如,你创建的Pod的YAML格式对不对,必填字段有没有填,资源名称是否合法。校验不通过的请求,会被直接拒绝。
第四,etcd交互。apiserver是唯一直接和etcd交互的组件。其他组件都不直接读写etcd,而是通过apiserver来操作。这样,etcd的访问就被统一管理了,保证了数据的一致性。
第五,Watch机制。apiserver提供了Watch API,让客户端可以实时监控资源的变化。比如,scheduler通过Watch来发现新创建的Pod,kubelet通过Watch来发现分配到自己节点上的Pod。Watch机制是K8s事件驱动的基础。
apiserver的设计有几个特点:
第一,无状态。apiserver本身是无状态的,所有的状态都存在etcd里。所以,apiserver可以水平扩展,部署多个实例,前面加个负载均衡器就行。
第二,聚合。apiserver支持API聚合,你可以把自定义的API注册到apiserver中,和原生的API一起提供服务。这样,K8s的API就可以无限扩展。
第三,准入控制。apiserver有一个准入控制链,请求在被处理之前,会经过一系列的准入控制器。比如,LimitRanger会检查资源限制,PodSecurityPolicy会检查Pod的安全策略,ResourceQuota会检查配额。你也可以写自己的准入控制器,通过Webhook的方式接入。
理解了apiserver,你就理解了K8s的入口。所有的操作,最终都会变成对apiserver的API调用。
etcd:集群的记忆
etcd是K8s的数据库,是整个集群的"记忆"。所有的集群状态,都存在etcd里。
etcd是一个分布式的、一致的键值存储。它基于Raft共识算法,保证在大多数节点正常的情况下,数据是一致的、可用的。
etcd在K8s中的作用:
第一,存储集群状态。所有的K8s资源对象,比如Pod、Service、Deployment、ConfigMap、Secret,都以键值对的形式存在etcd里。键的格式通常是/registry/<资源类型>/<命名空间>/<名称>。
第二,提供Watch机制。etcd支持Watch,客户端可以监控某个键或者某个前缀的变化。当数据变化的时候,etcd会实时通知客户端。K8s的各个组件,就是通过Watch来实时获取集群状态变化的。
第三,分布式锁。etcd提供了分布式锁的功能,K8s用它来实现选举和互斥。比如,scheduler和controller-manager在多实例部署的时候,会通过etcd的锁来选举主实例,保证只有一个实例在工作。
etcd的性能和可靠性,直接影响整个K8s集群的性能和可靠性。所以,在生产环境中,etcd的部署和运维非常重要。
几个关于etcd的最佳实践:
第一,etcd集群至少3个节点,保证高可用。3个节点可以容忍1个节点故障,5个节点可以容忍2个节点故障。
第二,etcd对磁盘IO很敏感,要用高性能的SSD。如果etcd的磁盘IO慢了,整个集群都会变慢。
第三,定期备份etcd数据。虽然etcd是分布式的,但还是有可能出问题。定期备份,出了问题才能恢复。
第四,监控etcd的状态。要监控etcd的延迟、磁盘使用、leader选举等指标,及时发现问题。
很多人在学习K8s的时候,会忽略etcd,觉得它只是个数据库。但实际上,etcd是K8s的核心,理解了etcd,才能理解K8s的状态管理。
调度器:Pod的分配者
kube-scheduler是K8s的调度器,负责决定Pod应该运行在哪个节点上。
调度的过程,分为两个阶段:过滤(Filter)和打分(Score)。
第一阶段,过滤。scheduler会遍历所有的节点,把不满足Pod要求的节点过滤掉。比如,Pod需要4核8G的资源,那些剩余资源不够的节点就会被过滤掉;Pod指定了要在有GPU的节点上运行,没有GPU的节点就会被过滤掉;Pod有节点亲和性、污点容忍等要求,不满足的节点也会被过滤掉。
过滤之后,剩下的节点就是"可调度"的节点。
第二阶段,打分。对于可调度的节点,scheduler会根据一系列的打分策略,给每个节点打分。比如,节点的资源利用率越低,分数越高;Pod的副本尽量分散在不同节点,分数越高;节点上已经有这个Pod需要的镜像,分数越高。
打分之后,scheduler会选择分数最高的节点,把Pod绑定到这个节点上。
这个过程看起来简单,但实际上非常复杂。K8s的调度器有很多调度策略,每个策略都有自己的逻辑。而且,调度器是可扩展的,你可以写自己的调度插件,实现自定义的调度逻辑。
K8s 1.32的调度器,基于调度框架(Scheduling Framework)。这个框架把调度过程分成了多个扩展点,比如PreFilter、Filter、PostFilter、PreScore、Score、Reserve、Permit、PreBind、Bind、PostBind等。你可以在每个扩展点插入自己的逻辑,实现各种复杂的调度需求。
除了默认的调度器,K8s还支持多调度器。你可以部署多个调度器,每个Pod可以指定用哪个调度器来调度。这样,不同类型的工作负载,可以用不同的调度策略。
调度器是K8s中最复杂的组件之一。理解了调度器的工作原理,你就能更好地配置调度策略,让Pod运行在最合适的节点上,提高集群的资源利用率和稳定性。
控制器:状态的调节者
K8s的核心思想是"声明式",你声明期望状态,K8s负责把当前状态调整到期望状态。负责这个调整工作的,就是各种控制器。
每个控制器,都在做同一件事情:监控资源的当前状态,和期望状态对比,如果不一致,就采取行动,把当前状态调整到期望状态。这个过程,叫做"调和循环"(Reconcile Loop)。
K8s有很多内置的控制器:
第一,Deployment Controller。负责管理Deployment。你创建一个Deployment,指定要3个副本,Deployment Controller就会创建ReplicaSet,然后由ReplicaSet Controller创建3个Pod。如果某个Pod挂了,ReplicaSet Controller会自动创建一个新的,保证始终有3个副本。
第二,StatefulSet Controller。负责管理有状态的应用,比如数据库。它会给每个Pod一个稳定的名称和网络标识,按顺序创建和删除Pod,保证有状态应用的稳定运行。
第三,DaemonSet Controller。负责管理DaemonSet,确保每个节点上都运行一个Pod的副本。当有新节点加入集群的时候,DaemonSet Controller会自动在新节点上创建Pod。
第四,Job Controller。负责管理一次性任务。它会创建Pod来运行任务,任务完成后Pod就退出。如果Pod失败了,Job Controller会根据配置决定是否重试。
第五,Node Controller。负责监控节点的状态。如果某个节点失联了,Node Controller会把它标记为NotReady,然后把这个节点上的Pod调度到其他节点上。
第六,Endpoint Controller。负责维护Service和Pod的关联。当Service对应的Pod发生变化的时候,Endpoint Controller会更新Endpoints对象,kube-proxy根据Endpoints来更新网络规则。
这些控制器,每个都有明确的职责,它们协同工作,保证集群的状态始终符合你的期望。
控制器的设计,是K8s最优雅的地方之一。每个控制器都是独立的,只关心自己负责的资源,通过apiserver来交互。这种设计,让K8s具有了很好的可扩展性,你可以很容易地添加自己的控制器,管理自定义的资源。
理解了控制器模式,你就理解了K8s的"自愈"能力是怎么来的。
kubelet:节点的管理者
kubelet是每个节点上的代理,是K8s在节点上的"代言人"。
kubelet的主要职责:
第一,Pod管理。kubelet通过apiserver的Watch机制,获取分配到这个节点上的Pod信息。然后,它调用容器运行时(containerd等),来创建和运行Pod中的容器。它还会监控容器的状态,如果容器挂了,会根据重启策略决定是否重启。
第二,节点上报。kubelet会定期向apiserver上报节点的状态,包括节点的资源使用情况(CPU、内存、磁盘)、节点的健康状态、节点上运行的Pod列表等。scheduler根据这些信息,来决定Pod调度到哪个节点。
第三,健康检查。kubelet会对Pod中的容器进行健康检查,包括livenessProbe(存活检查)和readinessProbe(就绪检查)。如果存活检查失败,kubelet会重启容器;如果就绪检查失败,kubelet会把这个Pod从Service的后端列表中移除,不让流量进来。
第四,资源管理。kubelet会管理节点上的资源,比如CPU、内存的分配。它会根据Pod的资源请求和限制,来保证每个Pod能获得它需要的资源,同时不会影响其他Pod。
第五,镜像管理。kubelet负责管理节点上的容器镜像,包括拉取镜像、清理无用的镜像等。
kubelet是通过CRI(Container Runtime Interface)来和容器运行时交互的。CRI是一个标准的接口,定义了kubelet和容器运行时之间的协议。这样,kubelet不需要关心具体的容器运行时是什么,只要实现了CRI接口,就能对接。目前主流的容器运行时是containerd,还有CRI-O等。
理解了kubelet,你就理解了Pod在节点上是怎么被创建和管理的。
网络模型:Pod之间的通信
K8s的网络,是很多人觉得最难理解的部分。但如果你掌握了它的核心原则,其实并不复杂。
K8s的网络模型,有几个核心原则:
第一,每个Pod都有一个独立的IP地址。Pod内的所有容器,共享这个IP地址和网络命名空间。也就是说,Pod内的容器可以通过localhost互相通信。
第二,Pod之间可以直接通信,不需要NAT。一个Pod的IP,可以直接访问另一个Pod的IP,中间不需要做网络地址转换。
第三,节点和Pod之间可以直接通信,也不需要NAT。
这三个原则,构成了K8s网络的基础。但具体怎么实现呢?这就要靠网络插件(CNI,Container Network Interface)了。
K8s本身不实现网络,而是定义了CNI接口,由第三方的网络插件来实现。常见的网络插件有Calico、Flannel、Cilium、Weave等。不同的插件,实现方式不同,但都遵循K8s的网络模型。
以Calico为例,它的实现方式是:
每个节点上,创建一个虚拟的网络设备(比如veth pair),Pod的网络命名空间通过这个设备和主机的网络命名空间连接。每个Pod分配一个IP,节点上有路由规则,把目标Pod的流量转发到对应的节点。节点之间通过BGP协议交换路由信息,知道每个Pod的IP在哪个节点上。
这样,当Pod A要访问Pod B的时候,流量从Pod A出来,到了节点A,节点A根据路由表,知道Pod B在节点B上,就把流量发到节点B。节点B收到流量后,根据路由表,把流量转发到Pod B。整个过程,没有NAT,Pod的IP是端到端可见的。
除了Pod之间的通信,K8s还有Service的概念。Service是一组Pod的访问入口,它有一个固定的IP(ClusterIP),访问这个IP的流量,会被负载均衡到后端的Pod。
Service的实现,靠的是kube-proxy。kube-proxy在每个节点上,通过iptables或者IPVS,设置网络规则。当访问Service的ClusterIP的时候,流量会被DNAT(目标地址转换)成某个后端Pod的IP,然后转发过去。
kube-proxy有三种模式:userspace(用户态,已经淘汰)、iptables(内核态,基于iptables规则)、IPVS(内核态,基于IPVS,性能更好)。在大规模集群中,IPVS模式的性能比iptables好很多。
还有一种更现代的方式,是eBPF。比如Cilium,用eBPF来实现网络和负载均衡,不需要kube-proxy,性能更好,功能更强大。K8s 1.32对eBPF的支持也越来越好。
理解了K8s的网络模型,你就能更好地排查网络问题,选择合适的网络插件。
存储系统:数据的持久化
容器是临时的,容器销毁了,里面的数据就没了。但很多应用需要持久化存储,比如数据库。K8s通过PV(PersistentVolume)和PVC(PersistentVolumeClaim)来管理持久化存储。
核心概念:
第一,PV(PersistentVolume)。这是集群中的一块存储资源,比如一块云盘、一个NFS共享、一个Ceph存储卷。PV是集群级别的资源,不属于某个命名空间。
第二,PVC(PersistentVolumeClaim)。这是用户对存储的请求,比如"我要一块10G的存储,读写权限是可读写"。PVC是命名空间级别的资源。
第三,StorageClass。这是存储的类型,比如"高性能SSD"、"普通云盘"、"NFS"。通过StorageClass,可以动态创建PV,不需要管理员提前创建。
工作流程是这样的:用户创建一个PVC,指定需要的存储大小和访问模式,以及StorageClass。K8s的存储控制器,会根据PVC的要求,通过StorageClass动态创建一个PV,然后把PV和PVC绑定。Pod引用这个PVC,就能使用这块存储了。
这个过程中,有几个关键的组件:
第一,PV Controller。负责PV和PVC的绑定。它会不断地寻找未绑定的PVC,然后找到匹配的PV,把它们绑定在一起。
第二,StorageClass Provisioner。负责动态创建PV。当有PVC需要动态创建PV的时候,Provisioner会调用存储后端的API,创建一块存储,然后创建对应的PV对象。
第三,Attach/Detach Controller。负责把存储卷挂载到节点上,或者从节点上卸载。当Pod被调度到某个节点上的时候,这个控制器会把PV对应的存储卷挂载到那个节点上。
第四,kubelet中的volume manager。负责在节点上把存储卷挂载到Pod的目录中。存储卷挂载到节点之后,kubelet会把它格式化(如果需要),然后挂载到Pod的容器中。
K8s的存储系统,支持很多种存储后端,比如云服务商的云盘、NFS、Ceph、GlusterFS、Local PV等。不同的存储后端,有不同的特点,适用于不同的场景。
K8s 1.32在存储方面也有一些改进,比如对CSI(Container Storage Interface)的支持更加完善,快照和克隆功能更加稳定,等等。
理解了K8s的存储系统,你就能更好地为有状态应用配置持久化存储,保证数据的安全和可靠。
1.32的新特性
最后,说说K8s 1.32的一些新特性和变化。
K8s每三个月发布一个版本,每个版本都会有一些新特性和改进。1.32版本,虽然还没正式发布,但从官方的路线图和Alpha/Beta版本来看,有几个值得关注的方向:
第一,对Sidecar容器的正式支持。Sidecar容器(比如服务网格的代理、日志收集器)在K8s中用得很多,但以前没有正式的支持,只能用普通容器来模拟,有很多问题,比如Sidecar的生命周期管理、资源计算等。1.32版本预计会正式支持Sidecar容器,让它有独立的生命周期和资源管理。
第二,调度器的增强。调度框架更加成熟,支持更多的扩展点和插件。同时,对多调度器、调度性能的优化也在持续进行。
第三,存储方面的改进。CSI的功能更加完善,快照、克隆、容量跟踪等功能更加稳定。对Local PV的支持也在增强。
第四,网络方面的改进。对eBPF的支持更好,Cilium等基于eBPF的网络插件能发挥更大的作用。Service的功能也在增强,比如对Service Internal Traffic Policy的优化。
第五,安全方面的增强。Pod安全标准(Pod Security Standards)更加成熟,取代了以前的PodSecurityPolicy。对Seccomp、AppArmor等安全特性的支持也在增强。
第六,可观测性的改进。Metrics Server、日志、追踪等功能更加完善,让你能更好地监控和排查集群的问题。
当然,这些都是基于目前的信息,最终的特性还要等正式发布。但可以看出,K8s正在朝着更加成熟、更加易用、更加安全的方向发展。
写在最后
K8s是一个复杂的系统,但它的底层机制是清晰的、优雅的。
从apiserver到etcd,从scheduler到controller,从kubelet到网络插件,每个组件都有明确的职责,它们协同工作,构成了一个强大的容器编排平台。
深入理解K8s的底层机制,不仅能让你更好地使用K8s,更高效地排查问题,还能让你学到分布式系统设计的很多思想。K8s的很多设计,比如声明式API、控制器模式、Watch机制、插件化架构,都是分布式系统设计的典范。
当然,K8s的内容太多了,一篇文章不可能讲完。这篇文章只是一个概览,帮你建立一个整体的理解。如果你想深入某个方面,建议去看官方文档、设计文档,甚至源码。
K8s还在不断发展,每个版本都有新的特性和改进。保持学习,跟上社区的步伐,才能真正掌握这个强大的工具。
最后,用一句话来结束这篇文章:"K8s的复杂,是为了让你的应用更简单。理解了它的底层机制,你就能驾驭这种复杂,让它为你所用。"
愿你在K8s的学习道路上,不断深入,不断进步。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录