client-go 四种客户端详解
写给源码学习者
- 代码分支 :
release-1.28- 前置文档 :
- client-go调用链精读-从kubeconfig到ListPods.md(连接 Config → Clientset)
- client-go第一阶段夯实基础指南.md(模块 1 rest 层、模块 2 CRUD)
- 本文回答 :client-go 里常见的 四种 API 客户端 分别是什么、怎么选、底层是否同一套 HTTP
- 建议阅读顺序 :先读 §0 Kubernetes API 基础 → 再读四种客户端
目录
- [Kubernetes API 基础(读四种客户端前必知)](#Kubernetes API 基础(读四种客户端前必知))
- 为什么需要「四种客户端」
- 总览:四种客户端对照表
- 架构关系图
- [§3.3 URL 拼装策略](#§3.3 URL 拼装策略)
- [§3.4 Do vs Watch 双路径](#§3.4 Do vs Watch 双路径)
- [§3.5 执行阶段对照](#§3.5 执行阶段对照)
4.~7. 四种客户端逐详解(含 §4.7 / §5.4a / §6.3a / §7.5a 执行深入)
- 选型决策
9.~12. [共同模式 / fake / 跟读 / 自测](#共同模式 / fake / 跟读 / 自测) - 架构深入(设计/并发/性能/面试)
0. Kubernetes API 基础(读四种客户端前必知)
讲四种 client 之前,要先弄清:apiserver 上到底有哪些 API、URL 怎么组织、内置资源和 CRD 有何不同。否则「为什么 Pod 用 Clientset、CRD 用 Dynamic」会显得很随意。
0.1 一个 apiserver,多套 REST API
Kubernetes 集群对外(对你的 Go 程序而言)主要是 kube-apiserver 的 HTTPS REST API:
你的程序 --HTTPS--> kube-apiserver --etcd--> 集群状态
- 内置资源 (Pod、Deployment、Service...)和 自定义资源 (CRD)在 apiserver 上 同一套机制 注册、同一套 REST 语义(List/Get/Create/Update/Delete/Watch)。
- client-go 四种客户端,是 访问这套 REST API 的不同 Go 封装层,不是四套不同的集群连接。
0.2 资源的「身份证」:GVK 与 GVR
每个 API 对象都有两组常见标识(初学先记这两个缩写):
| 缩写 | 全称 | 含义 | 例子 |
|---|---|---|---|
| GVK | Group / Version / Kind | 对象「是哪种类型」(YAML 里 apiVersion + kind) |
apps/v1, Deployment |
| GVR | Group / Version / Resource | REST URL 里的资源复数名 | apps, v1, deployments |
对应关系示例:
| kind(类型名) | apiVersion | GVR(REST 路径用) | HTTP 列表示例 |
|---|---|---|---|
| Pod | v1 |
"", v1, pods |
GET /api/v1/pods |
| Deployment | apps/v1 |
apps, v1, deployments |
GET /apis/apps/v1/namespaces/default/deployments |
| Service | v1 |
"", v1, services |
GET /api/v1/namespaces/default/services |
Kind vs Resource :Deployment(类型)→ URL 里写 deployments(复数、小写)。client-go typed client 的 .Resource("pods") 就是在填 Resource 名。
Go 里 GVR 常写成:
go
schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}
0.3 两类 API 路径:/api 与 /apis
apiserver 按 API Group 分前缀(rest/url_utils.go DefaultVersionedAPIPath):
| API 类别 | URL 前缀 | Group 特点 | 例子 |
|---|---|---|---|
| Core 组(遗留核心 API) | /api/{version} |
Group 为空字符串 | /api/v1/pods |
| 具名 Group | /apis/{group}/{version} |
Group 非空 | /apis/apps/v1/deployments |
client-go 里 typed client 创建时会设 APIPath + GroupVersion:
go
// CoreV1 --- core_client.go setConfigDefaults
config.APIPath = "/api"
config.GroupVersion = &{Version: "v1"} // Group 为空 → /api/v1
// AppsV1 --- 同理
config.APIPath = "/apis"
config.GroupVersion = &{Group: "apps", Version: "v1"} // → /apis/apps/v1
模块 1 复习 :RESTClient 的 versionedAPIPath 就是这里拼出来的;List Pod 的 /api/v1/pods 即 Core 组路径。
0.4 内置 API vs 自定义 API(CRD)
| 概念 | 是什么 | 谁定义 | Go 里常见访问方式 |
|---|---|---|---|
| 内置 API | K8s 自带的资源类型 | Kubernetes 项目(k8s.io/api/...) |
Clientset 强类型 |
| CRD(CustomResourceDefinition) | 用户在集群里 声明 的新资源类型 | 你安装的 CRD YAML | Dynamic Client(或 code-gen 后的 typed client) |
| Aggregated API | 通过聚合层扩展的 API | 如 metrics-server、自定义 APIService | 多为 Dynamic / REST |
内置 API 例子(Clientset 直接支持):
- Core:
Pod,Service,Namespace,ConfigMap... - Apps:
Deployment,ReplicaSet,StatefulSet... - Batch:
Job,CronJob...
自定义 API 例子(集群里装了 CRD 才有):
yaml
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: crontabs.stable.example.com
spec:
group: stable.example.com
versions:
- name: v1
served: true
storage: true
scope: Namespaced
names:
plural: crontabs # ← Resource 名
kind: CronTab # ← Kind
装好后可用 GVR {stable.example.com, v1, crontabs} 做 CRUD------Clientset 里没有 CronTab() 方法,要用 Dynamic 或自己 code-gen。
0.5 一张图:从 YAML 到 URL 到 client-go
YAML 文件 apiserver REST client-go
─────────────────────────────────────────────────────────────────────────────
apiVersion: v1 GET /api/v1/namespaces/ clientset.CoreV1()
kind: Pod default/pods .Pods("default").List()
metadata: name: nginx
apiVersion: apps/v1 GET /apis/apps/v1/ clientset.AppsV1()
kind: Deployment namespaces/default/deployments .Deployments("default").List()
apiVersion: stable.example.com/v1 GET /apis/stable.example.com/v1/ dynamic.NewForConfig
kind: CronTab namespaces/default/crontabs .Resource(gvr).Namespace().List()
0.6 API 类型 ↔ 四种客户端(先建立直觉)
| API 类型 | 是否编译期有 Go struct | 首选客户端 | 备注 |
|---|---|---|---|
| 内置 Core/Apps/Batch... | 有(k8s.io/api/...) |
Clientset | 第一阶段主力 |
| CRD / 未知 GVR | 默认无(用 Unstructured) |
Dynamic | dynamic-create-update-delete-deployment 对内置资源演示的是 同一套 API 形状 |
| 任意 GVR,只要 metadata | PartialObjectMetadata |
Metadata | 进阶优化 |
| 任意路径,手写 REST | 自己解析 | REST Client | 模块 1 底层 |
易混点 :
dynamic-create-update-delete-deployment操作的是 内置 Deployment ,是为了演示 Dynamic 的 GVR 用法;生产里操作内置资源仍优先 Clientset 。操作 CRD 才是 Dynamic 的主场。
0.7 与 kubectl 的对应(帮助建立感性认识)
| kubectl | 涉及的 API | client-go 大致对应 |
|---|---|---|
kubectl get pods |
内置 Core/v1 | clientset.CoreV1().Pods().List() |
kubectl get deploy |
内置 apps/v1 | clientset.AppsV1().Deployments().List() |
kubectl get crontabs.stable.example.com |
CRD | dynamicClient.Resource(gvr).List() |
kubectl api-resources |
发现集群支持哪些 GVR | discovery 客户端(本文不展开) |
0.8 本节小结(读 §1 前自检)
- GVK 看 YAML 的
apiVersion+kind;GVR 拼 REST URL 和 Dynamic 的Resource(gvr)。 - Core 资源走
/api/v1;具名 Group 走/apis/{group}/{version}。 - 内置 API → 有官方 Go 类型 → Clientset ;CRD → 常无类型 → Dynamic。
- 四种客户端共享
rest.Config和同构的Request.Do()HTTP 机制。
与模块 4 闭环 :读完四种客户端选型后,跟 第一阶段指南 模块 4 把 GVK/GVR 对应到 k8s.io/api 里的 struct 和 Scheme。
自测(§0)
- Deployment 的 GVR 是什么?List 的 URL 路径前缀?
- Pod 属于 Core 组还是 apps 组?
- 集群里新装的 CRD 资源,第一阶段默认用哪种 client?
参考答案
apps/v1/deployments;/apis/apps/v1/...- Core 组(
/api/v1/pods) - Dynamic Client(或对该 CRD 做 code-gen 后的 typed client)
1. 为什么需要「四种客户端」
前置 :请先读完 [§0 Kubernetes API 基础](#§0 Kubernetes API 基础)。
集群 API 分 内置 与 自定义(CRD) 等(§0.4);访问都是 REST,但 Go 侧是否已有 *v1.Pod 这类类型不同。client-go 因此提供不同抽象层:
| 你的需求 | 更合适的客户端 |
|---|---|
| 操作 Pod、Deployment 等内置类型,要类型安全 | Clientset(强类型) |
| 操作 CRD 或运行时才知道 GVR | Dynamic Client |
| 只要 metadata(name/labels/uid),不要 spec/status | Metadata Client |
| 自己拼 URL、最底层控制 | REST Client |
四种客户端 不是四种连集群方式 (连集群仍靠 rest.Config,见《调用链精读》§3.8~3.10)。
四种客户端是:在同一张 Config「门票」之上,选哪一层 API 封装。
2. 总览:四种客户端对照表
| # | 名称 | 包 / 入口 | 创建函数 | 操作对象类型 | 典型调用 |
|---|---|---|---|---|---|
| 1 | Clientset(强类型) | k8s.io/client-go/kubernetes |
kubernetes.NewForConfig |
*v1.Pod、*appsv1.Deployment 等 |
clientset.CoreV1().Pods(ns).List(...) |
| 2 | Dynamic Client(动态) | k8s.io/client-go/dynamic |
dynamic.NewForConfig |
*unstructured.Unstructured |
client.Resource(gvr).Namespace(ns).List(...) |
| 3 | Metadata Client(元数据) | k8s.io/client-go/metadata |
metadata.NewForConfig |
*metav1.PartialObjectMetadata |
client.Resource(gvr).Namespace(ns).Get(...) |
| 4 | REST Client(底层) | k8s.io/client-go/rest |
rest.RESTClientFor / RESTClientForConfigAndClient |
原始 JSON / runtime.Object |
client.Get().Resource("pods").Do(ctx) |
共同底座(模块 1 已学):
rest.Config
→ rest.HTTPClientFor 共享 TLS + Token(Transport 链)
→ rest.RESTClientForConfigAndClient 各客户端内部都会用到
→ rest.Request.Do() 真正发 HTTP
3. 架构关系图
3.1 纵向:抽象层从高到低
┌─────────────────────────────────────┐
│ rest.Config(门票) │
└─────────────────┬───────────────────┘
│
┌───────────────────────────┼───────────────────────────┐
│ │ │
▼ ▼ ▼
kubernetes.NewForConfig dynamic.NewForConfig metadata.NewForConfig
│ │ │
▼ ▼ ▼
*Clientset *DynamicClient metadata.Client
CoreV1().Pods()... Resource(gvr).Namespace()... Resource(gvr).Get()
│ │ │
└───────────────────────────┼───────────────────────────┘
▼
rest.Interface(*RESTClient)
│
rest.Request.Get/Post()...
│
▼
client.Do(req) ★ HTTP
3.2 横向:四种客户端与 RESTClient 关系
#mermaid-svg-Sbt9wtB0AyREC12L{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Sbt9wtB0AyREC12L .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Sbt9wtB0AyREC12L .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Sbt9wtB0AyREC12L .error-icon{fill:#552222;}#mermaid-svg-Sbt9wtB0AyREC12L .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Sbt9wtB0AyREC12L .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Sbt9wtB0AyREC12L .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Sbt9wtB0AyREC12L .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Sbt9wtB0AyREC12L .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Sbt9wtB0AyREC12L .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Sbt9wtB0AyREC12L .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Sbt9wtB0AyREC12L .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Sbt9wtB0AyREC12L .marker.cross{stroke:#333333;}#mermaid-svg-Sbt9wtB0AyREC12L svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Sbt9wtB0AyREC12L p{margin:0;}#mermaid-svg-Sbt9wtB0AyREC12L .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Sbt9wtB0AyREC12L .cluster-label text{fill:#333;}#mermaid-svg-Sbt9wtB0AyREC12L .cluster-label span{color:#333;}#mermaid-svg-Sbt9wtB0AyREC12L .cluster-label span p{background-color:transparent;}#mermaid-svg-Sbt9wtB0AyREC12L .label text,#mermaid-svg-Sbt9wtB0AyREC12L span{fill:#333;color:#333;}#mermaid-svg-Sbt9wtB0AyREC12L .node rect,#mermaid-svg-Sbt9wtB0AyREC12L .node circle,#mermaid-svg-Sbt9wtB0AyREC12L .node ellipse,#mermaid-svg-Sbt9wtB0AyREC12L .node polygon,#mermaid-svg-Sbt9wtB0AyREC12L .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Sbt9wtB0AyREC12L .rough-node .label text,#mermaid-svg-Sbt9wtB0AyREC12L .node .label text,#mermaid-svg-Sbt9wtB0AyREC12L .image-shape .label,#mermaid-svg-Sbt9wtB0AyREC12L .icon-shape .label{text-anchor:middle;}#mermaid-svg-Sbt9wtB0AyREC12L .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Sbt9wtB0AyREC12L .rough-node .label,#mermaid-svg-Sbt9wtB0AyREC12L .node .label,#mermaid-svg-Sbt9wtB0AyREC12L .image-shape .label,#mermaid-svg-Sbt9wtB0AyREC12L .icon-shape .label{text-align:center;}#mermaid-svg-Sbt9wtB0AyREC12L .node.clickable{cursor:pointer;}#mermaid-svg-Sbt9wtB0AyREC12L .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Sbt9wtB0AyREC12L .arrowheadPath{fill:#333333;}#mermaid-svg-Sbt9wtB0AyREC12L .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Sbt9wtB0AyREC12L .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Sbt9wtB0AyREC12L .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Sbt9wtB0AyREC12L .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Sbt9wtB0AyREC12L .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Sbt9wtB0AyREC12L .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Sbt9wtB0AyREC12L .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Sbt9wtB0AyREC12L .cluster text{fill:#333;}#mermaid-svg-Sbt9wtB0AyREC12L .cluster span{color:#333;}#mermaid-svg-Sbt9wtB0AyREC12L div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Sbt9wtB0AyREC12L .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Sbt9wtB0AyREC12L rect.text{fill:none;stroke-width:0;}#mermaid-svg-Sbt9wtB0AyREC12L .icon-shape,#mermaid-svg-Sbt9wtB0AyREC12L .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Sbt9wtB0AyREC12L .icon-shape p,#mermaid-svg-Sbt9wtB0AyREC12L .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Sbt9wtB0AyREC12L .icon-shape .label rect,#mermaid-svg-Sbt9wtB0AyREC12L .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Sbt9wtB0AyREC12L .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Sbt9wtB0AyREC12L .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Sbt9wtB0AyREC12L :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} rest.Config
http.Client via HTTPClientFor
kubernetes.Clientset
dynamic.DynamicClient
metadata.Client
rest.RESTClient 直接使用
CoreV1Client / AppsV1Client ...
pods / deployments 等 typed 方法
rest.Interface
dynamicResourceClient
rest.Interface
metadata client 包装
rest.RESTClient 字段
Request.Do
kube-apiserver
3.3 四种客户端的 URL 拼装策略(设计分歧点)
四种客户端最终都到 rest.Request,但 路径从哪来 有两种根本不同的策略:
| 客户端 | URL 来源 | RESTClient 的 versionedAPIPath |
单次请求如何定路径 |
|---|---|---|---|
| Clientset | Namespace + Resource + Name Builder |
固定 /api/v1 或 /apis/apps/v1 |
pathPrefix + namespaces/ns + pods |
| Dynamic | AbsPath(makeURLSegments...) |
占位 /if-you-see-this-search-for-the-break |
运行时按 GVR 拼完整 path |
| Metadata | AbsPath(makeURLSegments...) |
占位 /this-value-should-never-be-sent |
同 Dynamic + 特殊 Accept |
| REST Client | 直接用 Builder 或 AbsPath |
按 Group 绑定 | 完全手写 |
Clientset(编译期绑定 API 版本):
go
// core_client.go setConfigDefaults
config.APIPath = "/api"
config.GroupVersion = &{Version: "v1"} // → RESTClient.pathPrefix = /api/v1
// pod.go List
Get().Namespace(ns).Resource("pods") // → /api/v1/namespaces/default/pods
Dynamic(运行时 GVR 绑定):
go
// dynamic/simple.go:86-87 --- 故意不设真实 APIPath
config.GroupVersion = &schema.GroupVersion{}
config.APIPath = "/if-you-see-this-search-for-the-break"
// Create 时用 AbsPath 覆盖 pathPrefix
Post().AbsPath(c.makeURLSegments(name)...)
// makeURLSegments → ["apis","apps","v1","namespaces","default","deployments","demo"]
Metadata(同 Dynamic 路径 + 内容协商):
go
Get().AbsPath(c.makeURLSegments(name)...).
SetHeader("Accept", "...PartialObjectMetadata;g=meta.k8s.io;v=v1...")
apiserver 收到带 as=PartialObjectMetadata 的 Accept 后,只序列化 metadata 字段------这是 Metadata Client 省流量 的核心,不是少调几个字段那么简单。
3.4 共同执行底座:Do() vs Watch() 双路径
| 操作 | 入口 | 首次限流 | 成功时 body |
|---|---|---|---|
| List/Get/Create/... | Request.Do() → request() |
有 tryThrottle |
ReadAll → transformResponse |
| Watch | Request.Watch() 独立循环 |
无 | 保留 body → StreamWatcher |
四种客户端的 List/CRUD 全部走 Do() 路径;Watch 走独立路径。Dynamic/Metadata 也不例外。
3.5 四种客户端执行阶段对照(List 为例)
阶段 A --- NewForConfig(四种相同)
rest.Config → HTTPClientFor → RESTClientForConfigAndClient → 持有 rest.Interface
阶段 B --- Builder(四种不同)
Clientset: Get().Namespace().Resource("pods")
Dynamic: Get().AbsPath(makeURLSegments...)
Metadata: Get().AbsPath(...).SetHeader(Accept: PartialObjectMetadata...)
REST: 同 Clientset 或 AbsPath
阶段 C --- Execute(List 时四种汇合)
.Do(ctx) → request() → tryThrottle → newHTTPRequest → client.Do → transformResponse
阶段 D --- Decode(四种不同)
Clientset: Into(&PodList{}) → scheme.Codecs → *v1.PodList
Dynamic: Raw() + UnstructuredJSONScheme.Decode
Metadata: Get() / Raw fallback → *PartialObjectMetadataList
REST: Into 或 Raw 自行处理
apiserver http.Client request() rest.Request Typed/Dynamic/Metadata 用户代码 apiserver http.Client request() rest.Request Typed/Dynamic/Metadata 用户代码 #mermaid-svg-sojKDaZybq6M21mp{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-sojKDaZybq6M21mp .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-sojKDaZybq6M21mp .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-sojKDaZybq6M21mp .error-icon{fill:#552222;}#mermaid-svg-sojKDaZybq6M21mp .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-sojKDaZybq6M21mp .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-sojKDaZybq6M21mp .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-sojKDaZybq6M21mp .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-sojKDaZybq6M21mp .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-sojKDaZybq6M21mp .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-sojKDaZybq6M21mp .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-sojKDaZybq6M21mp .marker{fill:#333333;stroke:#333333;}#mermaid-svg-sojKDaZybq6M21mp .marker.cross{stroke:#333333;}#mermaid-svg-sojKDaZybq6M21mp svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-sojKDaZybq6M21mp p{margin:0;}#mermaid-svg-sojKDaZybq6M21mp .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-sojKDaZybq6M21mp text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-sojKDaZybq6M21mp .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-sojKDaZybq6M21mp .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-sojKDaZybq6M21mp .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-sojKDaZybq6M21mp .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-sojKDaZybq6M21mp #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-sojKDaZybq6M21mp .sequenceNumber{fill:white;}#mermaid-svg-sojKDaZybq6M21mp #sequencenumber{fill:#333;}#mermaid-svg-sojKDaZybq6M21mp #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-sojKDaZybq6M21mp .messageText{fill:#333;stroke:none;}#mermaid-svg-sojKDaZybq6M21mp .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-sojKDaZybq6M21mp .labelText,#mermaid-svg-sojKDaZybq6M21mp .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-sojKDaZybq6M21mp .loopText,#mermaid-svg-sojKDaZybq6M21mp .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-sojKDaZybq6M21mp .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-sojKDaZybq6M21mp .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-sojKDaZybq6M21mp .noteText,#mermaid-svg-sojKDaZybq6M21mp .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-sojKDaZybq6M21mp .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-sojKDaZybq6M21mp .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-sojKDaZybq6M21mp .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-sojKDaZybq6M21mp .actorPopupMenu{position:absolute;}#mermaid-svg-sojKDaZybq6M21mp .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-sojKDaZybq6M21mp .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-sojKDaZybq6M21mp .actor-man circle,#mermaid-svg-sojKDaZybq6M21mp line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-sojKDaZybq6M21mp :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} List/Get/Create... 1 Builder 链(路径策略因客户端而异) 2 Do(ctx) 3 request(ctx, fn) 4 tryThrottle 5 newHTTPRequest 6 client.Do(req) 7 HTTPS 8 JSON body 9 transformResponse (ReadAll) 10 Result 11 Result 12 Into / Raw / Get(解码策略因客户端而异) 13 强类型 / Unstructured / PartialObjectMetadata 14
4. 客户端 ①:Clientset(强类型,最常用)
4.1 是什么
- 结构体 :
*kubernetes.Clientset(kubernetes/clientset.go) - 特点:为每个 API Group 生成 typed 方法,编译期类型检查
- 第一阶段主力:List / CRUD example 都用它
4.2 怎么创建
go
config, _ := clientcmd.BuildConfigFromFlags("", kubeconfig)
clientset, err := kubernetes.NewForConfig(config)
内部(模块 1):HTTPClientFor → 各 Group NewForConfigAndClient → 共享一个 http.Client。
4.3 怎么调用(套娃)
go
clientset.CoreV1().Pods("default").List(ctx, metav1.ListOptions{})
clientset.AppsV1().Deployments("default").Create(ctx, dep, metav1.CreateOptions{})
| 层级 | 编译期类型 | 运行期类型 | New 还是 Getter |
|---|---|---|---|
Clientset |
*Clientset |
*Clientset |
大工厂 NewForConfig |
.CoreV1() |
CoreV1Interface |
*CoreV1Client |
Getter |
.Pods(ns) |
PodInterface |
*pods |
小包装 newPods |
.List() |
--- | (*pods).List |
方法 → RESTClient.Get()...Do() |
详见《第一阶段夯实基础指南》模块 2 §2.0。
4.4 适用场景
- 操作 core/apps/batch 等内置 API
- 需要
*v1.Pod等强类型 struct - 控制器、业务代码 默认首选
4.5 源码入口
| 文件 | 关注 |
|---|---|
kubernetes/clientset.go |
NewForConfig |
kubernetes/typed/core/v1/core_client.go |
CoreV1Client |
kubernetes/typed/core/v1/pod.go |
List / Create 链式调用 |
4.6 Example
examples/out-of-cluster-client-configurationexamples/create-update-delete-deploymentexamples/module1-debug
4.7 执行流程深入(List Pod)
阶段 B-Builder (pod.go:94-98,零网络):
go
c.client.Get().Namespace(c.ns).Resource("pods").
VersionedParams(&opts, scheme.ParameterCodec).Timeout(timeout)
pathPrefix已在NewRequest时设为/api/v1(来自 CoreV1Client 的setConfigDefaults)VersionedParams把labelSelector、fieldSelector等写入 query
阶段 C-Execute (Do(ctx) → request.go:965):
tryThrottle--- 默认 QPS=5(CoreV1Client创建时NewTokenBucketRateLimiter)newHTTPRequest---GET https://host:6443/api/v1/namespaces/default/pods?...client.Do--- RoundTripper 链注入 Bearer TokentransformResponse---io.ReadAll(resp.Body)读完整 JSON
阶段 D-Decode (Into(&PodList{})):
scheme.ParameterCodec/scheme.Codecs来自kubernetes/scheme- decoder 根据
Kind=PodList+apiVersion=v1反序列化
与 Dynamic 的关键差异 :Clientset 不需要 AbsPath;API 版本在 NewForConfig 时已绑定到 RESTClient,Resource 名在编译期由 client-gen 固定为 "pods"。
5. 客户端 ②:Dynamic Client(动态,CRD 友好)
5.1 是什么
- 结构体 :
*dynamic.DynamicClient(dynamic/simple.go:34) - 特点 :用 GVR (GroupVersionResource)指定资源,对象用
unstructured.Unstructured(类似map[string]interface{}) - 无编译期类型:CRD 未 code-gen 也能操作
5.2 怎么创建
go
config, _ := clientcmd.BuildConfigFromFlags("", kubeconfig)
client, err := dynamic.NewForConfig(config)
NewForConfig(:71-78)同样:ConfigFor → HTTPClientFor → RESTClientForConfigAndClient。
注意 Dynamic 的 Config 会把 GroupVersion 设为「占位」,真正 GVR 在调用 Resource(gvr) 时指定 (:86-87)。
5.3 怎么调用
go
gvr := schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}
client.Resource(gvr).Namespace("default").Create(ctx, obj, metav1.CreateOptions{})
client.Resource(gvr).Namespace("default").Get(ctx, "demo-deployment", metav1.GetOptions{})
对象示例(examples/dynamic-create-update-delete-deployment/main.go):
go
deployment := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{"name": "demo-deployment"},
// spec: ...
},
}
5.4 与 Clientset 的异同
| Clientset | Dynamic Client | |
|---|---|---|
| 资源定位 | CoreV1().Pods() |
Resource(gvr).Namespace() |
| 对象类型 | *v1.Pod |
*unstructured.Unstructured |
| CRD | 需 code-gen 或不用 typed | 直接指定 GVR |
| 底层 HTTP | rest.Interface + Do() |
相同 (simple.go:132-138) |
Dynamic 的 Create 内部仍是:
go
c.client.client.Post().AbsPath(...).Body(outBytes).Do(ctx)
与 typed client 同一套 rest.Request,只是 URL 用 AbsPath 拼 GVR 路径。
5.4a 执行流程深入(Create Deployment)
初始化 (dynamic/simple.go:83-93):
go
config.GroupVersion = &schema.GroupVersion{} // 空占位
config.APIPath = "/if-you-see-this-search-for-the-break"
restClient, _ := rest.RESTClientForConfigAndClient(config, h)
RESTClient 的 pathPrefix 是占位符------真实路径每次 CRUD 由 makeURLSegments 决定。
Builder + Execute (simple.go:132-138):
go
result := c.client.client.Post().
AbsPath(append(c.makeURLSegments(name), subresources...)...).
SetHeader("Content-Type", application/json).
Body(outBytes). // runtime.Encode(UnstructuredJSONScheme, obj)
SpecificallyVersionedParams(&opts, dynamicParameterCodec, versionV1).
Do(ctx)
makeURLSegments("demo-deployment") 展开为:
["apis", "apps", "v1", "namespaces", "default", "deployments", "demo-deployment"]
→ POST /apis/apps/v1/namespaces/default/deployments/demo-deployment
Decode (与 Clientset 的 Into 不同):
go
retBytes, _ := result.Raw() // 拿原始 JSON 字节
runtime.Decode(unstructured.UnstructuredJSONScheme, retBytes)
设计动机:
- 无编译期类型 → 不能
Into(&appsv1.Deployment{}),除非你知道类型 - AbsPath → 一个
DynamicClient实例可操作任意 GVR,不必像 Clientset 建几十个 Group client - UnstructuredJSONScheme → 对象存为
map[string]interface{},CRD 无 Go struct 也能 CRUD
5.5 适用场景
- CRD、Aggregated API
- 通用控制器、kubectl 类工具
- 运行时才知道资源类型
5.6 Example
examples/dynamic-create-update-delete-deployment
6. 客户端 ③:Metadata Client(只要元数据)
6.1 是什么
- 结构体 :
metadata.Client(metadata/metadata.go:56) - 特点 :只获取 PartialObjectMetadata(name、namespace、labels、uid 等),不拉完整 spec/status
- 省流量 / 省解码:大规模 List 元数据时更高效
6.2 怎么创建
go
config, _ := clientcmd.BuildConfigFromFlags("", kubeconfig)
client, err := metadata.NewForConfig(config)
ConfigFor(:64-72)使用 protobuf 为主的 Content-Type,与 Dynamic/Clientset 的 JSON 默认略有不同。
6.3 怎么调用
go
gvr := schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}
client.Resource(gvr).Namespace("default").Get(ctx, "demo-deployment", metav1.GetOptions{})
// 返回 *metav1.PartialObjectMetadata
API 形状与 Dynamic 类似(Resource(gvr).Namespace(ns)),但返回类型是 PartialObjectMetadata,不是 Unstructured 全对象。
6.3a 执行流程深入(Get Deployment metadata)
初始化 (metadata/metadata.go:103-114):
go
config.ContentType = "application/vnd.kubernetes.protobuf" // 默认 protobuf
config.NegotiatedSerializer = metainternalversionscheme.Codecs.WithoutConversion()
config.APIPath = "/this-value-should-never-be-sent" // 同 Dynamic 占位
Builder --- 内容协商是核心 (metadata.go:185-188):
go
result := c.client.client.Get().
AbsPath(append(c.makeURLSegments(name), subresources...)...).
SetHeader("Accept",
"application/vnd.kubernetes.protobuf;as=PartialObjectMetadata;g=meta.k8s.io;v=v1,"+
"application/json;as=PartialObjectMetadata;g=meta.k8s.io;v=v1,...").
SpecificallyVersionedParams(&opts, dynamicParameterCodec, versionV1).
Do(ctx)
apiserver 侧行为 :收到 as=PartialObjectMetadata 的 Accept 后,通过 Table/PartialObjectMetadata 转换 只返回 metadata(name、namespace、labels、uid、ownerReferences 等),不序列化 spec/status。
Decode --- 双路径 fallback (metadata.go:192-216):
go
obj, err := result.Get() // 优先走 NegotiatedSerializer
if runtime.IsNotRegisteredError(err) {
rawBytes, _ := result.Raw()
json.Unmarshal(rawBytes, &partial) // 老 apiserver 或 CRD:全量 JSON 再抽 metadata
}
与 Dynamic 对比:
| Dynamic Get | Metadata Get | |
|---|---|---|
| Accept | 默认 JSON/Pod 全对象 | PartialObjectMetadata |
| 返回 | *unstructured.Unstructured 全字段 |
*metav1.PartialObjectMetadata |
| 流量 | 完整 spec/status | 仅 metadata |
| URL 拼法 | makeURLSegments |
相同 |
6.4 适用场景
- 控制器只关心 labels / ownerReferences
- 全集群扫描资源「名单」
- 与 Dynamic 类似,支持 任意 GVR(含 CRD)
6.5 第一阶段建议
知道存在即可;日常 CRUD 用 Clientset,CRD 全对象用 Dynamic。Metadata 在 Informer/控制器优化场景更常见。
7. 客户端 ④:REST Client(最底层)
7.1 是什么
- 结构体 :
*rest.RESTClient(rest/client.go:81) - 特点 :直接提供
Get()/Post()/Put()/Delete()→*rest.Request - Clientset / Dynamic / Metadata 最终都委托它 (或同构的
rest.Interface)
7.2 怎么创建
方式 A(单独创建,需自己设 GroupVersion):
go
config, _ := clientcmd.BuildConfigFromFlags("", kubeconfig)
// 必须设置 GroupVersion、NegotiatedSerializer 等
client, err := rest.RESTClientFor(config)
// 或
client, err := rest.RESTClientForConfigAndClient(config, httpClient)
方式 B(间接使用,最常见):
go
clientset, _ := kubernetes.NewForConfig(config)
// 内部已创建 RESTClient;typed client 的 c.client 就是 rest.Interface
CoreV1 绑定 /api/v1(core_client.go setConfigDefaults)。
7.3 怎么调用
go
client.Get().
Namespace("default").
Resource("pods").
Do(ctx)
这就是模块 1 的 Request 链 ,无 Pods() 这种 typed 封装。
7.4 适用场景
- 写 client-gen 之前的原型、调试 HTTP
- 实现 新的 typed client(client-gen 生成的代码底层就是 RESTClient)
- 非标准 API 路径(如
AbsPath)
7.5 与 http.Client 的分工(模块 1 复习)
| 组件 | 职责 |
|---|---|
http.Client |
TLS、Token、连接池 |
RESTClient |
API 版本路径、限流、编解码器 |
Request |
单次 verb/URL/body |
client.Do(req) |
发 HTTP |
7.5a 执行流程深入:REST Client 在栈中的位置
REST Client 不是「第四种连法」,而是 三种高层客户端的公共底座:
kubernetes.Clientset
└─ CoreV1Client.restClient : rest.Interface → *RESTClient
dynamic.DynamicClient
└─ client : rest.Interface → *RESTClient(占位 APIPath)
metadata.Client
└─ client : *RESTClient → 直接持有(非 Interface)
Typed client 的 List 等价于手写:
go
// 等价于 pod.go List 的核心
restClient.Get().
Namespace("default").Resource("pods").
VersionedParams(&opts, scheme.ParameterCodec).
Do(ctx).Into(&v1.PodList{})
何时直接用 REST Client:
- 调试 HTTP(对照 curl)
- 实现 client-gen 未覆盖的 API
- 非标准 subresource 路径
读源码定位 :任何 typed 方法的 c.client.Get/Post/... 最终都是 rest.Interface 上的 Builder;跟模块 1 的 request() 即可,不必另学一套 HTTP。
8. 四种客户端选型决策
#mermaid-svg-JtAFOgaqHl4vE83X{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-JtAFOgaqHl4vE83X .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-JtAFOgaqHl4vE83X .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-JtAFOgaqHl4vE83X .error-icon{fill:#552222;}#mermaid-svg-JtAFOgaqHl4vE83X .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-JtAFOgaqHl4vE83X .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-JtAFOgaqHl4vE83X .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-JtAFOgaqHl4vE83X .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-JtAFOgaqHl4vE83X .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-JtAFOgaqHl4vE83X .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-JtAFOgaqHl4vE83X .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-JtAFOgaqHl4vE83X .marker{fill:#333333;stroke:#333333;}#mermaid-svg-JtAFOgaqHl4vE83X .marker.cross{stroke:#333333;}#mermaid-svg-JtAFOgaqHl4vE83X svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-JtAFOgaqHl4vE83X p{margin:0;}#mermaid-svg-JtAFOgaqHl4vE83X .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-JtAFOgaqHl4vE83X .cluster-label text{fill:#333;}#mermaid-svg-JtAFOgaqHl4vE83X .cluster-label span{color:#333;}#mermaid-svg-JtAFOgaqHl4vE83X .cluster-label span p{background-color:transparent;}#mermaid-svg-JtAFOgaqHl4vE83X .label text,#mermaid-svg-JtAFOgaqHl4vE83X span{fill:#333;color:#333;}#mermaid-svg-JtAFOgaqHl4vE83X .node rect,#mermaid-svg-JtAFOgaqHl4vE83X .node circle,#mermaid-svg-JtAFOgaqHl4vE83X .node ellipse,#mermaid-svg-JtAFOgaqHl4vE83X .node polygon,#mermaid-svg-JtAFOgaqHl4vE83X .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-JtAFOgaqHl4vE83X .rough-node .label text,#mermaid-svg-JtAFOgaqHl4vE83X .node .label text,#mermaid-svg-JtAFOgaqHl4vE83X .image-shape .label,#mermaid-svg-JtAFOgaqHl4vE83X .icon-shape .label{text-anchor:middle;}#mermaid-svg-JtAFOgaqHl4vE83X .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-JtAFOgaqHl4vE83X .rough-node .label,#mermaid-svg-JtAFOgaqHl4vE83X .node .label,#mermaid-svg-JtAFOgaqHl4vE83X .image-shape .label,#mermaid-svg-JtAFOgaqHl4vE83X .icon-shape .label{text-align:center;}#mermaid-svg-JtAFOgaqHl4vE83X .node.clickable{cursor:pointer;}#mermaid-svg-JtAFOgaqHl4vE83X .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-JtAFOgaqHl4vE83X .arrowheadPath{fill:#333333;}#mermaid-svg-JtAFOgaqHl4vE83X .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-JtAFOgaqHl4vE83X .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-JtAFOgaqHl4vE83X .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-JtAFOgaqHl4vE83X .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-JtAFOgaqHl4vE83X .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-JtAFOgaqHl4vE83X .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-JtAFOgaqHl4vE83X .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-JtAFOgaqHl4vE83X .cluster text{fill:#333;}#mermaid-svg-JtAFOgaqHl4vE83X .cluster span{color:#333;}#mermaid-svg-JtAFOgaqHl4vE83X div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-JtAFOgaqHl4vE83X .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-JtAFOgaqHl4vE83X rect.text{fill:none;stroke-width:0;}#mermaid-svg-JtAFOgaqHl4vE83X .icon-shape,#mermaid-svg-JtAFOgaqHl4vE83X .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-JtAFOgaqHl4vE83X .icon-shape p,#mermaid-svg-JtAFOgaqHl4vE83X .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-JtAFOgaqHl4vE83X .icon-shape .label rect,#mermaid-svg-JtAFOgaqHl4vE83X .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-JtAFOgaqHl4vE83X .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-JtAFOgaqHl4vE83X .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-JtAFOgaqHl4vE83X :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否 / CRD
是
否
是
是
否
是
否
需要访问 K8s API
先确认:内置 API 还是 CRD?
见 §0.4
资源是内置类型
Pod/Deploy/Service?
需要 spec/status
完整对象?
是 CRD 或
运行时 GVR?
只要 labels/name
等元数据?
在写 client-gen
或调试 HTTP?
① Clientset
③ Metadata Client
② Dynamic Client
④ REST Client
第一阶段学习顺序建议:
- §0 API 基础 (内置 / CRD、GVK/GVR、
/apivs/apis) - Clientset(必学,模块 1~2)
- REST Client(理解模块 1 时已间接学过)
- Dynamic Client (模块 5 选读 + dynamic example;操作 CRD 时必用)
- Metadata Client(知道即可,进阶再学)
9. 共同模式:NewForConfig 家族
四种客户端创建函数 名字高度相似,这是嵌套晕的来源之一。记住模式即可:
| 函数模式 | 含义 |
|---|---|
Xxx.NewForConfig(config) |
内部 HTTPClientFor + RESTClientForConfigAndClient |
Xxx.NewForConfigAndClient(config, httpClient) |
注入已有 httpClient(Clientset 共享用) |
Xxx.NewForConfigOrDie(config) |
失败 panic(example 少用) |
Xxx.New(rest.Interface) |
已有 RESTClient,只包一层 |
只有 kubernetes.NewForConfig 是「大工厂」 ------组装所有 Group。
Dynamic / Metadata 只创建 单个 客户端实例。
10. 与 fake client 的区别
| 四种正式客户端 | kubernetes/fake |
|
|---|---|---|
| 目的 | 连真 apiserver | 单元测试,内存假集群 |
| 网络 | 有(除 fake) | 无 |
| 第一阶段 | Clientset 必学 | 知道 fake.NewSimpleClientset() 即可 |
Fake 不是第五种生产客户端,测试替身。
11. 源码跟读路线(按学习阶段)
11.1 第一阶段(Clientset + REST 底座的理解)
1. kubernetes/clientset.go:460 NewForConfig
2. kubernetes/typed/core/v1/pod.go:88 List → RESTClient.Get()...Do()
3. rest/client.go:81 RESTClient 结构体
4. rest/request.go:1023 client.Do(req)
11.2 扩展(Dynamic 对比)
5. dynamic/simple.go:71 NewForConfig
6. dynamic/simple.go:112 Create → Post().AbsPath().Do()
7. examples/dynamic-create-update-delete-deployment/main.go
11.3 进阶(Metadata)
8. metadata/metadata.go:91 NewForConfig
9. metadata/metadata.go:125 Resource(gvr) API
12. 自测
API 基础(§0)
- Pod 和 Deployment 的 REST 路径前缀有何不同?
- GVK 和 GVR 分别对应 YAML 里什么字段?
- CRD 安装后,Clientset 为何没有
CronTab()方法?
四种客户端
- 四种客户端分别用什么函数创建?
- 操作 CRD 全对象,选 Clientset 还是 Dynamic?
- 四种客户端发 HTTP 的最终共同点是什么?
Clientset.CoreV1()和dynamic.NewForConfig哪个是「大工厂」?- Metadata Client 返回的典型类型是什么?
参考答案
§0
- Pod:
/api/v1/...(Core);Deployment:/apis/apps/v1/...。 - GVK →
apiVersion+kind;GVR → Group + Version + 复数 resource 名(如deployments)。 - CRD 是用户扩展的类型,未编入
kubernetes.Clientset;需 Dynamic 或 code-gen。
四种客户端
kubernetes.NewForConfig/dynamic.NewForConfig/metadata.NewForConfig/rest.RESTClientFor(AndClient)。- Dynamic(除非对 CRD 做了 code-gen)。
- 最终都到
rest.Request的.Do()→http.Client.Do(模块 1)。 - 只有
kubernetes.NewForConfig组装整棵 Clientset。 *metav1.PartialObjectMetadata。
13. 架构深入(按 source_code / client-go rules)
本节按
.cursor/skills/source_code.md与.cursor/rules/client-go.mdc框架,补全四种客户端的设计动机、并发、性能、调试、面试维度。
13.1 为什么需要四种抽象(而非一种万能 Client)
| 若没有分层 | 问题 |
|---|---|
| 只有 REST Client | 每次手写 URL/解码;无编译期类型安全 |
| 只有 Clientset | CRD 无法操作;每加一个 CRD 要 code-gen 或改 Clientset |
| 只有 Dynamic | 内置资源失去 IDE 补全;controller 易写错字段 |
| 只有 Metadata | 无法读 spec/status;不能替代 CRUD |
当前方案 :共享 rest.Config + http.Client + Request.Do(),在 对象类型 和 URL 策略 上分叉------这是「同一 REST API、多种 Go 封装」的工程折中。
13.2 设计模式对照
| 模式 | 体现 | 原因 |
|---|---|---|
| Factory | NewForConfig 家族 |
统一从 Config 建客户端 |
| Facade | Clientset 包几十 Group |
隐藏 REST 细节,暴露业务 API |
| Builder | Get().Namespace().Resource().Do() |
分步填 Request,.Do() 发网 |
| Strategy | Into vs Raw vs PartialObjectMetadata 解码 | 按客户端选解码策略 |
| Adapter | Dynamic AbsPath 适配任意 GVR |
运行时资源定位 |
| Decorator | RoundTripper 链 | TLS/Token 与业务解耦 |
13.3 并发与共享
| 对象 | 是否线程安全 | 说明 |
|---|---|---|
*Clientset / *DynamicClient |
可并发复用 | 底层 http.Client 连接池线程安全 |
*rest.Request |
不可跨 goroutine 共享 | 每次 Do 前 Builder 填参;Builder 非并发 |
watch.Interface |
单 consumer | 一个 ResultChan 通常一个 goroutine 读 |
实践 :多 goroutine 共享同一个 clientset;每个 goroutine 自己调 List/Get,不要共享正在 Builder 中的 *Request。
13.4 性能与限流
| 机制 | Clientset | Dynamic | Metadata |
|---|---|---|---|
| QPS 限流 | RESTClient 默认 5 QPS | 同左 | 同左 |
| 连接池 | 共享 http.Client |
可共享(若注入同一 client) | 同左 |
| payload | 全对象 JSON | 全对象 JSON | PartialObjectMetadata 更小 |
| Watch 限流 | 不走 tryThrottle | 同左 | 同左 |
Metadata 在大集群 List 全量资源 名字+标签 时,带宽和解码 CPU 显著低于 Dynamic List 全对象。
13.5 调试断点(四种客户端各一处)
| 客户端 | 推荐断点 | 观察 |
|---|---|---|
| Clientset List | pod.go:99 .Do(ctx) |
Resource("pods") + pathPrefix |
| Dynamic Create | simple.go:138 .Do(ctx) |
AbsPath 完整 GVR URL |
| Metadata Get | metadata.go:188 .Do(ctx) |
Accept 含 PartialObjectMetadata |
| 共同 | request.go:1023 client.Do |
四种最终汇合点 |
13.6 面试题(进阶)
源码级 :Dynamic 为何把 APIPath 设为 /if-you-see-this-search-for-the-break?
→ 强迫每次请求用 AbsPath 显式拼 GVR 路径;RESTClient 的 versionedAPIPath 对 Dynamic 无意义。
架构级 :四种客户端能否共用一个 http.Client?
→ 可以;NewForConfigAndClient(config, httpClient) 注入同一实例即可共享连接池与 TLS session。
对比级 :操作 CRD 全对象 vs 只要 labels,选谁?
→ 全对象 Dynamic ;只要 metadata Metadata Client。
13.7 一句话总结
四种客户端是 同一张 rest.Config 门票、同一条 Request.Do HTTP 通路 上的四种 Go 抽象:Clientset 用编译期类型 + 固定 Resource 名;Dynamic 用 GVR + Unstructured;Metadata 用 GVR + 内容协商只取 metadata;REST Client 是前三者的公共底座。
14. 相关文档
| 文档 | 关系 |
|---|---|
| 调用链精读 | Clientset + List 主线;/api/v1 路径来源 |
| 第一阶段夯实基础指南 | 模块 1 rest 层、模块 2 CRUD |
| 第一阶段指南 模块 4 | GVK/GVR ↔ Go struct ↔ Scheme 闭环 |
| 本文 §0 | 内置 API / CRD / GVK / GVR 基础 |
| Watch 源码架构详解 | Watch 与 List 分叉、StreamWatcher |
| 面试题集 | 模块 B 面试题(B-01~B-12) |
| Kubernetes API Concepts | 官方 API 概念 |
client-go 四种客户端详解 · release-1.28 · 配合第一阶段学习使用