前两篇笔记我们把集群搭好、网络通了,真正的高墙其实在后面:一堆抽象概念砸过来,Pod、Deployment、Service、Ingress、PV、PVC……英文缩写连在一起,每个字都认识,连起来就不知道它们在说什么。这篇k8s学习笔记(3)就想解决这个问题,把kubernetes核心技术概念挨个拆开讲清楚,讲明白它们各自管什么、彼此怎么协作,以及为什么非要有这些概念。这篇比较适合已经装好集群、但面对各种资源对象还犯晕的同学,也适合准备k8s面试、想理清概念关系的人。
学k8s和学其他技术不一样,它不是一个单点工具,而是一套分布式系统的操作系统。如果概念没串起来,照着文档敲命令都是云里雾里。我这套方法很朴素:先整体后局部,先知道谁在干活,再逐个抠细节,最后把一条请求链路完整走一遍,理解所有概念怎么串起来。这篇笔记就是这个思路。
1. 先看整体:k8s系统里到底谁在管谁
1.1 控制平面:整个集群的“大脑”
k8s架构首先得明白一个根本的分工:谁做决策,谁干活。做决策的这半边叫控制平面(Control Plane),通常跑在独立的master节点上,它由四个核心组件组成。
第一个是API Server,它是整个集群的“唯一入口”。执行kubectl命令时,请求最终都会打到API Server,不管是创建Deployment、查询Pod状态还是删除Service,都是先跟它打交道。API Server做的事情本质上就是接收请求、校验身份和权限、然后把这些期望状态写入存储。第二个是etcd,它是集群的“记事本”,所有配置、服务发现信息、节点状态全部存在这里。etcd是raft协议实现的高可用键值存储,集群挂了它不能挂,所以生产环境一般都建议单独部署至少三台etcd。
第三个是Scheduler,它的活儿只有一件:决定一个新Pod要跑到哪台节点上。听着简单,实际要考虑资源够不够、节点标签匹配不匹配、是否污点容忍、数据亲和性等等。第四个是Controller Manager,它是一系列控制器的集合,比如Node Controller、ReplicaSet Controller、Deployment Controller,它们干的事情可以理解成“纠偏”:不断检查当前实际状态是否符合期望状态,不符合就触发动作去改。控制器循环的英文叫controller loop,就是“我觉得应该是这样,我反复去对账”的过程。记住这个对账思维,后面几乎每个概念都能用上。
1.2 数据平面:真正跑业务的地方
另一半叫数据平面(Data Plane),就是真正运行业务容器的工作节点。每个worker节点上至少要有三个组件。
kubelet负责管理本节点的Pod生命周期,它通过API Server的watch机制监听与自己相关的Pod事件,然后调用容器运行时把容器拉起来。kube-proxy负责维护节点上的网络规则,Service的ClusterIP靠它写入iptables或ipvs规则才能转发到后端Pod。容器运行时(Container Runtime)是最底层执行容器命令的引擎,常见的有containerd、CRI-O,偶尔还有老项目用的Docker(现在通过cri-dockerd适配)。
把这些组件串起来看,你会发现一个清晰的模式:期望状态告诉API Server,etcd记下来,Controller负责比对,Scheduler负责安排,kubelet负责执行。这套模式叫声明式管理,它贯穿整个k8s的设计理念。想一下:如果你手动docker run去启动一个容器,机器挂了没人管;但k8s里你声明“我要3个副本”,如果节点宕了,Controller Manager会立刻在别的节点再拉起一个,让实际永远是3个。这才是k8s和传统容器管理最大的区别。
1.3 k8s 与 Docker 到底是什么关系
有个经典问题会劝退不少新手:k8s和Docker不是一回事吗?为什么有了Docker还要用k8s?这个我花了很长时间才彻底想通。Docker默认提供的核心能力是单机上的容器编排,docker run、docker compose这些都是在一台机器上玩。而k8s解决的是跨多台机器的资源调度、服务发现、弹性伸缩、自动故障恢复,这已经超出了Docker本身的职责范围。
更细致的说法是:k8s本身不直接运行容器,它通过容器运行时接口(CRI)与底层运行时通信。早期k8s默认使用的是Docker,后来为了减掉Docker Daemon这层额外DAEMON,直接换成了更轻量的containerd,所以现在新版的kubeadm安装默认就是containerd。你可以把Docker理解成“盖房子用的工具和材料”,k8s是“负责整栋楼的物业管理”。楼里的每套房子(Pod)用什么装修(镜像)、住多少人(副本数)、出了问题怎么修(自愈),这些是k8s管的;而一块砖头怎么烧制(容器怎么构建和运行),是容器运行时管的。搞清楚这层关系,去面试的时候再被问到“k8s和docker区别”,就不会答成分体式或继承式了。
2. 最小单位与工作负载:Pod、Deployment 和它们的兄弟
2.1 Pod:为什么是“最小调度单元”
很多人第一次接触Pod时有些困惑:既然容器已经能跑了,为什么k8s里面最小的调度单位不是Container,而是Pod?我当时的理解是:因为很多实际场景里,单个容器根本不够用,多个容器必须放在同一台主机上、共享网络和环境,这时候就得有个比容器高一层的东西来承载它们,这个就是Pod。
Pod是一组容器的集合,里面所有的容器共享同一个网络命名空间和存储卷。它们可以通过localhost直接互相访问,文件也可以通过共享卷来交换。典型例子是sidecar模式:一个主容器只做Web服务,旁边塞一个sidecar容器把日志转发到远端,或者负责刷新配置文件。这俩容器放在同一个Pod里,网络和存储共享,协作起来特别方便。Pod还有一个底层细节:真正让容器共享网络的那个基础设施容器叫pause容器,它先创建出网络命名空间,其余业务容器再join进去。所以就算我们在Pod里只写了nginx一个容器,节点上也会多出一个很小很小的pause容器,这个用crictl ps就能看到。
理解Pod,再去看后面的工作负载对象就轻松很多了,因为Deployment、StatefulSet这些对象并不直接操控容器,它们操控的底层对象是Pod。Pod的典型生命周期包括Pending、Running、Succeeded、Failed和Unknown这几种状态,我们排查问题时看的第一眼就是这里。
2.2 Deployment + ReplicaSet:一组完美的搭档
Deployment排在工作负载里的头号位置,因为它覆盖了绝大多数无状态应用的场景。它下面会管理ReplicaSet,ReplicaSet再负责管理Pod,这层关系一开始很容易搞混。其实可以记成:Deployment管“版本”,ReplicaSet管“数量”,Pod管“运行”。
当你创建一个Deployment时,k8s会自动创建一个ReplicaSet,并指定它维护几个副本;你更新镜像版本时,Deployment会新创建一个ReplicaSet,然后把旧的缩到0,新的加到期望数量,这个动作就是滚动更新。我经常这样类比:Deployment像项目经理,关心最终结果;ReplicaSet像执行者,只盯着“现在有几个Pod在跑”这件事。
实际操作中,滚动更新的两个参数最值得关注。maxSurge决定更新时最多能比期望多出几个副本,maxUnavailable决定最多允许几个副本暂时不可用。假如有10个副本,maxSurge=25%,maxUnavailable=25%,那更新过程中副本数会先到12个,最多允许2个不可用,这样业务永远不会瞬间全部中断。所以做发布时,先把这两个参数调整好,比直接调副本数有意义得多。下面这段yaml就是一个很标准的Deployment写法:
apiVersion: apps/v1 kind: Deployment metadata: name: blog-web namespace: production spec: replicas: 3 strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0 selector: matchLabels: app: blog-web template: metadata: labels: app: blog-web spec: containers: - name: blog-web image: registry.example.com/blog-web:1.4.2 ports: - containerPort: 8080 resources: requests: cpu: 250m memory: 512Mi limits: cpu: 1000m memory: 1Gi2.3 其他工作负载:什么时候不用Deployment
Deployment很强大,但它只适合无状态应用。所谓无状态,就是哪个副本死了或者换了个IP重新起来,业务完全不受影响。但数据库、消息队列这种应用就不行了,它们需要稳定的网络标识和独立的存储,这时候就要用StatefulSet。StatefulSet会给每个副本一个固定的序号和稳定的主机名,比如web-0、web-1,并且启动顺序也是从0到N逐个进行,方便主从初始化。
再看另外几种工作负载类型。DaemonSet保证每个节点上最多运行一个Pod,最适合做日志采集(比如Filebeat)、监控Agent(比如node-exporter)、网络插件(比如Calico),因为你希望每台机器都有一份,不能多也不能少。Job适合跑一次性任务,比如数据库迁移脚本、批量数据处理;CronJob就是按时间表定时执行的Job,比如每天凌晨清理日志。
它们之间的选择,用一张表就能看明白:
| 工作负载类型 | 适用场景 | 典型例子 | 副本策略 |
|---|---|---|---|
| Deployment | 无状态应用,可随时替换 | Web服务、API网关、前后端 | 任意数量 |
| StatefulSet | 有状态应用,需要稳定标识和独立存储 | MySQL主从、Redis集群、Kafka | 固定序号 |
| DaemonSet | 每个节点必须运行且只运行一个 | 日志采集、节点监控、网络组件 | 每节点一个 |
| Job | 一次性任务,跑完即结束 | 数据迁移、批量计算 | 完成即退出 |
| CronJob | 定时循环任务 | 定期备份、定时清理 | 按调度周期 |
2.4 副本数与资源请求怎么算
聊完工作负载类型,还有一个绕不开的话题:副本数该设几个,容器资源请求该写多少。很多新手直接抄模板,CPU写500m、内存写512Mi,也不管够不够。我的建议是先压测,再根据压测结果往回推。假设单副本在高峰期需要250m CPU和512Mi内存,线上QPS预期是峰值的两倍,单副本能扛住三分之一峰值流量,那副本数至少3个才稳妥。
这里有两个概念必须分清:requests和limits。requests是调度器的“最低工资要求”,节点剩余资源必须满足requests它才愿意收留这个Pod;limits是“最高预算”,超过就会触发CPU限流或内存驱逐。我见过不少事故就是只写limits不写requests,导致调度器瞎调度、节点超卖,Pod一启动就被驱逐。经验之谈:requests按日常负载的80%来填,limits按极端情况的120%~150%来填,两者都写,不要只写一个。
监控和弹性扩容在生产环境几乎是标配。HorizontalPodAutoscaler(HPA)可以按CPU利用率或自定义指标自动调整副本数,比如目标CPU利用率70%,Pod超过这个值就扩容,低于就缩容。但做HPA之前先把requests写对,不然基于错误的基座去伸缩,扩出来也是白扩。
3. 服务发现与流量入口:Service、Ingress 与 DNS
3.1 Service 的本质:为什么Pod必须有个“门牌号”
Pod是动态的,会随时被重建、漂移,每次IP都会变化。总不能每次重建都去改配置文件吧?Service就是来解决这个问题的,它给一组Pod提供一个稳定的虚拟IP(ClusterIP),并将流量负载均衡到后端的Pod上。从本质上看,Service做的事和nginx反向代理很像:一个固定入口,后面挂一组随时可能变化的后端。
Service怎么知道后端有哪些Pod?靠selector。它通过标签选择器匹配Pod的labels,然后自动创建Endpoints对象,里面记录所有匹配Pod的IP。创建Service的yaml也很直观:
apiVersion: v1 kind: Service metadata: name: blog-web-svc namespace: production spec: selector: app: blog-web ports: - protocol: TCP port: 80 targetPort: 8080这个Service的含义是:访问ClusterIP的80端口,转发到带有app=blog-web标签的Pod的8080端口。你可能会问:ClusterIP只在集群内部可达,怎么让外部访问?这就是下面要说的Service类型问题。
3.2 四种 Service 类型该选哪个
Service一共有四种类型,分别对应不同的访问场景。
ClusterIP是默认类型,只能在集群内部通过虚拟IP访问。它一般给内部组件互相调用用,比如前端调后端Service,不需要暴露到集群外。NodePort会在每个节点上开一个高位端口(30000-32767),访问节点IP+端口就能把流量转进集群,适合临时调试或小规模流量场景。LoadBalancer通常在公有云上使用,云厂商会创建一个云负载均衡器,自动绑定若干节点,流量先进负载均衡器再转发到NodePort,它实际上是对NodePort的一层封装。ExternalName则比较特殊,它不产生流量转发规则,只是把Service名解析到集群外部的DNS名称,比如让你在集群内部访问外部的RDS数据库时,代码里只需要写一个稳定的Service名。
| 类型 | 访问范围 | 原理 | 适用场景 |
|---|---|---|---|
| ClusterIP | 集群内部 | 虚拟IP + kube-proxy规则 | 内部微服务调用 |
| NodePort | 集群外部,节点IP:端口 | 节点端口映射到ClusterIP | 测试、小流量暴露 |
| LoadBalancer | 公网/云平台 | 云负载均衡器 + NodePort | 生产环境对外提供访问 |
| ExternalName | 集群内部 | DNS CNAME 到外部域名 | 访问外部组件 |
生产环境最推荐的路径是:Ingress -> Service(NodePort/LoadBalancer) -> Pod。Ingress作为七层负载均衡统一入口,管理域名的路由规则;Service作为四层负载均衡,把流量转发到具体的Pod上。两者分工明确,一个管“路”,一个管“分配”。
3.3 Ingress:一个门卫搞定所有域名路由
如果没有Ingress,每个Service都要暴露一个NodePort,企业域名一多,端口就乱成一锅粥。Ingress允许你通过域名+路径来路由到不同Service,相当于把“入口流量分发”这件事收口到一个组件上。
需要注意,Ingress本身只是一堆规则定义,真正干活的是Ingress Controller,比如nginx-ingress-controller或者traefik。我在实际部署时,先装好nginx-ingress-controller,再定义一个Ingress规则就能实现“同一IP、不同域名访问不同服务”的效果:
apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: example-ingress namespace: production spec: ingressClassName: nginx rules: - host: app.example.com http: paths: - path: /api pathType: Prefix backend: service: name: api-gateway-svc port: number: 80 - path: / pathType: Prefix backend: service: name: web-svc port: number: 80这个Ingress看起来很长,核心就两句:请求app.example.com/api的转发到api-gateway-svc,其余全部转发到web-svc。pathType重点说下:Prefix表示前缀匹配,Exact表示完全匹配。有时候写错pathType,路由死活不生效,就是这里的问题。
3.4 集群内 DNS 解析与排查命令
在集群内,服务之间通过Service名通信,靠的是CoreDNS这个集群内DNS组件。它自动监听集群里的Service和Pod变化,动态生成DNS记录。比如production命名空间下有个服务blog-web-svc,那么同一命名空间内的Pod直接访问blog-web-svc就能解析到Service的ClusterIP;跨命名空间则要写全名blog-web-svc.production.svc.cluster.local。
排查这个问题时,我习惯先看Service的Endpoints有没有值,如果没有值,说明selector没匹配到Pod。然后进一个Pod里试解析,比如kubectl exec -it pod-name -- nslookup blog-web-svc,如果解析不了,就先查CoreDNS的Pod是否存活、再查网络策略是否拦了53端口。这些是服务发现高频踩坑点,后面第五节还会详细展开。
4. 有状态应用、存储与配置管理的核心概念
4.1 StatefulSet 与有状态应用的特殊之处
Deployment创建的Pod都是一模一样的,没有顺序、没有身份,删除后重建的名字都变了。但很多中间件不是这样。比如MySQL主从,主节点的身份必须稳定,从节点初始化时需要知道主节点的地址;再比如Kafka,每个broker的ID不能随便变。StatefulSet就是为这类场景设计的。
StatefulSet会给每个副本分配一个固定序号,比如mysql-0、mysql-1、mysql-2,并且每个Pod有一个稳定的网络标识。即使Pod被删除重建,名字还是mysql-0,DNS记录也随之不变。它还会按照序号依次启动和关闭,保证主节点先就绪,从节点再跟进。配合Headless Service(即ClusterIP为None的Service),可以让Pod通过固定主机名直接访问,而不再经过负载均衡。这种“一个一个处理”的策略,对有状态应用非常友好,但也导致扩容速度比Deployment慢,这是合理的代价。
4.2 PV、PVC、StorageClass:三件套的存储抽象
刚接触存储的时候,我总觉得PV、PVC、StorageClass这三个概念绕来绕去。后来用一个类比理解透了:PV是“仓库里已经备好的货”,PVC是“你要采购的清单”,StorageClass是“自动供货的供应商”。系统管理员提前准备好一批PV,业务方提交PVC申请存储大小,k8s再把匹配的PV绑定给PVC。
PV的访问模式、回收策略也是理解重点。ReadWriteOnce表示只能被单个节点以读写方式挂载,ReadOnlyMany支持多个节点只读,ReadWriteMany支持多个节点读写。回收策略有Retain(保留)、Recycle(已废弃)、Delete(删除PV时同步删除底层存储)。生产环境我一般选Retain,虽然需要人工清理旧数据,但不会误删快照或云盘。
如果集群配置了StorageClass,那么业务根本不需要手动建PV,PVC一提交,csi-provisioner会自动从云盘或分布式存储里创建一块新卷并生成PV,这就是动态供给。下面这条PVC的声明,几分钟内就能看到Bound状态:
apiVersion: v1 kind: PersistentVolumeClaim metadata: name: mysql-data namespace: production spec: accessModes: - ReadWriteOnce storageClassName: csi-ssd resources: requests: storage: 20Gi4.3 ConfigMap 与 Secret:配置和敏感信息分开存放
一个应用往往有大量配置,数据库地址、日志级别、限流阈值等等。如果每次改配置都要重新构建镜像,那就太蠢了。k8s的解法是ConfigMap:把配置和镜像解耦,Pod运行时再将配置注入进去。ConfigMap支持三种使用方式:环境变量、文件挂载、命令行参数。文件挂载最常见,比如把nginx.conf挂进去,之后改ConfigMap,Pod里的文件会同步更新,不过nginx服务本身可能需要reload一下。
Secret和ConfigMap很像,但专门用来存敏感内容,比如密码、证书、Token。注意一个坑:Secret的值只是做了一层Base64编码,不是加密,直接用echo可以解出来。所以生产环境一定要开启etcd加密存储,并配合RBAC限制Secret的访问权限,这样才算安全。ConfigMap和Secret的注入逻辑基本一样,但更新后的生效机制不同:环境变量方式的配置改动不会自动更新到Pod里;文件挂载方式的配置改动,理论上一段时间后会同步,但很多应用不会自动重载配置文件。想稳妥的话,改完配置后滚动重启一下工作负载,让新Pod读取最新配置。
4.4 配置更新的实操心得
我踩过一个很实际的坑:某个服务把日志级别写在ConfigMap里,我改了ConfigMap,等了半天,Pod里的文件确实变了,但应用日志还是老样子,因为程序启动时就把配置加载到内存里了,不会自动重新读。后来我在团队里定了两条规矩:所有运行中的服务更新配置后必须做一次滚动重启;所有新服务必须支持SIGHUP信号或者提供一个reload接口,这样才能跟k8s的配置管理机制配合好。这个经验算是我在配置管理这块最重要的收获。
5. 概念没吃透时踩过的坑:排查记录与面试经验
5.1 Service 选不中 Pod,Endpoints 为空
症状:Service创建好之后,ClusterIP也能ping通,但业务访问不通。第一反应不是到处抓包,而是看这个Service的Endpoints:
kubectl get endpoints -n production blog-web-svc如果列表为空,说明selector匹配不到任何Pod。常见原因有三个:Pod的labels写错了,比如Service里写app: blog,Pod里是app: blog-web;Service定义的命名空间和Pod不在同一个命名空间;Pod虽然存在,但都因为健康检查失败没有Ready,不在Endpoints里体现。再扩展一点,如果Pod有多个端口,Service的targetPort记得要匹配容器里实际监听的端口,不能用SERVICE端口去撞。这是我排过最多的服务不通问题,基本一条命令就能找到方向。
5.2 Pod 一直 Pending,或者频繁重启
Pod状态卡在Pending,说明调度器还没把它安排到节点上,通常就是资源不足。用kubectl describe pod查看Events,会看到类似0/3 nodes are available: insufficient cpu这样的提示。要么给节点加资源,要么调小Pod的requests,要么加节点。还有一种情况是节点上有污点而Pod没有容忍,也会卡在Pending。
如果Pod能启动,但一直CrashLoopBackOff,大概率是启动命令或健康检查有问题。健康检查这里必须提三类探针:livenessProbe决定Pod是否需要重启,readinessProbe决定Pod是否加入Service的Endpoints,startupProbe则专门保护启动慢的应用。很多新手只配了liveness,漏了readiness,导致应用还在启动就被杀,或者还没就绪就接收流量。我的建议是:启动慢的Java应用先配startupProbe,把失败阈值设大一点;所有业务都加上readinessProbe,别指望k8s帮你猜。
5.3 从概念到落地:对面试和真实项目的帮助
经常有人问我学这些概念对面试到底有什么价值。其实k8s面试题翻来覆去就是那几类:k8s和docker区别、Pod和Deployment关系、Service几种类型怎么选、StatefulSet怎么保证稳定标识、PV和PVC的关系、滚动更新参数含义。这些如果你在本地集群亲手操作过,绝对比死记硬背强得多。
真到了项目里,概念清晰度直接决定你的架构设计能力。比如上周我们讨论新系统要不要上StatefulSet,团队里有人直接说“数据库都是有状态的,必须用”,其实数据库完全可以跑在云托管数据库上,集群内部的数据组件不一定都得自己管。这些判断,全靠对工作负载模型的理解。概念本身不值钱,但当你能把它们翻译成“这个场景该用哪个对象、为什么选它、不选它会出什么问题”的时候,这些知识就变得值钱了。
5.4 学习路径的实战建议
如果你刚学到这里,我的建议是不要急着背命令,先把整套概念按“声明式管理”的主线串起来:提交声明到API Server,etcd保存状态,Controller对比差异,Scheduler决定去向,kubelet执行并反馈状态,Service负责接入流量,Storage和Config负责能力和配置供应。可以把kubectl get all当成验证工具,每理解一个概念就创建一个对象,看看它的状态变化,再删掉重来。学完这篇笔记,你已经有了足够扎实的概念地基,下一篇我会沿着一个真实的Web应用,把这套概念完整走一遍,看一条请求从Ingress进入后,经过Service、Pod、存储和配置,最终是如何正常工作的。