kube-apiserver源码拆解:Pod创建请求从HTTP到etcd的完整旅程
kubectl apply 敲下去之后Pod 创建请求的第一站永远是 kube-apiserver。很多人对 apiserver 的理解停留在它就是一个转发请求的网关但实际上它是整个 Kubernetes 控制面里逻辑密度最高的组件之一认证、鉴权、审计、准入、资源配额、对象默认值、持久化、事件通知全都在这一层完成。上一篇我们从 Pod 对象的定义和客户端入手这一篇把镜头移到服务端沿着源码把一条POST /api/v1/namespaces/{ns}/pods的请求从进门到落盘完整走一遍重点拆解 kube-apiserver 的代码路径。先说明一点我下面对源码路径的描述基于近几个版本1.20 到 1.28 左右的主干逻辑Kubernetes 演进很快细节可能随版本微调但主干设计非常稳定。读源码时认准那条主线比盯着一行行 diff 重要得多。1. kube-apiserver 接收请求的第一道工序从 HTTP 到路由分发1.1 请求先撞上的是 Go 的 net/http 层无论发起方是 kubectl、client-go、controller 里的 informer 还是 kubeletPod 创建最终都收敛成同一个动作向 kube-apiserver 的 HTTPS 端口发送一个 POST 请求路径是/api/v1/namespaces/namespace/podsbody 是一个序列化后的 Pod 对象。kube-apiserver 启动时通过cmd/kube-apiserver/app/server.go里的Run函数拉起 HTTP 服务底层就是net/http的Server。genericapiserver包里的NewAPIServerHandler会生成一个完整的 HTTP Handler 链这段代码藏在staging/src/k8s.io/apiserver/pkg/server/config.go的BuildHandlerChain里。我简化后的调用顺序大致是handler : director handler genericapifilters.WithAudit(handler, ...) handler genericapifilters.WithAuthentication(handler, ...) handler genericapifilters.WithImpersonation(handler, ...) handler genericapifilters.WithAuthorization(handler, ...) handler genericfilters.WithMaxInFlightLimit(handler, ...) handler genericfilters.WithTimeout(handler, ...)每一层WithXxx都是标准的 Go 中间件模式包一层http.HandlerFunc在调用下一个 handler 前后做自己的事。很多人调试问题时只看/logs/kubernetes.log里的业务日志忽略了这几层中间件本身也是排查重点——比如用户报请求很慢很多时候问题不是出在你提交的 Pod 字段上而是卡在WithMaxInFlightLimit的队列里或者被 APFAPI Priority and Fairness的 flow schema 限流了。1.2 路由注册Pod 的 handler 是怎么挂上去的HTTP 请求经过一堆过滤器之后最终到达路由分发层。kube-apiserver 的路由用的是 gorilla/mux 那套模式但做了大量的自定义封装。Pod 这类核心资源的 REST 端点注册路径大致是k8s.io/kubernetes/pkg/registry/core/rest/storage.go里把 Pod 的 storage 组装好然后在APIInstallation的过程中endpoints/installer.go的registerResourceHandlers方法会遍历每个资源支持的 verbcreate、get、list、watch、update、delete...为每个 verb 生成对应的 HTTP handler。关键点在于Pod 的 create 请求最终对应的是staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go里的CreateResource。这个函数返回一个http.HandlerFunc内部处理完解码、默认值、准入、校验之后调用 rest.Store 的 Create 方法把对象真正写下去。我经常跟人说想要读懂 kube-apiserver一定要抓住两个文件create.go请求入口的加工流水线和registry/store.go对象存储的核心逻辑。这两个文件能把 80% 的创建类问题给你讲明白。不过在实际阅读时容易踩一个坑create.go里的代码往往被大家直接跳过了因为大家默认create 就是调 Storage.Create。但你跳过 decode、default、admission 这三步就永远搞不清楚为什么一个看起来合法的 Pod 会被拒绝——尤其是配额类和 webhook 类的问题根子全在这三步骤里。2. 认证、授权、审计创建请求进门前的三道安检2.1 认证链路先确认你是谁请求从路由分发层正式进入业务之前要先通过认证。认证器在staging/src/k8s.io/apiserver/pkg/authentication里实现核心接口是type Authenticator interface { AuthenticateRequest(req *http.Request) (*authenticator.Response, bool, error) }Response里的User携带用户名、UID、Groups 和 Extra 信息之后整条链路里的鉴权、准入、审计都用这组信息做判断。kube-apiserver 的认证不是单一策略而是一个链。启动时根据配置往链里塞一堆认证器Bearer Tokenservice account 的 JWT、静态 token、bootstrap token、客户端证书、Webhook Token 认证等等。源码里buildAuthenticator会把它们组合成authenticatorunion.NewUnionAuthenticator逐个尝试直到某个认证器明确返回成功。如果你的请求最终谁都没认出来会走到handler.go里对anonymous用户的兜底处理——把这个请求标记为system:anonymous用户然后继续向后走。注意这个设计点认证不通过并不会直接 401而是降级成匿名用户真正决定请求生死的在下一层鉴权。所以调试 401 的时候要分清是认证器本身报错还是匿名用户权限不够被 403。2.2 授权verb 和 resource 的匹配游戏认证通过后进入鉴权层核心接口在k8s.io/apiserver/pkg/authorizationtype Authorizer interface { Authorize(ctx context.Context, a Attributes) (Decision, string, error) }Attributes 里最关键的是几个字段User、Verb对创建请求来说是 create、Resourcepods、Namespace、Subresource。WithAuthorization过滤器把这几个字段从 RequestInfo 里解析出来去问配置的鉴权器——默认是 RBAC。RBAC 的源码在pkg/registry/rbac里但真正做匹配的规则引擎在staging/src/k8s.io/apiserver/pkg/authorization/authorizerfactory和pkg/registry/rbac/validation里。每个请求会跟集群里的 Role、ClusterRole、RoleBinding、ClusterRoleBinding 匹配先聚合出用户能用的全部规则再看目标操作是否命中某条规则的(verb, resource)。这里有个容易忽略的细节RBAC 匹配只看(verb, resource, namespace)三层元组完全不管 object body 里的具体内容。也就是说RBAC 只能回答你能不能创建 Pod不能回答你能不能创建带特权字段的 Pod——后者是准入控制Admission Control的责任。2.3 审计和追踪出了问题要找“谁在哪一层拒绝的”审计层在认证之前还是之后很多人记混我直接说结论WithAudit排在整个链很靠前的位置但真正的事件输出是开始处理和完成处理两个钩子分别记录请求进入和离开审计过滤器的时间点。也就是说审计能覆盖后面所有阶段——认证、鉴权、准入、存储都发生在审计过滤器之后。源码实现在genericapifilters/audit.go它把整个下流 handler 的执行包在一个audit.LogAnnotation加audit.ObserveRequest的结构里。如果启用了 record 策略你能在 audit log 里看到stageRequestReceived和stageResponseComplete两笔记录中间标记的requestURI、user、verb、responseStatus足够定位请求到底死在哪个环节。现在的版本还支持 OpenTelemetry tracing--feature-gatesAPIServerTracingtrue打开后create.go里的trace.Step会把每个阶段的时间片记录下来。排性能问题时这个很好用能看到时间耗在 authentication、admission 还是 etcd 写入。3. admission 与默认值真正决定 Pod 能长成什么样的关卡3.1 create handler 里 admission 的调用逻辑过了鉴权之后请求终于进入CreateResource的业务逻辑。这里我建议你先记住一句话admission 不是全局过滤器而是资源 handler 内部的显式调用。早期版本里 admission 确实作为 HTTP filter 统一执行后来被重构到各个资源 handler 里原因很简单不同操作create/update/delete的 AdmissionAttributes 不同全局 filter 很难精确表达这些差异。create.go里的核心片段简化后大概长这样if scope.Defaulter ! nil { if err : scope.Defaulter(obj); err ! nil { // 给对象填默认值 } } attributes : admission.NewAttributesRecord( obj, nil, scope.Kind, reqInfo.Namespace, reqInfo.Name, scope.Resource, admission.Create, opts, ) admissionObjectInterfaces : ... if m, ok : admit.(admission.MutationInterface); ok { if err : m.Admit(ctx, attributes, admissionObjectInterfaces); err ! nil { // 变更型准入失败 } } if v, ok : admit.(admission.ValidationInterface); ok { if err : v.Validate(ctx, attributes, admissionObjectInterfaces); err ! nil { // 校验型准入失败 } }注意NewAttributesRecord里明确把 Operation 标记为admission.Create而且传入的obj是经过 defaulter 处理后的对象。顺序是固定的先 mutating后 validating。这个顺序是硬约束因为 mutating admission 会修改对象内容比如 LimitRanger 补 requests 和 limitsvalidating admission 必须基于修改后的最终对象做判断。3.2 和 Pod 创建强相关的内置插件内置 admission 插件的注册表在pkg/kubeapiserver/admission和staging/src/k8s.io/apiserver/pkg/admission/plugin下。Pod 创建路径上常见的几个插件类型对 Pod 创建做了什么NamespaceLifecyclevalidatingnamespace 不存在或正在终止时拒绝创建LimitRangermutating / validating给 Pod 的容器补默认 request/limit并校验不超过 LimitRangeResourceQuotavalidating校验 namespace 的资源配额是否够用并登记占用PodSecurityvalidating校验 Pod 是否违反 Pod Security Standardsprivileged 等DefaultStorageClassmutating给 PVC 补默认 StorageClass间接影响 PodServiceAccountmutating给 Pod 补默认 ServiceAccount若没指定PodNodeSelectormutating / validating按 namespace 的注解约束 nodeSelector已标记弃用但还在以ResourceQuota为例它内部维护一个resourceQuotaRegistry创建请求到达时先检查配额余额通过后再“预占”资源。这就是为什么很多团队会遇到配额明明看着还有但创建就报 exceeded——预占和用量追踪之间存在时间窗口尤其并发创建时很容易踩到。3.3 内置 admission 与外置 Webhook 的衔接除了内置插件kube-apiserver 还会把配置的动态 admission webhook 聚合进来入口是MutatingAdmission和ValidatingAdmission这两个实现分别对应pkg/admission/plugin/webhook/mutating和.../validating。它们的执行时机和内置插件混在一起但有个细节值得注意webhook 配置里的failurePolicy决定 webhook 挂了时请求是 Fail 还是 IgnorematchPolicy决定是匹配当前对象还是历史对象。这两个字段不搞清楚生产环境出故障时排查路径会完全不同。另外mutating webhook 对对象做的修改会在之后 validate 阶段再次被检查。这不是简单的信任机制而是保证变更后对象仍然合法的安全兜底。所以在 webhook 里改了字段后报校验错误不要惊讶那是设计如此。4. 从 Store 到 etcdPod 对象落盘的最后一公里4.1 Strategy对象在内存里如何“成熟”admission 通过后create.go调用rest.NamedCreater的 Create 方法。Pod 的 REST 实现在pkg/registry/core/pod/rest.go它内部包了一个 generic registry 的store.Store。Store.Create 的第一件事是执行rest.BeforeCreate这里才是真正的 Pod 专属逻辑func BeforeCreate(strategy RESTCreateStrategy, ctx context.Context, obj runtime.Object, opts options.CreateOptions, validateFn ObjectValidateFunc) error { if err : strategy.PrepareForCreate(ctx, obj); err ! nil { ... } if err : validateFn(ctx, obj); err ! nil { ... } if errs : strategy.Validate(ctx, obj); len(errs) 0 { ... } return strategy.Canonicalize(ctx, obj) }PrepareForCreate是用户在源码里定制对象的关键点。Pod 的 strategy 实现pkg/registry/core/pod/strategy.go里简单逻辑是func (podStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) { pod : obj.(*api.Pod) pod.Status api.PodStatus{} pod.Status.Phase api.PodPending pod.Generation 1 // 抹掉由服务端填充字段、清理弃用字段等 }注意 Server 在这里会强制把用户提交的status清空并把Phase置为Pending。这就是为什么你 kubectl apply 时写再多status都没用——它根本不是客户端能控制的字段只有status子资源pods/status的更新才会写进 status。Validate阶段的实现是validation.ValidatePod它完成对 Pod 字段的全面语法校验比如nodeSelector格式、端口冲突、name的 DNS-1123 规范、容器镜像非空等。注意这里的 Validate 是对象格式合法性和 validating admission 的策略合法性是两层不同的事情经常被混淆。4.2 序列化、静态加密与 etcd 事务写入BeforeCreate通过后store.Store.Create拿到对象的 name 和 key然后调用底层 storage。跟 etcd 的交互实现在staging/src/k8s.io/apiserver/pkg/storage/etcd3/store.go里流程很清晰data, err : s.mediaCodec.EncodeWithAlloc(obj) // 序列化 newData, err : s.transformer.TransformToStorage(ctx, data, mediatype) // 静态加密 ... txn : s.client.KV.Txn(ctx) txn.If(notFound(key)) txn.Then(opPut(key, newData, leaseID)) tresp, err : txn.Commit()这里藏了几个很值得展开的细节EncodeWithAlloc走的是SERALIZERkube-apiserver 默认用 protobuf 格式做存储序列化而不是 JSON。你kubectl get pod -o yaml看到的 JSON/YAML 只是对外展示格式。TransformToStorage是静态加密插桩ETCD 静态加密 AES-CBC / AES-GCM / KMS如果没有启用data 就是明文。etcd 写入用了条件事务If(notFound(key))也就是key 不存在时才写入。同一 key 的第二次并发创建会在这一层直接失败。所以报Operation cannot be fulfilled on pods: the object has been modified不是 etcd 的模糊错误而是事务冲突的明确信号。写入时还会关联一个 leaseTTLPod 的 TTL 通常为 0表示不设自动过期。4.3 resourceVersion、Watch 通知和 201 响应事务提交成功后etcd 会返回写入后的 key-value 和ModRevision。store.go的afterCreation负责把 etcd 返回的二进制 value 重新 decode 回对象并把resourceVersion设置成 etcd 的版本号。这个resourceVersion不是 apiserver 自己造的随机数它直接来源于 etcd 的版本计数这也解释了为什么 resourceVersion 是全局单调递增、为什么对象更新后它一定变大。与此同时缓存层的 watch cache 在监听 etcd 的 watch 事件staging/src/k8s.io/apiserver/pkg/storage/cacher。写入完成后etcd watch 事件立刻被 cache 捕获cache 更新自己的内存并唤醒所有等待这个对象变化的 watcher。kube-scheduler、kubelet、各种 controller 之所以几乎同时感知到新 Pod不是因为大家一起轮询 apiserver而是因为大家都在 apiserver 上挂了 watch而 watch 的源头在 etcd 事件。最后create.go将创建后的完整对象带 resourceVersion、uid、creationTimestamp序列化以 HTTP 201 Created 返回给客户端。5. 源码级体检Pod 创建失败时如何逐层定位5.1 返回码和日志对应到哪一层调试 Pod 创建问题我建议先看返回的状态码再对照链路猜阶段。一个朴素的映射关系HTTP 状态码大概率阶段常见原因400 Bad Requestdecode / validatebody 不是合法 Pod字段类型错误401 Unauthorized认证认证器全部失败请求降级为匿名403 Forbidden鉴权 / admissionRBAC 拒绝、配额超限、PodSecurity 拒绝404 Not Found路由 / 资源namespace 不存在或 API 版本不存在409 Conflictetcd 事务同一 key 并发创建或修改422 Unprocessable Entityadmission validationadvisory validation 失败例如名字格式429 Too Many RequestsAPF / MaxInFlight流量整形或限流配合 audit 和 apiserver 日志验证时--v4起步很多 admission 日志在--v2就能看到etcd 层事务问题则要看--v6及以上。在源码里追踪时搜admission.Atomic和storage.AppendFunc这种标志性函数名会很有帮助。5.2 几个高频异常的实际排查路径我自己踩得比较多的问题有三个分别对应链路上不同位置。第一个是webhook 拒绝但日志里看不到拒绝原因。这类问题十有八九是failurePolicy或namespaceSelector配置把 webhook 过滤掉了或者 webhook 调用的配置里service指向的端口不通。查的时候先确认 webhook 配置里的matchPolicy和namespaceSelector是否命中了当前请求再看validatingwebhookconfigurations的事件日志。第二个是配额报错。返回体里通常带着exceeded quota和具体资源名。这种问题不要只盯 apiserver要同时看resourcequota.quota.admission.k8s.io的 reconcile 机制配额的“当前使用量”是周期性 update 的创建瞬间的余量可能被并发请求瞬间占满。源码上建议看pkg/quota/v1/resources的资源累加逻辑理解Quota计算方式是按 requests 算而不是按 limits 算。第三个是the object has been modified。刚才说过这是 etcd 事务冲突。出现频率最高的是 controller 同时更新同一个对象或者 informer 缓存和实际 etcd 版本脱节。你kubectl get pod -o yaml拿到的未必是最新版本修改前一定要用当前的resourceVersion或者干脆用update的语义让服务端做 compare-and-swap。5.3 版本迁移时最容易踩的坑最后提醒几件容易在升级 Kubernetes 版本后翻车的事admission 从 filter 到 handler 的重构让插件执行顺序在 1.20 前后有明显变化升级后如果自定义 webhook 行为异常先确认 webhook 是不是在错误阶段被误调。APF 引入后--max-requests-inflight之外还有 flow schema 的并发控制创建突发时 429 不一定代表资源不够可能只是被限流了。etcd 存储编码从 JSON 切换到 protobuf 是很早以前的事了但如果你还在自己写 apiserver 扩展CRD 或者自己聚合 API务必使用k8s.io/apiserver的 negotiation 框架别手搓序列化否则很容易产生存储和 API 版本不一致的诡异问题。静态加密启用后etcd 里是密文你直接用ETCDCTL去查对象是看不到明文 Pod 的。这不是数据丢了是加密转换器在起作用。排查的时候别在 etcd 层被误导。我个人在排查这类问题上的习惯是先看返回码再开审计最后进源码。返回码能快速缩小阶段审计能给出精确的 user/verb/resource 三元组源码能帮你确认具体代码路径里到底哪个条件把请求拦了下来。读源码的时候不要只盯着某一个文件从create.go往store.go往etcd3/store.go这条主线往下钻反复几次之后你会对整个控制面的调度节奏产生真实的体感。后续如果有时间我再把 kube-scheduler 和 kubelet 那两段写出来毕竟 Pod 真正跑起来后面还有更长的路要走。