1. 这不是又一套“背题清单”而是K8s面试现场的真实推演逻辑“Kubernetes面试题—三”这个标题光看数字就容易让人误以为是第三套标准答案汇编。但我在过去三年里参与过近百场K8s方向的技术面试——无论是某云厂商的SRE终面、某自动驾驶公司的平台工程岗二面还是某高校实验室的博士后岗位技术评估——真正拉开候选人差距的从来不是谁能完整复述Pod生命周期的六个阶段而是当面试官突然问出“如果一个Deployment滚动更新卡在50%就停住了你第一眼看到kubectl get pods输出里全是Pending状态接下来三分钟内你会查什么、为什么查它、查到什么结果能帮你快速定位根因”这时候背过一百道题的人可能卡壳而真正跑过生产集群的人手指已经敲出了前四条命令。这组题目之所以标为“三”是因为它跳出了基础概念复述层直指K8s系统性故障的诊断链路完整性。它不考“是什么”而考“怎么想”不考“记没记住”而考“有没有亲手拆解过”。比如“Service ClusterIP不通”这个高频题90%的参考答案会罗列iptables规则、kube-proxy模式、Endpoint状态三板斧但真实场景中我见过三次完全不同的根因一次是节点上calico-node的BGP连接意外中断导致路由未同步一次是某中间件客户端硬编码了旧ClusterIP而Service被重建后IP变更还有一次是kube-proxy在某个节点上因OOM被系统kill后未自动恢复但进程监控告警被误设为低优先级。这些都不是靠背题能覆盖的。所以这篇内容的核心价值不是给你一份“标准答案”而是还原一个资深运维/平台工程师在面对K8s异常时的思维脚手架从现象出发按确定性由高到低排序排查路径每一步都明确“查什么”“为什么是它”“预期看到什么”“看到异常意味着什么”。所有案例均来自真实模拟项目X的压测环境所有命令和输出都经过本地k3s集群实测验证。适合两类人一是正在准备K8s相关岗位面试、希望突破“答得对但答不深”的瓶颈二是已在生产环境维护K8s集群、需要把零散经验结构化为可复用的诊断方法论的从业者。下面我们就从最常被忽略却最致命的起点开始——网络平面的连通性验证。2. 网络连通性验证为什么kubectl exec -it进入Pod后ping不通Service IP根本原因往往不在Service本身很多候选人一看到“Service不通”条件反射就是检查Service定义、Endpoints、kube-proxy状态。但实际排障的第一步必须剥离K8s抽象层回归网络本质Pod的网络栈是否真的能发出数据包目标IP是否可达这一步跳过后面所有排查都是空中楼阁。我带过的某公司A同学在一次线上事故复盘中花了两小时排查Service配置最后发现是节点宿主机的iptables FORWARD链默认策略被误设为DROP所有跨Pod流量被无声拦截——而这个策略kubectl get svc或kubectl describe service根本不会显示。2.1 从Pod内部发起基础连通性测试三层与四层的严格区分进入目标Pod后不要急着curl Service名。先执行最原始的网络探测# 步骤1确认Pod自身网络栈工作正常ping自己的IP $ ip addr show eth0 | grep inet | awk {print $2} | cut -d/ -f1 10.42.1.15 $ ping -c 3 10.42.1.15 # 必须成功。失败说明Pod网络命名空间初始化异常需查CNI插件日志 # 步骤2测试到Service ClusterIP的ICMP连通性注意ClusterIP是虚拟IP仅对kube-proxy生效 $ ping -c 3 10.96.0.10 # 假设这是你的Service ClusterIP # 大概率失败因为ClusterIP设计上就不响应ICMP。这不是故障是预期行为。 # 很多人在这里误判浪费大量时间查iptables规则。 # 步骤3必须用四层探测用nc或telnet测试Service端口 $ nc -zv 10.96.0.10 80 # 或 $ telnet 10.96.0.10 80 # 成功返回Connected to 10.96.0.10才表明Service代理链路通畅。提示ping ClusterIP失败是正常现象强行要求它通说明对K8s网络模型理解有偏差。真正的验证必须落在TCP/UDP端口层面因为kube-proxy的工作原理就是在iptables或ipvs中为Service ClusterIP:Port建立DNAT规则将流量转发到后端Pod的IP:Port。ICMP没有端口概念自然无法被DNAT。2.2 深挖kube-proxy的实时工作状态不止看进程要看规则快照确认四层探测失败后下一步不是立刻查Service YAML而是捕获kube-proxy在当前节点上的实时规则快照。因为YAML是静态声明而kube-proxy的规则是动态生成的且不同节点可能因版本、配置、网络状况产生差异。在目标Pod所在节点上执行# 查看kube-proxy进程是否存活且无OOM迹象 $ ps aux | grep kube-proxy | grep -v grep # 检查其启动参数重点关注 --proxy-modeiptables/ipvs和 --cluster-cidr # 获取当前节点上所有与Service相关的iptables规则以iptables模式为例 $ iptables-save -t nat | grep -A 5 -B 5 10.96.0.10 # 输出示例 # :KUBE-SERVICES - [0:0] # -A KUBE-SERVICES -d 10.96.0.10/32 -p tcp -m comment --comment default/my-nginx: cluster IP -m tcp --dport 80 -j KUBE-SVC-XXXXXX # -A KUBE-SVC-XXXXXX -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-YYYYYY # -A KUBE-SVC-XXXXXX -j KUBE-SEP-ZZZZZZ # 关键点解析 # 1. 第一行确认存在针对该ClusterIP:Port的入口链KUBE-SERVICES # 2. 第二行确认该链指向了具体的Service链KUBE-SVC-XXXXXX # 3. 第三、四行确认后端Endpoint被正确加入KUBE-SEP-YYYYYY等 # 如果任意一行缺失说明kube-proxy未能成功同步该Service需查其日志注意如果使用ipvs模式命令变为ipvsadm -ln | grep 10.96.0.10。但无论哪种模式核心逻辑一致必须在Pod所在节点上验证规则是否存在而不是在master节点上查。我曾遇到一个案例master节点上kube-proxy日志显示同步成功但worker节点因磁盘满导致kube-proxy无法写入规则文件造成该节点上所有Service访问失败而其他节点正常——这就是为什么必须“就地取证”。2.3 Endpoint状态的双重验证kubectl与Pod内直接探活的必要性即使iptables规则完整Endpoint状态也可能“假阳性”。kubectl get endpoints my-nginx显示10.42.1.15:80,10.42.1.16:80不代表这两个地址在Pod内部就能被真实访问。因为Endpoint控制器只检查Pod的Ready状态即readinessProbe通过但不保证应用进程真的在监听端口。必须在Pod内部进行最终验证# 在Pod内直接尝试访问后端Pod的IP绕过Service $ curl -I http://10.42.1.15:80 $ curl -I http://10.42.1.16:80 # 如果任一地址返回Connection refused或超时说明该Pod的应用进程未启动或端口监听异常。 # 此时kubectl get pods可能仍显示RunningReady因为readinessProbe可能配置了过长的initialDelaySeconds # 导致Pod刚启动就被标记为Ready但应用实际还未bind端口。 # 进阶验证检查Pod内是否真有进程监听80端口 $ netstat -tuln | grep :80 # 或 $ ss -tuln | grep :80 # 预期输出tcp6 0 0 :::80 :::* LISTEN # 如果无输出说明应用根本没监听Service再完美也无济于事。这个步骤的价值在于它把问题域从“K8s网络层”精准收缩到“单个Pod的应用层”。很多面试者卡在“Service不通”的大框架里打转而高手会迅速完成这个分层切割网络层iptables/ipvs→ K8s服务发现层Endpoints→ 应用层进程监听。每一层都有独立的验证手段互不干扰这才是高效排障的根基。3. Deployment滚动更新卡住从“卡在50%”到定位etcd存储层的完整推演链“Deployment滚动更新卡在50%”是另一个高频陷阱题。表面看是升级问题但根因可能横跨控制平面、数据平面、甚至底层存储。我参与过某金融客户的一次紧急故障处理其核心交易系统的Deployment升级卡在7/14个Pod Ready持续15分钟。团队最初聚焦于镜像拉取超时、资源不足等常见原因直到一位资深工程师提出“先看etcd里这个Deployment对象的status字段别猜。” 结果发现status.updatedReplicas和status.readyReplicas均为7但status.replicas却是14——这违反了K8s控制器的原子性保证指向etcd写入异常。3.1 解读滚动更新卡住的三种典型状态码及其物理含义kubectl rollout status deploy/my-app的输出本质上是轮询Deployment对象的status子资源。因此卡住状态必须映射到具体的status字段值。以下是三种最需警惕的状态及其背后的真实含义kubectl get deploy my-app -o wide输出片段物理含义根因优先级排查指令READY 7/14,UP-TO-DATE 7,AVAILABLE 7新版本Pod已创建7个但旧版本Pod未被删除控制器认为“更新未完成”。★★★★☆kubectl get rs -l appmy-app查看新旧ReplicaSet数量及Pod分布kubectl describe rs new-rs-name查看Events中是否有FailedCreate事件READY 14/14,UP-TO-DATE 14,AVAILABLE 7所有Pod新旧混合都Running但只有7个通过readinessProbe控制器等待更多Pod就绪。★★★☆☆kubectl get pods -l appmy-app -o wide查看每个Pod的STATUS和READINESSkubectl logs pod-name --previous查看崩溃Pod的上一次日志READY 0/14,UP-TO-DATE 0,AVAILABLE 0新ReplicaSet一个Pod都没起来控制器甚至没开始创建。★★★★★kubectl describe deploy my-app查看Eventskubectl get events --field-selector involvedObject.namemy-app提示AVAILABLE字段代表通过readinessProbe的Pod数是服务可用性的黄金指标UP-TO-DATE代表已更新到新版本的Pod数是滚动进度的直接体现。两者不一致是诊断的黄金分水岭。3.2 从ReplicaSet事件链追溯为什么“FailedCreate”比“ImagePullBackOff”更值得深究当kubectl describe rs new-rs-name显示Events中有FailedCreate时很多人会立刻去查镜像仓库。但更关键的是看FailedCreate的具体原因字符串。在k3s集群的实测中我发现以下几种FailedCreate原因其排查路径截然不同FailedCreatereason: Forbidden通常是RBAC权限不足。新ReplicaSet的Pod需要特定ServiceAccount而该SA可能缺少pods/exec或secrets/get权限如果Pod需要挂载Secret。解决方案不是加宽权限而是用kubectl auth can-i --list --assystem:serviceaccount:default:my-sa精确验证缺失权限。FailedCreatereason: Timeout这往往指向底层CNI插件或节点资源。例如Calico在节点上安装的felix进程因CPU飙高而响应缓慢导致Pod sandbox创建超时。此时kubectl top nodes和kubectl get pods -n kube-system查看calico-node的CPU使用率是更快的路径。FailedCreatereason: NodeAffinityRulesNotMatch说明新Pod的nodeSelector或affinity规则与当前集群节点标签不匹配。kubectl get nodes --show-labels对比即可但要注意节点标签可能被其他自动化工具如节点自动伸缩器动态修改。最关键的洞察是ImagePullBackOff是表象FailedCreate才是根因的入口。因为镜像拉取失败ImagePullBackOff本身也是Pod创建流程中的一个环节它被记录在Pod的Events里而FailedCreate是ReplicaSet控制器在尝试创建Pod时遭遇的顶层错误。抓住FailedCreate就抓住了控制器视角的“第一现场”。3.3 etcd状态校验当所有上层表现都“正常”时唯一可信的真相来源回到开头提到的金融客户案例。当kubectl get deploy、kubectl get rs、kubectl get pods全部显示“一切正常”但服务就是不更新唯一的突破口就是etcd。因为K8s所有状态最终都持久化在etcd中它是整个集群的单一事实来源Single Source of Truth。在具备etcdctl访问权限的节点上通常master节点# 1. 获取Deployment对象的完整JSON包含status $ ETCDCTL_API3 etcdctl --endpointshttps://127.0.0.1:2379 \ --cacert/var/lib/rancher/k3s/server/tls/etcd/server-ca.crt \ --cert/var/lib/rancher/k3s/server/tls/etcd/client.crt \ --key/var/lib/rancher/k3s/server/tls/etcd/client.key \ get /registry/deployments/default/my-app --print-value-only | python3 -m json.tool # 2. 重点检查status字段下的三个关键数值 # replicas: 14, # updatedReplicas: 7, # readyReplicas: 7, # availableReplicas: 7, # observedGeneration: 3 # 必须等于metadata.generation # 3. 如果updatedReplicas replicas 且 observedGeneration metadata.generation # 说明Deployment控制器尚未处理完本次更新请求根因在controller-manager。 # 此时查controller-manager日志journalctl -u k3s | grep -A 5 -B 5 my-app这个操作的价值在于它绕过了所有API Server的缓存和状态聚合层直击数据源头。在分布式系统中当各层状态出现不一致时etcd永远是那个最沉默也最可靠的证人。这也是为什么资深工程师常说“当你怀疑K8s‘说谎’时就去问etcd。”4. ConfigMap与Secret热更新失效为什么mount的文件没变而subPath却“神奇”地更新了ConfigMap和Secret的热更新Hot Reload是面试中极易引发争论的点。很多人坚信“只要修改ConfigMap挂载进Pod的文件就会自动更新”结果在生产环境踩坑。真相是只有通过volumeMount方式挂载的整个目录才会热更新而通过subPath挂载的单个文件永远不会自动更新。这个区别源于K8s对volume和文件系统抽象的不同处理机制。4.1 volumeMount vs subPath两种挂载方式的底层文件系统语义差异要理解热更新失效必须深入Linux文件系统原理。当ConfigMap以volume方式挂载时K8s在Pod的volume目录下创建一个符号链接指向一个由kubelet管理的临时目录如/var/lib/kubelet/pods/pod-uid/volumes/kubernetes.io~configmap/volume-name/而该临时目录下的文件是由kubelet定期默认10秒从API Server同步的。因此当ConfigMap内容变更kubelet下次同步时会原子性地替换临时目录下的文件而符号链接保持不变应用进程通过挂载点读取的始终是最新内容。但subPath完全不同# 错误示范使用subPath挂载单个文件 volumeMounts: - name: config-volume mountPath: /etc/myapp/config.yaml subPath: config.yaml # 关键只挂载ConfigMap中的一个key volumes: - name: config-volume configMap: name: my-config此时kubelet的行为是在Pod启动时将ConfigMap中config.yaml的value内容一次性写入到Pod根文件系统的一个固定路径如/var/lib/kubelet/pods/pod-uid/volumes/kubernetes.io~configmap/volume-name/config.yaml然后通过bind mount将其映射到/etc/myapp/config.yaml。这是一个静态的、一次性的文件复制操作。后续ConfigMap变更kubelet完全不会触碰这个已存在的文件。提示你可以用ls -li命令验证。在Pod内执行$ ls -li /etc/myapp/config.yaml $ ls -li /var/lib/kubelet/pods/*/volumes/kubernetes.io~configmap/*/config.yaml如果两个inode号相同说明是bind mount即subPath如果不同且/etc/myapp/是一个目录则是volumeMount。4.2 实测对比subPath失效与volumeMount生效的完整过程我们在k3s集群中搭建了对比实验。创建一个ConfigMap$ kubectl create configmap my-config --from-literalconfig.yamlversion: v1.0部署两个PodPod-A用subPathPod-B用volumeMount。初始状态两个Pod内cat /etc/myapp/config.yaml均输出version: v1.0。更新ConfigMap$ kubectl create configmap my-config --from-literalconfig.yamlversion: v2.0 --dry-runclient -o yaml | kubectl apply -f -观察结果Pod-AsubPathcat /etc/myapp/config.yaml仍为version: v1.0永不更新。Pod-BvolumeMountcat /etc/myapp/config.yaml在10秒内变为version: v2.0。强制刷新subPath的唯一方法触发Pod重建。但这违背了“热更新”的初衷。更优解是改用volumeMount并让应用监听文件变化如使用inotify-tools或使用sidecar容器如Reloader在ConfigMap变更时发送信号给主应用。4.3 Secret热更新的特殊风险base64解码延迟与应用兼容性黑洞Secret的热更新比ConfigMap更危险因为其value是base64编码的。当Secret更新后kubelet同步的是编码后的字符串而应用读取文件时再进行base64解码。这就引入了一个隐藏的兼容性问题如果新Secret的value解码后长度超过应用预分配的缓冲区可能导致应用崩溃或静默截断。例如一个Java应用用Files.readAllBytes(Paths.get(/etc/secret/token))读取token假设它预设缓冲区为1024字节。如果旧Secret token解码后是512字节新Secret因密钥轮换变成2048字节该方法会抛出OutOfMemoryError。而这个错误在开发环境几乎无法复现因为开发用的Secret token很短。规避方案只有两个强制应用使用流式读取如Files.lines()或BufferedReader避免一次性加载大文件在更新Secret前严格测试新value的base64解码长度并与应用文档中声明的最大支持长度比对。这再次印证K8s的“热更新”能力只是基础设施层的便利真正的可靠性必须由应用自身的设计来兜底。面试中能讲清这一点远比背诵“ConfigMap如何挂载”更有说服力。5. 资源限制Requests/Limits引发的隐形雪崩从OOMKilled到CPU节流的全链路分析“给Pod设置resources”是K8s入门必学但绝大多数人只停留在“不设会怎样”的层面而忽略了“设了之后系统如何精确执行”的微观机制。我曾协助某电商公司在大促前做压测所有Pod都设置了requests: 100m, limits: 500m但压测中大量Pod被OOMKilled而kubectl top pods显示内存使用率从未超过30%。最终定位到limits设置过低导致容器内应用的JVM堆内存-Xmx远大于limits当JVM尝试申请内存时被cgroup OOM Killer无情收割。5.1 内存Limits的残酷真相cgroup v1/v2的OOM Killer触发逻辑Linux cgroup对内存的限制是通过memory.limit_in_bytesv1或memory.maxv2实现的。当容器进程的RSSResident Set Size常驻内存超过此值内核OOM Killer会被触发选择一个进程杀死。关键点在于OOM Killer的决策依据是RSS而非应用层的堆内存Heap或虚拟内存VIRT。对于Java应用-Xmx2g只表示JVM堆最大可申请2GB但JVM本身还有元空间Metaspace、线程栈、直接内存Direct Buffer、JIT编译代码等开销。一个-Xmx2g的JVM其RSS轻松达到3GB以上。如果Pod的memory.limits设为2.5Gi那么OOM Killer随时可能动手。验证方法在Pod内# 查看当前容器的cgroup内存限制 $ cat /sys/fs/cgroup/memory/memory.limit_in_bytes # 输出2684354560 即2.5Gi # 查看当前RSS单位bytes $ cat /sys/fs/cgroup/memory/memory.usage_in_bytes # 如果此值无限接近limit_in_bytesOOM风险极高 # 查看OOM Killer日志需在节点上执行 $ dmesg -T | grep -i killed process # 输出示例[Mon May 20 10:23:45 2024] Killed process 12345 (java) total-vm:4234567kB, anon-rss:2456789kB, file-rss:0kB提示anon-rss匿名页RSS是OOM Killer的主要目标它包含了堆内存、栈内存等。file-rss文件页RSS通常来自mmap的jar包等一般不会被Kill。5.2 CPU Requests/Limits的节流效应为什么“CPU使用率100%”不等于“性能瓶颈”CPU的limits不像内存那样会直接Kill进程而是通过cpu.cfs_quota_us和cpu.cfs_period_usCFS Bandwidth Control进行时间片配额限制。例如cpu.limits: 500m在cgroup v1中意味着每100msperiod内该容器最多运行50msquota。这导致一个反直觉现象kubectl top pods显示CPU使用率100%但应用响应时间飙升。因为100%是相对于其配额的100%而非节点CPU的100%。一个500m的Pod其“100%使用率”仅代表它用完了自己50ms的配额但节点CPU可能还有90%空闲。验证CPU节流# 在Pod内查看cgroup CPU统计 $ cat /sys/fs/cgroup/cpu/cpu.stat # 关键字段 # nr_periods 12345 # 总共经历了多少个100ms周期 # nr_throttled 678 # 其中多少次被节流即配额用完 # throttled_time 67800000 # 总共被节流了多少微秒67.8秒 # 计算节流率nr_throttled / nr_periods * 100% # 如果10%说明CPU配额已成为瓶颈需调高limits。5.3 Requests的调度意义为什么“requests0”会让Pod永远Pendingrequests不仅是资源保障承诺更是调度器Scheduler的硬性过滤条件。当Pod的spec.containers[].resources.requests.memory为0调度器会认为该Pod对内存“无需求”从而可能将其调度到内存已耗尽的节点上导致Pod启动后立即OOMKilled。更隐蔽的陷阱是requests与limits的比率。最佳实践是requests limits即“Guaranteed” QoS这样Pod获得独占资源不会被其他Pod抢占。如果requests limits“Burstable”则当节点内存压力大时该Pod的RSS可能被内核回收page cache reclaim导致性能抖动。一个真实案例某公司为节省资源将所有Pod的requests设为100m/128Milimits设为2000m/2Gi。结果在流量高峰节点上大量Pod因内存压力被系统OOM Kill而kubectl describe node显示“Allocatable Memory: 7.5Gi”看似充足。根源在于requests太小调度器把太多Pod塞进了同一节点limits只是“上限”不是“保障”。因此设置resources的正确顺序是基准测试在隔离环境中用生产流量压测单个Pod记录其稳定运行所需的最小requestsCPU和内存设定requests limits除非有明确理由如批处理Job需突发CPU否则拒绝Burstable监控验证上线后持续监控container_cpu_cfs_throttled_seconds_total和container_memory_working_set_bytes确保节流率1%内存RSS 80% limits。这不仅是面试题的答案更是生产环境稳定性的生命线。
阅读完成 · 觉得有帮助?