1 K8s 内核要理解什么
订单 API 的团队希望运行 3 个副本:有人提交新镜像,机器可能故障,请求量也会变化。如果平台只把启动命令执行 3 次,之后就没人负责检查实际结果。Kubernetes,简称 K8s,提供的核心能力是持续比较期望与实际,并推动两者接近。本课的“内核”指这组控制机制,不是 Linux 内核,也不是要求读者编写底层代码。
先把订单服务拆成能观察的对象。Deployment 表达副本数和更新策略;Pod 是被调度的运行单元;Service 为符合选择条件的后端提供相对稳定的访问入口。订单记录放在外部数据库,避免把业务数据的持久性误当成 Pod 的生命周期。这样,运行副本、请求入口和订单数据分别有明确责任,删除并重建 Pod 不应导致已提交的订单消失。
控制面接受声明、保存状态并作出安排;节点上的组件执行容器运行、网络连接和卷挂载等工作。API Server 是访问资源对象的主要入口,etcd 保存集群状态,控制器负责持续协调,调度器负责为尚未安排节点的 Pod 选择位置。kubelet 在选定节点上让 Pod 对应的容器运行起来。它们通过对象状态合作,不需要某个组件从头到尾同步指挥所有动作。

声明“需要 3 个副本”不等于承诺“任何时刻都有 3 个健康副本”。镜像下载、资源不足、探针失败都会形成差距。平台应展示期望副本、就绪副本以及差距持续时间,让读者理解收敛需要条件。所谓最终趋近,依赖控制面可用、依赖可达以及故障能够恢复;它不能把错误镜像变成可用程序。
对象通常包含 spec 和 status。spec 描述用户希望得到什么,status 记录组件观察到什么;具体字段随资源类型不同。metadata 中的名称、命名空间、UID、标签和版本信息用于定位与协作。名称可能复用,UID 区分不同生命期的对象。订单控制器若把外部资源只按名称关联,旧对象删除后的清理就可能误伤同名新对象。
身份与网络也要放入这张图。控制器以 ServiceAccount 等身份访问 API,RBAC 规定它可对哪些资源执行哪些动作;Pod 访问订单数据库则可能使用另一套业务凭据。Service 不自动代表公网入口,网络连通也不代表获得数据库权限。理解这些边界,才能把“Pod 已运行”与“订单服务可交付”分开验收。
2 控制器、Informer 与协调循环
控制器可以看成持续工作的检查员。它读到某个对象,检查相关资源,计算差距,再执行必要动作。例如 Deployment 相关控制器通过 ReplicaSet 管理副本;副本不足时创建 Pod,副本过多时减少 Pod。这里不是把每条通知机械翻译成 1 条创建命令,而是每次都根据当前观察重新判断应该做什么。
典型协调循环可以概括为“取键、读状态、算差距、作修改、再观察”。队列里常放命名空间与名称组成的键,而不是完整业务指令。多个通知可能合并,同一对象也可能被多次处理。工作进程取到键后读取对象,若对象已不存在,就按删除后的规则处理。控制器不应假定通知中的旧快照仍代表此刻的世界。
Informer 是常见的资源观察与本地缓存机制。它先获取资源列表,再持续接收 Watch 变化,把对象保存在本地缓存,并通知处理逻辑。这样,很多读取无需每次都直接请求 API Server。但缓存可能暂时落后,Watch 连接也可能中断并恢复。业务正确性必须建立在能容忍重复、延迟和重新观察之上,不能把通知当成永不丢失的业务消息账本。

