首页 文章 精选 留言 我的

精选列表

搜索[main],共10003篇文章
优秀的个人博客,低调大师

kubernetes 进阶知识--API Server 源码:从 main 到 etcd 的一条主线

## 一、三个 apiserver 串成一条链,不是三个进程 ![src-1-chain.png](https://developer.qcloudimg.com/http-save/yehe-11479707/e7cee0797a5a95b48c9a5ca8b2380b4f.png) *图 1:同一进程里的三层 handler,按路径分工。* ```go // cmd/kube-apiserver/app/server.go (v1.34.0) func CreateServerChain(config CompletedConfig) (*aggregatorapiserver.APIAggregator, error) { apiExtensionsServer, err := config.ApiExtensions.New(genericapiserver.NewEmptyDelegateWithCustomHandler(notFoundHandler)) kubeAPIServer, err := config.KubeAPIs.New(apiExtensionsServer.GenericAPIServer) // aggregator comes last in the chain aggregatorServer, err := controlplaneapiserver.CreateAggregatorServer( config.Aggregator, kubeAPIServer.ControlPlane.GenericAPIServer, ...) return aggregatorServer, nil } ``` 顺序是 **apiextensions → kube → aggregator**,每一层把下一层当作 `delegate`。 | server | 管什么 | | --- | --- | | apiextensions | `/apis/apiextensions.k8s.io` 和所有 CRD | | kube | `/api/v1`、`/apis/apps` 这些内置资源 | | aggregator | `APIService` 注册的外部 apiserver(metrics-server 等) | 请求先到最外层的 aggregator:路径属于某个 `APIService` 就代理出去,否则交给 kube;kube 不认识就交给 apiextensions。三层共享同一套 filter、认证、审计和存储。 ## 二、GenericAPIServer 的三层结构 ```go // staging/src/k8s.io/apiserver/pkg/server/handler.go // FullHandlerChain -> Director -> {GoRestfulContainer, NonGoRestfulMux} type APIServerHandler struct { FullHandlerChain http.Handler GoRestfulContainer *restful.Container NonGoRestfulMux *mux.PathRecorderMux Director http.Handler } ``` `Director` 的源码注释自己都承认: > This looks a bit bonkers, but here's what's happening... 原因:`/apis` 必须在 go-restful 里注册(为了生成 swagger),但注册之后 go-restful 会对 `/apis` 下所有未注册路径直接 404,把本该 fallthrough 给聚合层的请求也挡住。所以只能在前面加一个 mux,先判断"这个路径像不像 go-restful 管的",再决定转发给谁。 读源码时看到这类注释,基本都能还原出一段兼容性设计史。 ## 三、过滤器链:执行顺序从最外层往里读 ![src-2-filters.png](https://developer.qcloudimg.com/http-save/yehe-11479707/bf096395f934a802637caac0cfdca9bd.png) *图 2:代码里从内往外包,所以最后一个包装就是最先执行的一层。* ```go // staging/src/k8s.io/apiserver/pkg/server/config.go func DefaultBuildHandlerChain(apiHandler http.Handler, c *Config) http.Handler { handler := apiHandler handler = genericapifilters.WithAuthorization(handler, ...) handler = genericfilters.WithPriorityAndFairness(handler, ...) handler = genericapifilters.WithImpersonation(handler, ...) handler = genericapifilters.WithAudit(handler, ...) handler = genericapifilters.WithAuthentication(handler, ...) handler = genericfilters.WithCORS(handler, ...) handler = genericfilters.WithTimeoutForNonLongRunningRequests(handler, ...) ... handler = genericapifilters.WithRequestInfo(handler, c.RequestInfoResolver) handler = genericfilters.WithPanicRecovery(handler, ...) handler = genericapifilters.WithAuditInit(handler) return handler } ``` **实际执行顺序是从最后一个往回读**: ``` AuditInit → PanicRecovery → RequestInfo → HTTPLogging/Timeout → CORS → Authentication → Audit → Impersonation → PriorityAndFairness → Authorization → handler ``` 三个值得记住的点: 1. **AuditInit 在最外层**:认证失败也要留下审计记录,所以它必须比认证更早; 2. **RequestInfo 在认证之前**:`RequestInfoFactory` 把 URL 解析成 `{verb, resource, namespace, name}`,审计和授权都要用; 3. **PanicRecovery 包在最外面**(AuditInit 之内):任何 filter 或 handler panic,都不会把进程带崩。 ## 四、路由:从 GVR 到具体的 handler ```go // staging/src/k8s.io/apiserver/pkg/endpoints/groupversion.go func (g *APIGroupVersion) InstallREST(container *restful.Container) error { installer := &APIInstaller{group: g, info: ...} ws, err := installer.Install(container) ... } ``` `registerResourceHandlers` 是 apiserver 里最长的函数之一。它按 scope 生成路径,再根据 storage 实现了哪些接口,决定注册哪些动作: ```go // staging/src/k8s.io/apiserver/pkg/endpoints/installer.go(节选) actions = appendIf(actions, action{request.MethodList, resourcePath, ...}, isLister) actions = appendIf(actions, action{request.MethodPost, resourcePath, ...}, isCreater) actions = appendIf(actions, action{request.MethodDeleteCollection, ...}, isCollectionDeleter) actions = appendIf(actions, action{request.MethodWatchList, "watch/" + resourcePath, ...}, allowWatchList) // item 路径 actions = appendIf(actions, action{request.MethodGet, itemPath, ...}, isGetter) actions = appendIf(actions, action{request.MethodPut, itemPath, ...}, isUpdater) actions = appendIf(actions, action{request.MethodPatch, itemPath, ...}, isPatcher) actions = appendIf(actions, action{request.MethodDelete, itemPath, ...}, isDeleter) ``` **这是个很好的设计**:资源支持哪些 verb,不是写死的,而是看它的 storage 实现了哪些接口。只读资源不实现 `Creater`,就不会注册 POST。 ## 五、一次 Create 的调用栈 ![src-3-callstack.png](https://developer.qcloudimg.com/http-save/yehe-11479707/18cff6a5762691d6c8f44c5434899245.png) *图 3:左侧调用、右侧文件;每一步都可能"到这里就失败了"。* ``` POST /apis/apps/v1/namespaces/prod/deployments → Director 判定走 go-restful → CreateResource → createHandler decode(含默认值) → mutating admission → rest.BeforeCreate(PrepareForCreate + Validate) → validating admission → store.Create → registry.Store.Create → etcd3 Store.Create(事务写入,PrevExist=false) → 返回 201,编码成请求的版本 ``` `registry.Store` 是"策略"和"存储"的分界: - `BeforeCreate`、`Validate`、`Canonicalize` 是**策略**,每个资源可以不一样; - `Storage.Create` 是**存储**,所有资源共用一套。 这也解释了内置资源和 CRD 为什么能共用大半个 apiserver:handler 和 registry 的骨架是通用的,差异只在 strategy 和 storage 实现。 ## 六、读与 watch:真正的热路径 `GET` 和 `WATCH` 走另一条路: - `getHandler` 直接从 **cacher** 读,不落 etcd; - `watchHandler` 从 `WatchCache` 拿事件,转成流式响应写出。 `cacher` 做三件事: 1. **缓存全量对象**(每个 resource 一份 `watch_cache`); 2. **维护 resourceVersion**,支持从任意历史点 watch; 3. **处理 410 Gone**:请求的 resourceVersion 太老、已被 compact,就让客户端重新 list。 所以 apiserver 能扛住大量 watch:**它几乎不直接读 etcd**,etcd 只负责写和一条长连接。 ## 七、启动顺序:为什么 informer 要等 apiserver ready `GenericAPIServer.Run` 的顺序是:起 HTTPS 监听 → 装好 handler chain → 执行 postStartHooks(informer 在这里启动)。 关键在于 `MuxAndDiscoveryComplete` 这个 filter:路由和 discovery 就绪之前,所有请求返回 503。因为 informer 和 controller 启动时会立刻 list/watch,如果路由还没装好,就会拿到 404 并进入错误重试。 所以 apiserver 的启动是**分阶段**的:先能接客(返回 503),再装好路由,最后才放行。 ## 八、读源码的建议入口 1. `cmd/kube-apiserver/app/server.go` 的 `CreateServerChain`:先建立"三个 server"的心智模型; 2. `pkg/controlplane/instance.go` 的 `CreateKubeAPIServer`:看每个 group 怎么注册; 3. `endpoints/installer.go` 的 `registerResourceHandlers`:看一个 URL 怎么变成 handler; 4. `endpoints/handlers/create.go`:看一个请求怎么穿过准入; 5. `registry/generic/registry/store.go`:看策略与存储怎么分界; 6. `storage/cacher/`:最后再看,这块最难,但理解了它,watch 的性能问题就都有答案了。 调试时 `--v=6` 最有用:它会打出每个请求的 `RequestInfo` 和准入链,比读源码快得多。 ## 九、设计追问 **1. 为什么用 filter 链而不是中间件框架?** 因为顺序本身就是语义。审计必须在认证前初始化,认证必须在授权前,超时必须包住非长请求——显式的、可读的包装比"注册一堆 middleware 再靠排序"更可控。 **2. 为什么 registry 和 storage 分两层?** registry 装"这个资源的规则"(默认值、校验、准入),storage 装"怎么存"。分开之后,内置资源和 CRD 才能共用同一套存储层。 **3. 为什么 watch cache 不放在 etcd?** etcd 的 watch 是全量的,每个 apiserver 都开一条会打爆 etcd;cache 放在本地,etcd 只需要一条长连接,扇出由 apiserver 承担。 **4. 为什么是三个 apiserver 而不是一个?** 因为发布节奏和依赖不同:CRD 依赖 apiextensions-apiserver,聚合层依赖外部服务。拆开之后,一层出问题不会把另外两层带崩。 --- **一句话收尾**:API Server 的源码庞大,但主线只有一条——**filter 链进来,路由到 handler,穿过准入和 registry,落进 etcd 或 cacher**。看懂这条线,剩下的都是分支。 --- ## 参考资料 1. Kubernetes 源码(v1.34.0):`cmd/kube-apiserver/`、`staging/src/k8s.io/apiserver/pkg/server/`、`pkg/registry/` 2. Kubernetes 文档:[Kubernetes API](https://kubernetes.io/docs/concepts/overview/kubernetes-api/)、[API Server](https://kubernetes.io/docs/concepts/architecture/) 3. 源码导读:[kubernetes/community · contributors/devel](https://github.com/kubernetes/community/tree/master/contributors/devel) > 本文为原创内容,基于 Kubernetes v1.34.0 源码整理;版本号以对应 release note 为准。

资源下载

更多资源
优质分享App

优质分享App

近一个月的开发和优化,本站点的第一个app全新上线。该app采用极致压缩,本体才4.36MB。系统里面做了大量数据访问、缓存优化。方便用户在手机上查看文章。后续会推出HarmonyOS的适配版本。

腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

用户登录
用户注册