1. 先搞清楚:资源和对象到底是不是一回事
1.1 对象是什么:K8s 里的“持久化实体”
很多人刚开始接触 Kubernetes 时,会把“资源”和“对象”混着说,好像它们是一回事。其实在 K8s 的术语体系里,这两个词指的不是同一个层面的东西,但又有极强的关联。先讲对象。
Kubernetes 里的对象,是指在集群里被"持久化保存"的一个实体。简单说,只要你能通过kubectl get查到的、能写到 etcd 里的东西,都可以叫对象。比如你运行kubectl get pods,查到的每个 Pod 都是一个对象;kubectl get deployment查到的每个 Deployment 也是一个对象。这些对象记录了“我们希望这个集群处于什么状态”。
为什么要搞得这么绕?因为 K8s 本质上是一个“状态协调系统”。它不像传统运维那样,直接执行一条命令把进程启动起来就算完事。K8s 的做法是:你把想要的状态写成一个声明,交给 API Server,然后集群里的各种控制器(Controller)不停地在后台比对“期望状态”和“当前状态”,发现有差异就去修正,直到两者一致。对象就是这个声明的载体,它存在 etcd 里,就像数据库里的一条记录,甚至你重启整个集群,只要 etcd 数据还在,这些对象就还在。
在这个体系里,Pod、Service、ConfigMap、Secret、Namespace、Node、PersistentVolume,全都被建模成了对象。每个对象有一个名字、一组标签、一段期望状态的描述,以及一段实际状态的反馈。你可以把对象理解成“K8s 认知世界的最小单元”。
1.2 资源是什么:API 端点与对象类型的映射
那“资源”又是什么?在 K8s 的语境里,资源指的是 API 层面对某种对象类型的暴露方式。打开一份 K8s API 文档,你会看到/api/v1/pods、/apis/apps/v1/deployments这样的路径,这些路径背后对应的就是某种资源。换句话说,资源是“对象类型”在 HTTP API 层面的名字,它决定了你通过 RESTful 接口可以访问哪些对象。
所以你会发现kubectl get pods和kubectl get pod都能用。因为 K8s 的资源名通常同时支持单数和复数。在 API 层面,复数形式更像“资源名”,而单数形式是 Kind(对象类型)。类似地,deployments是一种资源,它的 Kind 是Deployment,你创建出来的那个具体的东西才是“对象”。
听起来有点学院派,但这个区分在实际排错中真的有用。比如你看到报错信息说“无法枚举容器中的对象,访问被拒绝”,十有八九是 RBAC 权限里没有给你授权对应的资源。这时候你就要搞清楚,你操作的到底是一个叫什么名字的资源,是pods还是deployments,是在哪个 API 组下面,然后去检查 Role 和 RoleBinding。搞不清楚资源和对象的关系,连错误信息都读不懂。
另外一个特别容易踩的坑:K8s 里有些“资源”并不对应“对象”。比如kubectl get events,events 是一个资源,你在 API Server 里可以查询到,它可以被归类为资源,但你不太会把它叫“事件对象”。还有kubectl api-resources命令列出的所有资源,也不全是对象,比如bindings这个资源,你没法kubectl edit bindings。所以准确地说,对象是资源的一种实体化存在,而资源是对象类型的对外接口。
1.3 两者关系的通俗类比
为了帮助记忆,我喜欢用一个类比:把“资源”理解成数据库里的“表”,把“对象”理解成“表里的一行记录”。pods是表名,具体的nginx-pod-abc123是一行记录。你能对表做增删改查,但你读出来的数据是以“行”为单位的。
这个类比有两点特别贴合 K8s 的实际:
- 每个表(资源)都有固定的“列”,对应到 K8s 里就是对象的字段,比如 metadata、spec、status,你不能随意往里加列,改了 schema 就算换了一种资源。
- 一个资源类型下可以有很多对象。比如你跑了一个 30 个副本的 Deployment,那么
pods资源下会有 30 个 Pod 对象,但deployments资源下只有 1 个 Deployment 对象。这也就是为什么kubectl get pods会列出一长串,而kubectl get deployment只有一行。
明白了这个区分之后,我们再来聊一聊对象本身是怎么设计的。因为理解了对象的内部结构,你才能真正读懂一个 YAML 文件,而不是靠复制粘贴。
2. 对象的内部构造与设计逻辑
2.1 五大核心部分:apiVersion、kind、metadata、spec、status
任何一个 K8s 对象,无论简单还是复杂,在 API 层面都逃不出这几个字段:apiVersion、kind、metadata、spec、status。其中前四个是你写 YAML 时要关心的,status则是 API Server 和控制器写进去的,你正常不需要也不应该去手动改它。
apiVersion和kind是“身份信息”,它们决定了你写的这一段 YAML 到底会被 API Server 识别成什么。比如apiVersion: apps/v1加kind: Deployment,API Server 就会把它路由到 Deployment 对应的校验逻辑和存储位置。如果你把apiVersion写错,比如写成了v1,API Server 会直接拒绝,因为v1这个版本下根本没有 Deployment 这种资源。这也是 K8s 升级时常见的兼容性问题——旧版本的对象用的是extensions/v1beta1,新版本早就移除了,所以升级前必须把清单文件里的 apiVersion 全部刷新。
metadata是“身份标签”,至少包含name和namespace,还可以带labels、annotations、ownerReferences等。名字在同一个 namespace 下必须是唯一的;labels 是给对象打标签,供选择器(selector)匹配;annotations 是存放非标识性的附加信息;ownerReferences 用于表达对象之间的父子关系——比如 Pod 的 owner 是 ReplicaSet,ReplicaSet 的 owner 是 Deployment。这个字段极其重要,删 Deployment 的时候,它下面的 ReplicaSet 和 Pod 会被级联删除,靠的就是 ownerReferences 的链式传递。
spec是“期望状态”,这是你最常写、最常改的部分。对于不同的对象类型,spec 的结构完全不同。Deployment 的 spec 里有 replicas、selector、template;Pod 的 spec 里有 containers、volumes、restartPolicy。你写的 spec 是指导集群“该干什么”的蓝图。status是“实际状态”,由集群里的控制器负责实时更新。Pod 的 status 会告诉你容器是否 Ready、IP 是多少、为什么没起来;Deployment 的 status 会告诉你当前副本数、可用副本数。
2.2 spec 到 status:控制循环如何“照单上菜”
K8s 最核心的运行机制,就是“控制器模式”。几乎所有对象管理,都是靠控制循环(Control Loop)实现的。控制循环的逻辑可以浓缩成三句话:看 status 和 spec 差多少,做动作去缩小差距,更新 status 反馈新状态。
你可以想象一个餐厅后厨:客人(你)把菜单(spec)递进去,厨师(控制器)看了一眼现在厨房里有什么菜(status),发现缺了一道,就开始做(reconcile),做完之后把菜品端上桌,并更新厨房的库存记录(status)。这个过程不断地重复,所以即使有人手动删掉了一个 Pod,Deployment 控制器马上会发现实际的 Pod 数量少于期望的副本数,于是再创建一个新的 Pod,让系统重新回到声明中的状态。
理解了控制循环,你就能回答面试里常被问到的一个问题:为什么 K8s 不需要手动去重启挂掉的容器?因为 Kubelet 控制器会盯着 Pod 的 status,发现容器退出了,就按 restartPolicy 去拉起一个,或者由 ReplicaSet 控制器新建 Pod 顶上。控制器模式把"管理动作"从"人"转移给了"程序",这是声明式管理方式能成立的根本前提。
不过这里也藏着一个坑:控制循环只能识别出“期望状态和实际状态有偏差”,但它不会问为什么会有偏差。比如你把 Deployment 的副本数从 3 改成 10,控制器会去创建 7 个新 Pod;如果集群资源不够,它会一直尝试,但最终只是把 Pod 挂在 Pending 状态。你去看 Deployment 的 status,会看到 readyReplicas 小于 replicas,但控制器不会主动告诉你“亲,集群没资源了”,你得自己从事件或者 Pod 的条件里去分析原因。
2.3 实操心得:哪些字段最容易写错
我见过很多新手甚至中级开发,在写对象清单时反复踩同一批坑,这里集中说一下。
第一,spec.selector必须和template.metadata.labels匹配。Deployment 的 selector 用来圈定它管理的 Pod,如果 selector 里写的标签和 template 里的标签不一致,Deployment 创建会直接被 API Server 拒绝,或者创建出来之后控制器找不到它该管的 Pod,行为会非常诡异。有的版本会直接报“selector does not match template labels”这种错误,有的则不会。我自己曾经把 selector 写成app: nginx,但 template 的标签写成了app: web,结果 Deployment 创建成功却一直不产生 ReplicaSet,查了半天才发现是标签对不上。特别提醒:Deployment 的 selector 一旦创建就不可修改,这是 K8s 硬性限制。所以创建前最好想清楚标签方案。
第二,metadata.name不能带大写字母和下划线。K8s 对对象名称有一套 DNS 子域的规范,小写字母、数字、中划线、点都可以,但下划线不行。很多从 Java 或 Python 习惯里带过来的人,下意识地在 Deployment 名字里写下划线,结果 apply 直接报错“Invalid value: ... must be lowercase”。这个错见多了你反而觉得很亲切,因为它至少明确告诉你怎么改。
第三,spec.template里千万别写name。Pod 模板的 metadata 确实可以没有 name,因为 Pod 的实际名字由控制器自动生成。如果你在 template 里写死了 name,会造成所有副本的 Pod 使用同一个名字,控制器生成对象的时候会互相冲突,立刻报错。记住:模板的 metadata 只需要 labels,name 交给控制器去拼接。
第四,status字段是只读的。有些人图省事,想直接改 Pod 的 status 让它变成 Running,这不可能。API Server 会拒绝所有对 status 的写操作,status 只能由控制器更新。换句话说,你不能“骗”系统,只能老老实实让容器真正启动起来。
3. 资源的管理方式:三种流派怎么选
3.1 命令式指令:kubectl run、expose、scale
K8s 自带的 kubectl 天然支持一种“命令式”管理方式。所谓命令式,就是你直接告诉它“我要创建一个名为 nginx 的 Deployment,镜像用 nginx:1.25”,一条命令下去,对象创建完毕,kubectl run nginx --image=nginx:1.25 就是这么用的。想扩容?kubectl scale deployment nginx --replicas=5。想暴露服务?kubectl expose deployment nginx --port=80。
这种方式的优点是快,适合测试环境和临时调试。缺点是它“不可追溯、不可复现”。你执行完命令之后,如果想知道当前 Deployment 到底有哪些配置,你确实可以 kubectl get deployment -o yaml,但你很难回答“这个 Deployment 是谁在什么时候改成这样的”。而且,如果你的同事也执行了一条命令修改同一个 Deployment,你俩的命令会互相覆盖,没有冲突检测,没有合并逻辑,生产环境用这种方式管理,基本等于裸奔。
我在实战中见过不少把命令式指令用进生产环境的人,最后基本都被"配置漂移"坑过。比如某天有个开发为了快速排查问题,直接 kubectl scale 了一下无状态服务,扩到 10 个副本,然后忘了缩回来。账单上是小事,关键是监控告警里突然多出了很多副本,大家还以为集群被攻击了。所以我的结论是:命令式指令只适合当"瑞士军刀"用来应急,不能作为日常管理手段。
3.2 命令式对象配置:kubectl create -f
介于命令式和声明式之间,还有一个过渡流派:命令式对象配置。做法是你写一个 YAML 文件,然后用kubectl create -f file.yaml来创建。这种方式比裸命令好一些,因为配置至少落在文件里,有了一份可读的"配置快照"。
但问题在于,kubectl create的语义是"创建",如果你对同一个文件执行两次,第二次会报错"AlreadyExists",因为对象已经存在了。想修改的话,你得先kubectl delete -f file.yaml,再kubectl create -f file.yaml。这个“先删后建”的过程在生产环境就是灾难,因为删除和重建之间存在窗口期,服务会中断。而且一旦你执行了 delete,不仅 Deployment 没了,它下面的 ReplicaSet 和 Pod 也会连带没掉,影响面比你预期的大得多。
另一个隐患是:kubectl create不会自动更新所有字段。比如你想给 Deployment 加一个 env 变量,如果只是修改 YAML 再 create,大概率只会提示已存在,不会真正变更新配置。所以命令式对象配置在 K8s 的“三驾马车”里,算是一个过渡方案,适合学习阶段用来感受"配置即代码",但不适合生产。
3.3 声明式对象配置:kubectl apply -f 与 GitOps
生产环境真正推荐的是声明式对象配置,核心命令就是kubectl apply -f file.yaml。apply和create最大的区别在于:apply 会计算当前对象和 YAML 文件的差异,然后做一次“合并更新”。文件里写的字段会覆盖集群里已有的值,但文件里没写的字段会被保留,不会清零。所以你可以安全地对同一个文件反复执行 apply,对象会被一次次“收敛”到文件所描述的状态。
这个机制实现的关键是注释里的kubectl.kubernetes.io/last-applied-configuration。你在集群里kubectl get deployment -o yaml时,会看到一个名叫kubectl.kubernetes.io/last-applied-configuration的 annotation,里面存着上次 apply 的完整 YAML 内容。apply 时,K8s 会把新的 YAML 跟这个 annotation 做三方合并:旧文件、新文件、当前对象。最终生成一份新的对象配置,并更新这个 annotation。
有了 apply,你就可以放心大胆地引入 GitOps 了。把所有的 YAML 文件放进 Git 仓库,合并请求就是变更流程,CI/CD 管道检测到仓库变化后自动执行kubectl apply,集群状态永远和仓库保持同步。想要回滚?git revert 一个提交,再 apply 一次,就回去了。这条链路我听过的实战案例太多了,比如 Argo CD 就是干这个的。它的好处不只是“自动化”,更重要的是“审计”:每一次对集群的修改都有记录,谁改的、改了什么、为什么改,全部有据可查。
3.4 三种方式对比表格与选型建议
| 管理方式 | 典型命令 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 命令式指令 | kubectl run、kubectl scale | 快速、灵活 | 不可复现、无审计、易覆盖 | 测试环境、临时调试 |
| 命令式对象配置 | kubectl create -f | 配置可读、可复用 | 无法增量更新、先删后建有中断风险 | 学习阶段、一次性初始化 |
| 声明式对象配置 | kubectl apply -f | 增量合并、可回滚、适合自动化 | 需要理解合并原理、首次上手略抽象 | 生产环境、GitOps、团队协作 |
选型建议很简单:如果你在写一个学习笔记、搭一个测试环境,随便怎么玩;如果你负责的是生产集群,哪怕只有 3 个节点、只有 5 个服务,也请务必走声明式配置,并且尽早把清单文件纳入版本控制。我会在下一节用一个实际例子带你走一遍完整的声明式管理流程。
4. 实操:声明式部署一套应用并配置资源配额
4.1 编写完整的 Deployment 清单
理论知识说再多,不如手写一遍。下面我来构建一个真实场景:部署一个 nginx 应用,3 个副本,然后给每个副本设置资源请求和限制,再创建对应的 Service 对外暴露,最后配置命名空间级别的资源配额。
先看 Deployment 清单:
apiVersion: apps/v1 kind: Deployment metadata: name: nginx-web namespace: web labels: app: nginx-web spec: replicas: 3 selector: matchLabels: app: nginx-web template: metadata: labels: app: nginx-web spec: containers: - name: nginx image: nginx:1.25 imagePullPolicy: IfNotPresent ports: - containerPort: 80 name: http resources: requests: cpu: 250m memory: 128Mi limits: cpu: 500m memory: 256Mi readinessProbe: httpGet: path: /nginx-health port: 80 initialDelaySeconds: 5 periodSeconds: 10这里有几个细节值得讲。requests和limits不是摆设,它们直接决定 Pod 落在哪个节点上,以及节点在资源紧张时怎么处理容器。requests是调度器做资源分配的依据,比如一个节点总 CPU 是 4 核,已经分配出去的 requests 达到 3.75 核时,再想调度一个请求 CPU 为 500m 的 Pod,就会失败。limits是运行时资源上限,容器超了内存 limit 会触发 OOM Killed,超了 CPU limit 会被限流而不是被杀死。热词里提到的"容器资源隔离"、"资源受限机器人",本质就是在讲 requests/limits 的调度与隔离效果。
readinessProbe是就绪探针,只有探针成功后,Pod 才会被加入 Service 的负载均衡池。这个是一个容易被忽略但非常重要的字段,特别是多副本服务滚动更新时,如果就绪探针配置不当,新版本 Pod 还没真正就绪就会被 Endpoint 选中,导致部分请求打到不可用实例上。生产环境常见故障案例"k8s生产环境中常见的故障影响到用户",不少都和技术健康检查缺失有关。
4.2 编写 Service 与 ExternalIP 配置
Deployment 只是对象的“第一极”,它管理的是 Pod 的生命周期。但 Pod 的 IP 是随时变化的,你总不能让客户端去追着 Pod IP 访问。所以需要 Service 对象来提供稳定的访问入口。
apiVersion: v1 kind: Service metadata: name: nginx-web-svc namespace: web spec: type: ClusterIP selector: app: nginx-web ports: - port: 80 targetPort: 80 protocol: TCP这个 Service 的selector会去匹配所有带app: nginx-web标签的 Pod,之后 Service 的 ClusterIP 会作为统一入口,请求会被负载均衡到后端的多个 Pod 上。
如果你希望让 Service 在集群外部也能访问,而且你有固定的机房 IP 可以分配,可以用 type: LoadBalancer 或者直接配置 ExternalIP。ExternalIP 这个概念比较冷门,但挺实用。做法是在 Service 的 spec 里直接加一个externalIPs: [192.168.1.100],集群外的流量如果访问192.168.1.100:80,会被节点上的 kube-proxy 转发到对应的后端 Pod。这种方式不需要 Cloud Provider 的支持,在裸机部署 K8s 时很常见。需要注意的是,externalIPs 不会自动分配,你需要保证这个 IP 在集群网络里是可达的,而且要避免和别的服务冲突。
4.3 配额与隔离:LimitRange、ResourceQuota 配置
再往下,我建议每个团队都配置命名的资源配额。生产环境里多个团队共享一个集群时,如果不设配额,一个"膨胀"的应用可能把集群资源吃光,影响其他所有租户,这比服务故障本身还让人头疼。
先创建命名空间:
kubectl create namespace web然后创建一个 ResourceQuota:
apiVersion: v1 kind: ResourceQuota metadata: name: web-quota namespace: web spec: hard: requests.cpu: "4" requests.memory: 8Gi limits.cpu: "8" limits.memory: 16Gi persistentvolumeclaims: "10"这个对象创建之后,凡是在web命名空间里创建 Pod,它们的 requests 和 limits 总和都不能越过这些硬上限。超出时,API Server 会拒绝创建,并返回 "exceeded quota" 的报错。
不过要注意:ResourceQuota 只管总量,不管单个 Pod 大小。如果你希望限制单容器最大或最小资源,还需要配合 LimitRange。比如有一种很典型的场景:开发同学部署 Pod 时忘了写 resources,K8s 默认为 requests 和 limits 都是 0,这种 Pod 在调度上几乎"无法无天"。LimitRange 可以强制给这种没写 resources 的 Pod 自动填充一个默认值:
apiVersion: v1 kind: LimitRange metadata: name: web-limit-range namespace: web spec: limits: - type: Container defaultRequest: cpu: 250m memory: 128Mi default: cpu: 500m memory: 256Mi加了 LimitRange 之后,如果一个容器没写 resources,API Server 会把 defaultRequest 和 default 写进去。这样一来,你的 ResourceQuota 才能真正发挥作用,因为之前那些 0 资源的 Pod 是没法限制的。再配合上 Namespace 隔离,你就在共享集群里给每个团队画出了一个资源边界。
4.4 应用变更、回滚与版本检查
声明式管理的一个重要优势是变更和回滚都很安全。假设我把 nginx 版本从 1.25 升级到 1.27,只需要修改镜像版本,然后kubectl apply -f deployment.yaml。这时候 Deployment 控制器会创建一个新的 ReplicaSet,然后逐步增加新副本、减少旧副本,这个过程叫滚动更新。
你可以用kubectl rollout status deployment/nginx-web -n web观察进度。如果中途想停,可以kubectl rollout pause deployment/nginx-web -n web。发布完后如果发现新版有问题,可以用kubectl rollout undo deployment/nginx-web -n web --to-revision=1一键回滚到上一个版本。
这里我要强调一个经验:永远要记录版本号。K8s 会自动保留 Deployment 的修订历史,默认保留 10 个版本,但很多人不会主动去看kubectl rollout history deployment/nginx-web,结果回滚时只能盲滚。我建议你在每次发布后,立即把版本号和对应的镜像版本记录到团队的发布文档里,哪怕只是在群里说一声“1.2.7 发布,镜像 tag 是 1.27”也行。因为你永远不知道哪次回滚会发生在凌晨 3 点。
另外,改配置不能光看 YAML,也要看真实对象。kubectl apply之后,我习惯用kubectl describe deployment nginx-web -n web看事件,看看有没有镜像拉取失败、调度失败、探针失败这类信息。这些事件只保留一小时,错过就没了。
4.5 清理资源:正确删除顺序与注意事项
最后说删除。很多人在清理环境时直接kubectl delete namespace web,这一条命令确实把命名空间下的所有对象干掉了,但有一个坑:如果这个命名空间里有 PVC 挂在使用中的 PV,删除会阻塞,命名空间会长时间卡在 Terminating 状态。
更稳妥的做法是:先删除 Deployment 和 Service,再删除 PVC,最后删 Namespace。不过哪怕你想清理得再干净,还有一个东西特别容易残留——kubectl get all只会列出部分资源类型,像 ConfigMap、Secret、PVC、ServiceAccount、Role 这些并不会出现在get all里。所以清理环境时要逐个检查,或者直接删 namespace 图省事(前提是你不怕卡在 Terminating)。关于 Terminating 怎么排查,我放到下一节详细讲。
5. 常见问题与排查技巧实录
5.1 apply 与 create 混用导致的“更新失败”
关于apply和create混用的问题,我是在实际生产环境里踩过坑的。当时一个同事用kubectl create创建了一个 Deployment,后来 CI 脚本里改成了kubectl apply,结果报了一堆冲突。原因是:create创建的对象不会记录last-applied-configuration,apply 需要基于这个 annotation 做合并计算,找不到就不知道哪些字段该被删除,于是部分字段更新失败。
解决办法有两条路。最简单的:先kubectl delete再apply,代价是中断服务,适合测试环境。生产环境的推荐做法:用kubectl apply -f file.yaml --force虽然可以强制覆盖,但会有打断服务的风险;更优雅的是先手动kubectl edit deployment把资源里对应的 annotation 补上,或者直接删掉旧的再用 apply 重建。
5.2 Pod 一直 Pending / CrashLoopBackOff
Pod 一直 Pending 的一大原因是调度器找不到合适的节点,常见触发点是资源不足。排查顺序我建议按这个来:先kubectl describe pod <pod-name>,看 Events 里有没有FailedScheduling,如果有,看提示是 Insufficient cpu 还是 Insufficient memory。然后kubectl get nodes查看每个节点的资源使用情况,或者kubectl describe node <node-name>看 Allocated resources 那一块。
CrashLoopBackOff 则是另一个坑,表示容器不断崩溃。最常见原因是启动命令失败、端口冲突、配置文件不对、探针失败。我提供一个小技巧:用kubectl logs <pod-name> --previous查看上次退出的容器日志,因为 CrashLoop 的当前容器可能已经没有日志了。之前有个服务一直在 CrashLoop,查了半天才发现是 env 变量拼写错了,代码里读了一个不存在的环境变量直接 panic。日志里的报错往往非常直白,很多时候多看一眼日志就能定位。
5.3 删除 namespace 卡在 Terminating
删除 namespace 卡在 Terminating,这个问题在共享集群里很常见。原因是命名空间下有某些"终结器"(finalizer)没有处理完,控制器在等待资源清理完成,但清理一直不成功。
排查方法:kubectl get namespace <ns> -o json,看 spec.finalizers 里有哪些终结器,以及 status.conditions 里的信息。如果是常见的kubernetes终结器,说明还有些资源没删干净。你可以先看看这个命名空间下有哪些资源:
kubectl get all -n <ns> kubectl get pvc -n <ns> kubectl get configmap -n <ns> kubectl get secret -n <ns>把它们先清掉,命名空间通常能恢复删除。如果确实遇到无法清理的顽固资源,还可以把 namespace 的 finalizers 临时清空,让它强制删除。具体做法是导出一个 JSON,删掉 finalizers 字段,再直接调用 API 用 PUT 更新这个 namespace。但我不建议轻易这么干,强制删除可能导致孤儿对象残留,需要后续手动清理。
5.4 GPU 与资源隔离场景下的节点调度问题
在热词里有“k8s调用gpu”、“容器资源隔离”,这些场景下的调度问题其实和普通资源有相似之处,但又有特殊性。GPU 在 K8s 里通常通过 Device Plugin 来暴露,节点上会有类似nvidia.com/gpu这样的扩展资源。要调用 GPU,你必须在 Pod spec 里显式声明:
resources: limits: nvidia.com/gpu: 1这里有个非常关键的坑:如果 GPU 只出现在limits里,没有写在requests里,K8s 调度器在分配时会自动把 requests 视为和 limits 相等。这个行为主要是为了调度一致性,但也意味着你申请了 1 个 GPU,即使实际用到一半,它也被整个占用。另一个 GPU 调度的坑是:如果 GPU 节点上有多个 Pod 都请求 GPU,但 Pod 的显存申请方式不一致,可能造成"显存碎片"。这已经不是 K8s 层面能解决的问题,需要你在写 GPU 资源配额时就规划好任务类型。
5.5 常见问题速查表
| 症状 | 常见原因 | 排查命令 | 解法方向 |
|---|---|---|---|
| apply 后无变化 | last-applied 缺失 | kubectl get deploy -o yaml | 补 annotation 或重建 |
| Pod Pending | 资源不足/节点亲和性不满足 | kubectl describe pod | 加节点/调 requests/检查亲和性 |
| CrashLoopBackOff | 启动失败、探针失败、配置错误 | kubectl logs --previous | 修复启动命令/调整探针参数 |
| namespace 卡 Terminating | finalizer 未清理 | kubectl get ns -o json | 清理子资源/临时清空 finalizer |
| Service 访问不通 | selector 不匹配、targetPort 错误 | kubectl describe svc / endpoints | 检查 selector 和 Endpoints |
| 镜像拉取失败 | tag 不存在/私有仓库认证失败 | kubectl describe pod | 修改镜像 tag/配置 imagePullSecret |
这个表格不可能覆盖所有场景,但它代表了我在实际运维中最常遇到的问题类别。排查的核心思路永远是:"先看事件,再看日志,再看配置。"
写在最后的几条个人建议
如果你刚开始学 K8s,我建议你先拿三到五个节点搭一个集群,然后亲手把这一节里的 Deployment、Service、ResourceQuota、LimitRange 全部用声明式方式部署一遍。不要急着去看那些面试题,先在集群里把 yaml 写对、把故障排好,面试题很多只是概念的包装。
我自己在使用 K8s 的过程中有一点体会最深:一定要尽早习惯 "一切皆文件、一切皆声明" 的工作方式。哪怕只是在测试环境,也把 YAML 文件存成一个目录,用kubectl apply -f 目录/来部署,而不是敲命令。因为这个习惯一旦养成,将来上 GitOps 就是水到渠成的事;反过来,如果习惯了一上来就kubectl run,后面迁移到生产时会有很痛的适应期。
还有一个非常实用的建议:给你的集群里的每个对象都打上完整标签,包括环境、应用名、负责人,然后养成用kubectl的-l参数筛选的习惯。比如kubectl get pod -n web -l app=nginx-web。标签体系是 K8s 世界里做资源归类和权限控制的基础,越早建立规范,后期维护越轻松。
最后再多说一句:K8s 的"资源与对象"这个话题,看起来是基础概念,但它是整个系统的骨架。如果你能准确说出来"我创建一个 Deployment,其实是在创建哪些对象,控制器是怎么把它体现为一个运行的 Pod 的",你对 K8s 的理解就已经超过一半的"会用但不懂"的同行了。