以自定义的订单环境对象为例,它声明需要 1 个测试数据库。控制器发现外部数据库不存在,发起创建;下次协调发现它已经存在,就记录标识并检查可用状态,而不再重复购买。幂等表示重复执行不会不断产生额外副作用。常见办法是使用稳定的外部请求标识、查询已有资源,并把对象 UID 与外部资源关联起来。
幂等不等于并发安全。同一资源可能因为进程切换、外部重试或多个相关对象同时变化而受到交叠操作。更新 API 对象时,resourceVersion 可用于检测读取后发生的变化;遇到冲突应重新读取并计算,不能无限覆盖他人的字段。资源版本是并发控制用的标识,不应当作跨对象可比较的业务时钟。
失败应进入有节奏的重试。临时网络错误适合退避后再试,配置错误则需要状态说明并等待输入改变。控制器还需要限制并发,避免很多键同时压向数据库服务。status 的 Conditions 可以表达就绪与失败原因,但频繁写入相同状态也会制造更新通知。只有观察结果实质变化时才更新,通常能减少无效循环。
不要把每轮协调写成等待整个业务完成的长事务。外部数据库创建可能耗时数分钟,控制器可以记录进展、返回队列,稍后再检查。进程在中途重启后,应从持久状态和外部事实继续工作。内存里的“已经执行到第 3 步”不能成为唯一依据;这也是控制器比普通顺序脚本更需要设计恢复路径的原因。
3 调度器扩展与绑定
Pod 被创建后,常会暂时没有节点。调度器观察这些待调度 Pod,从候选节点中选择位置,再通过绑定过程把选择写入 API。绑定建立 Pod 与节点的安排关系;真正拉取镜像和启动容器的是节点上的 kubelet。把“已经绑定”解释为“已经启动”,会让排查在错误的组件上浪费时间。
调度可以先理解为过滤与评分。过滤剔除不满足硬条件的节点,例如可用资源无法满足 requests、节点选择条件不匹配,或存在 Pod 不能容忍的污点。评分在可行节点之间表达偏好,例如资源分布或亲和性。硬条件若排除了全部节点,再好的评分也无用。订单 API 希望分散到不同故障范围时,还要确认集群确实具备足够的候选节点。
requests 是调度判断资源需求的重要输入,limits 则主要参与运行期约束,两者不能混用。假设某节点可分配 CPU 为 4 核,已安排 Pod 的 CPU requests 合计为 3 核,新 Pod 请求 2 核,即使此刻监控显示 CPU 很空闲,也可能无法放入。这个保守安排帮助集群避免仅凭瞬时低负载过度承诺,但不会保证应用性能一定达标。
调度器框架提供扩展点,允许插件参与过滤、评分以及安排过程的其他阶段。本课只要求理解扩展点用于加入额外决策规则,例如识别特殊硬件或平台约束。不要为了给订单 API 设置普通节点偏好就马上编写插件;现有的节点亲和性、污点与容忍、拓扑分布约束往往已经足够,维护成本也更容易预估。
绑定前后的状态可能变化,因此调度不是读完节点列表后就永远正确的计算。调度器会处理资源竞争及失败后的重新尝试。扩展逻辑若调用缓慢的外部服务,会拖长安排时间;若提前占用了外部名额,还必须考虑失败时释放名额。插件接口的细节可以留到实际扩展时学习,先掌握决策有输入、安排有提交、失败有回收这 3 个边界。
还要区分调度失败和启动失败。Pod 的 nodeName 尚未确定,且事件提示资源或约束不满足,重点看调度条件;节点已经确定,事件提示拉镜像或挂卷失败,重点看节点执行和相关依赖。调度器通常不会因为一个正在运行的 Pod 所在节点后来变忙,就自动把该 Pod 无缝搬到另一个节点。迁移通常涉及新的 Pod 和新的启动过程。
4 动手:追踪一次 Pod 创建事件链
在获准使用的练习命名空间中,准备可拉取的订单 API 镜像和包含 Deployment、Service 的清单。Deployment 先设 2 个副本,并配置合理的 requests 与 readinessProbe。镜像地址、端口和探针路径需要按实际应用填写。本实验观察资源变化,不需要接入真实订单数据库,也不应在共享生产命名空间故意制造故障。
先执行以下命令观察已有对象,再应用自己的清单。这里的 orders 是练习命名空间,orders.yaml 是已准备好的文件;这些命令不是本课附带的可直接运行应用。
kubectl get deployment,replicaset,pod,service -n orders
kubectl apply -n orders -f orders.yaml
kubectl get pods -n orders -w
观察时记录 Pod 名称、UID、节点、阶段和就绪情况。Deployment 创建或调整 ReplicaSet,ReplicaSet 使 Pod 数量接近期望,调度器为待调度 Pod 安排节点,kubelet 再处理容器启动。不同对象异步变化,所以终端中看到的时间顺序可能交错;不要把几条命令的输出拼成跨组件严格同步的执行日志。
kubectl describe deployment order-api -n orders
kubectl get replicasets -n orders
kubectl describe pod POD_NAME -n orders
kubectl get events -n orders --sort-by=.metadata.creationTimestamp
kubectl get endpointslices -n orders
将 POD_NAME 替换为实际名称。通过 ownerReferences 查明 Pod 属于哪个 ReplicaSet,再追到 Deployment;这比仅凭名称前缀猜测可靠。查看 Pod 的调度条件和事件,确认它何时获得节点、何时拉取镜像、何时启动。事件是有保留期限的辅助线索,可能聚合重复发生的情况,不能替代完整审计记录。

