ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Kubernetes Pod 完全指南:从 kubeadm init 到日常排障实战

Kubernetes Pod 完全指南:从 kubeadm init 到日常排障实战 我第一次用 kubeadm 初始化集群的时候屏幕上滚过一长串[preflight]检查项当时对 Kubernetes 的了解还停留在“容器编排工具”这个层面根本没意识到这一串输出背后藏着一个理解整个系统的钥匙——Pod。后来在生产和测试环境里折腾得多了才慢慢确认一个判断Kubernetes 里几乎所有看似复杂的东西网络、存储、调度、滚动更新、故障自愈最后都落脚在 Pod 这个最小调度单元上。如果你正准备入门 Kubernetes或者已经在用但总感觉哪里没想透这篇文章值得看完我会从 Pod 的底层逻辑、创建流程、配置挂载到日常排障把实操经验一次性讲清楚。1. 先搞清楚 Pod 到底是什么1.1 为什么 Kubernetes 不直接调度容器很多人第一次接触 Pod 的时候都会问同一个问题Docker 容器已经能把应用跑起来了为什么 Kubernetes 还要在外面包一层 Pod直接调度容器不是更简单吗这个问题问得特别好因为它恰恰是理解 Kubernetes 设计哲学的入口。容器本身是进程级别的隔离但现实中的应用程序很少是“一个进程打天下”的。比如一个 Web 服务通常需要 nginx 处理请求、业务逻辑容器跑后端代码、日志采集 sidecar 在旁边把标准输出转发到日志平台再比如一个 Java 应用可能需要单独的 agent 容器做链路追踪。这些容器之间有着强烈的部署相关性它们要调度到同一台机器上、要能通过 localhost 互相访问、要共享同一份网络命名空间和存储卷。如果 Kubernetes 直接调度单容器就得为“一组容器必须在一起”这个需求额外发明一套亲和性机制。与其那样绕路不如直接把“一组需要共享资源的容器”定义成一个原子调度单位这就是 Pod。Pod 是 Kubernetes 里的最小调度单位这句话不是官方的定义口号而是实打实的设计结论调度器以 Pod 为粒度做决策kubelet 以 Pod 为粒度做健康检查HPA 以 Pod 为粒度做扩缩容。从技术实现上看一个 Pod 里的所有容器共享同一个网络命名空间和同一个 IPC 命名空间。什么意思呢就是 Pod 里的每个容器都有相同的 IP 地址和端口空间它们通过 localhost 就能互相访问不需要走任何 Service 或者 DNS。同时它们也能共享同一个 volume比如一个容器往 /var/log/app.log 写日志另一个 sidecar 容器可以挂载同一个 volume 实时读取。这种模型对多进程协作的现代应用架构来说非常顺手。1.2 一个 Pod 里的多个容器怎么分工明确了 Pod 是一组容器的集合之后下一个问题是什么情况下该把多个容器塞进一个 Pod什么情况下该拆成多个 Pod这是一个架构取舍问题经验法则有三条。第一条是“进程组密不可分原则”。如果多个进程必须部署在同一台主机上、必须共享网络栈、必须共享存储那就适合放进同一个 Pod。典型例子是主容器加 sidecar主业务容器负责核心逻辑sidecar 容器负责日志采集、流量代理、配置热更新等辅助功能。第二条是伸缩维度一致原则。如果两个服务的副本数必须始终保持一致、必须在同一时刻一起扩缩容那它们应该放在一个 Pod 里如果一个服务的流量增长不需要带动另一个服务那它们应该拆开。第三条是资源维度一致原则。如果一个容器需要独立控制 CPU 和内存配额、独立设置资源上限那它应该单独占一个 Pod否则两个容器共享一个配额没法精细化管控。拿最常见的 sidecar 模式举例。我维护过一个 Golang API 服务主容器跑 HTTP 接口旁边就挂了一个 fluent-bit 容器收集日志。两个容器在同一个 Pod 里IP 相同、端口空间相同fluent-bit 直接通过 localhost 的方式读取主容器的日志文件目录。这个设计最大的好处是当 Pod 被重新调度到另一台节点时sidecar 会一起迁移日志采集逻辑不会因为环境变化而断档。如果把 fluent-bit 独立部署成一个 DaemonSet 再去匹配容器网络配置复杂度会直线上升。2. 从 kubeadm init 输出看 Pod 是如何诞生的2.1 preflight 检查背后的机制回到开头那个场景。执行kubeadm init的时候第一行输出是[init] using kubernetes version: v1.26.0紧接着就是[preflight] running pre-flight checks。这行输出看起来只是例行公事但它背后藏着一个重要的事实控制平面组件的核心进程本质上都是通过 Pod 的方式运行在集群里的而这些 Pod 的启动方式有点特殊。preflight 检查做的事大致分为四类系统环境检查、容器运行时检查、端口占用检查、资源可用性检查。系统环境检查包括主机名是否合法、swap 是否关闭、内核模块比如 br_netfilter、overlay是否加载、sysctl 参数是否配置容器运行时检查包括 CRI 是否可用、cgroup 驱动是否匹配端口检查包括 6443、2379、2380、10250、10259、10257 这些端口是否被占用资源检查包括 CPU 核数、内存大小是否满足最低要求。这些检查项里最容易踩坑的是 cgroup 驱动不匹配。Docker 默认的 cgroup driver 是 cgroupfs而 kubeadm 默认使用的是 systemd 模式。如果 /etc/docker/daemon.json 里配置的 exec-opts 没有把 native.cgroupdriver 改成 systemd集群初始化之后 kubelet 会因为无法创建 Pod Sandbox 而一直报错表现就是节点 NotReady。我在 1.24 版本之后还遇到过 containerd 作为运行时的情况containerd 的配置里同样要保证 SystemdCgroup true否则即使 preflight 能过运行 Pod 的时候也会出现偶发性的 CPU 和内存统计异常。2.2 控制平面核心 Pod 的启动过程跑完 preflight 之后kubeadm 会执行一个关键动作生成一系列静态 Pod 的 manifest 文件放到 /etc/kubernetes/manifests 目录下。这个目录里躺着四份 yamlkube-apiserver.yaml、kube-controller-manager.yaml、kube-scheduler.yaml、kube-etcd.yaml。kubelet 会定时扫描这个目录一发现文件存在就自动创建对应的 Pod。这就是静态 Pod 的工作方式也是整个控制平面自举的基础。有意思的地方在于apiserver、etcd、controller-manager、scheduler 这四兄弟它们自己就是 Pod而它们被创建的过程中还没有 apiserver 可以依赖。所以静态 Pod 的创建走的是 kubelet 直连容器运行时这条路径不需要通过 apiserver 调调度器。这也解释了为什么 preflight 之后会有一句[wait-control-plane] waiting for the kubelet to boot up the control plane as static Pods然后才是[kubelet-finalize]。整套机制的设计意图很明确控制平面组件和节点上的普通 Pod 不是同一个调度层级前者依赖 kubelet 的本地发现机制后者依赖调度器。静态 Pod 还有一个特点它不归 apiserver 管所以 kubectl delete 删不掉。你在 /etc/kubernetes/manifests 目录里把对应的 yaml 文件挪走或改名kubelet 才会把对应的静态 Pod 停掉。这个特性在排查控制平面故障时很有用如果 apiserver 一直在 CrashLoopBackOff最快的恢复方式不是 kubectl delete而是到节点上手动修改对应的 manifest 文件然后等待 kubelet 重新拉起。2.3 Pod 生命周期与状态机搞清楚了创建路径还要理解 Pod 的运行状态。用kubectl get pods看到的那一列 STATUS背后是一套完整的状态机。Pending 表示 Pod 已经被 apiserver 接受但容器还没完全启动可能是节点没有可用资源也可能是镜像还在拉取Running 表示容器已经正常运行Succeeded 表示所有容器成功退出且不会再重启Failed 表示至少有一个容器以非零状态退出Unknown 表示 kubelet 无法获取 Pod 状态通常是节点失联或者 kubelet 挂了。但是真实世界里你见到的状态远不止这五种。CrashLoopBackOff 表示容器反复启动后崩溃kubelet 正在指数退避重启ImagePullBackOff 表示镜像拉取失败可能是地址拼错、私有仓库鉴权失败、或者镜像仓库本身不可达Evicted 表示节点内存或磁盘压力过大kubelet 把低优先级 Pod 扫地出门。排查时先用kubectl describe pod看 Events再决定下一步不要只盯着状态值猜。Events 里会给出最原始的原因比如FailedScheduling、BackOff、OutOfMemory、InsufficientMemory这些信息比状态值可靠得多。Pod 的 restartPolicy 也直接影响上面的状态展示。restartPolicy 支持 Always、OnFailure、Never 三种。默认值是 Always这意味着不管容器以什么状态退出kubelet 都会把它重新拉起来。像跑一次性批处理任务的 JobrestartPolicy 一般设成 Never 或 OnFailure否则任务执行完还会被无限重启状态永远变不成 Succeeded。正确理解这一点能帮你少踩不少隐蔽的坑。3. 把配置交给 ConfigMap日常部署绕不开的组合3.1 ConfigMap 与 Pod 的三种挂载方式生产环境里部署应用几乎不可能把配置硬编码进镜像里。环境地址、日志级别、数据库连接串、超时时间这些在开发、测试、生产环境各不相同。遇到这种需求Kubernetes 给的标准解法就是 ConfigMap用法上基本就是 pod configmap deploy 这个稳定组合。ConfigMap 本质是一个键值对存储。你可以用一个简单的 yaml 声明它也可以用kubectl create configmap app-config --from-fileconfig.yaml直接从文件创建。创建完之后把它挂载到 Pod 里有三种方式先说最常用的一种通过 volume 挂载。你可以在 Pod 的 spec 里声明一个 volume来源指向这个 ConfigMap然后挂载到容器内的某个目录这样容器读取该目录下的文件时拿到的是 ConfigMap 里的值。注意挂载的动态更新特性如果 ConfigMap 本身被更新了kubelet 会同步更新已挂载卷中的文件内容但容器内正在运行的进程不会自动感知需要进程自己监听文件变化或重启才能吃到新配置。第二种方式是通过环境变量注入。spec.containers[].env 里可以写valueFrom.configMapKeyRef把 ConfigMap 的某个 key 直接映射成环境变量。好处是应用代码零改动直接读环境变量坏处是环境变量注入是一次性的Pod 创建之后修改 ConfigMap 不会影响已有环境变量必须重建 Pod。第三种方式是用 ConfigMap 生成命令行参数把配置内容当作容器启动命令的参数这个适合启动参数比较固定的场景。3.2 为什么大家都用 Deployment 而不是裸 Pod有了一组包含 ConfigMap 的 yaml直接kubectl create -f pod.yaml也能把 Pod 拉起来为什么社区里所有人都默认用 Deployment 管理 Pod原因很简单裸 Pod 没有自愈能力。你手动创建的那个 Pod所在的节点宕机了kubelet 和调度器都不知道它的存在它就永远躺在那里Deployment 则不同它通过 ReplicaSet 维持期望副本数Pod 挂掉会立刻创建替身节点失联也会有调度器在其他节点补位。Deployment 另一个杀手级能力是滚动更新。你更新了镜像版本执行kubectl set image deployment/webapp webappv2或kubectl apply -f更新了 deployment yaml控制器会启动新的 ReplicaSet先把新 Pod 拉起来等它通过 readinessProbe 之后再把旧 Pod 一个个下线。整个过程不需要你在深夜人少的时候手动停机重启也不会有全量不可用。这背后其实是控制器模式的又一次体现Deployment 不断对比期望状态和实际状态然后驱动实际状态向期望状态收敛。还有一个容易被忽略的好处版本回滚。Deployment 每次变更都会生成一条 revision 记录用kubectl rollout history deployment/webapp能看到历史版本出问题的时候kubectl rollout undo deployment/webapp --to-revision2一键回滚。裸 Pod 想实现这个效果得靠手工备份一份份 yaml费时费力还容易出错。所以我的个人建议是除非你是在做纯测试或演示否则永远不要把裸 Pod 直接跑在集群里。3.3 一份可直接抄的部署示例光说不练是假把式。这里给一份生产上常见的组合示例一个 Nginx 应用服务端口 8080通过 ConfigMap 注入一个自定义的 nginx.conf 片段。apiVersion: v1 kind: ConfigMap metadata: name: webapp-config namespace: default data: nginx.conf: | server { listen 8080; location /healthz { return 200 ok; add_header Content-Type text/plain; } location / { root /usr/share/nginx/html; index index.html; } }apiVersion: apps/v1 kind: Deployment metadata: name: webapp namespace: default spec: replicas: 3 selector: matchLabels: app: webapp template: metadata: labels: app: webapp spec: containers: - name: nginx image: nginx:1.25 ports: - containerPort: 8080 readinessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 5 periodSeconds: 5 resources: requests: memory: 64Mi cpu: 100m limits: memory: 128Mi cpu: 250m volumeMounts: - name: nginx-config mountPath: /etc/nginx/conf.d/custom.conf subPath: nginx.conf volumes: - name: nginx-config configMap: name: webapp-config这里有一个细节值得专门强调volumeMounts 里我用了 subPath。如果直接挂载整个 ConfigMap 到 /etc/nginx/conf.d/ConfigMap 里的所有 key 会各自生成文件同时还可能在目录里创建符号链接导致原本目录里已有的其他配置文件被隐藏或遮挡。用 subPath 指定单一文件就能精准挂载避免误伤目录里原有的文件。代价是 subPath 方式挂载的文件不会跟随 ConfigMap 更新这是 Kubernetes 的设计取舍知道就好。4. 新手必看Pod 排障三板斧4.1 一直 Pending到底被卡在哪一步Pending 是新手最常遇到的状态也是最容易排查的状态。先用kubectl describe pod pod-name看 Events如果里面有FailedScheduling说明是调度器没有找到合适的节点。排除思路按频率排序第一节点资源不足。你给 Pod 的 requests 远大于节点剩余可分配资源调度器当然拒单。调低 requests或者扩容节点是最直接的解法。第二节点上有 taints 而 Pod 没有对应的 tolerations。这是集群管理员保护敏感节点的常用手段比如给 master 节点加了node-role.kubernetes.io/control-plane:NoSchedule普通 Pod 就不会被打上去。第三节点选择器和亲和性条件不满足。你的 yaml 里写了 nodeSelector 指定了某个标签但集群里没有节点带这个标签调度器无从下手。如果 Events 里没有 FailedScheduling而是卡在ContainerCreating那问题出在容器运行时层面。这时候去节点上执行crictl ps -a查看容器状态或者直接看 kubelet 日志。常见原因包括镜像拉取超时、镜像名称不存在、私有仓库未登录、卷挂载失败。有一个排查 Pending 的小技巧kubectl get events --sort-by.lastTimestamp -A按时间排序查看全集群事件比一个一个 Pod 翻效率高很多。4.2 CrashLoopBackOff反复崩溃的真相CrashLoopBackOff 意味着容器不是起不来而是起来之后立刻死掉循环往复kubelet 按 10 秒、20 秒、40 秒……的指数退避节奏不断重启它。遇到这种情况第一件事是看日志如果 Pod 里有多个容器需要指定容器名kubectl logs pod-name -c container-name。如果容器已经崩了看上一轮的日志要加--previous参数这是最容易被新手遗漏的一招。日志无异常但依然崩溃那要往两个方向排查。一个是启动命令的问题比如镜像默认 entrypoint 分配了过大的内存容器刚启动就被 OOM Killer 干掉还有一个是探针的问题readinessProbe 或 livenessProbe 的路径配错了kubelet 认为容器不健康反复杀掉重启。遇到过最典型的例子一个 Spring Boot 服务health 检查路径没暴露出来配置了 livenessProbe 之后一直被判定为不健康10 分钟内重启了 20 多次。先临时注释掉探针确认应用本身能跑再逐步调整探针参数这是最稳妥的顺序。4.3 镜像拉取失败与 QoS 下的驱逐问题ImagePullBackOff 的排查路径比较固定先kubectl describe pod看具体报错。如果是401 Unauthorized说明没有正确的 imagePullSecrets如果报403 Forbidden大概率是私有仓库权限配置不对如果报manifest unknown说明镜像 tag 不存在如果是context deadline exceeded则是网络问题本机无法访问镜像仓库。私有仓库的场景需要在 Pod 的 spec 里加 imagePullSecrets指向一个 docker-registry 类型的 Secret这个配置是经常被遗漏的。还有一类容易被误解的状态是 Evicted。这不是容器崩溃而是节点资源压力过大kubelet 按 QoS 优先级把某些 Pod 驱逐出节点。理解 QoS 对预防 Eviction 很重要。Kubernetes 把 Pod 分成 Guaranteed、Burstable、BestEffort 三个等级。如果 Pod 里每个容器的 requests 和 limits 都相等就是 Guaranteed最不容易被驱逐只要有一个容器 requests 小于 limits 或被漏配就变成 Burstable所有容器都没设置资源规格就是 BestEffort驱逐顺序排在最后。不想让核心业务被误伤请务必给所有容器配置 requests 和 limits把关键服务推到 Guaranteed 级别。4.4 一份实用的排障命令速查表把上面所有排查手段浓缩成一张速查表每次用的时候照着敲就行。症状优先使用的命令关注输出Pod Pendingkubectl describe pod NAMEEvents 里是否出现 FailedScheduling容器反复重启kubectl logs NAME --previous上一轮容器退出日志节点失联kubectl get nodes, kubectl describe node NODEReady / NotReadyConditions镜像拉取失败kubectl describe pod NAME事件里的具体错误码端口冲突kubectl describe pod NAME事件里的 Port already in use被驱逐kubectl describe pod NAME事件里的 Evicted / OutOfMemory调度不均衡kubectl get pods -o wide检查各节点 Pod 分布资源使用过高kubectl top pod NAME实时 CPU / 内存5. 我踩过的坑和一些真实经验5.1 探针参数真的会把服务搞挂我见过太多生产事故根因都是探针参数拍脑袋乱填。比如 livenessProbe 的 timeoutSeconds 设成 1服务一有抖动超过 1 秒没响应就被 kubelet 判定为不健康直接杀掉容器。Kubernetes 的 liveness 探针设计初衷是恢复病态进程不是做秒级监控报警所以阈值要留足余量。我的建议是livenessProbe 的 periodSeconds 设 10 秒以上failureThreshold 设 3readinessProbe 的 initialDelaySeconds 要大于应用启动时间否则服务还没完全初始化就被挂到 Service 后端流量打过来全是 5xx。这两组配置看起来不起眼却是线上稳定性的隐形防线。还有一个细节探针路径尽量用只读接口。别把健康检查指向依赖数据库的接口数据库抖动一次探针跟着失败容器被反复重启这事我经历过两次每次都紧张得冒汗。健康检查只验证“进程活着 基础请求能通”就够了不要贪多。5.2 镜像拉取策略和 tag 的坑镜像 tag 用 latest 是开发环境省事的办法但生产环境千万别这么干。latest 是个移动指针同一份 yaml 在不同时间 apply拉到的镜像内容可能不一样出问题都不知道是代码变没变。我现在的做法是CI 流水线构建镜像时把 git commit SHA 打进去作为 tagdeployment yaml 里用这个不可变 tag。回滚的时候能精确恢复到历史某一次提交的代码配合 rollout undo 非常丝滑。imagePullPolicy 同样有讲究。如果 tag 是 latest默认策略是 Always每次 Pod 重建都会拉镜像如果 tag 是具体版本号默认策略是 IfNotPresent本地有镜像就不拉。批量更新节点时遇到 Registry 抖动把 imagePullPolicy 改成 IfNotPresent 可以减轻仓库压力但要确定性避免“从缓存里拿到过期镜像”的问题。5.3 多容器 Pod 里哪个容器最先退出多容器 Pod 的生命周期管理有一个容易忽略的细节Pod 终止时每个容器不一定同时退出。kubelet 收到删除请求后会并行触发所有容器的 preStop hook如果没有 preStop就直接发 SIGTERM 给主进程。容器收到 SIGTERM 之后优雅退出需要时间所以需要在 yaml 里配置 terminationGracePeriodSeconds否则默认 30 秒一到kubelet 就会发 SIGKILL 强制杀死。我在一个多容器 Pod 场景里遇到过微妙的问题业务容器在收到 SIGTERM 后不到 1 秒就退出了但 sidecar 容器还要继续处理剩余日志被 SIGKILL 直接弄断导致最后几秒的日志丢失。解决办法是给业务容器加 preStop hooksleep 5 秒给 sidecar 留出收尾时间。这类跨容器的优雅退出编排光读文档很难想到只能在真实业务里慢慢体会到。6. 从 Pod 扩展出去还想再深入可以往哪个方向走这篇文章从头到尾都在讲 Pod其实 Pod 只是 Kubernetes 的起点。把 Pod 的底层机制搞清楚之后往上叠 Service 和 Ingress 管理流量往下叠存储和网络插件理解数据面往外叠 Operator 和 CRD 做自动化运维都会顺畅很多。我自己最深刻的体会是Kubernetes 的学习曲线之所以陡峭不是因为它概念多而是因为这些概念之间有极强的嵌套关系不理解 Pod后面理解网络策略、存储类、安全上下文都会是空中楼阁。所以如果你想系统地掌握 Kubernetes我建议按这个顺序推进先用手动方式反复创建、修改、删除 Pod掌握 describe 和 logs 两个命令然后学 Deployment Service 组合理解控制器模式和负载均衡接着引入 ConfigMap 和 Secret把配置和代码解耦最后再看 Kubelet 的静态 Pod 机制和调度器原理。每推进一层都回到“Pod 在这里面扮演什么角色”这个问题思路会越来越清晰。根据我个人的操作经验任何技术文档都不如亲手把一个 Pod 从 Pending 状态调到 Running 更能建立信心。挑一个最简单的 Nginx 镜像在集群里反复演练状态分析和排障流程折腾几次之后你的 Kubernetes 基本功就真正过关了。
返回列表