这段时间我们团队要把一套内部系统从虚拟机迁到 Kubernetes 上,不少同事之前只写过 Docker Compose,对容器本身并不陌生,但一接触 K8s 就被 Pod 这个概念绕住了。有人照着文档直接写了一个 Pod 的 YAML,却把它当容器一样原地更新;有人想知道 Pod 崩溃了为什么有时候会自动重启、有时候不会;还有人看到kubectl get pods里一个容器明明是 Running,应用却死活连不上,排查了半天也没想明白从何下手。
这个系列的第一篇,我决定把 Pod 这个最基础、也最容易被忽视的核心对象彻底讲清。这篇笔记要解决的几个问题非常明确:为什么 K8s 的最小调度单位是 Pod 而不是容器;如何写好一个 Pod 的 YAML;Pod 从创建到销毁到底经历了什么;以及它出问题时,该怎么一步步定位。适合所有刚开始学 K8s、或者用过但没系统梳理过 Pod 的开发者、运维和 SRE 阅读。
1. K8s 为什么把 Pod 作为调度最小单元:被设计逼出来的选择
很多没系统学过 K8s 的人会产生一个很自然的疑问:我最终要跑的是 nginx 容器、redis 容器,为什么不直接把容器当成调度单位?为什么中间还要包一层 Pod?
1.1 没有 Pod 之前,容器之间怎么协作
我们回想一下用 Docker 部署一个典型架构时的场景。一个后端服务依赖一个本地 Redis,最简单的拓扑是:后端容器通过--network=container或--link的方式去访问 Redis,或者两个容器共享一个 volume 来交换文件。这种方式单机跑还好,但一旦放入集群,调度、生命周期管理、网络编排都没办法把“一组相互依赖的容器”当成一个整体来对待。
K8s 当初在设计时,参考了 Borg 里对“进程组”的管理思路。核心洞察在于:有些进程必须紧密耦合在一起运行,它们要共享 localhost 网络栈、共享临时文件、共享进程间通信,这种关系类比到传统服务器上,就是同一台机器里的多个进程。所以 K8s 干脆提出了 Pod 这个中间抽象,用它来表达一组有着“超亲密关系”的容器,而它们作为一个整体被调度、被部署、被伸缩。
1.2 Pod 里的容器共享什么、不共享什么
为了理解这个“超亲密关系”,最直观的办法是看一张共享清单。两个容器进了同一个 Pod,执行kubectl exec进去后你会发现,它们的网络视图几乎是一模一样的。
| 维度 | 是否共享 | 说明 |
|---|---|---|
| 网络命名空间 | 共享 | Pod 内所有容器共享同一个 IP 和端口空间,访问对端用 localhost 即可 |
| IPC 命名空间 | 共享 | 可以使用 POSIX 消息队列等 IPC 机制 |
| UTS 命名空间 | 共享 | 共享同一个 hostname,Pod 内容器 hostname 一致 |
| 存储卷 | 共享 | 通过 volumeMount 挂载同一份数据卷 |
| PID 命名空间 | 默认不共享 | 旧版本里容器只看到自己的进程,新版本可通过 shareProcessNamespace 开启 |
| 文件系统 | 不共享 | 各容器镜像的文件系统相互隔离,只有挂载的卷能互通 |
很多初学者以为 Pod 里的容器能像 docker compose 那样用服务名互访,这是误解。在一个 Pod 内,两个容器直接用localhost:端口互相访问,因为它们的网络栈是同一个。但如果你写的是两个独立的 Pod,在默认扁平网络下,就得通过 Service 或者对方的 Pod IP 去访问。
1.3 每个 Pod 里藏着一个 pause 容器
这里有个很多人没注意到的细节。你在集群节点上执行crictl ps或docker ps,会看到每个 Pod 里除了业务容器之外,还有一个永远处于运行状态的 pause(或叫 sandbox)容器。它的作用就是先创建并持有整个 Pod 的网络命名空间和 IPC、UTS 等各类命名空间,业务容器启动时再去 JOIN 到这套命名空间里。
pause进程本身什么都不干,但它是整个 Pod 的“命根子”。正因为这个容器先于业务容器存在,所以哪怕业务容器全部退出了,只要 Pod 没有被删除,这套网络命名空间依然活着,IP 不会释放。这也是为什么你在节点上看到“一个 Pod 对应一个 IP”,而不是“一个容器对应一个 IP”的根本原因。理解了这一点,后面看网络排查会省掉很多疑惑。
2. 手写一个 Pod 的 YAML:核心字段逐个拆
YAML 是 K8s 里的“话语体系”,你所有的意图都需要通过它来表达。学习 Pod 最好的方式不是看一堆文档截图,而是自己写一个 YAML,逐字段去验证它的行为。下面这个例子是我平时最常用来给新人演示的多容器 Pod 模板,包含了最核心的字段。
apiVersion: v1 kind: Pod metadata: name: demo-pod namespace: default labels: app: demo tier: frontend spec: restartPolicy: Always initContainers: - name: init-db-wait image: busybox:1.36 command: - sh - -c - until nslookup db-service.default.svc.cluster.local; do sleep 1; done containers: - name: nginx image: nginx:1.25 imagePullPolicy: IfNotPresent ports: - name: http containerPort: 80 env: - name: TZ value: Asia/Shanghai - name: DB_ADDR value: db-service resources: requests: cpu: 100m memory: 128Mi limits: cpu: 200m memory: 256Mi volumeMounts: - name: shared-data mountPath: /usr/share/nginx/html - name: app-config mountPath: /etc/nginx/conf.d readOnly: true volumes: - name: shared-data emptyDir: {} - name: app-config configMap: name: nginx-config2.1 metadata 里那些容易被忽略的字段
name是 Pod 的唯一标识,同 namespace 下不能重名。labels是最容易被新人忽略的一个字段,它决定了这个 Pod 如何被 Service、Deployment 等上层对象发现和管理。K8s 里没有传统意义上的“按名称查找”,它靠的是 label selector 这套“键值对查询系统”。
所以我建议你从第一天就开始规划 label 命名习惯。不要随便打app: nginx这种模糊标签,至少要有app、tier、env三个维度。否则后面写 Service selector、Deployment selector、监控告警分组时,你会被自己当初的懒散坑到。
2.2 spec.containers:镜像、端口、环境变量
image决定了容器从哪个镜像启动。很多人踩过这样的坑:本地 nodejs 应用每次代码更新,都改一个:latesttag,然后发现集群里的 Pod 怎么都不拉新镜像。原因就在imagePullPolicy的默认规则里:如果镜像 tag 是latest或没写 tag,默认策略是Always;如果 tag 是非 latest,比如v1.2.3,默认是IfNotPresent。也就是说,你写image: my-app:latest的话,只要镜像策略没显式设置,每次启动都会去拉一次;但你写的是带版本号的 tag,节点上已经有镜像,就不会再拉。
关于ports字段,我一直跟新人强调:它只是一个“信息性”字段,不是防火墙规则。写上containerPort: 80并不会让你这个端口“被开放”,K8s 也不会基于它去做端口限制。真正决定 Pod 能不能被外部访问到的,是 Service、NetworkPolicy 这样的网络层对象。你可以不写 ports,应用照样能监听端口。
环境变量是给业务容器传配置最常见的方式,也支持从 ConfigMap 和 Secret 批量注入,envFrom配合 ConfigMap 是生产环境里我非常推荐的一种做法。环境变量的值写死在 YAML 里会让配置和代码耦合,通过 ConfigMap 管理后,改配置不需要重新构建镜像,只需要滚动重启 Pod。
2.3 挂载的卷:emptyDir、configMap、persistentVolumeClaim
Pod 里的volumeMounts和 Docker 的-v参数很像,但它有几个独特场景值得专门说。
emptyDir: {}是生命周期等于 Pod 的临时卷,Pod 删了,目录也就没了。这个字段我不建议理解成“临时目录”,它的真正用武之地是给多容器共享数据。比如上面例子中,一个日志采集 sidecar 容器通过挂载同一个 emptyDir,就能持续读取业务容器写入的日志文件。
configMap作为卷挂载到容器,本质是把 ConfigMap 里的一个个 key 映射成文件。上面例子中我把 nginx 的配置放进了 ConfigMap,然后挂载成/etc/nginx/conf.d下的只读文件。这种方式的好处是:配置文件和镜像分离,可以独立更新。
persistentVolumeClaim(PVC)则是把持久化存储挂进 Pod,适合有状态应用。一个 Pod 的 PVC 生命周期比 Pod 长,Pod 删了 PVC 还在,数据不丢。后面第 5 节讲 StatefulSet 时你还会看到它的重要性。
3. Pod 生命周期:状态流转、容器重启与优雅终止
刚接触 K8s 的时候,我一度以为一个 Pod 从创建到被删除是一条简单的直线。后来发现它的状态机比想象中复杂,而且理解它的关键,不在于背几个 phase 名字,而在于搞清楚“谁在管、谁能重启、什么时候杀了重来”。
3.1 Pod Phase 与 Conditions:一张表看清状态
先给一张我自己查过无数次的表格:
| Phase | 含义 | 常见场景 |
|---|---|---|
| Pending | Pod 已创建,但容器还没完全启动 | 正在调度、拉镜像、等待 PVC 就绪 |
| Running | 至少一个主容器在运行 | 正常工作状态 |
| Succeeded | 所有容器正常退出,退出码为 0 | Job 跑完任务后出现 |
| Failed | 至少一个容器以非 0 退出码退出 | 容器崩溃且不再重启 |
| Unknown | API Server 丢失了节点上 Pod 的状态 | 节点宕机或失联 |
Pending阶段最容易被误读。很多人看见 Pod 处于 Pending 就以为是“正在启动”,其实它可能卡在调度失败上,根本没有被放到任何节点。经验法则:Pod 在 Pending 超过一分钟,就要立刻kubectl describe看事件。
除了 phase,K8s 还维护一组 Conditions:PodScheduled、Initialized、ContainersReady、Ready。phase 是一个汇总状态,conditions 是更细颗粒度的开关。排障时kubectl describe pod输出的 Conditions 一栏,每一行会告诉你这个条件是否满足,以及不满足的原因。很多莫名其妙的“Running 了但服务不可用”,根源就是 conditions 里Ready这一项没有变为 True。
3.2 容器从拉取到退出的完整推进路径
一个 Pod 从被提交到进入 Running,实际经历了这样一条链路:你提交 YAML 给 API Server,写入 etcd;调度器组件一直 watch 未调度的 Pod,按资源、污点、亲和性等条件选一个节点;kubelet 收到调度结果后,先启动 pause 容器,再逐个启动 initContainers(如果有),最后启动业务容器。业务容器变成 Running 后,如果配置了探针,还需探针通过才会把Ready翻为 True。
这条链路里有个隐藏知识点:initContainers和containers是不同批次启动的,initContainers 按顺序逐个执行,必须全部成功退出,主容器才开始启动。所以它特别适合做“数据库连接等待”“目录权限初始化”“下载初始化数据”这类前置任务。如果 init 容器一直启动失败,Pod 会卡在Init:CrashLoopBackOff,很多人只看主容器日志查半天,结果问题根本不在主容器上。
3.3 删除 Pod 时,超时才会被强杀
通过kubectl delete pod <name>删除 Pod 时,实际发生的是:API Server 给 Pod 标记一个宽限期(默认 30 秒),kubelet 收到后给主容器发 SIGTERM 信号;容器进程若在宽限期内没退出,kubelet 会补发 SIGKILL,强制杀掉。terminationGracePeriodSeconds字段可以修改这个宽限期。
这一点对生产环境非常重要。如果你的应用不监听 SIGTERM、不做优雅退出,滚动更新时就会有一批正在处理的请求被直接掐断,用户端看到的就是“502”。我一直建议:业务容器主进程至少要写一段处理 SIGTERM 的逻辑,等待存量请求处理完再退出,尤其是有 http 长连接或消息队列消费的服务。容器跑起来固然重要,能体面地退出同样重要。
3.4 RestartPolicy:谁来决定崩溃后是否重启
Pod 的restartPolicy只有三个值:Always、OnFailure、Never。默认是Always。它限制的不是“Pod”,而是 Pod 里的容器。举个例子:一个容器因为业务代码 panic,退出码非 0,在Always策略下,kubelet 会不断重启它;如果换成Never,它退出后 Pod 就停留在 Failed 状态,不再拉起。
理解这个机制,你才能看懂kubectl get pods里RESTARTS这一列的价值。RESTARTS 数字不断增加,说明容器进程反复退出,通常意味着“程序自己崩了”,而不是“Pod 被节点驱逐”。这个区分是排查的第一道分岔口,很多人上来就去看网络、看配置,实际上看一眼退出码和重启次数就能少走一半弯路。
4. 资源请求与健康探针:Pod 上线前必须做的两件事
如果让我给 Pod 配置按重要性排序,除了镜像和命令之外,resources和探针绝对并列第一。这两个字段不加,开发环境一切正常,一上生产立刻出问题。
4.1 requests 与 limits:调度预算是它们,运行限制也是它们
先看一段最简配置:
resources: requests: cpu: 100m memory: 128Mi limits: cpu: 200m memory: 256Mirequests的作用是给调度器看的。调度器在决定“这个 Pod 放哪个节点”时,会累加节点上所有 Pod 的 requests,如果节点剩余资源小于新 Pod 的 requests,就不会调度上去。也就是说,requests 是这张节点上的“承诺预算”。
limits的作用是给运行时的 kubelet 看的。容器内存超过 limit 后,会被 cgroup 直接 OOMKilled;CPU 超过 limit 后,会被限制(throttling),而不是杀掉。
这里有一个常见误解:有人以为“我只要设置了 limits,就能保证应用获得这么多资源”,其实不对。limits 是天花板,不是保底。真正保证的是 requests,requests 才决定调度预留。
4.2 QoS 等级:K8s 如何在极端情况下牺牲 Pod
根据 requests 和 limits 的配置方式,Pod 会被划分成三个服务质量等级,这在节点资源紧张时决定了“先杀谁、后杀谁”。
| QoS 等级 | 配置特征 | 节点 OOM 时的命运 |
|---|---|---|
| Guaranteed | 所有容器 requests == limits,且都设置了 | 最优先保留,几乎不会被杀 |
| Burstable | 至少一个容器 requests < limits,且 requests > 0 | 在 Guaranteed 之后被杀 |
| BestEffort | 没有任何容器设置 requests 和 limits | 第一个被 kill |
曾经有一次节点内存爆了,我们挂在节点上的一些非关键 sidecar 容器全部被驱逐,留下了一台看起来还活着但业务没响应的节点,我花了很久才想明白是 QoS 在起作用。后来我把核心业务全部提成 Guaranteed,把日志采集、metrics 采集这类非关键 sidecar 有意配置成 Burstable,让它们在极端情况下自动让位,集群整体稳定性反而更好了。这个设计不是玄学,它就是这么运作的。
4.3 三种探针的职责边界与探测方式
探针是我认为 Pod 里最值得细品的机制。一共三种:
livenessProbe:决定 kubelet 要不要重启容器。失败次数达到阈值,容器被杀掉重启。readinessProbe:决定容器要不要接收流量。失败时 Pod 从 Service 的后端列表里被摘除,但容器本身不会被杀。startupProbe:给启动慢的容器一个“保护期”,成功之前,另外两个探针不会参与。适合 Java 这种可能要十几秒才能起来的应用。
探测方式有三种:httpGet、exec、tcpSocket。
readinessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 5 periodSeconds: 10 failureThreshold: 3 livenessProbe: httpGet: path: /healthz port: 8080 periodSeconds: 10 timeoutSeconds: 2 failureThreshold: 3 startupProbe: httpGet: path: /startup port: 8080 failureThreshold: 30 periodSeconds: 2我强烈建议给新项目同时配置readinessProbe和startupProbe,livenessProbe可以看情况。原因是:liveness 探针不小心配置过严,会导致容器在启动高峰期被杀,而 startupProbe 用更长的失败阈值覆盖慢启动场景后,liveness 的 initialDelaySeconds 就可以写得很小,不用担心误杀。
readinessProbe的价值体现在滚动更新时。新 Pod 的容器起来了,但应用内部还要加载缓存、建立连接池,这个时候如果不让流量进来,用户就会打到半初始化的实例。加一个 readiness 探针指向/healthz,接口返回 200 之前,Service 不会把请求转给这个 Pod,这是最优雅的“上线踩刹车”。
4.4 探针的隐蔽坑
探针是排障频率最高的对象之一,常见的坑我列几个:
httpGet探针只要 HTTP 状态码不是 2xx/3xx 就算失败。不是 TCP 能连通就代表健康,你的健康检查接口如果本身逻辑太重(比如每次查数据库),会把数据库压力放大数倍。initialDelaySeconds太短会让容器刚启动还没来得及监听到端口就连续失败。尤其是网络模型下 kubelet 探活是走 Pod IP 的,如果应用监听的是 IPv6 或只绑了 127.0.0.1,探针也会失败。periodSeconds太短会带来不必要的系统负担,尤其物理机节点上 Pod 数量多时,探针风暴会让节点负载异常。一般 10 秒一次已经很够。
5. Deployment、StatefulSet、DaemonSet 下的 Pod:什么时候用哪个
直接创建单个 Pod 在生产环境几乎没有任何意义,因为节点故障了,这个 Pod 就永远没了。Pod 之上真正干活的是各类控制器,它们负责维持你期望的副本数。
5.1 为什么说别直接创建 Pod
Pod 本身的定位是“可替换的”,控制器通过 label selector 和 ownerReferences 两个机制管理你创建的 Pod。用kubectl delete pod去删控制器管理的 Pod,控制器会立刻重建,你删多少次都没用。反过来,如果你手动改了 Pod 的 labels 让它脱离 selector,控制器就会认为 Pod“走丢”了,重新拉起一个副本,而那个被你改掉的孤儿 Pod 照样在跑。这相当于是“脱离党组织”的 Pod,除非主动删除,没人会再管它。
所以生产环境里,永远别直接定义 Pod 对象,要定义控制器。
5.2 Deployment:无状态应用的主力
Deployment 是 99% 无状态业务的首选。它的核心能力是滚动更新和回滚。你更新 Deployment 的模板之后,它会创建一个新的 ReplicaSet,逐步把旧副本替换成新副本,期间可用副本数始终保持在期望值左右。回滚也简单,kubectl rollout undo可以回到上一个版本。
还有一个判断技巧:Deployment 创建的 Pod 名是deployment名-随机串-随机串,你看到的 Pod 名没有顺序编号,这类 Pod 无状态、可随时替换。
5.3 StatefulSet:为稳定标识和有序操作而生
很多人用 StatefulSet 的理由就一句话“我的应用有状态”。其实这不够准确。StatefulSet 真正解决的问题是三个:
- 每个 Pod 的名字固定,且带有序号,例如
web-0、web-1、web-2。创建和销毁都按顺序执行,先web-0后web-1,有序缩容时从末尾开始。 - 每个 Pod 可以有独立的、稳定的存储卷。PVC 的命名模式是
卷名-名-序号,Pod 被重建后,新 Pod 会重新挂载同一块数据卷,数据不丢。 - 有稳定的网络标识。配合 Headless Service,每个 Pod 有独立的 DNS 名字。
数据库、消息队列、ZooKeeper 这类节点身份很重要的应用适合它。如果你只需要简单的无状态多个副本,用 StatefulSet 反而会增加复杂度。
5.4 DaemonSet:每个节点保证一个 Pod
DaemonSet 的语义是“集群里每个节点(或满足条件的节点)都运行一个 Pod”,并且新增节点时自动在新节点上创建。日志采集、监控 agent、kube-proxy 这类基础设施组件几乎都用它。DaemonSet 不需要关心副本数,因为副本数由节点数决定。
判断一个 Pod 该归谁来管,其实可以按照一个简单顺序:是给每个节点跑一个的基础组件?用 DaemonSet;是需要稳定身份的有状态应用?用 StatefulSet;否则,一律 Deployment。下面这张表是我经常发给同事的速查版:
| 控制器 | 副本语义 | 命名规律 | 典型场景 |
|---|---|---|---|
| Deployment | 无状态,任意多副本 | deployment名-随机串-随机串 | Web、API、Worker |
| StatefulSet | 有状态、有顺序 | sts名-序号 | 数据库、ZK、MQ |
| DaemonSet | 每个节点一个 | ds名-随机串 | kube-proxy、日志采集 |
| Job | 一次性任务 | job名-随机串 | 数据迁移、批处理 |
| CronJob | 定时任务 | cronjob名-随机串 | 定时备份、定时清理 |
6. 从 kubectl 到排障:Pod 异常状态的完整排查链路
这里我想重点分享的是一套“排查动作的次序”。Pod 出问题时,大多数人不缺命令,缺的是先看什么、后看什么、怎么串起来形成结论。
6.1 第一步:看总览,别急着钻进细节
先执行:
kubectl get pods -A重点看STATUS和RESTARTS两列。常见的 STATUS 值有Running、Pending、CrashLoopBackOff、ImagePullBackOff、ContainerCreating、Terminating。RESTARTS 超过几次就要留意。
你看输出可能得到这样一行:
my-app-7d8f9b6b6c-htdvk 0/1 CrashLoopBackOff 5 12m到了这一步,方向基本已经明确了:这是一个反复自杀的容器,大概率不是调度问题。接下来我们用 describe 看细节,确认它到底为什么崩。
6.2 第二步:describe 拿到 Events,这场排障就成功一半
kubectl describe pod my-app-7d8f9b6b6c-htdvk这条命令输出很长,关键信息有三块:
Conditions:每一行说明某个条件是否满足,Ready是否为 True。Events:近期的调度、拉镜像、容器启动事件。事件顺序在底部是最新的,你重点看带Warning的行。
举一个典型的输出片段:
Events: Type Reason Age From Message ---- ------ ---- ---- ------- Warning BackOff 45s kubelet Back-off restarting failed container Normal Pulled 44s kubelet Successfully pulled image Warning Unhealthy 3s (x12 over 2m) kubelet Liveness probe failed: HTTP probe failed with statuscode: 500看到Liveness probe failed,这个 CrashLoopBackOff 的根因就浮出水面了:不是镜像问题,不是调度问题,而是 liveness 探针把容器判定为不健康,kubelet 反复重启它。那你接下来就要把注意力转到应用的健康检查接口上,而不是去翻网络配置。
6.3 第三步:日志和 exec 是临门一脚
describe 能定位“它为什么被重启”,日志则能告诉我们“它为什么活不好”。查看日志:
kubectl logs my-app-7d8f9b6b6c-htdvk如果容器里跑了多个容器,必须加-c 容器名指定容器,否则 kubectl 只会返回主容器的日志。看崩溃前的退出,加上--previous参数能看到上一次退出的日志:
kubectl logs my-app-7d8f9b6b6c-htdvk --previous如果 Pod 是 Running 但不是 Ready,还可以直接进入容器自测:
kubectl exec -it my-app-7d8f9b6b6c-htdvk -- /bin/sh # 在容器里 curl -v http://localhost:8080/healthz cat /etc/resolv.conf ss -tlnp这一步能快速判断是“容器内部监听异常”“DNS 解析失败”,还是“网络层压根没通”。
6.4 高频异常对照表
下面这张表是我做技术支持时最常用到的异常速查,列出来供你直接对照:
| 现象 | 大概率根因 | 首要排查入口 |
|---|---|---|
| ImagePullBackOff / ErrImagePull | 镜像名打错、私有仓库没配镜像拉取凭据、镜像 tag 不存在 | 查看 describe 的 Events,确认镜像拉取报错信息 |
| Pending 长时间不动 | 节点资源不足、有污点无法容忍、Pending 的 PVC 未就绪 | describe 看 FailedScheduling 事件,看调度器给的策略 |
| CrashLoopBackOff | 容器进程启动后反复退出,或 liveness 探针失败 | describe 看退出码和 Events;日志看应用报错 |
| OOMKilled | 容器实际内存超过 limits | describe 看 Last State 的 Reason;配合可观测性工具查内存趋势 |
| 0/1 Running 但服务访问不到 | readiness 探针失败、Service selector 没匹配上 Pod labels | describe 看 Conditions;检查 selector 与 labels 是否一致 |
| ContainerCreating 卡住 | 卷挂载失败、拉镜像卡住、底层运行时异常 | describe 看 Events,关注 MountVolume 相关错误 |
OOMKilled 我有一个深刻教训。当时一个内部工具的容器频繁被杀,我查了代码、查了配置,都没发现问题,直到对节点上的 cgroup 指标做了一次采集,才发现应用的堆外内存涨到了几百 MB,而 limits 只有 128Mi。调高 limits 只能临时救火,真正根因是某开源库默认的缓存无上限。所以遇到 OOM,请记住两点:limits 是最后防线,不是应用内存预算;不把应用自身的内存增长轨迹监控起来,limits 永远只是碰运气。
7. 进阶细节与我在实际项目里踩过的坑
7.1 静态 Pod:控制平面自己的隐藏用法
还有一种不归任何控制器管理的特殊 Pod,叫静态 Pod。它由 kubelet 直接读取节点上/etc/kubernetes/manifests目录里的 YAML 文件生成,不经过 API Server 的调度流程。你甚至能在kubectl get pods里看到它,但删除它做不到——只要文件还在,kubelet 会一直重建。
K8s 把控制平面组件 kube-apiserver、etcd 等部署成静态 Pod,正是为了简化部署过程。运维层面理解静态 Pod 有助于你排查控制面组件频繁重启的问题,不过日常业务中我们很少直接用到它。
7.2 securityContext:容器跑起来不等于容器是安全的
securityContext是 Pod 与容器安全配置的入口。最常见的三个动作:
securityContext: runAsNonRoot: true runAsUser: 1000 runAsGroup: 1000 fsGroup: 2000runAsNonRoot: true能直接拒绝镜像里默认 root 用户运行的容器,避免攻击者进入容器后获得节点 root 权限。一直以来我在团队内部都要求,新上线的镜像必须提供非 root 启动配置。一开始会有些麻烦,因为有些镜像的日志目录写权限是 root 的,但用 fsGroup 调整挂载卷的属组后基本都能解决。这个“麻烦”是值得的:容器里越界攻击的第一步,几乎都是 root。
7.3 hostNetwork 与 hostPID:能不用就不用
Pod 可以设置hostNetwork: true直接使用节点 IP 和端口,也能用hostPID: true看见宿主机全部进程。这类配置非常激进,会让容器的隔离性大幅下降。常见用途是运行 kube-proxy、节点级网络插件这类基础设施,业务 Pod 不建议碰。
如果你确实需要用 hostNetwork 做排查,记得它会绕过 Service 网络层,所以你无法用一个普通的 ClusterIP 来访问它。这部分细节很容易引起认知混乱,遇到问题先确认自己是不是把 Pod 的网络模式理解错了。
7.4 我给新人的学习路线建议
回顾我和周围同事学习 K8s 的路径,最容易出问题的地方就是“跳级”——还没理解 Pod 就开始折腾 Helm、Service Mesh、自定义控制器,结果遇到问题全都悬在 Pod 这一层。以我个人的带教经验,比较稳妥的顺序是:
- 先会
kubectl run和kubectl exec,从命令行把 Pod 跑起来; - 再手写简单 YAML,理解 Pod、Deployment、Service 三者关系;
- 然后专门花一个周末做“页面不可访问”的排障实验,把探针和 selector 练熟;
- 最后再上 ConfigMap、Secret、StatefulSet 这类进阶对象。
踩过几次灰之后我的体会是,K8s 里百分之七十的诡异问题,归根结底都发生在 Pod 这一层:要么是 Pod 的状态没被正确理解,要么是 Pod 的探针和资源设置不符合业务模型。把这篇笔记里的内容用一个星期消化掉,后续你学 Service、Ingress、Helm,都会轻松一大截。