接着区分 Running、Ready 和业务成功。Running 表示 Pod 已进入相应运行阶段,不代表所有容器已通过就绪检查;Ready 也只是配置的检查条件成立,不自动证明下单和数据库提交成功。默认情况下,Service 的可用后端会随就绪情况更新,可通过 EndpointSlice 观察;特殊发布配置可能改变这一行为,需要读清单确认。
最后把副本数从 2 调到 3,重新应用清单,观察新增 Pod 经过相同路径。若出现 Pending,就用事件验证是否是资源或约束问题;若出现镜像拉取错误,就检查地址、凭据与节点网络;若容器运行却不就绪,就检查应用日志和探针。不要一次同时修改镜像、资源和探针,否则很难判断哪项修改改变了结果。
实验记录至少包含期望、实际、证据和结论。例如“期望 3 个就绪副本;实际 2 个就绪,新增 Pod 未绑定;事件显示 CPU 请求无法满足;下一步确认节点容量和 requests 是否合理”。这比写“集群有问题”更可操作。日志、指标和事件可能使用不同采集时刻,要保留时间范围和对象 UID,减少把不同生命期的 Pod 混在一起分析。
5 扩展性与升级复杂度取舍
平台经常从“统一部署清单”走向“提供订单环境资源”。自定义资源定义 CRD 让 API 能保存新的资源类型,控制器赋予它实际行为。只有 CRD,没有协调逻辑,通常只得到可读写的数据对象;只有脚本,没有持久声明和恢复机制,又难以持续管理生命周期。扩展前先写出用户需要表达的意图,再判断是否真的需要新增 API。
现成资源组合、准入策略、自定义控制器与调度扩展解决的问题不同。Deployment 适合声明普通应用副本;准入机制适合在请求进入时检查或修改对象;控制器适合持续推动外部或集群内状态收敛;调度扩展适合影响位置决策。把耗时数据库开通塞进同步准入调用,会把外部服务延迟带进 API 请求路径,扩大故障影响。
扩展带来的成本不只是代码行数。团队需要维护 API 字段含义、权限、重试、指标、故障恢复以及版本兼容。用户一旦把自定义资源写进发布流程,字段改名就变成迁移问题。删除某个控制器之前,还要确认它管理的资源如何交接,Finalizer 如何清理。功能越深入公共控制路径,失败时影响的范围通常越大。
升级时至少检查 Kubernetes 支持版本、客户端库、CRD schema、存储中的资源版本和依赖组件。多版本自定义资源可能涉及转换,但入门阶段只需知道“新控制器能读取旧对象”必须得到验证。不要把修改 API 的表现形式与迁移已经存在的数据当成同一件事。回滚程序也可能读不懂新字段或新状态,因此回滚边界应预先写明。
可靠的取舍是先定义成功条件。例如平台希望应用团队在 10 分钟内获得可用测试环境,同时能撤销和回收;如果现有资源加少量自动化已经满足目标,就没有必要立刻建立复杂插件体系。确实需要控制器时,应先限制资源类型、命名空间和外部权限,以少量对象验证恢复能力,再逐步扩大范围。
6 常见坑:控制器无重入导致风暴
“无重入”在这里指协调逻辑不能安全应对再次进入,而不是要求某个函数一定同时运行。控制器收到重复通知、处理失败重试或进程重启后再次处理,都是正常情况。若每轮都无条件创建外部资源、更新相同 status,并立刻把自己放回队列,少量对象也可能引发大量 API 请求和重复副作用。
设订单环境需要 1 个数据库。控制器调用创建接口后等待超时,误以为没有创建成功,于是下轮再次创建。实际上服务端已接受请求,只是响应丢失。若外部接口不支持稳定请求标识,控制器就需要可靠的关联与查询机制,并承认某些结果需要人工核对。仅仅把重试次数从 10 改成 3,不能消除重复创建的根因。
另一个循环来自状态更新:协调发现资源健康,写入 Healthy;这次更新触发通知,下轮又写入相同值。如果每次还改写当前时间,更新就永远“不同”。正确做法是比较实际语义,只在状态转变或确有必要时写入;需要定期检查时采用明确的检查周期,而不是靠持续制造对象变化获得下一轮执行机会。
处理错误要区分可恢复性与紧迫度。连接短暂失败可以指数退避并加入抖动,让多个对象错开重试;鉴权失败需要明确提示权限问题;非法配置应等待用户修改。队列重试限制、外部请求超时与最大并发共同决定压力。它们需要配合使用,不能把无界积压转移成“每次请求都等很久”的工作进程耗尽。
发现风暴时,先看每秒协调次数、队列深度、重试比例、API 错误和外部调用量,再关联最近发布与具体资源。必要时暂停有问题的控制器或缩小处理范围,但要说明期间哪些资源失去协调。修复后先以少量对象恢复,确认重复处理不会增加副作用。删除全部对象、清空所有 Finalizer 不是通用止损办法,反而可能留下无法追踪的外部资产。
7 案例:Finalizer 卡住命名空间
团队删除临时订单环境所在的命名空间,界面却长期显示 Terminating。排查发现某个自定义资源带有控制器的 Finalizer,而控制器已经随旧平台下线。Finalizer 是 metadata 中的清理标记,用来让负责组件在对象最终删除前完成工作;它不是后台自动执行的函数,必须有活着且有权限的组件承担清理责任。
对带 Finalizer 的对象提出删除请求后,API 会设置 deletionTimestamp,对象仍可能继续存在。控制器看到删除意图,应停止常规创建流程,清理自己负责的外部资源,确认完成后移除自己的 Finalizer。多个组件的标记可能共存,不能依赖列表顺序执行清理,也不能随手移除其他控制器的标记。清理动作本身仍需容忍重试和部分成功。

