
前两篇笔记我们把集群搭好、网络通了真正的高墙其实在后面一堆抽象概念砸过来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 Serveretcd记下来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排在工作负载里的头号位置因为它覆盖了绝大多数无状态应用的场景。它下面会管理ReplicaSetReplicaSet再负责管理Pod这层关系一开始很容易搞混。其实可以记成Deployment管“版本”ReplicaSet管“数量”Pod管“运行”。当你创建一个Deployment时k8s会自动创建一个ReplicaSet并指定它维护几个副本你更新镜像版本时Deployment会新创建一个ReplicaSet然后把旧的缩到0新的加到期望数量这个动作就是滚动更新。我经常这样类比Deployment像项目经理关心最终结果ReplicaSet像执行者只盯着“现在有几个Pod在跑”这件事。实际操作中滚动更新的两个参数最值得关注。maxSurge决定更新时最多能比期望多出几个副本maxUnavailable决定最多允许几个副本暂时不可用。假如有10个副本maxSurge25%maxUnavailable25%那更新过程中副本数会先到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 其他工作负载什么时候不用DeploymentDeployment很强大但它只适合无状态应用。所谓无状态就是哪个副本死了或者换了个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它才愿意收留这个Podlimits是“最高预算”超过就会触发CPU限流或内存驱逐。我见过不少事故就是只写limits不写requests导致调度器瞎调度、节点超卖Pod一启动就被驱逐。经验之谈requests按日常负载的80%来填limits按极端情况的120%~150%来填两者都写不要只写一个。监控和弹性扩容在生产环境几乎是标配。HorizontalPodAutoscalerHPA可以按CPU利用率或自定义指标自动调整副本数比如目标CPU利用率70%Pod超过这个值就扩容低于就缩容。但做HPA之前先把requests写对不然基于错误的基座去伸缩扩出来也是白扩。3. 服务发现与流量入口Service、Ingress 与 DNS3.1 Service 的本质为什么Pod必须有个“门牌号”Pod是动态的会随时被重建、漂移每次IP都会变化。总不能每次重建都去改配置文件吧Service就是来解决这个问题的它给一组Pod提供一个稳定的虚拟IPClusterIP并将流量负载均衡到后端的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端口转发到带有appblog-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-0DNS记录也随之不变。它还会按照序号依次启动和关闭保证主节点先就绪从节点再跟进。配合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那么业务根本不需要手动建PVPVC一提交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挂进去之后改ConfigMapPod里的文件会同步更新不过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 选不中 PodEndpoints 为空症状Service创建好之后ClusterIP也能ping通但业务访问不通。第一反应不是到处抓包而是看这个Service的Endpointskubectl get endpoints -n production blog-web-svc如果列表为空说明selector匹配不到任何Pod。常见原因有三个Pod的labels写错了比如Service里写app: blogPod里是app: blog-webService定义的命名空间和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的EndpointsstartupProbe则专门保护启动慢的应用。很多新手只配了liveness漏了readiness导致应用还在启动就被杀或者还没就绪就接收流量。我的建议是启动慢的Java应用先配startupProbe把失败阈值设大一点所有业务都加上readinessProbe别指望k8s帮你猜。5.3 从概念到落地对面试和真实项目的帮助经常有人问我学这些概念对面试到底有什么价值。其实k8s面试题翻来覆去就是那几类k8s和docker区别、Pod和Deployment关系、Service几种类型怎么选、StatefulSet怎么保证稳定标识、PV和PVC的关系、滚动更新参数含义。这些如果你在本地集群亲手操作过绝对比死记硬背强得多。真到了项目里概念清晰度直接决定你的架构设计能力。比如上周我们讨论新系统要不要上StatefulSet团队里有人直接说“数据库都是有状态的必须用”其实数据库完全可以跑在云托管数据库上集群内部的数据组件不一定都得自己管。这些判断全靠对工作负载模型的理解。概念本身不值钱但当你能把它们翻译成“这个场景该用哪个对象、为什么选它、不选它会出什么问题”的时候这些知识就变得值钱了。5.4 学习路径的实战建议如果你刚学到这里我的建议是不要急着背命令先把整套概念按“声明式管理”的主线串起来提交声明到API Serveretcd保存状态Controller对比差异Scheduler决定去向kubelet执行并反馈状态Service负责接入流量Storage和Config负责能力和配置供应。可以把kubectl get all当成验证工具每理解一个概念就创建一个对象看看它的状态变化再删掉重来。学完这篇笔记你已经有了足够扎实的概念地基下一篇我会沿着一个真实的Web应用把这套概念完整走一遍看一条请求从Ingress进入后经过Service、Pod、存储和配置最终是如何正常工作的。