client-go :Watch 源码架构详解
写给源码学习者
- 代码分支 :
release-1.28- 前置文档 :
- client-go调用链精读-从kubeconfig到ListPods.md(Config → Clientset → List)
- client-go第一阶段夯实基础指南.md(模块 1 rest 层、模块 2 CRUD)
- 本文范围 :裸 Watch(
Pods().Watch())的源码架构;不读Reflector/SharedInformer(留给第二阶段)- 对应指南章节 :第一阶段指南 · 模块 3:Watch 初体验
- 学完标志:能解释 Watch 与 List 的差异、跟完调用链、能写 20 行 Watch 小程序并通过自测
目录
- 模块定位
- [为什么需要 Watch](#为什么需要 Watch)
- [List vs Watch 对照](#List vs Watch 对照)
- 核心结构体分析
- 核心接口分析
- 调用链分析
- 源码执行流程(深入)
- 7.1 时间线总览(#两条时间线 + Watch 特有的第三条)
- [7.2 Builder 阶段](#7.2 Builder 阶段)
- [7.3 Request.Watch 建连](#7.3 Request.Watch 建连)
- [7.4 newStreamWatcher 解码栈](#7.4 newStreamWatcher 解码栈)
- [7.5 StreamWatcher.receive](#7.5 StreamWatcher.receive)
- 数据流分析(深入)
- 并发模型分析
- 设计模式分析
- 性能优化分析
- [与 Kubernetes 组件关系](#与 Kubernetes 组件关系)
- [Mermaid 架构图](#Mermaid 架构图)
- 调试断点清单
- [最小 Watch 示例](#最小 Watch 示例)
- 事件类型速查
- 面试题
- 实战案例
- 本章小结
- 自测
1. 模块定位
Watch 横跨 client-go 多层,从 typed client 一直到 HTTP 长连接:
用户代码(for event := range watcher.ResultChan())
↓
kubernetes/typed/core/v1/pod.go typed 封装:opts.Watch = true
↓
rest/request.go Request.Watch():发 HTTP、建 StreamWatcher
↓
rest/watch/decoder.go 把 HTTP 流解码成 watch.Event
↓
apimachinery/pkg/watch watch.Interface / StreamWatcher
↓
http.Client.Do(req) 长连接 HTTPS
↓
kube-apiserver Watch 子系统 → etcd Watch
| 包 | 位置 | 职责 |
|---|---|---|
kubernetes/typed/.../pod.go |
typed client 层 | 设 Watch=true,调 RESTClient.Watch() |
rest/request.go |
REST 传输层 | 建 HTTP 请求、读响应体、包装成 watch.Interface |
rest/watch/decoder.go |
解码适配层 | 把 metav1.WatchEvent 解码成 *v1.Pod |
apimachinery/pkg/watch |
通用抽象层 | watch.Interface、StreamWatcher、事件类型 |
tools/watch/retrywatcher.go |
工具层(选读) | 断线自动重连(Informer 之前的手动增强) |
在 Kubernetes 架构中的位置 :client-go Watch 是 controller / operator / scheduler 感知集群变化 的最低层 API。Informer 是在此之上的「List + Watch + 本地缓存 + 回调」封装。
轻量结论:模块 1~2 是「打一次电话问完就挂」(List/CRUD);模块 3 是「电话不挂,有变化就推给你」(Watch)。
2. 为什么需要 Watch
2.1 没有 Watch 会怎样
- Controller 只能 轮询 List:浪费带宽、延迟高、apiserver 压力大。
- 无法实时响应 Pod 创建、Deployment 扩缩容等事件。
2.2 Kubernetes 的解决方案
apiserver 提供 HTTP 长连接 + 流式 JSON(或 Protobuf 帧):
- 客户端发
GET /api/v1/pods?watch=true - apiserver 保持连接,etcd 有变化就推送
WatchEvent - client-go 后台 goroutine 读流 → 写入 channel → 你的
for range消费
2.3 为什么不直接用 Watch,还要 Informer?(第二阶段 Preview)
| 问题 | 裸 Watch | Informer(以后学) |
|---|---|---|
| 断线重连 | 要自己写(RetryWatcher) |
Reflector 自动 List + Watch |
| 本地缓存 | 无,只有事件 | Store/Indexer 有全量快照 |
| 启动全量 | resourceVersion="" 行为复杂 |
List 一次再 Watch |
| 重复事件 / 漏事件 | 需自己处理 RV | DeltaFIFO 去重 |
本文目标:先理解裸 Watch 的 HTTP 流和 channel 模型;Informer 是它的「工业化封装」。
3. List vs Watch 对照
| List | Watch | |
|---|---|---|
| 干什么 | 一次性拉全量 | 持续接收变化 |
| HTTP | 普通 GET,响应完就结束 | GET + watch=true,连接保持 |
| client-go | .List() → .Do().Into(PodList) |
.Watch() → watch.Interface |
| kubectl | kubectl get pods |
kubectl get pods -w |
| 返回 | *PodList, error |
watch.Interface, error |
| 网络时机 | .Do() 时一次性收发 |
.Watch() 时建立长连接,后续持续收事件 |
与 List 的分叉点(模块 3 自测核心):
| 步骤 | List | Watch |
|---|---|---|
| opts | 默认 Watch=false |
opts.Watch = true |
| 结尾 | .Do(ctx).Into(&PodList{}) |
.Watch(ctx) |
| 返回 | *PodList, error |
watch.Interface, error |
| HTTP 语义 | 短请求,读完 body 就关 | 长连接,body 持续有数据 |
4. 核心结构体分析
4.1 pods(typed client 小包装)
go
// kubernetes/typed/core/v1/pod.go
type pods struct {
client rest.Interface
ns string
}
- 职责 :把
Watch(ctx, ListOptions)翻译成 REST 链式调用 - 生命周期 :
Pods(ns)时创建,随 Clientset 存活
4.2 rest.Request(单次行程单)
Watch 阶段填的关键字段:
| 字段 | Watch 时的值 |
|---|---|
verb |
"GET" |
resource |
"pods" |
namespace |
"default" 或 "" |
params |
含 watch=true、可选 resourceVersion |
4.3 watch.Event(apimachinery)
go
// k8s.io/apimachinery/pkg/watch
type Event struct {
Type EventType // Added / Modified / Deleted / Bookmark / Error
Object runtime.Object // 如 *v1.Pod
}
- 职责:一个 Watch 推送单元
- 生命周期 :从 StreamWatcher 写入 channel,被
for range消费
4.4 watch.StreamWatcher(核心运行时对象)
go
type StreamWatcher struct {
source Decoder // 从 HTTP body 读帧
reporter Reporter // 解码错误时转成 Status
result chan Event // 无缓冲 channel
done chan struct{} // Stop 信号
}
- 职责 :把 HTTP 长连接 异步 转成
<-chan Event - 生命周期 :
Request.Watch()成功 →NewStreamWatcher→ 启动go receive()Stop()或流 EOF → 关闭 channel → goroutine 退出
4.5 rest/watch.Decoder
go
// rest/watch/decoder.go
func (d *Decoder) Decode() (watch.EventType, runtime.Object, error) {
var got metav1.WatchEvent
res, _, err := d.decoder.Decode(nil, &got)
// ...
obj, err := runtime.Decode(d.embeddedDecoder, got.Object.Raw)
return watch.EventType(got.Type), obj, nil
}
- 职责 :外层
WatchEvent信封 → 内层*v1.Pod对象
5. 核心接口分析
5.1 watch.Interface --- 消费者唯一需要的合同
go
type Interface interface {
Stop()
ResultChan() <-chan Event
}
| 实现 | 用途 |
|---|---|
*StreamWatcher |
真连 apiserver 的生产实现 |
FakeWatcher |
单元测试 |
RetryWatcher |
断线重连包装 |
emptyWatch |
特殊错误场景返回已关闭的空 channel |
为什么抽象接口?
- typed client 返回
watch.Interface,不关心底层是 HTTP 还是 fake - Informer / RetryWatcher 可以包装任意
watch.Interface
5.2 watch.Decoder --- 流解码抽象
go
type Decoder interface {
Decode() (action EventType, object runtime.Object, err error)
Close()
}
rest/watch.Decoder是 HTTP JSON 流的实现StreamWatcher只依赖Decoder,与 HTTP 细节解耦
5.3 rest.Interface --- typed → REST 的桥梁
pods.client 是 rest.Interface,Watch() 最终落到 *RESTClient 的 Request.Watch()。
6. 调用链分析
以 clientset.CoreV1().Pods("default").Watch(ctx, opts) 为例:
main
clientset.CoreV1().Pods("default").Watch(ctx, ListOptions{})
│
├─ [Getter] Clientset.CoreV1() → CoreV1Interface (*CoreV1Client)
├─ [小包装] CoreV1Client.Pods("default") → PodInterface (*pods)
│
└─ [方法] (*pods).Watch(ctx, opts)
│
├─ opts.Watch = true ★ 关键差异 1
│
└─ c.client.Get()
.Namespace("default")
.Resource("pods")
.VersionedParams(&opts, ...) → query: watch=true
.Watch(ctx) ★ 关键差异 2(不是 Do().Into())
│
└─ rest/request.go:703 Request.Watch()
│
├─ newHTTPRequest() GET .../pods?watch=true
├─ client.Do(req) 建立长连接,200 OK
└─ newStreamWatcher(resp)
│
├─ framer.NewFrameReader(resp.Body)
├─ streaming.NewDecoder(...)
├─ restclientwatch.NewDecoder(...)
└─ watch.NewStreamWatcher(decoder, reporter)
└─ go sw.receive() ★ 后台 goroutine 启动
└─ 循环 Decode → result chan
6.1 typed 层入口
go
// kubernetes/typed/core/v1/pod.go:105-116
func (c *pods) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) {
opts.Watch = true
return c.client.Get().
Namespace(c.ns).
Resource("pods").
VersionedParams(&opts, scheme.ParameterCodec).
Timeout(timeout).
Watch(ctx)
}
6.2 REST 层入口:Request.Watch() 是独立实现
Watch 不走 Do() → request() 那条路,而是 rest/request.go:703 自己实现了一套循环。这是读源码时最容易误判的地方。
go
// rest/request.go:703-769 --- 完整逻辑骨架
func (r *Request) Watch(ctx context.Context) (watch.Interface, error) {
// ① 不走 tryThrottle(注释 704-705)
// ② 不走 requestPreflightCheck()
// ③ 不在此处 ctx.WithTimeout(与 request() 不同)
retry := r.retryFn(r.maxRetries)
for {
retry.Before(ctx, r) // 首次仅 backoff;重试时才 tryThrottle
req, _ := r.newHTTPRequest(ctx)
resp, err := client.Do(req) // ★ 建立长连接
if err == nil && resp.StatusCode == http.StatusOK {
return r.newStreamWatcher(resp) // 成功:body 不读闭,交给 StreamWatcher
}
// 失败:readAndCloseResponseBody + 判断是否重试
}
}
6.3 Watch vs Do:双路径对照(必读)
| 维度 | List → .Do().Into() |
Watch → .Watch() |
|---|---|---|
| 入口 | request.go:1061 Do() |
request.go:703 Watch() |
| 核心循环 | request() (:965) |
Watch() 内联 for 循环 |
| 首次限流 | 有 tryThrottle() (:989) |
无(注释明确说明) |
| 重试时限流 | retry.Before → tryThrottleWithInfo |
同左(仅第 2 次起) |
| preflight | requestPreflightCheck() |
无 |
| ctx 超时 | r.timeout > 0 时 WithTimeout 包住 ctx |
不在 REST 层包;timeout 走 URL query |
| 成功时 body | io.ReadAll 一次读完 → transformResponse |
不读闭 ;resp.Body 交给 framer |
| 返回值 | Result{body, decoder} |
watch.Interface(异步 channel) |
| _metrics | RequestLatency 在 request() 里 |
Watch 路径不 走 request() 的 latency defer |
设计动机 :Watch 是长连接,若走 tryThrottle 会在连接建立前被 QPS 令牌桶阻塞;若走 io.ReadAll 会把本应持续的流一次性读完。
6.4 Builder 阶段:Request 字段如何被填满
链式调用修改的是同一个 *Request 实例(模块 1 Fluent Interface)。Watch Pod 时字段终态:
| 字段 | 值 | 设于 |
|---|---|---|
verb |
"GET" |
RESTClient.Get() → Verb("GET") |
pathPrefix |
/api/v1 |
NewRequest 时由 base.Path + versionedAPIPath 拼出 |
namespaceSet |
true |
Namespace("default") |
namespace |
"default" |
同上 |
resource |
"pods" |
Resource("pods") |
params["watch"] |
"true" |
VersionedParams(&opts, ParameterCodec) |
params["resourceVersion"] |
可选 | ListOptions 里若指定 |
params["timeout"] |
可选 | Timeout(d) 或 TimeoutSeconds 换算 |
headers["Accept"] |
application/json, */* 等 |
NewRequest 时按 ContentConfig 设置 |
timeout |
0 或 opts.TimeoutSeconds |
pod.go 里 Timeout(timeout) |
rateLimiter |
指向 RESTClient 的 limiter | NewRequest 继承(Watch 首次不用) |
watch=true 怎么进 URL?
go
// pod.go 强制 opts.Watch = true 后:
VersionedParams(&opts, scheme.ParameterCodec)
→ codec.EncodeParameters(obj, gv) // ListOptions 序列化为 url.Values
→ r.params["watch"] = []string{"true"}
→ URL().RawQuery = "watch=true&..."
ListOptions.Watch 字段定义(apimachinery/pkg/apis/meta/v1/types.go:339):
go
Watch bool `json:"watch,omitempty"`
ParameterCodec 把它编码为 query 参数 watch=true,apiserver 据此切换 List/Watch 处理器。
最终 URL 示例:
GET https://127.0.0.1:6443/api/v1/namespaces/default/pods?watch=true
Accept: application/json
Authorization: (在 RoundTripper 链注入,此处 Header 仍可能没有 Bearer)
7. 源码执行流程(深入)
7.1 时间线总览:两条时间线 + Watch 特有的第三条
时间线 A(程序启动,模块 1)
NewForConfig → http.Client + RESTClient + CoreV1Client + pods 小包装
★ 零网络
时间线 B-Builder(Watch 调用瞬间,仍零网络)
opts.Watch=true → Get().Namespace().Resource().VersionedParams().Timeout().Watch(ctx)
★ 只改 Request 字段
时间线 B-Execute(进入 Request.Watch 之后)
retry.Before → newHTTPRequest → client.Do → newStreamWatcher → go receive()
★ 第一次网络 IO
时间线 B-Stream(连接建立后,与用户 main 并发)
receive() 循环 Decode → result chan → 用户 for range
★ 持续网络读,直到 EOF 或 Stop()
7.2 阶段 B-Builder:typed 层细节
105:116:d:\gocode\client-go\kubernetes\typed\core\v1\pod.go
func (c *pods) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) {
var timeout time.Duration
if opts.TimeoutSeconds != nil {
timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
}
opts.Watch = true
return c.client.Get().
Namespace(c.ns).
Resource("pods").
VersionedParams(&opts, scheme.ParameterCodec).
Timeout(timeout).
Watch(ctx)
}
三个容易忽略的细节:
-
opts.Watch = true是就地修改传入的
ListOptions会被改掉。若同一个opts先 List 再 Watch,第二次 List 可能带着watch=true出错------生产代码应传副本或分开构造。 -
TimeoutSeconds→ 两层 timeout- REST 层:
Timeout(d)把r.timeout设为 duration,并在URL()里写入 querytimeout=5s(apiserver 侧限制 Watch 最长存活时间)。 - 不会 像
request()那样context.WithTimeout(ctx, r.timeout)包住整个 Watch 生命周期;连接建立后的流式读靠 apiserver 的timeoutquery 或连接断开结束。
- REST 层:
-
与 List 共用 Builder,仅最后一环不同
List 是
.Do(ctx).Into(result);Watch 是.Watch(ctx)。前面Get/NAMESPACE/Resource/VersionedParams完全一致。
7.3 阶段 B-Execute-1:Request.Watch() 建立连接
逐步跟读 rest/request.go:703-769:
步骤 1:前置校验
go
if r.err != nil { return nil, r.err } // Builder 链上任何一步出错都会落在这里
步骤 2:重试循环开始
go
retry := r.retryFn(r.maxRetries) // 默认 maxRetries=10,与 Do 相同
for {
if err := retry.Before(ctx, r); err != nil { return nil, ... }
retry.Before 行为(with_retry.go:195-236):
| 第几次尝试 | 行为 |
|---|---|
| 第 1 次 | 仅 backoff.Sleep(通常 NoBackoff,几乎不睡) |
| 第 2+ 次 | backoff + tryThrottleWithInfo + 打 Retry 日志 |
因此:Watch 首次建连不限流;只有建连失败重试时才可能被 QPS 限流 ------与 List 首次必 tryThrottle 不同。
步骤 3:构造 HTTP 请求
915:934:d:\gocode\client-go\rest\request.go
func (r *Request) newHTTPRequest(ctx context.Context) (*http.Request, error) {
url := r.URL().String()
req, err := http.NewRequestWithContext(
httptrace.WithClientTrace(ctx, newDNSMetricsTrace(ctx)),
r.verb, url, body)
req.Header = r.headers
return req, nil
}
Watch 时 body 为 nil(GET),req.Method == "GET"。
ctx 来自用户传入;若用户在 main 里用 context.TODO(),只有显式 Stop() 或 apiserver 断连才会结束。
步骤 4:client.Do(req) --- 与 List 相同的 HTTP 出口
- 请求进入 RoundTripper 链(模块 1):BearerAuth → UserAgent → TLS Transport
- apiserver 返回
200 OK,Content-Type通常为application/json;stream=watch或类似 streaming 类型 - 关键 :此时
resp.Body是一个未读完的长流 ,Watch 路径不能readAndCloseResponseBody
步骤 5:失败分支
非 200 或 client.Do 错误时:
go
defer readAndCloseResponseBody(resp) // 失败才关 body
if retry.IsNextRetry(...) { continue }
// 不可重试 → return error
isErrRetryableFunc(Watch 专用,比 request() 更宽):
go
if net.IsProbableEOF(err) || net.IsTimeout(err) { return true }
Watch 认为「建连阶段的 EOF/Timeout」可重试,因为流式协议本身会处理半包错误。
特殊返回值 :若最终错误仍被判为 ProbableEOF/Timeout,返回 watch.NewEmptyWatch()------一个已关闭的空 channel ,而不是 error。这是为了某些「watch 正常结束但无更多事件」的边缘场景,消费者 for range 会立即退出。
7.4 阶段 B-Execute-2:newStreamWatcher() 解码栈
成功 200 后,resp.Body 被装配成三层解码器:
771:792:d:\gocode\client-go\rest\request.go
func (r *Request) newStreamWatcher(resp *http.Response) (watch.Interface, error) {
contentType := resp.Header.Get("Content-Type")
objectDecoder, streamingSerializer, framer, err := r.c.content.Negotiator.StreamDecoder(mediaType, params)
frameReader := framer.NewFrameReader(resp.Body)
watchEventDecoder := streaming.NewDecoder(frameReader, streamingSerializer)
return watch.NewStreamWatcher(
restclientwatch.NewDecoder(watchEventDecoder, objectDecoder),
errors.NewClientErrorReporter(...),
), nil
}
三层解码职责:
resp.Body (原始 TCP/TLS 字节流)
↓ framer.NewFrameReader 按 HTTP/2 或 length-delimited 切帧
streaming.Decoder 读一帧 JSON → 反序列化为 metav1.WatchEvent
↓ rest/watch.Decoder.Decode() 从 WatchEvent.Object.Raw 再解码 → *v1.Pod
watch.StreamWatcher.receive() 包装为 watch.Event 写入 channel
rest/watch.Decoder.Decode() 一次迭代 (decoder.go:47-66):
go
// 1. 从流读一帧 → metav1.WatchEvent { Type: "ADDED", Object: RawExtension }
res, _, err := d.decoder.Decode(nil, &got)
// 2. 校验 Type ∈ {ADDED, MODIFIED, DELETED, ERROR, BOOKMARK}
// 3. runtime.Decode(d.embeddedDecoder, got.Object.Raw) → *v1.Pod
return watch.EventType(got.Type), obj, nil
embeddedDecoder 来自 CoreV1Client 的 NegotiatedSerializer(scheme.Codecs),与 List 的 Into() 用的是同一套 Scheme。
7.5 阶段 B-Stream:StreamWatcher.receive() 运行时
NewStreamWatcher 立即 go sw.receive()(streamwatcher.go:76)。此后 两个 goroutine 并发:
| goroutine | 在干什么 |
|---|---|
receive() |
阻塞在 source.Decode() 读网络 |
| 用户 main | 阻塞在 <-result 或处理 event |
receive() 循环逻辑 (streamwatcher.go:100-136):
go
func (sw *StreamWatcher) receive() {
defer utilruntime.HandleCrash()
defer close(sw.result) // 退出时关闭 channel → 用户 for range 结束
defer sw.Stop() // 确保 source.Close()
for {
action, obj, err := sw.source.Decode()
if err != nil {
switch err {
case io.EOF: // apiserver 正常关流 → 静默退出
case io.ErrUnexpectedEOF: // 半包 → 打 V(1) 日志后退出
default:
if IsProbableEOF/Timeout → V(5) 后退出
else → 向 result 发 Event{Type: Error, Object: Status}
}
return
}
select {
case <-sw.done: // 用户 Stop() 了
return
case sw.result <- Event{Type: action, Object: obj}:
}
}
}
背压(backpressure)机制:
result是无缓冲 channel(注释 66-68:消费者可自行加缓冲,生产者不加)- 若用户处理太慢,
sw.result <- Event阻塞 →Decode()不再读网络 → TCP 窗口反压 → apiserver 侧也会减缓推送 - 若用户已
Stop()但receive还在Decode()阻塞:通过donechannel 在发送前检查,避免向无人消费的 channel 写 Error 事件死锁
Error 事件 vs Go error:
- 解码逻辑错误:尽量发
Event{Type: Error, Object: *Status}到 channel,让 consumer 决定怎么处理 - 连接 EOF:通常不发 Error 事件,直接
close(result)
7.6 阶段 B-Consume:用户侧语义
go
watcher, err := clientset.CoreV1().Pods("default").Watch(ctx, opts)
// err != nil:建连失败(401/403/500、网络不可达等),此时还没有 goroutine
defer watcher.Stop()
for event := range watcher.ResultChan() {
switch event.Type {
case watch.Added, watch.Modified, watch.Deleted:
pod := event.Object.(*v1.Pod)
case watch.Error:
status := event.Object.(*metav1.Status) // 或 apierrors.FromObject
case watch.Bookmark:
// 仅 metadata.resourceVersion,第一阶段可忽略
}
}
| 时机 | Watch() 返回值 |
ResultChan() |
|---|---|---|
| 建连失败 | nil, err |
无 |
| 建连成功 | watcher, nil |
陆续收到 Event |
| 流正常结束 | 已是 nil err | channel close,for range 退出 |
| 用户 Stop | --- | 同上 |
7.7 阶段 B-Exit:退出与资源释放
调用 watcher.Stop() 时序:
用户 Stop()
→ StreamWatcher.Stop() 加锁
→ close(done)
→ source.Close()
→ streaming.Decoder.Close()
→ frameReader.Close()
→ resp.Body.Close() ★ 通知 apiserver 客户端不再读
→ receive() 中 Decode() 返回 error 或 done 分支触发
→ defer close(result)
→ defer Stop()(二次 Stop 因 done 已 close 而 no-op)
→ 用户 for range 退出
注意 :若不 Stop() 就退出 main,receive goroutine 可能泄漏直到进程结束------务必 defer watcher.Stop()。
7.8 Watch 与 List 在同一文件里的「对照实验」
建议并排读 pod.go:88-101(List)与 :105-116(Watch):
go
// List
err = c.client.Get().Namespace(c.ns).Resource("pods").
VersionedParams(&opts, scheme.ParameterCodec).
Timeout(timeout).
Do(ctx).Into(result)
// Watch
opts.Watch = true // 唯一多出的 typed 层逻辑
return c.client.Get().Namespace(c.ns).Resource("pods").
VersionedParams(&opts, scheme.ParameterCodec).
Timeout(timeout).
Watch(ctx) // 分叉:不进 Do/Into
List 的 Do() 内部:tryThrottle → client.Do → ReadAll(body) → transformResponse → Into 同步解码。
Watch 的 Watch():无限流 → client.Do → 保留 body → 异步 receive 解码。
7.9 执行阶段时序图(推荐调试对照)
渲染错误: Mermaid 渲染失败: Parse error on line 39: ...: for range and Pod 变更 ----------------------^ Expecting 'SPACE', 'NEWLINE', 'INVALID', 'create', 'box', 'end', 'autonumber', 'activate', 'deactivate', 'title', 'legacy_title', 'acc_title', 'acc_descr', 'acc_descr_multiline_value', 'loop', 'rect', 'opt', 'alt', 'par', 'par_over', 'critical', 'break', 'participant', 'participant_actor', 'destroy', 'note', 'links', 'link', 'properties', 'details', 'ACTOR', got 'and'
8. 数据流分析(深入)
8.1 从 etcd 到 Go struct 的完整链路
etcd Watch 通知
↓
kube-apiserver storage/watch 层
↓ 封装为 metav1.WatchEvent
HTTPS 响应体(streaming JSON,多帧连续)
↓
[client-go] framer 切帧
↓
streaming.Decoder:一帧 → metav1.WatchEvent
↓
rest/watch.Decoder:WatchEvent.Object.Raw → runtime.Decode → *v1.Pod
↓
StreamWatcher:watch.Event{Type, Object}
↓
用户:event.Object.(*v1.Pod)
8.2 单帧 on-the-wire 结构(JSON 模式)
apiserver 推送的每一帧逻辑上类似:
json
{
"type": "MODIFIED",
"object": {
"apiVersion": "v1",
"kind": "Pod",
"metadata": {
"name": "nginx",
"resourceVersion": "98765",
"uid": "..."
},
"spec": { ... },
"status": { ... }
}
}
对应 Go 类型链:
metav1.WatchEvent.Type → watch.EventType
metav1.WatchEvent.Object → runtime.RawExtension(原始 JSON 字节)
runtime.Decode(scheme) → *v1.Pod
为什么要解两次?
外层 WatchEvent 是通用信封(所有资源共用);内层对象类型由 Kind/apiVersion + Scheme 决定。List 一次性解码 PodList;Watch 每帧解一个 WatchEvent 再解一个 Pod。
8.3 streaming.Decoder 如何读一帧
streaming/decoder.go:74-106:
- 初始 buffer 1024 字节,遇
ErrShortBuffer指数扩容至最大 16MB - 读满一帧后调用
runtime.Decoder.Decode(buf, defaults, into) - 下一帧继续
Read,直到io.EOF(apiserver 关闭连接)
这与 List 的 io.ReadAll(resp.Body) 一次性读完形成鲜明对比。
8.4 resourceVersion 在数据流中的位置
| 阶段 | RV 作用 |
|---|---|
| List 返回 | PodList.metadata.resourceVersion --- 集群当前 RV |
| Watch 请求 | ListOptions.ResourceVersion --- 从哪个 RV 之后开始推事件 |
| 每个 Event | Pod.metadata.resourceVersion --- 该对象的 RV |
| Bookmark | 只带 RV,无 spec/status,用于 Informer 续传 |
模块 3 裸 Watch 若不先 List,ResourceVersion="" 时 apiserver 行为依赖版本与实现(可能 synthetic list 再 watch)------生产应用应 List → 拿 RV → Watch(RV),这也是 Informer/Reflector 的标准做法。
8.5 错误在数据流中的两条路径
| 错误发生点 | 传递方式 | 用户如何感知 |
|---|---|---|
| 建连前/HTTP 4xx/5xx | Watch() 返回 error |
if err != nil |
| 建连后解码失败 | Event{Type: Error, Object: Status} |
switch event.Type |
| 连接 EOF | close(result),通常无 Error 事件 |
for range 正常结束 |
| ProbableEOF 且不可重试 | NewEmptyWatch() |
for range 立即结束 |
9. 并发模型分析
9.1 goroutine 创建位置
| 位置 | goroutine | 作用 |
|---|---|---|
watch.NewStreamWatcher |
go sw.receive() |
后台读 HTTP 流,写 channel |
| 用户代码 | 主 goroutine | for range ResultChan() 消费 |
经典 Producer-Consumer:
- Producer :
receive()读网络 - Consumer :你的
for range - 通道 :无缓冲
chan Event(有意设计,避免隐藏缓冲导致内存膨胀)
9.2 channel 用途
| channel | 方向 | 用途 |
|---|---|---|
StreamWatcher.result |
内部 → 用户 | 传递 Event |
StreamWatcher.done |
Stop → receive | 通知退出,防阻塞 |
9.3 锁与 Context
StreamWatcher用sync.Mutex保证Stop()只执行一次(防 double close panic)Watch(ctx)传入newHTTPRequest(ctx):ctx 取消会中断 HTTP 请求建立;连接建立后主要靠Stop()关闭
10. 设计模式分析
| 模式 | 位置 | 为什么 |
|---|---|---|
| Builder / Fluent Interface | Get().Namespace().Resource().Watch() |
与 List 共用同一套 Request 构建 |
| Decorator | RoundTripper 链(模块 1) | Token/TLS 与 Watch 无关,仍走同一 Transport |
| Producer-Consumer | receive() + ResultChan() |
网络 IO 与业务消费解耦 |
| Strategy | Negotiator.StreamDecoder |
按 Content-Type 选 JSON/Protobuf 解码策略 |
| Adapter | rest/watch.Decoder |
把 streaming 帧适配成 watch.Decoder 接口 |
| Observer(雏形) | 你的 for range 处理事件 |
Informer 的 EventHandler 是工业化版 Observer |
11. 性能优化分析
| 机制 | 说明 |
|---|---|
| Watch 不限流 | Request.Watch() 明确不用 rateLimiter(长连接不能被 QPS 卡住) |
| 流式解码 | 不一次性读完全部 body,按帧 Decode |
| 无缓冲 channel | 背压直接传到网络读端,避免内存堆积 |
| resourceVersion | ListOptions.ResourceVersion 指定从哪个版本开始 Watch |
| Bookmark | 事件类型 Bookmark,只带 RV,用于断点续传(Informer 常用) |
| Watch 重试 | Request.Watch 对 EOF/Timeout 可重试;RetryWatcher 在更高层自动重连 |
List-Watch 组合(第二阶段 Preview):
List(拿 resourceVersion + 全量快照)
→ Watch(从该 RV 开始只收增量)
模块 3 裸 Watch 若 resourceVersion="",apiserver 可能先 synthetic list 再 watch ------ 行为较复杂,生产环境推荐先 List 再 Watch。
12. 与 Kubernetes 组件关系
┌─────────────┐
│ etcd │ 存储 + Watch 源
└──────┬──────┘
│
┌──────▼──────┐
│ kube-apiserver │ Watch HTTP 流
└──────┬──────┘
│ GET ?watch=true
┌────────────┼────────────┐
│ │ │
client-go Watch controller- scheduler
(模块3) manager (Watch Node/Pod)
│ │ │
└────────────┼────────────┘
│
Informer/Operator
(List+Watch+Cache,第二阶段)
| 组件 | 与 Watch 关系 |
|---|---|
| kube-apiserver | 提供 Watch HTTP 端点,转发 etcd 变化 |
| etcd | 真正的变更源头 |
| controller-manager | 大量 controller 通过 Informer(底层是 Watch) reconcile |
| scheduler | Watch 未调度 Pod |
| Operator | 通常用 controller-runtime → Informer → 底层 Watch |
| client-go 模块 3 | 最薄一层:直接 Pods().Watch() |
13. Mermaid 架构图
13.1 架构图(flowchart)
#mermaid-svg-22MdajAWRxiq1V2d{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-22MdajAWRxiq1V2d .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-22MdajAWRxiq1V2d .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-22MdajAWRxiq1V2d .error-icon{fill:#552222;}#mermaid-svg-22MdajAWRxiq1V2d .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-22MdajAWRxiq1V2d .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-22MdajAWRxiq1V2d .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-22MdajAWRxiq1V2d .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-22MdajAWRxiq1V2d .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-22MdajAWRxiq1V2d .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-22MdajAWRxiq1V2d .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-22MdajAWRxiq1V2d .marker{fill:#333333;stroke:#333333;}#mermaid-svg-22MdajAWRxiq1V2d .marker.cross{stroke:#333333;}#mermaid-svg-22MdajAWRxiq1V2d svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-22MdajAWRxiq1V2d p{margin:0;}#mermaid-svg-22MdajAWRxiq1V2d .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-22MdajAWRxiq1V2d .cluster-label text{fill:#333;}#mermaid-svg-22MdajAWRxiq1V2d .cluster-label span{color:#333;}#mermaid-svg-22MdajAWRxiq1V2d .cluster-label span p{background-color:transparent;}#mermaid-svg-22MdajAWRxiq1V2d .label text,#mermaid-svg-22MdajAWRxiq1V2d span{fill:#333;color:#333;}#mermaid-svg-22MdajAWRxiq1V2d .node rect,#mermaid-svg-22MdajAWRxiq1V2d .node circle,#mermaid-svg-22MdajAWRxiq1V2d .node ellipse,#mermaid-svg-22MdajAWRxiq1V2d .node polygon,#mermaid-svg-22MdajAWRxiq1V2d .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-22MdajAWRxiq1V2d .rough-node .label text,#mermaid-svg-22MdajAWRxiq1V2d .node .label text,#mermaid-svg-22MdajAWRxiq1V2d .image-shape .label,#mermaid-svg-22MdajAWRxiq1V2d .icon-shape .label{text-anchor:middle;}#mermaid-svg-22MdajAWRxiq1V2d .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-22MdajAWRxiq1V2d .rough-node .label,#mermaid-svg-22MdajAWRxiq1V2d .node .label,#mermaid-svg-22MdajAWRxiq1V2d .image-shape .label,#mermaid-svg-22MdajAWRxiq1V2d .icon-shape .label{text-align:center;}#mermaid-svg-22MdajAWRxiq1V2d .node.clickable{cursor:pointer;}#mermaid-svg-22MdajAWRxiq1V2d .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-22MdajAWRxiq1V2d .arrowheadPath{fill:#333333;}#mermaid-svg-22MdajAWRxiq1V2d .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-22MdajAWRxiq1V2d .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-22MdajAWRxiq1V2d .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-22MdajAWRxiq1V2d .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-22MdajAWRxiq1V2d .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-22MdajAWRxiq1V2d .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-22MdajAWRxiq1V2d .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-22MdajAWRxiq1V2d .cluster text{fill:#333;}#mermaid-svg-22MdajAWRxiq1V2d .cluster span{color:#333;}#mermaid-svg-22MdajAWRxiq1V2d 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-22MdajAWRxiq1V2d .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-22MdajAWRxiq1V2d rect.text{fill:none;stroke-width:0;}#mermaid-svg-22MdajAWRxiq1V2d .icon-shape,#mermaid-svg-22MdajAWRxiq1V2d .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-22MdajAWRxiq1V2d .icon-shape p,#mermaid-svg-22MdajAWRxiq1V2d .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-22MdajAWRxiq1V2d .icon-shape .label rect,#mermaid-svg-22MdajAWRxiq1V2d .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-22MdajAWRxiq1V2d .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-22MdajAWRxiq1V2d .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-22MdajAWRxiq1V2d :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 网络层
rest/watch + apimachinery
rest/request.go
kubernetes/typed/core/v1
用户代码
opts.Watch=true
for event := range watcher.ResultChan()
(*pods).Watch()
Request.Watch()
newStreamWatcher()
Decoder.Decode()
StreamWatcher.receive()
chan Event
http.Client.Do()
kube-apiserver
etcd
13.2 时序图(sequenceDiagram)
result chan StreamWatcher kube-apiserver http.Client Request.Watch pods.Watch 用户 main result chan StreamWatcher kube-apiserver http.Client Request.Watch pods.Watch 用户 main #mermaid-svg-vZ2wxXtzHbjiZ0WX{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-vZ2wxXtzHbjiZ0WX .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-vZ2wxXtzHbjiZ0WX .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-vZ2wxXtzHbjiZ0WX .error-icon{fill:#552222;}#mermaid-svg-vZ2wxXtzHbjiZ0WX .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-vZ2wxXtzHbjiZ0WX .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-vZ2wxXtzHbjiZ0WX .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-vZ2wxXtzHbjiZ0WX .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-vZ2wxXtzHbjiZ0WX .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-vZ2wxXtzHbjiZ0WX .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-vZ2wxXtzHbjiZ0WX .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-vZ2wxXtzHbjiZ0WX .marker{fill:#333333;stroke:#333333;}#mermaid-svg-vZ2wxXtzHbjiZ0WX .marker.cross{stroke:#333333;}#mermaid-svg-vZ2wxXtzHbjiZ0WX svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-vZ2wxXtzHbjiZ0WX p{margin:0;}#mermaid-svg-vZ2wxXtzHbjiZ0WX .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-vZ2wxXtzHbjiZ0WX text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-vZ2wxXtzHbjiZ0WX .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-vZ2wxXtzHbjiZ0WX .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-vZ2wxXtzHbjiZ0WX .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-vZ2wxXtzHbjiZ0WX .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-vZ2wxXtzHbjiZ0WX #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-vZ2wxXtzHbjiZ0WX .sequenceNumber{fill:white;}#mermaid-svg-vZ2wxXtzHbjiZ0WX #sequencenumber{fill:#333;}#mermaid-svg-vZ2wxXtzHbjiZ0WX #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-vZ2wxXtzHbjiZ0WX .messageText{fill:#333;stroke:none;}#mermaid-svg-vZ2wxXtzHbjiZ0WX .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-vZ2wxXtzHbjiZ0WX .labelText,#mermaid-svg-vZ2wxXtzHbjiZ0WX .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-vZ2wxXtzHbjiZ0WX .loopText,#mermaid-svg-vZ2wxXtzHbjiZ0WX .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-vZ2wxXtzHbjiZ0WX .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-vZ2wxXtzHbjiZ0WX .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-vZ2wxXtzHbjiZ0WX .noteText,#mermaid-svg-vZ2wxXtzHbjiZ0WX .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-vZ2wxXtzHbjiZ0WX .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-vZ2wxXtzHbjiZ0WX .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-vZ2wxXtzHbjiZ0WX .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-vZ2wxXtzHbjiZ0WX .actorPopupMenu{position:absolute;}#mermaid-svg-vZ2wxXtzHbjiZ0WX .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-vZ2wxXtzHbjiZ0WX .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-vZ2wxXtzHbjiZ0WX .actor-man circle,#mermaid-svg-vZ2wxXtzHbjiZ0WX line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-vZ2wxXtzHbjiZ0WX :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} opts.Watch = true loop Pod 变化时 Watch(ctx, ListOptions{}) 1 Get().Resource("pods").Watch(ctx) 2 newHTTPRequest() watch=true 3 Do(req) 长连接 4 GET /api/v1/namespaces/default/pods?watch=true 5 200 stream open 6 NewStreamWatcher(decoder) 7 go receive() 8 watch.Interface 9 watcher 10 WatchEvent JSON 帧 11 Decode → *Pod 12 Event{Added/Modified/Deleted} 13 for range 收到 event 14 Stop() 15 close body, close chan 16
13.3 类图(classDiagram)
#mermaid-svg-AsWClasn2nbWhr6d{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-AsWClasn2nbWhr6d .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-AsWClasn2nbWhr6d .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-AsWClasn2nbWhr6d .error-icon{fill:#552222;}#mermaid-svg-AsWClasn2nbWhr6d .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-AsWClasn2nbWhr6d .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-AsWClasn2nbWhr6d .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-AsWClasn2nbWhr6d .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-AsWClasn2nbWhr6d .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-AsWClasn2nbWhr6d .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-AsWClasn2nbWhr6d .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-AsWClasn2nbWhr6d .marker{fill:#333333;stroke:#333333;}#mermaid-svg-AsWClasn2nbWhr6d .marker.cross{stroke:#333333;}#mermaid-svg-AsWClasn2nbWhr6d svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-AsWClasn2nbWhr6d p{margin:0;}#mermaid-svg-AsWClasn2nbWhr6d g.classGroup text{fill:#9370DB;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#mermaid-svg-AsWClasn2nbWhr6d g.classGroup text .title{font-weight:bolder;}#mermaid-svg-AsWClasn2nbWhr6d .cluster-label text{fill:#333;}#mermaid-svg-AsWClasn2nbWhr6d .cluster-label span{color:#333;}#mermaid-svg-AsWClasn2nbWhr6d .cluster-label span p{background-color:transparent;}#mermaid-svg-AsWClasn2nbWhr6d .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-AsWClasn2nbWhr6d .cluster text{fill:#333;}#mermaid-svg-AsWClasn2nbWhr6d .cluster span{color:#333;}#mermaid-svg-AsWClasn2nbWhr6d .nodeLabel,#mermaid-svg-AsWClasn2nbWhr6d .edgeLabel{color:#131300;}#mermaid-svg-AsWClasn2nbWhr6d .edgeLabel .label rect{fill:#ECECFF;}#mermaid-svg-AsWClasn2nbWhr6d .label text{fill:#131300;}#mermaid-svg-AsWClasn2nbWhr6d .labelBkg{background:#ECECFF;}#mermaid-svg-AsWClasn2nbWhr6d .edgeLabel .label span{background:#ECECFF;}#mermaid-svg-AsWClasn2nbWhr6d .classTitle{font-weight:bolder;}#mermaid-svg-AsWClasn2nbWhr6d .node rect,#mermaid-svg-AsWClasn2nbWhr6d .node circle,#mermaid-svg-AsWClasn2nbWhr6d .node ellipse,#mermaid-svg-AsWClasn2nbWhr6d .node polygon,#mermaid-svg-AsWClasn2nbWhr6d .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-AsWClasn2nbWhr6d .divider{stroke:#9370DB;stroke-width:1;}#mermaid-svg-AsWClasn2nbWhr6d g.clickable{cursor:pointer;}#mermaid-svg-AsWClasn2nbWhr6d g.classGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-AsWClasn2nbWhr6d g.classGroup line{stroke:#9370DB;stroke-width:1;}#mermaid-svg-AsWClasn2nbWhr6d .classLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-AsWClasn2nbWhr6d .classLabel .label{fill:#9370DB;font-size:10px;}#mermaid-svg-AsWClasn2nbWhr6d .relation{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-AsWClasn2nbWhr6d .dashed-line{stroke-dasharray:3;}#mermaid-svg-AsWClasn2nbWhr6d .dotted-line{stroke-dasharray:1 2;}#mermaid-svg-AsWClasn2nbWhr6d #compositionStart,#mermaid-svg-AsWClasn2nbWhr6d .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-AsWClasn2nbWhr6d #compositionEnd,#mermaid-svg-AsWClasn2nbWhr6d .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-AsWClasn2nbWhr6d #dependencyStart,#mermaid-svg-AsWClasn2nbWhr6d .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-AsWClasn2nbWhr6d #dependencyStart,#mermaid-svg-AsWClasn2nbWhr6d .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-AsWClasn2nbWhr6d #extensionStart,#mermaid-svg-AsWClasn2nbWhr6d .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-AsWClasn2nbWhr6d #extensionEnd,#mermaid-svg-AsWClasn2nbWhr6d .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-AsWClasn2nbWhr6d #aggregationStart,#mermaid-svg-AsWClasn2nbWhr6d .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-AsWClasn2nbWhr6d #aggregationEnd,#mermaid-svg-AsWClasn2nbWhr6d .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-AsWClasn2nbWhr6d #lollipopStart,#mermaid-svg-AsWClasn2nbWhr6d .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-AsWClasn2nbWhr6d #lollipopEnd,#mermaid-svg-AsWClasn2nbWhr6d .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-AsWClasn2nbWhr6d .edgeTerminals{font-size:11px;line-height:initial;}#mermaid-svg-AsWClasn2nbWhr6d .classTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-AsWClasn2nbWhr6d .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-AsWClasn2nbWhr6d .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-AsWClasn2nbWhr6d :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} creates
produces
<<interface>>
PodInterface
Watch(ctx, opts) : watch.Interface
List(ctx, opts) : PodList
pods
-client rest.Interface
-ns string
+Watch() : watch.Interface
<<interface>>
rest_Interface
Get() : Request
Request
-params url.Values
+Watch(ctx) : watch.Interface
+Do(ctx) : Result
<<interface>>
watch_Interface
Stop()
ResultChan() : chan Event
StreamWatcher
-source Decoder
-result chan Event
-done chan struct
+receive()
Event
+Type EventType
+Object runtime.Object
RESTClient
14. 调试断点清单(按执行阶段)
对照 [§7.9 执行阶段时序图](#§7.9 执行阶段时序图) 逐步打断点。
14.1 Builder 阶段(无网络)
| 顺序 | 文件:行 | 观察什么 | 预期 |
|---|---|---|---|
| 1 | pod.go:110 |
opts.Watch = true |
就地修改 ListOptions |
| 2 | request.go:383-387 |
VersionedParams 后 r.params |
含 watch: [true] |
| 3 | request.go:492-523 |
URL().String() |
.../pods?watch=true,path 含 namespace |
14.2 建连阶段(第一次网络 IO)
| 顺序 | 文件:行 | 观察什么 | 预期 |
|---|---|---|---|
| 4 | request.go:704 |
确认未 调用 tryThrottle |
与 request():989 对比 |
| 5 | request.go:726-728 |
retry.Before 首次 |
retryAfter == nil,仅 backoff |
| 6 | request.go:730-735 |
newHTTPRequest + client.Do |
req.Method=GET,Header 可能仍无 Bearer |
| 7 | RoundTripper 链 | bearerAuthRoundTripper.RoundTrip |
此处才出现 Authorization |
| 8 | request.go:737-738 |
200 分支 | 进入 newStreamWatcher,不 readAndClose body |
14.3 解码与消费阶段(长连接 + 第二 goroutine)
| 顺序 | 文件:行 | 观察什么 | 预期 |
|---|---|---|---|
| 9 | request.go:777 |
StreamDecoder 选的 mediaType |
含 stream=watch |
| 10 | request.go:787 |
NewStreamWatcher |
立即 spawn receive goroutine |
| 11 | apimachinery streamwatcher.go:105 |
source.Decode() |
阻塞直到有帧或 EOF |
| 12 | rest/watch/decoder.go:47 |
双层解码 | 先 WatchEvent,再 *Pod |
| 13 | streamwatcher.go:130-133 |
写入 result chan | 与 main goroutine 并发 |
| 14 | 你的 main for range |
event.Type / event.Object |
增删 Pod 触发 Added/Deleted |
14.4 退出阶段
| 顺序 | 文件:行 | 观察什么 | 预期 |
|---|---|---|---|
| 15 | streamwatcher.go:86-96 |
Stop() |
close(done) → source.Close() |
| 16 | streamwatcher.go:102-103 |
receive defer | close(result) → for range 结束 |
跟踪技巧 :另开终端 kubectl run test --image=nginx 或删 Pod;对比 List 时在 request.go:1023 断点与 Watch 在 735 断点的 resp.Body 处理方式差异。
读前准备(并排打开):
kubernetes/typed/core/v1/pod.go:88-116--- List 与 Watch 对照rest/request.go:703-793--- Watch 完整路径rest/request.go:965-1072---request()/Do()对照rest/with_retry.go:195-236--- 重试与限流时机rest/watch/decoder.go--- 双层解码- apimachinery
pkg/watch/streamwatcher.go--- receive 循环
15. 最小 Watch 示例
可自行创建 examples/watch-pods/main.go:
go
package main
import (
"context"
"fmt"
"path/filepath"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/util/homedir"
)
func main() {
kubeconfig := filepath.Join(homedir.HomeDir(), ".kube", "config")
config, _ := clientcmd.BuildConfigFromFlags("", kubeconfig)
clientset, _ := kubernetes.NewForConfig(config)
watcher, err := clientset.CoreV1().Pods("default").Watch(
context.TODO(), metav1.ListOptions{})
if err != nil {
panic(err)
}
defer watcher.Stop()
for event := range watcher.ResultChan() {
fmt.Printf("type=%s name=%s\n",
event.Type,
event.Object.(*v1.Pod).Name)
}
}
在另一个终端 kubectl run 或删 Pod,观察 Added / Deleted 事件。
16. 事件类型速查
| event.Type | 含义 | Object 内容 |
|---|---|---|
Added |
新建 | 新对象完整状态 |
Modified |
更新 | 更新后完整状态 |
Deleted |
删除 | 删除前最后一刻的状态 |
Bookmark |
书签 | 只有 resourceVersion(续传用) |
Error |
错误 | 通常是 *metav1.Status |
17. 面试题
完整题集 (含考察点、追问、源码锚点):client-go面试题集-调用链四种客户端与Watch.md 模块 C(C-01~C-11)及模块 D 综合题。
17.1 初级
Q1:Watch 和 List 在 HTTP 层有什么区别?
A:都是 GET;Watch 多 query 参数 watch=true,响应是长连接流而非一次性 JSON。
Q2:client-go Watch Pod 返回什么类型?
A:watch.Interface,通过 ResultChan() 读 <-chan watch.Event。
Q3:事件类型有哪些?
A:Added、Modified、Deleted、Bookmark、Error。
17.2 中级
Q4:为什么 Watch 不走 rateLimiter?
A:Watch 是长连接,QPS 限流适用于短请求;对 Watch 限流会导致连接异常。
Q5:StreamWatcher 为什么用 goroutine + channel?
A:网络读是阻塞 IO,与业务消费解耦;用户通过 for range 异步收事件。
Q6:resourceVersion 有什么用?
A:etcd 逻辑时钟;Watch 从指定 RV 开始,用于断线续传、避免漏事件。
17.3 高级 / 源码级
Q7:pod.go 里 Watch 和 List 最关键的两处差异?
A:opts.Watch = true;结尾 .Watch(ctx) 而非 .Do().Into()。
Q8:裸 Watch 和 Informer 的本质区别?
A:裸 Watch 只有事件流;Informer = List + Watch + 本地 Store + 重连 + Delta 去重 + Handler 回调。
Q9:谁负责把 HTTP 流变成 channel 事件?
A:StreamWatcher.receive() goroutine。
17.4 架构级
Q10:为什么 controller 不轮询 List?
A:延迟高、apiserver 负载大、无法高效感知增量;Watch + 本地 cache 是标准模式。
18. 实战案例
| 场景 | 怎么用模块 3 知识 |
|---|---|
| Pod 生命周期监控 | Watch Pod → Added 告警新建、Deleted 清理 |
| 发布平台 | Watch Deployment → Modified 检测 rollout 状态 |
| CMDB 同步 | Watch 多种资源,推送到外部系统(生产用 Informer 更稳) |
| 故障排查 | 等价于 kubectl get pods -w,用代码自动化 |
| Operator 入门 | 理解「事件驱动」后再学 controller-runtime |
19. 本章小结
轻量结论(必背)
- Watch = List 的「持续版」:
opts.Watch = true+.Watch(ctx)而非.Do().Into() - 返回
watch.Interface,通过ResultChan()读事件 StreamWatcher后台 goroutine 读 HTTP 流,写入 channel- Informer 以后会把 List+Watch 包成「本地缓存 + 回调」,那是第二阶段
深入结论(理解设计)
- Watch 与 List 共用 Builder ,但 分叉为两条 REST 执行路径 :
Watch()≠Do()→request() - 首次建连 不
tryThrottle;重试建连才tryThrottleWithInfo(with_retry.go:229) - 成功时 保留
resp.Body给 framer;失败才readAndCloseResponseBody - 解码是 三层栈 :framer →
metav1.WatchEvent→*v1.Pod(Scheme 与 List 的Into同源) StreamWatcher无缓冲 channel +done实现背压与优雅退出- 错误分 两条路径 :建连失败 → Go
error;流中失败 →Event{Type: Error} - 裸 Watch 适合学习和小工具;生产 controller 应 List→RV→Watch 或 Informer
一句话概括
Watch 把 kube-apiserver 的资源变更,通过 HTTP 长连接 + 流式解码 + channel,变成 Go 里可消费的
watch.Event流------这是 Kubernetes 事件驱动架构(Controller / Operator / Informer)的基石。
20. 自测
基础
- Watch 和 List 在
pod.go里哪一行最关键的差异? kubectl get pods -w对应 client-go 的哪个方法?- Watch 在哪个阶段发网络?
深入
- 为什么 Watch 首次不走
tryThrottle,重试却可能走? Watch()和Do()成功时对resp.Body的处理有何不同?StreamWatcher的 goroutine 什么时候退出?- 建连错误与
Event{Type: Error}分别在什么阶段出现? - 裸 Watch 和 Informer 各解决什么问题?
参考答案
基础
opts.Watch = true和最后调用.Watch(ctx)而不是.Do().Into()。clientset.CoreV1().Pods(ns).Watch(ctx, opts)。Request.Watch()内部的client.Do(req)(建立长连接时)。
深入
- 长连接首次不应被 QPS 卡住(
request.go:704-705);重试时走retry.Before里的tryThrottleWithInfo(with_retry.go:229),避免风暴式重连打爆 apiserver。 Do()→request()→io.ReadAll(resp.Body)一次读完;Watch()200 时直接把 body 交给newStreamWatcher,由receive()持续读。- HTTP body EOF、解码不可恢复错误、或用户
Stop()关闭done后;receive()deferclose(result)。 - 建连/HTTP 4xx/5xx →
Watch()返回error;流已建立后解码失败 → channel 里Event{Type: Error}。 - 裸 Watch:最低层事件流;Informer:List+Watch+缓存+重连+去重+回调。
相关文档
| 文档 | 关系 |
|---|---|
| 调用链精读 | Config → Clientset → List 主线 |
| 第一阶段夯实基础指南 · 模块 3 | 提纲版 + 最小示例 |
| 四种客户端详解 | Clientset 选型 |
| 面试题集 | 模块 C 面试题(C-01~C-11) |
| Kubernetes API Concepts | 官方 Watch 概念 |
源码文件清单(模块 3)
| 文件 | 关注 |
|---|---|
kubernetes/typed/core/v1/pod.go:105 |
Watch 入口 |
rest/request.go:703 |
Request.Watch() 主循环(与 request():965 对照读) |
rest/request.go:965 |
request() --- List/Do 路径 |
rest/with_retry.go:195 |
重试 Before:首次 vs 重试的限流差异 |
rest/request.go:771 |
newStreamWatcher() |
rest/watch/decoder.go |
流解码 |
apimachinery pkg/watch/watch.go |
Interface、Event |
apimachinery pkg/watch/streamwatcher.go |
receive() 循环 |
tools/watch/retrywatcher.go |
断线重连(选读) |
下一步
| 阶段 | 做什么 |
|---|---|
| 现在 | 写 §15 的 Watch 小程序,增删 Pod 观察事件 |
| 模块 4 | 理解 event.Object.(*v1.Pod) 的类型从哪来(GVK/Scheme) |
| 第二阶段 | List + Watch → Reflector → SharedInformer → WorkQueue |