先查看命名空间的 Conditions,了解发现资源、删除内容或清理 Finalizer 是否受阻。随后定位残留资源及其 deletionTimestamp、Finalizer、所属控制器和外部资源标识。命名空间卡住不一定由 Finalizer 导致:某些 API 服务不可用可能阻碍资源发现。因此应从 Conditions 提供的证据出发,而不是看到 Terminating 就直接执行强制结束。
本案例的外部数据库仍存在。恢复兼容的控制器及必要凭据后,它确认删除意图,删除数据库,查询确认已完成,再移除自身标记,命名空间随后完成删除。这里最关键的是能把 Kubernetes 对象与外部数据库准确关联;若只能靠相似名称猜测,自动恢复就可能删除错误资源,必须先核实身份和所有权。
如果控制器无法恢复,需要负责人确认外部资源已清理或已被明确接管,记录资源标识、账单责任和处理结果后,再评估人工移除标记。强制移除只是绕过等待,不会执行原本的清理。对命名空间执行强制结束还可能留下难以访问的残留对象,因此它应是有证据的恢复动作,而不是删除按钮失效时的默认下一步。
设计阶段应把删除作为主要流程:何时添加 Finalizer、清理失败怎样重试、凭据失效如何恢复、控制器退役前如何交接,都要写清楚。通常应在可能产生外部资源之前建立可靠的清理责任,并保证对象开始删除后不再新增业务资源。这样,创建成功与删除完成才构成完整生命周期。
8 把控制器设计写进平台方案
平台方案首先写资源契约:订单环境对象允许用户声明哪些字段,哪些字段由控制器填写,什么状态表示可用,删除代表回收还是保留数据。名称相同的新对象是否能接管旧数据库,也要明确决定。模糊的契约会把实现细节变成用户猜测,升级时更容易破坏已有自动化。
再写协调边界。列出控制器读取的集群资源、写入的字段以及调用的外部接口,为每个副作用说明去重方法和失败恢复方式。对 API 更新考虑冲突,对外部调用考虑超时后结果未知,对进程重启考虑从何处继续。不要在方案中只画“创建成功”的箭头;等待、失败、删除和重新执行都属于正式行为。
权限应沿责任拆分。控制器的 ServiceAccount 只获取必要资源与动作权限,读取业务密钥需要单独说明。不要因为控制器管理订单环境,就默认给它读取整个集群 Secret 的能力。业务容器访问数据库的身份与平台控制器管理数据库的身份也应分开,前者泄露不应天然获得删除全部测试环境的能力。
可观测性需要同时描述机制和用户体验。机制侧关注队列等待、协调耗时、错误、重试与外部调用;体验侧关注从提交到可用的时间、长期未就绪对象数量和删除完成时间。日志中保留资源 UID、协调关联信息与错误类别,避免打印凭据。告警应告诉值班者哪个目标未达成,并链接到可用证据,而不是只报一个内部函数失败。
容量与升级同样属于设计。估算对象数量、变化频率、并发上限与外部接口额度,说明大量同时创建或删除时如何限流。发布前验证重复事件、进程中断、权限被撤回和删除失败等场景。先在小范围运行,观察收敛和回收,再扩大覆盖。高级的选主细节、缓存调优与复杂调度插件实现,可以在具体容量证据出现后深入。
9 从K8s 内核走向云原生弹性与成本(C50)
现在可以解释为什么把副本数从 3 改成 6,不代表服务能力立刻翻倍。控制器需要创建 Pod,调度器需要找到容量,节点需要启动容器,应用需要预热并通过就绪检查,入口才能把流量送过去。任何环节形成瓶颈,扩容都可能滞后于需求。订单数据库或下游限流也可能让新增副本只带来更多等待。
弹性策略给出新的期望值,K8s 控制机制负责尝试实现它,云资源则决定可供安排的容量及其费用。应用副本扩容与节点容量扩容是相互关联但不同的过程。观察待调度时间、启动时间和就绪时间,才能知道该提前扩容、改善镜像启动,还是调整资源请求;只看最终副本数无法回答这些问题。
例如订单 API 从 3 个副本扩到 6 个,新增的 3 个 Pod 都因 CPU requests 无法满足而 Pending。配置了节点自动扩容组件时,它可以根据不可调度 Pod 的需求评估是否增加节点;但若 Pod 要求的节点标签在可扩容节点组中不存在,增加普通节点仍无法满足过滤条件。应把 Pod 的调度事件与节点组可提供的标签、污点及容量对应起来,确认扩容能解除哪条约束。即使云主机已创建,也要等节点注册、进入 Ready,并具备可用网络后,Pod 才有机会完成启动。若新增 Pod 已经 Ready,订单吞吐却没有提高,则应继续检查数据库连接池和下游额度,避免把业务瓶颈误判成调度器没有生效。
成本也与声明直接关联。requests 过高会让节点看似有空闲却难以再放 Pod,过低则可能造成竞争和性能波动。探针配置错误会使健康副本退出服务,反复重启或重建又消耗容量。下节 C50 将围绕需求、延迟、容量与费用建立弹性取舍,本课先留下判断基础:每次调整期望,都应追问收敛条件、实际等待和失败代价。
放进方案里
为订单 API 建设平台时,可以将交付目标写成“提交有效声明后,系统持续推进到可用;失败时提供可定位的状态;删除时完成资源回收”。Deployment 管理应用副本,Service 提供集群内访问入口,订单数据由独立数据库保存。业务验收同时检查就绪副本与真实请求结果,避免把容器启动作为唯一成功标准。
若平台增加订单环境控制器,方案应定义 spec、status、所有权和删除语义。控制器根据当前状态计算差距,允许重复通知和进程重启;外部创建使用稳定关联标识,更新冲突后重新读取。临时失败采用有上限的并发和退避,配置错误通过状态反馈。缓存只是观察手段,不能承担业务操作已经完成的唯一证明。
调度侧先使用现有资源请求、节点约束与拓扑能力,并记录容量不足时的处置路径。只有现有机制无法表达明确需求时才评估调度扩展,同时列出外部依赖延迟、版本兼容和维护责任。平台管理身份与业务访问身份分别授权,网络可达、API 权限和数据库权限分别验收。
删除流程应与创建流程同时评审。使用 Finalizer 的资源必须写明清理负责人、外部资源标识、重试规则和控制器退役交接方法。命名空间长期删除不完时,先检查 Conditions 与残留资源,再恢复清理能力;人工移除标记必须以清理或接管证据为依据,并留下操作记录。
最后,把提交到可用耗时、待调度时间、重试比例与删除耗时纳入运行指标。升级验证包含旧对象读取、部分失败恢复和回滚边界;推广从小范围开始。容量方案同时记录期望副本、就绪副本、资源请求和费用假设,为 C50 的弹性与成本设计提供可核对的输入。
10 小结:下一步云原生弹性与成本(C50)
K8s 的核心是通过对象协作与持续协调,把期望逐步变成实际。Informer 帮助观察,队列组织处理,控制器推动变化,调度器安排节点,节点组件执行运行。重复、延迟和失败都应在设计范围内;幂等、并发控制、退避和清理责任决定系统能否长期稳定工作。
检查自己能否解释 3 个问题:Pod 已绑定为什么仍可能不能服务?控制器调用超时后为何不能直接再次创建数据库?移除 Finalizer 为什么不等于完成清理?若能分别指出节点执行、结果未知和外部责任,就具备继续学习 C50 的基础:在这条收敛链上,让容量及时到位,并让成本与业务收益相称。