路线图

面试 · 原理追问型

星辉 2026-07-02 阅读 10 min 2,102 字 路线图
面试 · 原理追问型 封面

3. 原理题#

考察对 K8s 内部工作机制的理解,每题含工作流程描述和关键步骤说明。


题目 1:Pod 调度流程#

问题: 请详细描述一个新 Pod 从创建到运行在某个 Node 上的完整调度流程。

参考答案:

Pod 调度由 kube-scheduler 负责,整个过程分为三个阶段:

阶段一:预选(Filtering / Predicates)

Scheduler 遍历所有 Node,通过一系列过滤插件排除不满足条件的节点:

  • NodeResourcesFit:Node 的剩余 CPU/内存是否满足 Pod 的 requests
  • NodeAffinity:Node 的标签是否满足 Pod 的 nodeSelector/nodeAffinity 要求
  • TaintToleration:Pod 是否容忍 Node 上的 Taint(如 GPU 节点的 nvidia.com/gpu:NoSchedule
  • PodTopologySpread:是否符合跨 Zone/Node 的分布约束
  • VolumeBinding:PVC 是否可以绑定到该 Node(Local PV 场景下尤为重要)
  • NodeUnschedulable:Node 是否已标记为不可调度(cordoned)

经过预选后得到"可行节点"列表,如果为空,Pod 保持 Pending,Events 中显示原因(如 Insufficient cpuno nodes available)。

阶段二:优选(Scoring / Priorities)

对可行节点打分(0-100 分),常见的打分策略:

  • LeastAllocated:优先选择剩余资源多的节点(让负载更均衡)
  • MostAllocated:优先选择剩余资源少的节点(让节点更满,给其他节点更多空闲空间)
  • BalancedAllocation:优先选 CPU 和内存利用率差异小的节点
  • NodeAffinityPriority:满足 preferred nodeAffinity 的节点得更高分
  • InterPodAffinityPriority:考虑 Pod 间亲和性约束

每个打分插件分配 0-100 分,最终分数为加权和。得分最高的节点胜出。

阶段三:绑定(Binding)

Scheduler 创建 Binding 对象,将 Pod 与选定 Node 的关联写入 API Server → etcd。对应节点的 kubelet 监听到绑定事件,通过 CRI 调用容器运行时(containerd)拉取镜像、创建容器。

注意:Scheduler 只负责"选节点 + 写 Binding",不亲自启动容器;真正拉镜像、起容器的是目标节点的 kubelet。这是控制面(决策)与节点代理(执行)分离的体现。

追问:预选后没有可行节点会怎样?抢占(Preemption)如何工作?

如果预选后可行节点为空,Pod 保持 Pending。此时若该 Pod 配了较高的 PriorityClass,Scheduler 会触发抢占

  1. 找一个"把上面某些低优先级 Pod 赶走后,本 Pod 就能放下"的节点
  2. 选择牺牲代价最小的一组低优先级 Pod 作为 victims,对它们发起优雅驱逐(尊重其 terminationGracePeriodSeconds 和 PDB——注意抢占会尽量遵守 PDB,但当无法同时满足时高优先级 Pod 优先)
  3. 把待调度 Pod 的 status.nominatedNodeName 标记为该节点,占位等待 victims 腾空后再正式绑定

若设了 preemptionPolicy: Never,则该 Pod 虽有高优先级参与排队排序,但不会抢占别人。这解释了"为什么低优先级批处理会在高优先级上线时被杀"——正是抢占机制。

怎么验证/排查调度失败: kubectl describe pod <pod> 看 Events——Insufficient cpu/memory(资源不足)、node(s) had untolerated taint(污点未容忍)、node(s) didn't match Pod's node affinity(亲和性不匹配)、0/N nodes are available(综合原因)都会在这里显式给出;抢占则会出现 preempted 相关事件和 nominatedNodeName

扩展机制: Scheduler 支持自定义调度器(schedulerName 字段)、Scheduler Framework Plugin 扩展(在预选/优选阶段插入自定义逻辑)、Scheduler Extender(HTTP Webhook 扩展,但性能较差)。


题目 2:kube-proxy 流量转发机制#

问题: 描述 kube-proxy 如何实现 Service ClusterIP 到后端 Pod 的流量转发。

参考答案:

kube-proxy 是运行在每个 Node 上的网络代理,监听 API Server 中 Service 和 Endpoints 的变化,在本地维护转发规则。

工作流程(以 iptables 模式为例):

  1. 监听变化:kube-proxy 通过 Watch 机制监听 Service 和 Endpoints/EndpointSlice 的变化。当新 Service 创建或后端 Pod 状态变更时,触发规则更新。

  2. 规则生成:kube-proxy 为每个 Service 生成一组 iptables NAT 规则,写入 Node 的 iptables。规则链条为:

    text
    PREROUTING → KUBE-SERVICES → KUBE-SVC-XXXX → KUBE-SEP-XXXX(DNAT to Pod IP)
    • KUBE-SERVICES 链:匹配目标 IP 是否为某个 Service 的 ClusterIP
    • KUBE-SVC-XXXX 链:每个 Service 对应一条链,使用 statistic probability 实现随机负载均衡
    • KUBE-SEP-XXXX 链:每个后端 Pod(Endpoint)对应一条链,执行 DNAT 将 ClusterIP:Port 替换为 PodIP:targetPort
  3. 流量转发:当 Pod A 访问 Service ClusterIP 时,数据包到达 Node 网络栈,命中 iptables DNAT 规则,目标地址被替换为某个后端 Pod IP。注意:数据包离开 Node 前不会经过 Service 的虚拟 IP,而是直接路由到 Pod IP

  4. Conntrack 优化:Linux conntrack 记录每个连接的后端 Pod 分配结果,同一 TCP 连接后续数据包直接复用该映射,不需要重新遍历 iptables 规则。

其它模式的转发机制(简述): IPVS 用内核哈希表把查找从 O(n) 降到 O(1)、支持多种 LB 算法(轮询/最小连接/源哈希);nftables(1.33 GA、非默认)用 map/verdict 增量下发规则;Cilium eBPF 直接在内核处理转发、完全绕过 kube-proxy,延迟可降至微秒级。三种模式的完整定义与选型见概念篇 题14,性能/规模横向对比见对比篇 题14;本题聚焦 iptables 模式的链路机制。

怎么验证转发规则: iptables 模式 iptables -t nat -L KUBE-SERVICES -n、IPVS 模式 ipvsadm -Ln、nftables 模式 nft list table ip kube-proxy;连接映射看 conntrack -L经典排查:Service 能通但概率性失败,多半是某个后端 Pod 没通过 Readiness、却仍被算进后端——查 kubectl get endpointslice 确认后端列表是否与预期一致。


题目 3:Controller Manager 工作原理#

问题: 解释 Controller Manager 中控制器的工作原理和调谐循环。

参考答案:

Controller Manager 是一个运行多个控制器的集合进程,每个控制器负责一个特定的资源类型。核心工作机制是调谐循环(Reconcile Loop)

text
for {
  desired := 获取期望状态(从 API Server 读取 spec)
  actual := 获取实际状态(从 API Server 读取 status 或查询集群)
  if desired != actual {
    执行操作,使 actual → 趋向 → desired
  }
  sleep(或等待下次事件)
}

以 Deployment Controller 为例:

  1. Watch 监听:Controller 通过 Informer 机制监听 Deployment 资源和 ReplicaSet 资源的变化(增/删/改)。Informer 维护本地缓存,减少 API Server 压力。

  2. 触发调谐:当监听到变化事件时,对应的 Deployment 被放入 WorkQueue。

  3. 调谐逻辑

    • 读取 Deployment 的 spec.replicas(期望副本数)
    • 找到当前关联的 ReplicaSet(新旧各一个)
    • 计算新 ReplicaSet 需要多少 Pod,旧 ReplicaSet 需要缩减多少 Pod
    • 创建/删除 Pod(实际上是通过操控 ReplicaSet 的 spec.replicas 间接实现)
  4. 状态更新:调谐完成后,更新 Deployment 的 status 字段(availableReplicas、updatedReplicas 等),写入 API Server。

关键设计:

  • 水平触发(Level-triggered):Controller 基于当前状态差异做调谐,而非基于事件。即使中间漏掉某个事件,下次调谐也能纠正。
  • 幂等性:调谐逻辑必须幂等——多次执行相同逻辑应得到相同结果。
  • Leader Election:Controller Manager 和 Scheduler 通过 etcd 实现 Leader Election,同一时刻只有一个实例是活跃的(处理调谐),其他实例待命。

题目 4:kubelet Pod 生命周期管理#

问题: 描述 kubelet 创建和管理 Pod 生命周期的完整流程。

参考答案:

kubelet 是每个 Node 上的"节点代理",负责将 Pod Spec 转换为实际运行容器的进程。完整生命周期如下:

1. Pod 创建流程:

text
API Server → kubelet Watch → Pod Admission(校验)→ CRI → CNI → CSI
  • 监听 Pod:kubelet 通过 Watch 监听绑定到本节点的 Pod 列表
  • Pod Admission:执行 Pod 准入检查(资源是否足够、Sysctl 配置等),通过后更新 Pod 状态为 Pending
  • 创建 Pod Sandbox:调用 CRI(容器运行时接口)创建 Pod 级别的 sandbox(包含网络命名空间、IPC 命名空间)
  • 网络配置:调用 CNI 插件创建 veth pair、分配 IP、配置路由
  • 挂载卷:等待 Volume Manager 完成 PV/PVC、ConfigMap、Secret 等卷挂载
  • 启动容器:调用 CRI 依次启动 Init Container(按定义顺序,普通 init 容器跑完退出才进行下一个),再启动普通容器(每个容器按 startupProbe → readinessProbe → livenessProbe 顺序执行检查)。1.33 GA 的原生 Sidecar 是一类特殊 init 容器(restartPolicy: Always):它会先于业务容器启动、常驻运行,并在业务容器全部退出后才终止,解决了 sidecar 启动/关停顺序错乱的老问题

2. 运行中管理:

  • 探针检查:定期执行 Readiness Probe(失败:把 Pod IP 从 EndpointSlice 摘除、停止导流,但不重启)和 Liveness Probe(失败:原地重启该容器
  • 重启的边界(高频误区)restartPolicy(Always/OnFailure/Never)作用的是容器——Liveness 失败或容器崩溃时,kubelet 在原 Pod 内重启这个容器RESTARTS 计数增加,Pod 本身不会被重建、Pod IP/所在节点不变。真正"重建 Pod"(换新 Pod 名、可能换节点)只发生在 Pod 对象被删除、节点失联被驱逐、或被上层控制器(ReplicaSet 等)新建时。所以"容器挂了整个 Pod 重启"这种说法是错的
  • 资源监控:通过 cAdvisor 采集容器 CPU/内存使用,上报给 API Server
  • 卷同步:定期同步 ConfigMap/Secret 卷内容到挂载目录

3. Pod 终止流程:

text
发送 SIGTERM → 等待 terminationGracePeriodSeconds → 发送 SIGKILL → 清理
  • API Server 给 Pod 打上 deletionTimestamp、状态转为 Terminating,同时 EndpointSlice 控制器把该 Pod IP 摘除(停止新流量导入)
  • 宽限期从进入 Terminating 起算:kubelet 先执行 PreStop Hook(如有),向容器主进程发 SIGTERM——PreStop + SIGTERM 后应用优雅退出的时间共用同一个 terminationGracePeriodSeconds(默认 30s),并非各给 30s。若 PreStop 耗时 25s,留给 SIGTERM 后优雅退出的就只剩 5s
  • 到期仍未退出,发送 SIGKILL 强杀(对应退出码 137)
  • 清理容器、sandbox、网络命名空间、挂载点

两个常见坑:(1)摘 Endpoint 与发 SIGTERM 是并发的,若应用收到 SIGTERM 立刻停止 accept,而此时 kube-proxy 规则尚未更新完,仍会有少量新连接打进来被拒——生产上常在 PreStop 里 sleep 几秒等 Endpoint 传播完成再退出。(2)应用必须自己处理 SIGTERM 做优雅关闭;很多进程(尤其 shell 形式的 ENTRYPOINT)收不到或不处理 SIGTERM,最终只能等超时被 SIGKILL 硬杀,丢掉在途请求。


题目 5:etcd Raft 协议核心#

问题: 描述 etcd 如何通过 Raft 协议保证分布式一致性。

参考答案:

etcd 使用 Raft 共识算法保证分布式集群中所有节点的数据一致性。

Raft 核心机制:

  1. Leader 选举:

    • 集群中任一时刻只有一个 Leader,其余节点为 Follower
    • Leader 定期发送心跳(heartbeat)给 Follower。如果 Follower 在选举超时(150-300ms)内未收到心跳,转为 Candidate 并发起选举
    • Candidate 向其他节点请求投票,获得半数以上(quorum)选票即当选为新 Leader
    • Term(任期)递增保证不会出现两个 Leader 同时存在(同一 Term 最多一个 Leader)
  2. 日志复制(数据写入流程):

    • 客户端写请求发送到 Leader
    • Leader 将操作追加到自己的日志(WAL),标记为 uncommitted
    • Leader 并行发送日志条目给所有 Follower
    • 当超过半数节点(含 Leader)确认收到后,Leader 将条目提交(committed),返回客户端成功
    • Leader 通知 Follower 提交该条目
    • 关键:客户端收到成功响应意味着数据已被超过半数节点持久化,即使 Leader 立即宕机,新 Leader 一定包含已提交数据
  3. 一致性保证:

    • etcd 默认提供线性一致性读(Linearizable Read)——读操作会去 Leader 确认,保证读到最新已提交数据,不会出现"读旧写新"的异常
    • 序列化读(--consistency=s):允许从任意节点读,可能读到稍旧的数据,性能更高(适合 Watch)

K8s 运维关键点:

  • 3 节点 etcd 可容忍 1 节点故障,5 节点可容忍 2 节点
  • 必须是奇数节点(偶数节点会增加 quorum 门槛但不多容错)
  • etcd 磁盘 IO 性能至关重要——Scheduler Leader 选举失败、API Server 写入慢通常与 etcd 磁盘延迟相关

题目 6:CNI 插件工作流程#

问题: 描述 kubelet 调用 CNI 插件为 Pod 配置网络的完整流程。

参考答案:

CNI(Container Network Interface)规范定义了容器运行时(kubelet/containerd)和网络插件之间的标准接口。

完整工作流程:

text
kubelet → containerd/CRI → CNI Plugin (/opt/cni/bin/) → 配置网络
  1. 创建 Pod Sandbox:kubelet 通过 CRI 调用容器运行时(如 containerd)创建 Pod Sandbox。Sandbox 包含一个独立的网络命名空间(network namespace),所有 Pod 容器将加入该命名空间。

  2. CNI ADD 调用:容器运行时调用 CNI 插件二进制(如 /opt/cni/bin/calico),传入网络配置(从 /etc/cni/net.d/ 读取 JSON 配置)和 Pod network namespace 路径。

  3. 创建 veth pair:CNI 插件创建一对虚拟以太网接口(veth pair):

    • 一端放入 Pod 的 network namespace(命名如 eth0
    • 另一端留在宿主机网络命名空间(命名如 caliXXXX
  4. IP 分配(IPAM):CNI 插件通过 IPAM 子插件(如 host-localcalico-ipam)从 Pod CIDR 中分配一个可用 IP,配置到 Pod 的 eth0 接口上。

  5. 路由配置:在 Pod 内添加默认路由(通过 eth0 到宿主机 gateway),在宿主机添加到达该 Pod IP 的路由。

  6. 跨节点路由:根据 CNI 插件类型处理跨节点通信:

    • Overlay(VXLAN):创建 VXLAN 隧道接口,Pod 间流量经过 VXLAN 封装后通过物理网络传输
    • Underlay(BGP):通过 BGP 协议向路由器通告 Pod CIDR,直接路由
  7. CNI DEL 调用:Pod 删除时,再次调用 CNI 插件执行清理(删除 veth pair、释放 IP 等)。


题目 7:DNS 解析链路(FQDN 规则)#

问题: 描述 K8s 集群中 Pod 通过 DNS 访问 Service 的完整解析链路。

参考答案:

K8s 集群中的 DNS 解析由 CoreDNS(或 kube-dns)负责,Pod 的 /etc/resolv.conf 由 kubelet 自动配置。

DNS 解析链路:

  1. Pod 内 DNS 配置/etc/resolv.conf 典型内容:

    text
    nameserver 10.96.0.10        # CoreDNS ClusterIP
    search <namespace>.svc.cluster.local svc.cluster.local cluster.local
    options ndots:5
  2. FQDN 规则:Service 的完整域名格式为 <service-name>.<namespace>.svc.cluster.local。Pod 可以通过以下方式使用:

    • 简短名称myapp(同 Namespace 内直接用服务名)
    • 跨 Namespacemyapp.productionmyapp.production.svc.cluster.local
    • ndots:5 规则:如果域名中点号少于 5 个,DNS 客户端会依次尝试 search domain 拼接。例如查询 myapp 时会依次尝试 myapp.default.svc.cluster.local
  3. 解析流程

    text
    Pod 发起 myapp.default.svc.cluster.local 查询
      → CoreDNS(10.96.0.10:53)
        → 匹配 Service 记录
        → 返回 ClusterIP(如 10.96.1.5)
      → Pod 获得 Service ClusterIP
      → kube-proxy iptables/ipvs 规则将请求 DNAT 到后端 Pod IP
  4. Headless Service 的 DNS 差异(概念与三大使用场景见概念篇 题18,此处只列 DNS 差异):

    • 普通 Service:myapp.svc → ClusterIP
    • Headless Service(clusterIP: None):myapp.svc → [Pod1 IP, Pod2 IP, …]
    • StatefulSet Pod 固定 DNS:<pod-name>.<service>.<ns>.svc.cluster.local
  5. StatefulSet SRV 记录:CoreDNS 还会为 StatefulSet 的每个 Pod 创建 SRV 记录(_port._protocol.<service>.<ns>.svc.cluster.local),方便应用做服务发现。

常见 DNS 问题:

  • NetworkPolicy 未放行 CoreDNS UDP 53 → DNS 解析超时
  • CoreDNS Pod 全部不可用 → 所有服务发现失败
  • ndots:5 过高导致外网域名查询时产生大量无效 search domain 尝试

题目 8:PVC 绑定流程#

问题: 详细描述 PVC 从创建到绑定 PV 的完整流程,以及动态 provisioning 的实现。

参考答案:

PVC 绑定涉及 PV Controller、Provisioner 和 Scheduler 的协同工作。

流程一:静态绑定(已有匹配 PV)

  1. 用户创建 PVC,声明需要的资源(容量、访问模式、StorageClass)
  2. PV Controller 监听新 PVC,遍历集群中状态为 Available 的 PV
  3. 匹配条件:PV 容量 ≥ PVC 请求容量、PV 访问模式覆盖 PVC 需求、PV StorageClass 匹配 PVC 指定、Label Selector 匹配
  4. 选择最优匹配 PV(通常选能满足需求的最小 PV),将 PV 状态改为 Bound
  5. 更新 PVC spec.volumeName,PVC 状态从 Pending 变为 Bound

流程二:动态 provisioning(指定 StorageClass)

  1. PVC 创建后,PV Controller 发现没有匹配的 Available PV,且 PVC 指定了 StorageClass
  2. PV Controller 查找对应 StorageClass 的 provisioner(如 ebs.csi.aws.comnfs.csi.k8s.io
  3. 调用 provisioner 的 RPC 接口,传入 PVC 参数(容量、StorageClass 参数)
  4. Provisioner 创建底层存储(如调用 AWS API 创建 EBS 卷),生成 PV 对象并绑定到该 PVC
  5. PV 的 claimRef 指向具体 PVC,PersistenceVolume 标记为 Bound

流程三:Pod 挂载使用

  1. Scheduler 在调度 Pod 时执行 VolumeBinding 检查:PVC 是否已绑定、PV 是否支持在当前 Node 使用(如 Local PV 或块存储只能挂载到同可用区 Node)
  2. Pod 调度到 Node 后,kubelet 的 Volume Manager 调用 CSI 插件(或内置卷插件)执行 NodeStageVolumeNodePublishVolume
  3. 卷挂载到 Pod 指定目录,容器可以读写

PVC Pending 排查路径:

  • kubectl describe pvc 看 Events
  • StorageClass 是否存在且正确 → provisioner 是否健康 → 底层存储系统是否可用

题目 9:滚动更新机制#

问题: 详细描述 Deployment 滚动更新的内部机制和关键参数。

参考答案:

Deployment 的滚动更新通过操控新旧 ReplicaSet 的副本数实现逐批替换。

关键参数:

  • maxSurge:滚动过程中允许超过期望副本数的最大数量(可以是数字或百分比),影响更新速度
  • maxUnavailable:滚动过程中允许不可用 Pod 的最大数量,影响服务可用性
  • revisionHistoryLimit:保留旧 ReplicaSet 的数量(默认 10),用于回滚

滚动更新流程(以 replicas=3, maxSurge=1, maxUnavailable=0 为例):

  1. 用户更新 Deployment(如修改镜像版本)
  2. Deployment Controller 检测到 spec 变更,创建新的 ReplicaSet
  3. 第 1 步:新 ReplicaSet 扩到 1 个 Pod(因为 maxSurge=1,总数可达 4)
  4. 等待新 Pod 变为 Ready(Readiness Probe 通过)
  5. 第 2 步:旧 ReplicaSet 缩到 2 个 Pod(因为 maxUnavailable=0,不可用 Pod 必须保持 0)
  6. 第 3 步:新 ReplicaSet 扩到 2 个 Pod
  7. 第 4 步:旧 ReplicaSet 缩到 1 个 Pod
  8. 重复直到全部替换完成

零停机配置: maxUnavailable: 0 + maxSurge: 1 是关键——先起新 Pod,等新 Pod Ready 后再删旧 Pod,确保整个过程随时满足至少 replicas 个 Pod 在运行。

回滚机制:

bash
kubectl rollout undo deployment/myapp --to-revision=3

回滚时,旧 ReplicaSet(历史版本)被重新激活,执行逆向滚动更新。关键前提:镜像版本可追溯、ConfigMap/Secret 版本与 Deployment 版本一致。

必须配合 Readiness Probe——没有 Readiness Probe,K8s 认为容器 Running 就是 Ready,可能在应用还没启动完成时就把流量转发过去。


题目 10:HPA 计算原理#

问题: 详细说明 HPA 的完整工作流程,包括指标获取、副本数计算和防抖机制。

参考答案:

HPA Controller 持续运行调谐循环,每隔固定间隔(通过 --horizontal-pod-autoscaler-sync-period 配置,默认 15s)执行一次扩缩容决策。

副本数计算公式的概念解释、"2 副本 80%→4 副本"算例、以及"为何必须设 resources.requests"见概念篇 题12;本题聚焦完整调谐流程、指标 API 链路、防抖与排查。

完整工作流程:

  1. 指标获取

    • HPA 向 Metrics API 查询当前 Pod 的资源使用指标
    • Resource Metrics(CPU/内存):通过 metrics.k8s.io API → Metrics Server → kubelet cAdvisor
    • Custom Metrics:通过 custom.metrics.k8s.io API → Prometheus Adapter 等自定义指标适配器
    • External Metrics:通过 external.metrics.k8s.io API → 外部系统(如云监控、KEDA)
  2. 副本数计算

    对于每种指标,分别计算期望副本数:

    text
    desiredReplicas = ceil( currentReplicas × (currentMetricValue / targetMetricValue) )
    • 利用率指标(CPU/内存):currentReplicas × (actualUtilization / targetUtilization)
    • 绝对值指标(QPS/队列深度):ceil(currentMetricValue / targetMetricValue)

    如果有多个指标,取最大计算值作为最终期望副本数。

  3. 防抖与稳定机制(autoscaling/v2 现行默认;旧版 --horizontal-pod-autoscaler-upscale-delay(3m) / downscale-delay(5m) 已随算法重写废弃):

    • 容差范围:指标在目标的 ±10% 范围内波动时不触发扩缩
    • 扩容:默认无稳定窗口scaleUp.stabilizationWindowSeconds=0),可立即扩,每 15s 最多翻倍或加 4 个(取较大者)
    • 缩容:默认稳定窗口 scaleDown.stabilizationWindowSeconds=300(5 分钟)——取窗口内所有推荐值的最大值再决定是否缩,避免指标瞬间下探就抖动缩容
    • behavior 策略:通过 scaleUp.policies / scaleDown.policies 自定义每次扩缩步长(Percent/Pods + periodSeconds)
  4. 执行扩缩容

    • HPA 通过 API Server 修改目标资源(Deployment/StatefulSet)的 spec.replicas 字段
    • 目标 Controller 收到变化后执行实际扩缩

重要限制(原理同概念篇 题12): 依赖 resources.requests 计算利用率,不设则 TARGETS 显示 <unknown>;不能缩容到 0(minReplicas 最小为 1,缩到 0 用 KEDA,见对比篇 题27);内存因 GC 释放慢,不适合做缩容依据。

怎么验证/排查: kubectl get hpaTARGETS<unknown> 多为漏设 requests 或 Metrics Server 不健康)与 REPLICASkubectl describe hpa 的 Events 会显式给出 FailedGetResourceMetricScalingLimited(受 min/maxReplicas 或 behavior 限流)等原因。


4. 设计理念题#

考察对"为什么这样设计"的深层理解,每题含设计原因和不这样设计的后果。


题目 1:为什么 Pod 是最小调度单元而非容器?#

问题: 为什么 K8s 设计成以 Pod 而非 Container 作为最小调度单元?

参考答案:

Pod 的概念定义与"三点概括"见概念篇 题1;本题展开完整的设计动因。

设计原因:

  1. 紧密协作的容器组需求。很多场景需要多个容器共享网络和存储并部署在一起:主业务容器需要日志采集 sidecar 把日志推送到日志平台;Service Mesh 中的 Envoy/Linkerd sidecar 代理进出流量;AI 推理服务需要模型缓存容器。如果以容器为调度单元,需要额外的"容器组"概念来维护这种协作关系,而 Pod 直接提供了这种抽象。

  2. 共享网络和 IPC 命名空间的价值。Pod 内所有容器共享同一个网络命名空间(同一个 IP 地址),可以通过 localhost 直接通信,无需服务发现或端口注册。这大大简化了 sidecar 模式的实现——业务容器通过 localhost:9090 访问 sidecar 的 metrics 端点。(共享网络命名空间的完整设计理由单列于题目 7。)

  3. 共享 Volume 的便利性。Pod 内容器可以通过共享 Volume 实现文件级别的数据交换和协调。比如 Init Container 下载配置文件到共享 Volume,主容器读取该文件;或者 sidecar 从共享 Volume 读取日志文件转发。

  4. 统一的生命周期管理。Pod 提供了三层探针(Startup/Readiness/Liveness)、PreStop Hook、terminationGracePeriod 等生命周期钩子,使得容器组的整体状态可以被 K8s 统一管理。

如果以容器为调度单元:

  • 无法表达"这些容器需要部署在同一台机器上"的强约束
  • Sidecar 模式需要额外的通信开销(从 localhost 变为网络通信)
  • 容器间共享存储和生命周期管理会变得复杂且容易出错

题目 2:为什么 K8s 使用声明式 API?#

问题: 为什么 K8s 选择声明式而非命令式 API 设计?

参考答案:

声明式与命令式的逐项对比表见对比篇 题25;本题展开"为何选择声明式"的完整设计动因。

设计原因:

  1. 自愈能力(Self-healing)。声明式 API 配合 Controller 的持续调谐循环,使得系统具备自动修复能力。例如有人手动删除了 Deployment 下的一个 Pod,Deployment Controller 发现"实际副本数 2 ≠ 期望副本数 3",会自动创建新的 Pod 补齐。如果是命令式系统,手动删除后状态丢失,系统不会自我纠正。

  2. 复杂性的封装。声明式 API 把"如何达到目标"的复杂性封装在控制器内部,用户只需描述"目标是什么"(如 replicas: 3),不用关心具体步骤。滚动更新涉及创建新 Pod、等待 Ready、删除旧 Pod 等复杂操作序列,但用户只需修改镜像版本字段即可触发。

  3. 基础设施即代码(IaC)。声明式 YAML 配置天然适合版本管理——存入 Git、通过 PR Review、CI/CD 自动化 apply。这催生了 GitOps(ArgoCD/FluxCD 自动从 Git 同步到集群),使得基础设施变更可审计、可回滚、可复现。

  4. 幂等性。声明式 API 的 kubectl apply 天然幂等——多次执行相同文件不会产生副作用。而命令式操作(如 kubectl scale --replicas=5)第二次执行和第一次语义相同,但需要在执行前先查询当前状态才能做到"增量式命令"。

  5. 水平触发的可靠性。声明式 Controller 基于当前状态的差异做调谐(Level-triggered),即使中间漏掉某个事件,下次调谐也能纠正。Edge-triggered 的事件驱动系统在事件丢失时需要额外的补偿机制。

如果使用命令式 API:

  • 没有自愈能力,需要人工介入处理故障
  • 操作不可审计,无法通过 Git 管理基础设施
  • 需要大量脚本来处理各种异常路径,运维复杂度呈指数级增长

题目 3:为什么 etcd 推荐 3 或 5 个节点(奇数节点)?#

问题: 为什么 etcd 集群推荐使用奇数节点,而不是 2 或 4 个节点?

参考答案:

设计原因:

  1. Raft 算法的 Quorum 机制。Raft 要求任何写入操作必须获得**超过半数(多数派)**节点的确认才能提交。设节点数为 N,容忍故障数 F = floor((N-1)/2)。具体数值:

    • 1 节点:F = 0(无容错)
    • 3 节点:F = 1(允许 1 节点故障,可用性 2 节点)
    • 4 节点:F = 1(允许 1 节点故障,但需要 3 节点确认 —— 比 3 节点多 1 个节点却没多容错)
    • 5 节点:F = 2(允许 2 节点故障,高可用性)
    • 6 节点:F = 2(允许 2 节点故障,但需要 4 节点确认 —— 比 5 节点多 1 个节点却没多容错)
  2. 偶数节点的浪费。比较 3 节点 vs 4 节点:两者都只能容忍 1 个节点故障,但 4 节点集群需要额外 1 个节点的资源(CPU/内存/磁盘),且写入时需要 3 个节点确认(vs 3 节点集群只需 2 个确认),写入延迟反而更高。同理,5 节点 vs 6 节点:都容忍 2 个故障,6 节点需要更多资源和更高确认数,没有任何额外收益。

  3. 脑裂(Split-Brain)预防。在偶数节点的集群中,如果网络分区导致节点分成两组(各 2 节点),双方都无法获得 3 票(多数),集群整体不可用。但奇数节点天然避免了平分情况,任何多数派都是明确的。

  4. 生产实践证明的性价比最优

    • 3 节点:最常见配置,平衡了容错性(容忍 1 故障)和成本
    • 5 节点:高可用需求(容忍 2 故障),通常跨 3 个可用区部署(2+2+1)
    • 7 节点:极少使用,维护成本高且 quorum 写入延迟更大

如果使用偶数节点:

  • 资源浪费:多一个节点但不增加容错能力
  • 写入延迟更高:需要更多节点确认
  • 网络分区风险:平分时无法形成多数派
  • 运维成本更高:多维护一个节点但没有额外收益

题目 4:为什么 Service 使用 ClusterIP 而非直接路由到 Pod?#

问题: 为什么 K8s 不直接将客户端流量路由到 Pod IP,而是引入 Service ClusterIP 这一层间接?

参考答案:

Service 的职责与四种类型见概念篇 题2,ClusterIP/NodePort/LoadBalancer 横向对比见对比篇 题3;本题展开"为何要有 ClusterIP 这层间接"的设计动因。

设计原因:

  1. Pod IP 不稳定性。Pod 是短暂易失的资源——Pod 会被删除、重建、重新调度到不同节点,每次重建 IP 都会变化。如果客户端直接依赖 Pod IP,Pod 变化时所有客户端都需要更新目标地址,这在微服务架构中不可行。

  2. 服务发现与解耦。ClusterIP 是一个"固定不变"的虚拟 IP,配合稳定的 DNS 名称(myapp.ns.svc.cluster.local),为服务消费者提供一个永久不变的入口。无论后端 Pod 如何变化(扩缩、更新、故障重建),客户端始终访问同一个 ClusterIP 和 DNS。

  3. 负载均衡。Service 通过 kube-proxy 在 ClusterIP 层面实现负载均衡(iptables 随机选择、IPVS 多种算法),将流量均匀分发到后端健康 Pod。如果让客户端直接管理 Pod IP 列表,每个客户端都要实现负载均衡逻辑。

  4. 健康检查隔离。Service 只将流量转发到 Readiness Probe 通过的 Pod。当 Pod 不可用时(如启动中、过载、依赖故障),其 Endpoint 从 Service 中移除,流量自动避开。直接路由到 Pod IP 需要更复杂的客户端健康检查机制。

  5. 统一管理入口。Service 提供统一的流量管理入口——可以为同一组 Pod 创建多个 Service(不同 ClusterIP、不同端口、不同协议),也可以为不同端口选择不同的 Service 类型(ClusterIP 内部 + LoadBalancer 外部)。

如果直接路由到 Pod:

  • 客户端需要持续监听 Pod 变化并更新地址列表(复杂度巨大)
  • 每个客户端需要自己实现负载均衡和健康检查
  • 流量管理(金丝雀、蓝绿、A/B 测试)需要修改应用代码
  • Pod 变更时会出现连接中断

题目 5:为什么 RBAC 分为 Role 和 ClusterRole 两级?#

问题: 为什么 K8s 的 RBAC 设计了 Role(命名空间级)和 ClusterRole(集群级)两个层次?

参考答案:

设计原因:

  1. 资源范围的自然分层。K8s 中资源天然分为两类:Namespace 级资源(Pod、Service、Deployment 等,属于某个 Namespace)和集群级资源(Node、PV、Namespace、StorageClass 等,全局唯一)。两种不同范围的资源需要用不同级别的权限来控制,Role 管理 Namespace 内权限,ClusterRole 管理跨 Namespace 和集群级权限。

  2. 最小权限原则的落地。将权限分为两级使得安全实践更精细化——开发团队在 team-a Namespace 内通过 Role 获得 Deployment 管理权限,但无法访问其他 Namespace 的资源和集群级资源(Node、其他 PVC)。如果不分层,要么权限过大(给跨 Namespace 权限),要么权限碎片化(每个 Namespace 手动管理)。

  3. RoleBinding 可以引用 ClusterRole(权限降级)。这是一个精妙的设计——定义一个通用的 ClusterRole(如 view:只读所有 Namespace 的 Pod),在特定 Namespace 中通过 RoleBinding 引用该 ClusterRole,将权限范围限制在该 Namespace。这样实现了"一次定义 ClusterRole,多 Namespace 复用,但权限仅限绑定范围"。如果不分层,每个 Namespace 都需要创建独立的 Role,大量重复配置。(这一"RoleBinding 引用 ClusterRole"的绑定机制与规则细节见对比篇 题11。)

  4. 系统组件的权限需求。K8s 系统组件(如 kube-proxy、CoreDNS、CNI 插件)需要访问集群级资源或跨所有 Namespace 操作(如 kube-proxy 需要监听所有 Namespace 的 Service)。这些场景必须用 ClusterRole 和 ClusterRoleBinding。

如果不分层:

  • 权限管理要么过于粗放(全集群权限),要么过于繁琐(每个 Namespace 手工创建相同规则)
  • 集群级资源(Node、PV)无法被精确授权
  • 没有"复用权限定义但限制范围"的机制

题目 6:为什么 kube-proxy 使用 iptables/IPVS 而非用户空间代理?#

问题: 为什么 kube-proxy 从早期的用户空间代理(userspace mode)演进为 iptables/IPVS 内核级代理?

参考答案:

各转发模式的定义见概念篇 题14,横向对比见对比篇 题14,iptables 链路机制见原理篇 题2;本题聚焦"为何从用户态演进到内核级代理"的设计动因。

设计原因:

  1. 用户空间代理的性能瓶颈。早期的 userspace 模式中,kube-proxy 自身作为一个 TCP 代理监听在每个 Service 端口上。所有到 Service 的流量先到达 kube-proxy(用户态),再由 kube-proxy 建立到后端 Pod 的新连接(用户态 → 内核 → 用户态 → 内核)。每个数据包都需要在用户态和内核态之间来回复制(context switch),吞吐量受限,延迟显著增加。正因如此,userspace 模式已在 1.26 彻底移除,仅作历史背景理解。

  2. 内核级转发的天然优势。iptables/IPVS 工作在内核态,通过 Netfilter 框架在数据包经过网络栈时直接修改目标地址(DNAT),整个转发过程不经过用户态。数据包路径简化为"内核 → DNAT → 路由 → 出接口",消除了 context switch 和内存拷贝开销。

  3. 从 iptables 到 IPVS 再到 nftables/eBPF 的演进。iptables 模式解决了用户态代理的性能问题,但引入了规则规模的问题(O(n) 链式查找、规则全量同步慢,大集群场景下延迟增加至数百毫秒)。IPVS 用内核哈希表将查找复杂度降为 O(1);**nftables 模式(1.33 GA,非默认)**用现代内核框架从根子上替代老旧的 iptables 后端;Cilium 则用 eBPF 直接在内核处理转发、彻底绕开 kube-proxy。这条主线始终是"把转发下沉到内核、并降低规则规模的复杂度"。

  4. 减少故障点。用户空间代理模式中,如果 kube-proxy 进程崩溃,所有 Service 不可用。iptables/IPVS 规则一旦写入内核,即使 kube-proxy 暂时不可用,已有的连接继续转发(网络数据面和控制面分离)。

  5. 连接跟踪(conntrack)的利用。基于 iptables/IPVS 的 DNAT 可以充分利用 Linux 内核的 conntrack 机制——同一 TCP 连接的第一个包匹配 DNAT 规则后,后续包直接通过 conntrack 表查找转发,无需重复遍历 iptables 规则。

如果使用用户空间代理:

  • 吞吐量受限于用户态-内核态切换(现代网络场景不可接受)
  • 每个 Service 需要额外监听端口和代理资源
  • kube-proxy 成为关键单点(挂了全局不可用)
  • 延迟增加数倍

题目 7:为什么 Pod 内多容器共享网络命名空间?#

问题: 为什么 K8s 设计成 Pod 内所有容器共享同一个网络命名空间?

参考答案:

本题是设计理念篇 题1"Pod 为何是最小调度单元"中"共享网络"一点的深入展开;Pod 概念见概念篇 题1。

设计原因:

  1. Sidecar 模式的根本需求。Service Mesh(Istio/Linkerd)的流量拦截依赖于 Pod 内所有容器共享网络。Envoy sidecar 通过 iptables 规则拦截 Pod 内所有进出流量,如果主容器和 sidecar 不在同一网络命名空间,无法做到透明拦截。同样,日志 sidecar 通过 localhost 采集主容器的日志输出也需要共享网络。

  2. 简化容器间通信。同一 Pod 内的容器通过 localhost 和不同端口直接通信,不需要服务发现、DNS 解析、负载均衡。这对辅助容器(如 Prometheus exporter sidecar 在 localhost:9090 暴露指标)的集成至关重要。

  3. 端口空间隔离。虽然共享网络,但每个 Pod 有自己独立的网络命名空间,不同 Pod 的容器可以使用相同的端口号而不冲突。这为应用提供了标准化的端口选择自由(如 Web 服务都可以监听 8080)。

  4. IP 地址经济的平衡。如果每个容器都有独立 IP,IP 消耗将成倍增加,且需要额外的容器间路由。Pod 一个 IP 的设计在满足 sidecar 需求的同时控制了 IP 地址数量。

  5. Linux Namespace 的自然表达。在 Linux 层面,Pod 的 Sandbox 就是一个 network namespace,kubelet 创建这个 namespace 后所有容器加入它。这是 Linux Namespace 机制的自然映射。

如果不共享网络命名空间:

  • Sidecar 模式需要通过网络通信而非 localhost(大幅增加复杂度和延迟)
  • Service Mesh 流量拦截需要更复杂的机制
  • 容器间通信依赖完整的服务发现链路(DNS + ClusterIP + kube-proxy)
  • 每个容器独立 IP 会快速耗尽 IP 地址池

附录:面试建议#

  1. 概念题和对比题是面试基础关,建议每个概念都能用一句话概括"是什么、解决什么问题"
  2. 原理题体现技术深度,回答时要展示"知其然也知其所以然"的理解
  3. 设计理念题是高级工程师的分水岭,能说出"为什么这样设计"比能记住"怎么配置"更有说服力
  4. 回答时按"定义 → 原因 → 例子 → 坑点"的结构展开,一般 1-2 分钟可以讲清楚一道题
  5. 对比题建议先给出对比表(1分钟),再挑 2-3 个关键差异展开(1分钟),最后给选型建议