【client-go v0.36.1】(Reflector Part 3) Reflector 超深度分析 — watchList 流式初始化

Reflector 超深度分析 --- watchList 流式初始化

基于 client-go v0.36.1 reflector.go


十一、watchList() --- WatchList 流式初始化逐行解析

11.1 设计背景

传统 Informer 初始化流程是 List(分页GET)→ Replace Store → Watch(增量),存在两个问题:

  1. 分页不一致性:多次分页请求之间数据可能变更,导致快照不完全一致
  2. API Server 压力大:每个分页请求都走 etcd,大型集群可能数百次分页

WatchList(KEP-3157)通过 单次 Watch 请求 获取完整初始数据:

复制代码
WatchList Request:
  SendInitialEvents: true
  ResourceVersionMatch: NotOlderThan
  ResourceVersion: "" 或具体RV

API Server Response:
  ADDED event 1 → ADDED event 2 → ... → ADDED event N → Bookmark(initial-events-end)
  └─ 初始数据 ──────────────────────────────────────────┘  └─ 初始结束标记 ─┘
  → 继续正常 Watch 事件流 →

11.2 完整流程图

#mermaid-svg-E7nzvfto2jW206pp{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-E7nzvfto2jW206pp .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-E7nzvfto2jW206pp .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-E7nzvfto2jW206pp .error-icon{fill:#552222;}#mermaid-svg-E7nzvfto2jW206pp .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-E7nzvfto2jW206pp .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-E7nzvfto2jW206pp .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-E7nzvfto2jW206pp .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-E7nzvfto2jW206pp .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-E7nzvfto2jW206pp .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-E7nzvfto2jW206pp .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-E7nzvfto2jW206pp .marker{fill:#333333;stroke:#333333;}#mermaid-svg-E7nzvfto2jW206pp .marker.cross{stroke:#333333;}#mermaid-svg-E7nzvfto2jW206pp svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-E7nzvfto2jW206pp p{margin:0;}#mermaid-svg-E7nzvfto2jW206pp .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-E7nzvfto2jW206pp .cluster-label text{fill:#333;}#mermaid-svg-E7nzvfto2jW206pp .cluster-label span{color:#333;}#mermaid-svg-E7nzvfto2jW206pp .cluster-label span p{background-color:transparent;}#mermaid-svg-E7nzvfto2jW206pp .label text,#mermaid-svg-E7nzvfto2jW206pp span{fill:#333;color:#333;}#mermaid-svg-E7nzvfto2jW206pp .node rect,#mermaid-svg-E7nzvfto2jW206pp .node circle,#mermaid-svg-E7nzvfto2jW206pp .node ellipse,#mermaid-svg-E7nzvfto2jW206pp .node polygon,#mermaid-svg-E7nzvfto2jW206pp .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-E7nzvfto2jW206pp .rough-node .label text,#mermaid-svg-E7nzvfto2jW206pp .node .label text,#mermaid-svg-E7nzvfto2jW206pp .image-shape .label,#mermaid-svg-E7nzvfto2jW206pp .icon-shape .label{text-anchor:middle;}#mermaid-svg-E7nzvfto2jW206pp .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-E7nzvfto2jW206pp .rough-node .label,#mermaid-svg-E7nzvfto2jW206pp .node .label,#mermaid-svg-E7nzvfto2jW206pp .image-shape .label,#mermaid-svg-E7nzvfto2jW206pp .icon-shape .label{text-align:center;}#mermaid-svg-E7nzvfto2jW206pp .node.clickable{cursor:pointer;}#mermaid-svg-E7nzvfto2jW206pp .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-E7nzvfto2jW206pp .arrowheadPath{fill:#333333;}#mermaid-svg-E7nzvfto2jW206pp .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-E7nzvfto2jW206pp .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-E7nzvfto2jW206pp .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-E7nzvfto2jW206pp .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-E7nzvfto2jW206pp .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-E7nzvfto2jW206pp .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-E7nzvfto2jW206pp .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-E7nzvfto2jW206pp .cluster text{fill:#333;}#mermaid-svg-E7nzvfto2jW206pp .cluster span{color:#333;}#mermaid-svg-E7nzvfto2jW206pp 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-E7nzvfto2jW206pp .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-E7nzvfto2jW206pp rect.text{fill:none;stroke-width:0;}#mermaid-svg-E7nzvfto2jW206pp .icon-shape,#mermaid-svg-E7nzvfto2jW206pp .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-E7nzvfto2jW206pp .icon-shape p,#mermaid-svg-E7nzvfto2jW206pp .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-E7nzvfto2jW206pp .icon-shape .label rect,#mermaid-svg-E7nzvfto2jW206pp .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-E7nzvfto2jW206pp .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-E7nzvfto2jW206pp .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-E7nzvfto2jW206pp :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Yes
No
Yes
Yes
No
No
Yes
Yes
No
Yes
No
No
Yes
No
watchList(ctx)
定义 isErrorRetriableWithSideEffectsFn

(可重试错误判断+副作用)
获取 TransformFunc

(从 TransformingStore)
创建 Trace

(Reflector WatchList)
WatchList 重试循环
ctx.Done()?
重置状态:

resourceVersion=''

temporaryStore = NewStore()
构造 WatchOptions:

ResourceVersion = rewatchResourceVersion()

SendInitialEvents = true

ResourceVersionMatch = NotOlderThan

AllowWatchBookmarks = true

TimeoutSeconds = randommin,max
listerWatcher.WatchWithContext(ctx, options)
Watch 请求错误?
isErrorRetriableWithSideEffectsFn(err)?
等待 delayHandler() → continue
返回 nil, err
handleListWatch(ctx, start, w, temporaryStore, ...)
handleListWatch 错误?
errorStopRequested?
isErrorRetriableWithSideEffectsFn(err)?
w.Stop()
continue (重试)
返回 nil, err
watchListBookmark

Received?
跳出循环
checkWatchListDataConsistencyIfRequested

(CI 数据一致性校验)
setIsLastSyncResourceVersionUnavailable(false)
store.Replace(temporaryStore.List(), rv)
setLastSyncResourceVersion(rv)
返回 w, nil

(复用 Watch 流)
返回 nil, nil (ctx 取消)
返回 nil, nil

11.3 逐行解析

go 复制代码
func (r *Reflector) watchList(ctx context.Context) (watch.Interface, error) {
    stopCh := ctx.Done()
    logger := klog.FromContext(ctx)
    var w watch.Interface
    var err error
    var temporaryStore Store              // 临时 Store(收集初始数据)
    var resourceVersion string            // 最终 RV

    // ─── 定义可重试错误判断函数(带副作用) ───
    // 此函数在判断错误是否可重试的同时,会执行副作用(如设置 RV 状态)
    isErrorRetriableWithSideEffectsFn := func(err error) bool {
        // 副作用1:连接拒绝 / 429 → 退避重试
        if canRetry := isWatchErrorRetriable(err); canRetry {
            logger.V(2).Info("watch-list failed - backing off",
                "reflector", r.name, "type", r.typeDescription, "err", err)
            <-r.clock.After(r.delayHandler())  // 阻塞等待退避时间
            return true
        }
        // 副作用2:RV 过期/过大 → 标记不可用 + 退避重试
        // 下次循环中 rewatchResourceVersion() 会返回 ""
        if isExpiredError(err) || isTooLargeResourceVersionError(err) {
            r.setIsLastSyncResourceVersionUnavailable(true)
            // 不等待退避,立即重试(降级到 RV="" 应该能成功)
            return true
        }
        return false
    }

    // ─── 获取 TransformFunc(保持与主 Store 一致) ───
    var transformer TransformFunc
    storeOpts := []StoreOption{}
    if tr, ok := r.store.(TransformingStore); ok && tr.Transformer() != nil {
        transformer = tr.Transformer()
        storeOpts = append(storeOpts, WithTransformer(transformer))
        // 为什么需要一致?因为 temporaryStore 收集的数据
        // 最终通过 store.Replace() 写入主 Store
        // 如果 TransformFunc 不同,Replace 后数据格式不一致
    }

    // ─── 创建 Trace ───
    initTrace := trace.New("Reflector WatchList",
        trace.Field{Key: "name", Value: r.name})
    defer initTrace.LogIfLong(10 * time.Second)

    // ─── WatchList 重试循环 ───
    for {
        select {
        case <-stopCh:
            return nil, nil  // ctx 取消 → 静默退出
        default:
        }

        // ─── 重置每次重试的状态 ───
        resourceVersion = ""                    // 每次重试从空 RV 开始
        lastKnownRV := r.rewatchResourceVersion() // 获取当前 RV
        temporaryStore = NewStore(DeletionHandlingMetaNamespaceKeyFunc, storeOpts...)
        // DeletionHandlingMetaNamespaceKeyFunc:
        //   处理 DeletedFinalStateUnknown 墓碑对象的 key 提取

        // ─── 计算 Watch 超时 ───
        // 随机化在 [minWatchTimeout, maxWatchTimeout] 之间
        timeoutSeconds := int64(r.minWatchTimeout.Seconds() +
            rand.Float64()*(r.maxWatchTimeout.Seconds()-r.minWatchTimeout.Seconds()))

        // ─── 构造 WatchList 专用选项 ───
        options := metav1.ListOptions{
            ResourceVersion:      lastKnownRV,
            AllowWatchBookmarks:  true,           // 必须:接收 Bookmark 事件
            SendInitialEvents:    ptr.To(true),   // 关键:请求初始事件流
            ResourceVersionMatch: metav1.ResourceVersionMatchNotOlderThan,
            // NotOlderThan 语义:返回的数据至少和 ResourceVersion 一样新
            // 如果 RV="",返回最新的一致性快照
            TimeoutSeconds:       &timeoutSeconds,
        }
        start := r.clock.Now()

        // ─── 发起 Watch 请求 ───
        w, err = r.listerWatcher.WatchWithContext(ctx, options)
        if err != nil {
            if isErrorRetriableWithSideEffectsFn(err) {
                continue  // 可重试 → 重新循环
            }
            return nil, err  // 不可重试 → 返回错误
        }

        // ─── 消费初始事件流 ───
        // handleListWatch 消费事件直到收到初始结束 Bookmark
        // 使用 temporaryStore 而非主 Store,避免中途失败污染主 Store
        watchListBookmarkReceived, err := handleListWatch(
            ctx, start, w, temporaryStore,
            r.expectedType, r.expectedGVK,
            r.name, r.typeDescription,
            func(rv string, eventReceivedBesidesAdded bool) {
                // ─── setLastSyncResourceVersion 回调 ───
                // 只有收到非 Added 事件(Modified/Deleted/Bookmark)时才更新 RV
                // 原因:Added 事件在初始流中是无序的(非 RV 排序)
                // 只有过了初始流(收到 Bookmark 或其他类型事件),
                // RV 才是可靠的
                if eventReceivedBesidesAdded {
                    resourceVersion = rv
                }
            },
            r.clock, make(chan error))

        if err != nil {
            w.Stop()  // 出错 → 停止当前 Watch
            if errors.Is(err, errorStopRequested) {
                return nil, nil  // ctx 取消
            }
            if isErrorRetriableWithSideEffectsFn(err) {
                continue  // 可重试
            }
            return nil, err  // 不可重试
        }

        // ─── Bookmark 检查 ───
        if watchListBookmarkReceived {
            break  // 收到初始结束标记 → 退出重试循环
        }
        // 未收到 Bookmark → 继续重试
        // 这可能是因为 Watch 超时但未出错
    }

    // ─── 初始数据收集完成 ───
    initTrace.Step("Objects streamed",
        trace.Field{Key: "count", Value: len(temporaryStore.List())})
    r.setIsLastSyncResourceVersionUnavailable(false)

    // ─── 数据一致性校验(仅 CI) ───
    // 比对 WatchList 结果与标准 List 结果是否一致
    // 如果不一致 → panic(CI 中必须捕获)
    checkWatchListDataConsistencyIfRequested(ctx, r.name, resourceVersion,
        r.listerWatcher.ListWithContext, transformer, temporaryStore.List)

    // ─── 写入主 Store ───
    // Replace 是原子操作:清空旧数据 + 写入新数据
    if err := r.store.Replace(temporaryStore.List(), resourceVersion); err != nil {
        return nil, fmt.Errorf("unable to sync watch-list result: %w", err)
    }
    initTrace.Step("SyncWith done")

    // ─── 更新 RV ───
    r.setLastSyncResourceVersion(resourceVersion)

    // ─── 返回 Watch 流(可复用) ───
    // 关键:WatchList 的流在初始事件之后继续推送增量事件
    // 返回 w 让调用方继续消费
    return w, nil
}

11.4 WatchList 事件流时序

Main Store temporaryStore API Server Reflector Main Store temporaryStore API Server Reflector #mermaid-svg-xpIPzioXnCdlXQqm{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-xpIPzioXnCdlXQqm .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-xpIPzioXnCdlXQqm .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-xpIPzioXnCdlXQqm .error-icon{fill:#552222;}#mermaid-svg-xpIPzioXnCdlXQqm .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-xpIPzioXnCdlXQqm .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-xpIPzioXnCdlXQqm .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-xpIPzioXnCdlXQqm .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-xpIPzioXnCdlXQqm .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-xpIPzioXnCdlXQqm .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-xpIPzioXnCdlXQqm .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-xpIPzioXnCdlXQqm .marker{fill:#333333;stroke:#333333;}#mermaid-svg-xpIPzioXnCdlXQqm .marker.cross{stroke:#333333;}#mermaid-svg-xpIPzioXnCdlXQqm svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-xpIPzioXnCdlXQqm p{margin:0;}#mermaid-svg-xpIPzioXnCdlXQqm .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-xpIPzioXnCdlXQqm text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-xpIPzioXnCdlXQqm .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-xpIPzioXnCdlXQqm .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-xpIPzioXnCdlXQqm .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-xpIPzioXnCdlXQqm .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-xpIPzioXnCdlXQqm #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-xpIPzioXnCdlXQqm .sequenceNumber{fill:white;}#mermaid-svg-xpIPzioXnCdlXQqm #sequencenumber{fill:#333;}#mermaid-svg-xpIPzioXnCdlXQqm #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-xpIPzioXnCdlXQqm .messageText{fill:#333;stroke:none;}#mermaid-svg-xpIPzioXnCdlXQqm .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-xpIPzioXnCdlXQqm .labelText,#mermaid-svg-xpIPzioXnCdlXQqm .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-xpIPzioXnCdlXQqm .loopText,#mermaid-svg-xpIPzioXnCdlXQqm .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-xpIPzioXnCdlXQqm .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-xpIPzioXnCdlXQqm .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-xpIPzioXnCdlXQqm .noteText,#mermaid-svg-xpIPzioXnCdlXQqm .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-xpIPzioXnCdlXQqm .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-xpIPzioXnCdlXQqm .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-xpIPzioXnCdlXQqm .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-xpIPzioXnCdlXQqm .actorPopupMenu{position:absolute;}#mermaid-svg-xpIPzioXnCdlXQqm .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-xpIPzioXnCdlXQqm .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-xpIPzioXnCdlXQqm .actor-man circle,#mermaid-svg-xpIPzioXnCdlXQqm line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-xpIPzioXnCdlXQqm :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 开始发送初始事件 初始事件结束 watchListBookmarkReceived = true 继续使用同一 Watch 流 Watch(SendInitialEvents=true, RV="")ADDED Pod/AAdd(Pod/A)ADDED Pod/BAdd(Pod/B)ADDED Pod/CAdd(Pod/C)Bookmark (RV=1000, initial-events-end=true)Replace(Pod/A, Pod/B, Pod/C, "1000")setLastSyncResourceVersion("1000")MODIFIED Pod/A (RV=1001)Update(Pod/A)ADDED Pod/D (RV=1002)Add(Pod/D)DELETED Pod/B (RV=1003)Delete(Pod/B)

11.5 WatchList 的两种启动模式

#mermaid-svg-cciPdlnFMIaB8tb2{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-cciPdlnFMIaB8tb2 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-cciPdlnFMIaB8tb2 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-cciPdlnFMIaB8tb2 .error-icon{fill:#552222;}#mermaid-svg-cciPdlnFMIaB8tb2 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-cciPdlnFMIaB8tb2 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-cciPdlnFMIaB8tb2 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-cciPdlnFMIaB8tb2 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-cciPdlnFMIaB8tb2 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-cciPdlnFMIaB8tb2 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-cciPdlnFMIaB8tb2 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-cciPdlnFMIaB8tb2 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-cciPdlnFMIaB8tb2 .marker.cross{stroke:#333333;}#mermaid-svg-cciPdlnFMIaB8tb2 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-cciPdlnFMIaB8tb2 p{margin:0;}#mermaid-svg-cciPdlnFMIaB8tb2 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-cciPdlnFMIaB8tb2 .cluster-label text{fill:#333;}#mermaid-svg-cciPdlnFMIaB8tb2 .cluster-label span{color:#333;}#mermaid-svg-cciPdlnFMIaB8tb2 .cluster-label span p{background-color:transparent;}#mermaid-svg-cciPdlnFMIaB8tb2 .label text,#mermaid-svg-cciPdlnFMIaB8tb2 span{fill:#333;color:#333;}#mermaid-svg-cciPdlnFMIaB8tb2 .node rect,#mermaid-svg-cciPdlnFMIaB8tb2 .node circle,#mermaid-svg-cciPdlnFMIaB8tb2 .node ellipse,#mermaid-svg-cciPdlnFMIaB8tb2 .node polygon,#mermaid-svg-cciPdlnFMIaB8tb2 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-cciPdlnFMIaB8tb2 .rough-node .label text,#mermaid-svg-cciPdlnFMIaB8tb2 .node .label text,#mermaid-svg-cciPdlnFMIaB8tb2 .image-shape .label,#mermaid-svg-cciPdlnFMIaB8tb2 .icon-shape .label{text-anchor:middle;}#mermaid-svg-cciPdlnFMIaB8tb2 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-cciPdlnFMIaB8tb2 .rough-node .label,#mermaid-svg-cciPdlnFMIaB8tb2 .node .label,#mermaid-svg-cciPdlnFMIaB8tb2 .image-shape .label,#mermaid-svg-cciPdlnFMIaB8tb2 .icon-shape .label{text-align:center;}#mermaid-svg-cciPdlnFMIaB8tb2 .node.clickable{cursor:pointer;}#mermaid-svg-cciPdlnFMIaB8tb2 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-cciPdlnFMIaB8tb2 .arrowheadPath{fill:#333333;}#mermaid-svg-cciPdlnFMIaB8tb2 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-cciPdlnFMIaB8tb2 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-cciPdlnFMIaB8tb2 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-cciPdlnFMIaB8tb2 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-cciPdlnFMIaB8tb2 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-cciPdlnFMIaB8tb2 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-cciPdlnFMIaB8tb2 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-cciPdlnFMIaB8tb2 .cluster text{fill:#333;}#mermaid-svg-cciPdlnFMIaB8tb2 .cluster span{color:#333;}#mermaid-svg-cciPdlnFMIaB8tb2 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-cciPdlnFMIaB8tb2 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-cciPdlnFMIaB8tb2 rect.text{fill:none;stroke-width:0;}#mermaid-svg-cciPdlnFMIaB8tb2 .icon-shape,#mermaid-svg-cciPdlnFMIaB8tb2 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-cciPdlnFMIaB8tb2 .icon-shape p,#mermaid-svg-cciPdlnFMIaB8tb2 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-cciPdlnFMIaB8tb2 .icon-shape .label rect,#mermaid-svg-cciPdlnFMIaB8tb2 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-cciPdlnFMIaB8tb2 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-cciPdlnFMIaB8tb2 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-cciPdlnFMIaB8tb2 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Case 2: Start at Exact (RV>'0')
WatchList(RV=500, SendInitialEvents=true)
ADDED objects at RV≥500
Bookmark(RV≥500, initial-events-end)
从指定 RV 继续监听

(断点续传)
Case 1: Start at Most Recent (RV='')
WatchList(RV='', SendInitialEvents=true)
ADDED all existing objects
Bookmark(RV=latest, initial-events-end)
等价于一致性快照

(如同 etcd quorum read)

11.6 temporaryStore 的设计意图

为什么用临时 Store 而不是直接写入主 Store?

  1. 原子性:WatchList 可能在中途失败(超时、网络断开),直接写主 Store 会导致半完成状态
  2. 可重试:临时 Store 失败后直接丢弃重来,主 Store 不受影响
  3. 隔离性:初始事件的 Add 操作在临时 Store 上执行,不影响 Controller 消费主 Store 的 DeltaFIFO

#mermaid-svg-4d4E6GrNsxebbGag{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-4d4E6GrNsxebbGag .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-4d4E6GrNsxebbGag .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-4d4E6GrNsxebbGag .error-icon{fill:#552222;}#mermaid-svg-4d4E6GrNsxebbGag .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-4d4E6GrNsxebbGag .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-4d4E6GrNsxebbGag .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-4d4E6GrNsxebbGag .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-4d4E6GrNsxebbGag .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-4d4E6GrNsxebbGag .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-4d4E6GrNsxebbGag .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-4d4E6GrNsxebbGag .marker{fill:#333333;stroke:#333333;}#mermaid-svg-4d4E6GrNsxebbGag .marker.cross{stroke:#333333;}#mermaid-svg-4d4E6GrNsxebbGag svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-4d4E6GrNsxebbGag p{margin:0;}#mermaid-svg-4d4E6GrNsxebbGag .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-4d4E6GrNsxebbGag .cluster-label text{fill:#333;}#mermaid-svg-4d4E6GrNsxebbGag .cluster-label span{color:#333;}#mermaid-svg-4d4E6GrNsxebbGag .cluster-label span p{background-color:transparent;}#mermaid-svg-4d4E6GrNsxebbGag .label text,#mermaid-svg-4d4E6GrNsxebbGag span{fill:#333;color:#333;}#mermaid-svg-4d4E6GrNsxebbGag .node rect,#mermaid-svg-4d4E6GrNsxebbGag .node circle,#mermaid-svg-4d4E6GrNsxebbGag .node ellipse,#mermaid-svg-4d4E6GrNsxebbGag .node polygon,#mermaid-svg-4d4E6GrNsxebbGag .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-4d4E6GrNsxebbGag .rough-node .label text,#mermaid-svg-4d4E6GrNsxebbGag .node .label text,#mermaid-svg-4d4E6GrNsxebbGag .image-shape .label,#mermaid-svg-4d4E6GrNsxebbGag .icon-shape .label{text-anchor:middle;}#mermaid-svg-4d4E6GrNsxebbGag .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-4d4E6GrNsxebbGag .rough-node .label,#mermaid-svg-4d4E6GrNsxebbGag .node .label,#mermaid-svg-4d4E6GrNsxebbGag .image-shape .label,#mermaid-svg-4d4E6GrNsxebbGag .icon-shape .label{text-align:center;}#mermaid-svg-4d4E6GrNsxebbGag .node.clickable{cursor:pointer;}#mermaid-svg-4d4E6GrNsxebbGag .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-4d4E6GrNsxebbGag .arrowheadPath{fill:#333333;}#mermaid-svg-4d4E6GrNsxebbGag .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-4d4E6GrNsxebbGag .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-4d4E6GrNsxebbGag .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4d4E6GrNsxebbGag .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-4d4E6GrNsxebbGag .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4d4E6GrNsxebbGag .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-4d4E6GrNsxebbGag .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-4d4E6GrNsxebbGag .cluster text{fill:#333;}#mermaid-svg-4d4E6GrNsxebbGag .cluster span{color:#333;}#mermaid-svg-4d4E6GrNsxebbGag 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-4d4E6GrNsxebbGag .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-4d4E6GrNsxebbGag rect.text{fill:none;stroke-width:0;}#mermaid-svg-4d4E6GrNsxebbGag .icon-shape,#mermaid-svg-4d4E6GrNsxebbGag .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4d4E6GrNsxebbGag .icon-shape p,#mermaid-svg-4d4E6GrNsxebbGag .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-4d4E6GrNsxebbGag .icon-shape .label rect,#mermaid-svg-4d4E6GrNsxebbGag .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4d4E6GrNsxebbGag .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-4d4E6GrNsxebbGag .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-4d4E6GrNsxebbGag :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 成功场景(用临时 Store)
WatchList 成功

(收到 Bookmark)
临时 Store 有完整数据
store.Replace() → 原子替换
主 Store 数据一致
失败场景(不用临时 Store)
WatchList 失败

(超时)
主 Store 中有部分数据
数据不一致!

需要清空重试

11.7 isErrorRetriableWithSideEffectsFn --- 带副作用的错误判断

#mermaid-svg-SGibtVPbWi2GUpiP{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-SGibtVPbWi2GUpiP .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-SGibtVPbWi2GUpiP .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-SGibtVPbWi2GUpiP .error-icon{fill:#552222;}#mermaid-svg-SGibtVPbWi2GUpiP .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-SGibtVPbWi2GUpiP .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-SGibtVPbWi2GUpiP .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-SGibtVPbWi2GUpiP .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-SGibtVPbWi2GUpiP .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-SGibtVPbWi2GUpiP .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-SGibtVPbWi2GUpiP .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-SGibtVPbWi2GUpiP .marker{fill:#333333;stroke:#333333;}#mermaid-svg-SGibtVPbWi2GUpiP .marker.cross{stroke:#333333;}#mermaid-svg-SGibtVPbWi2GUpiP svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-SGibtVPbWi2GUpiP p{margin:0;}#mermaid-svg-SGibtVPbWi2GUpiP .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-SGibtVPbWi2GUpiP .cluster-label text{fill:#333;}#mermaid-svg-SGibtVPbWi2GUpiP .cluster-label span{color:#333;}#mermaid-svg-SGibtVPbWi2GUpiP .cluster-label span p{background-color:transparent;}#mermaid-svg-SGibtVPbWi2GUpiP .label text,#mermaid-svg-SGibtVPbWi2GUpiP span{fill:#333;color:#333;}#mermaid-svg-SGibtVPbWi2GUpiP .node rect,#mermaid-svg-SGibtVPbWi2GUpiP .node circle,#mermaid-svg-SGibtVPbWi2GUpiP .node ellipse,#mermaid-svg-SGibtVPbWi2GUpiP .node polygon,#mermaid-svg-SGibtVPbWi2GUpiP .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-SGibtVPbWi2GUpiP .rough-node .label text,#mermaid-svg-SGibtVPbWi2GUpiP .node .label text,#mermaid-svg-SGibtVPbWi2GUpiP .image-shape .label,#mermaid-svg-SGibtVPbWi2GUpiP .icon-shape .label{text-anchor:middle;}#mermaid-svg-SGibtVPbWi2GUpiP .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-SGibtVPbWi2GUpiP .rough-node .label,#mermaid-svg-SGibtVPbWi2GUpiP .node .label,#mermaid-svg-SGibtVPbWi2GUpiP .image-shape .label,#mermaid-svg-SGibtVPbWi2GUpiP .icon-shape .label{text-align:center;}#mermaid-svg-SGibtVPbWi2GUpiP .node.clickable{cursor:pointer;}#mermaid-svg-SGibtVPbWi2GUpiP .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-SGibtVPbWi2GUpiP .arrowheadPath{fill:#333333;}#mermaid-svg-SGibtVPbWi2GUpiP .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-SGibtVPbWi2GUpiP .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-SGibtVPbWi2GUpiP .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SGibtVPbWi2GUpiP .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-SGibtVPbWi2GUpiP .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SGibtVPbWi2GUpiP .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-SGibtVPbWi2GUpiP .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-SGibtVPbWi2GUpiP .cluster text{fill:#333;}#mermaid-svg-SGibtVPbWi2GUpiP .cluster span{color:#333;}#mermaid-svg-SGibtVPbWi2GUpiP 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-SGibtVPbWi2GUpiP .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-SGibtVPbWi2GUpiP rect.text{fill:none;stroke-width:0;}#mermaid-svg-SGibtVPbWi2GUpiP .icon-shape,#mermaid-svg-SGibtVPbWi2GUpiP .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SGibtVPbWi2GUpiP .icon-shape p,#mermaid-svg-SGibtVPbWi2GUpiP .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-SGibtVPbWi2GUpiP .icon-shape .label rect,#mermaid-svg-SGibtVPbWi2GUpiP .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SGibtVPbWi2GUpiP .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-SGibtVPbWi2GUpiP .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-SGibtVPbWi2GUpiP :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Yes
No
Yes
No
Yes
No
错误 err
isWatchErrorRetriable(err)?

(ConnectionRefused / 429)
等待 delayHandler()

(退避后重试)
isExpiredError(err)?

(410 Gone)
isTooLargeResourceVersionError(err)?

(RV 过大)
setIsLastSyncResourceVersion

Unavailable(true)
立即重试

(不退避,降级 RV)
不可重试

返回错误
return true

设计意图

  • ConnectionRefused / 429:API Server 暂时不可用或限流 → 退避后重试
  • Expired / TooLargeRV:使用的 RV 已过期 → 降级到 RV=""(最新数据)立即重试
  • 其他错误:不可恢复 → 返回错误,由上层决定

11.8 checkWatchListDataConsistencyIfRequested --- CI 一致性校验

go 复制代码
func checkWatchListDataConsistencyIfRequested[T runtime.Object, U any](
    ctx context.Context,
    identity string,
    lastSyncedResourceVersion string,
    listFn consistencydetector.ListFunc[T],
    listItemTransformFunc func(interface{}) (interface{}, error),
    retrieveItemsFn consistencydetector.RetrieveItemsFunc[U],
) {
    // 仅在 KUBE_WATCHLIST_INCONSISTENCY_DETECTOR 环境变量设置时执行
    if !consistencydetector.IsDataConsistencyDetectionForWatchListEnabled() {
        return
    }
    // 用标准 List 请求获取同一 RV 的数据,与 WatchList 结果逐项对比
    // 如果不一致 → panic
    consistencydetector.CheckDataConsistency(ctx, identity,
        lastSyncedResourceVersion, listFn, listItemTransformFunc,
        metav1.ListOptions{}, retrieveItemsFn)
}

设计意图 :WatchList 是新功能,需要验证其数据一致性。在 CI 中启用此检测,如果 WatchList 返回的数据与标准 List 不同,说明 WatchList 实现有 bug → panic 让测试失败。生产环境不启用


11.9 WatchList 与传统 List 的完整对比

维度 传统 List WatchList
初始化方式 GET 请求(可能分页)→ Replace Watch Stream → 逐个 Add → Bookmark
API 请求次数 N 次(每页一次)+ 1 次 Watch 1 次 Watch(复用流)
一致性 分页间可能不一致 流式保证一致性(etcd revision)
API Server 内存 每页需完整序列化 流式逐条发送,内存占用低
RV 来源 List Response 的 RV Bookmark 事件的 RV
Watch 复用 不复用,需重新建立 复用 WatchList 的流
失败恢复 重新 List 重新 WatchList(用临时 Store 隔离)
Feature Gate 默认 WatchListClient (v0.36 ALPHA)
最低 API Server 版本 所有版本 1.27+ (SendInitialEvents)

11.10 WatchList 请求参数详解

go 复制代码
options := metav1.ListOptions{
    ResourceVersion:      lastKnownRV,
    // 决定起始位置:
    //   "" → 最新一致性快照
    //   "500" → 从 RV=500 继续监听
    AllowWatchBookmarks:  true,
    // 必须为 true,Bookmark 是初始结束标记的唯一信号
    SendInitialEvents:    ptr.To(true),
    // 核心参数:告诉 API Server 先发送初始事件流
    // 初始流 = ADDED events + Bookmark(initial-events-end)
    ResourceVersionMatch: metav1.ResourceVersionMatchNotOlderThan,
    // 配合 ResourceVersion 使用:
    //   NotOlderThan = "返回数据至少和 RV 一样新"
    //   如果 RV="",等价于"返回最新数据"
    TimeoutSeconds:       &timeoutSeconds,
    // 随机超时,防惊群
}

SendInitialEvents + ResourceVersionMatch 的组合语义
#mermaid-svg-58A7aOiqvfH25IzE{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-58A7aOiqvfH25IzE .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-58A7aOiqvfH25IzE .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-58A7aOiqvfH25IzE .error-icon{fill:#552222;}#mermaid-svg-58A7aOiqvfH25IzE .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-58A7aOiqvfH25IzE .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-58A7aOiqvfH25IzE .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-58A7aOiqvfH25IzE .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-58A7aOiqvfH25IzE .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-58A7aOiqvfH25IzE .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-58A7aOiqvfH25IzE .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-58A7aOiqvfH25IzE .marker{fill:#333333;stroke:#333333;}#mermaid-svg-58A7aOiqvfH25IzE .marker.cross{stroke:#333333;}#mermaid-svg-58A7aOiqvfH25IzE svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-58A7aOiqvfH25IzE p{margin:0;}#mermaid-svg-58A7aOiqvfH25IzE .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-58A7aOiqvfH25IzE .cluster-label text{fill:#333;}#mermaid-svg-58A7aOiqvfH25IzE .cluster-label span{color:#333;}#mermaid-svg-58A7aOiqvfH25IzE .cluster-label span p{background-color:transparent;}#mermaid-svg-58A7aOiqvfH25IzE .label text,#mermaid-svg-58A7aOiqvfH25IzE span{fill:#333;color:#333;}#mermaid-svg-58A7aOiqvfH25IzE .node rect,#mermaid-svg-58A7aOiqvfH25IzE .node circle,#mermaid-svg-58A7aOiqvfH25IzE .node ellipse,#mermaid-svg-58A7aOiqvfH25IzE .node polygon,#mermaid-svg-58A7aOiqvfH25IzE .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-58A7aOiqvfH25IzE .rough-node .label text,#mermaid-svg-58A7aOiqvfH25IzE .node .label text,#mermaid-svg-58A7aOiqvfH25IzE .image-shape .label,#mermaid-svg-58A7aOiqvfH25IzE .icon-shape .label{text-anchor:middle;}#mermaid-svg-58A7aOiqvfH25IzE .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-58A7aOiqvfH25IzE .rough-node .label,#mermaid-svg-58A7aOiqvfH25IzE .node .label,#mermaid-svg-58A7aOiqvfH25IzE .image-shape .label,#mermaid-svg-58A7aOiqvfH25IzE .icon-shape .label{text-align:center;}#mermaid-svg-58A7aOiqvfH25IzE .node.clickable{cursor:pointer;}#mermaid-svg-58A7aOiqvfH25IzE .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-58A7aOiqvfH25IzE .arrowheadPath{fill:#333333;}#mermaid-svg-58A7aOiqvfH25IzE .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-58A7aOiqvfH25IzE .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-58A7aOiqvfH25IzE .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-58A7aOiqvfH25IzE .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-58A7aOiqvfH25IzE .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-58A7aOiqvfH25IzE .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-58A7aOiqvfH25IzE .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-58A7aOiqvfH25IzE .cluster text{fill:#333;}#mermaid-svg-58A7aOiqvfH25IzE .cluster span{color:#333;}#mermaid-svg-58A7aOiqvfH25IzE 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-58A7aOiqvfH25IzE .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-58A7aOiqvfH25IzE rect.text{fill:none;stroke-width:0;}#mermaid-svg-58A7aOiqvfH25IzE .icon-shape,#mermaid-svg-58A7aOiqvfH25IzE .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-58A7aOiqvfH25IzE .icon-shape p,#mermaid-svg-58A7aOiqvfH25IzE .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-58A7aOiqvfH25IzE .icon-shape .label rect,#mermaid-svg-58A7aOiqvfH25IzE .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-58A7aOiqvfH25IzE .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-58A7aOiqvfH25IzE .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-58A7aOiqvfH25IzE :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} RV>0 场景
RV='' 场景
SendInitialEvents = true
ResourceVersionMatch = NotOlderThan
RV = ''
RV > '0'
API Server 确定一致性点
发送该点所有对象的 ADDED 事件
发送 Bookmark(RV=一致性点RV, initial-events-end)
继续正常 Watch 事件
API Server 从指定 RV 开始
发送 RV 之后变更的 ADDED 事件
发送 Bookmark(RV≥指定RV, initial-events-end)
继续正常 Watch 事件

11.11 WatchList 重试场景分析

场景1:首次 WatchList 成功

Main Store temporaryStore API Server Reflector Main Store temporaryStore API Server Reflector #mermaid-svg-HbGG2O6w2bqjTpNx{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-HbGG2O6w2bqjTpNx .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-HbGG2O6w2bqjTpNx .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-HbGG2O6w2bqjTpNx .error-icon{fill:#552222;}#mermaid-svg-HbGG2O6w2bqjTpNx .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-HbGG2O6w2bqjTpNx .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-HbGG2O6w2bqjTpNx .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-HbGG2O6w2bqjTpNx .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-HbGG2O6w2bqjTpNx .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-HbGG2O6w2bqjTpNx .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-HbGG2O6w2bqjTpNx .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-HbGG2O6w2bqjTpNx .marker{fill:#333333;stroke:#333333;}#mermaid-svg-HbGG2O6w2bqjTpNx .marker.cross{stroke:#333333;}#mermaid-svg-HbGG2O6w2bqjTpNx svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-HbGG2O6w2bqjTpNx p{margin:0;}#mermaid-svg-HbGG2O6w2bqjTpNx .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-HbGG2O6w2bqjTpNx text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-HbGG2O6w2bqjTpNx .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-HbGG2O6w2bqjTpNx .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-HbGG2O6w2bqjTpNx .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-HbGG2O6w2bqjTpNx .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-HbGG2O6w2bqjTpNx #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-HbGG2O6w2bqjTpNx .sequenceNumber{fill:white;}#mermaid-svg-HbGG2O6w2bqjTpNx #sequencenumber{fill:#333;}#mermaid-svg-HbGG2O6w2bqjTpNx #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-HbGG2O6w2bqjTpNx .messageText{fill:#333;stroke:none;}#mermaid-svg-HbGG2O6w2bqjTpNx .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-HbGG2O6w2bqjTpNx .labelText,#mermaid-svg-HbGG2O6w2bqjTpNx .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-HbGG2O6w2bqjTpNx .loopText,#mermaid-svg-HbGG2O6w2bqjTpNx .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-HbGG2O6w2bqjTpNx .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-HbGG2O6w2bqjTpNx .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-HbGG2O6w2bqjTpNx .noteText,#mermaid-svg-HbGG2O6w2bqjTpNx .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-HbGG2O6w2bqjTpNx .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-HbGG2O6w2bqjTpNx .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-HbGG2O6w2bqjTpNx .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-HbGG2O6w2bqjTpNx .actorPopupMenu{position:absolute;}#mermaid-svg-HbGG2O6w2bqjTpNx .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-HbGG2O6w2bqjTpNx .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-HbGG2O6w2bqjTpNx .actor-man circle,#mermaid-svg-HbGG2O6w2bqjTpNx line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-HbGG2O6w2bqjTpNx :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} watchListBookmarkReceived = true Watch(SendInitialEvents=true, RV="")ADDED×100Add×100Bookmark(RV=500, initial-events-end=true)Replace(100 items, RV="500")setLastSyncResourceVersion("500")return (w, nil) ← 复用流

场景2:WatchList 超时后重试

temporaryStore API Server Reflector temporaryStore API Server Reflector #mermaid-svg-DohpUwrInSZck5i3{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-DohpUwrInSZck5i3 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-DohpUwrInSZck5i3 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-DohpUwrInSZck5i3 .error-icon{fill:#552222;}#mermaid-svg-DohpUwrInSZck5i3 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-DohpUwrInSZck5i3 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-DohpUwrInSZck5i3 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-DohpUwrInSZck5i3 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-DohpUwrInSZck5i3 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-DohpUwrInSZck5i3 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-DohpUwrInSZck5i3 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-DohpUwrInSZck5i3 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-DohpUwrInSZck5i3 .marker.cross{stroke:#333333;}#mermaid-svg-DohpUwrInSZck5i3 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-DohpUwrInSZck5i3 p{margin:0;}#mermaid-svg-DohpUwrInSZck5i3 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-DohpUwrInSZck5i3 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-DohpUwrInSZck5i3 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-DohpUwrInSZck5i3 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-DohpUwrInSZck5i3 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-DohpUwrInSZck5i3 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-DohpUwrInSZck5i3 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-DohpUwrInSZck5i3 .sequenceNumber{fill:white;}#mermaid-svg-DohpUwrInSZck5i3 #sequencenumber{fill:#333;}#mermaid-svg-DohpUwrInSZck5i3 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-DohpUwrInSZck5i3 .messageText{fill:#333;stroke:none;}#mermaid-svg-DohpUwrInSZck5i3 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-DohpUwrInSZck5i3 .labelText,#mermaid-svg-DohpUwrInSZck5i3 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-DohpUwrInSZck5i3 .loopText,#mermaid-svg-DohpUwrInSZck5i3 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-DohpUwrInSZck5i3 .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-DohpUwrInSZck5i3 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-DohpUwrInSZck5i3 .noteText,#mermaid-svg-DohpUwrInSZck5i3 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-DohpUwrInSZck5i3 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-DohpUwrInSZck5i3 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-DohpUwrInSZck5i3 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-DohpUwrInSZck5i3 .actorPopupMenu{position:absolute;}#mermaid-svg-DohpUwrInSZck5i3 .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-DohpUwrInSZck5i3 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-DohpUwrInSZck5i3 .actor-man circle,#mermaid-svg-DohpUwrInSZck5i3 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-DohpUwrInSZck5i3 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 未收到 Bookmark → watchListBookmarkReceived = false 重新创建 temporaryStore(丢弃旧数据) 成功! Watch(SendInitialEvents=true, RV="")ADDED×50 (超时,流断开)Add×50Watch(SendInitialEvents=true, RV="")ADDED×100Add×100 (新的 temporaryStore)Bookmark(RV=500, initial-events-end=true)

场景3:RV 过期后降级重试

API Server Reflector API Server Reflector #mermaid-svg-YwDe96xGU1ynRdT0{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-YwDe96xGU1ynRdT0 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-YwDe96xGU1ynRdT0 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-YwDe96xGU1ynRdT0 .error-icon{fill:#552222;}#mermaid-svg-YwDe96xGU1ynRdT0 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-YwDe96xGU1ynRdT0 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-YwDe96xGU1ynRdT0 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-YwDe96xGU1ynRdT0 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-YwDe96xGU1ynRdT0 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-YwDe96xGU1ynRdT0 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-YwDe96xGU1ynRdT0 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-YwDe96xGU1ynRdT0 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-YwDe96xGU1ynRdT0 .marker.cross{stroke:#333333;}#mermaid-svg-YwDe96xGU1ynRdT0 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-YwDe96xGU1ynRdT0 p{margin:0;}#mermaid-svg-YwDe96xGU1ynRdT0 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-YwDe96xGU1ynRdT0 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-YwDe96xGU1ynRdT0 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-YwDe96xGU1ynRdT0 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-YwDe96xGU1ynRdT0 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-YwDe96xGU1ynRdT0 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-YwDe96xGU1ynRdT0 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-YwDe96xGU1ynRdT0 .sequenceNumber{fill:white;}#mermaid-svg-YwDe96xGU1ynRdT0 #sequencenumber{fill:#333;}#mermaid-svg-YwDe96xGU1ynRdT0 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-YwDe96xGU1ynRdT0 .messageText{fill:#333;stroke:none;}#mermaid-svg-YwDe96xGU1ynRdT0 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-YwDe96xGU1ynRdT0 .labelText,#mermaid-svg-YwDe96xGU1ynRdT0 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-YwDe96xGU1ynRdT0 .loopText,#mermaid-svg-YwDe96xGU1ynRdT0 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-YwDe96xGU1ynRdT0 .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-YwDe96xGU1ynRdT0 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-YwDe96xGU1ynRdT0 .noteText,#mermaid-svg-YwDe96xGU1ynRdT0 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-YwDe96xGU1ynRdT0 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-YwDe96xGU1ynRdT0 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-YwDe96xGU1ynRdT0 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-YwDe96xGU1ynRdT0 .actorPopupMenu{position:absolute;}#mermaid-svg-YwDe96xGU1ynRdT0 .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-YwDe96xGU1ynRdT0 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-YwDe96xGU1ynRdT0 .actor-man circle,#mermaid-svg-YwDe96xGU1ynRdT0 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-YwDe96xGU1ynRdT0 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} lastSyncResourceVersion = "500" setIsLastSyncResourceVersionUnavailable(true) rewatchResourceVersion() = "" (降级) 成功!数据来自最新一致性快照 Watch(SendInitialEvents=true, RV="500")410 Gone (RV 已过期)Watch(SendInitialEvents=true, RV="")ADDED×200 + Bookmark(RV=800, initial-events-end=true)

11.12 WatchList Bookmark 的注解格式

go 复制代码
// Bookmark 事件的对象是一个带特殊注解的 Unstructured
// metav1.InitialEventsAnnotationKey = "k8s.io/initial-events-end"

// 检测代码:
if meta.GetAnnotations()[metav1.InitialEventsAnnotationKey] == "true" {
    watchListBookmarkReceived = true
}

为什么用注解而非新的 event type? 因为 Bookmark 是已存在的事件类型(用于 Watch 进度通知),不需要新增事件类型(向后兼容)。用注解区分"普通 Bookmark"和"初始结束 Bookmark"。

11.13 ResourceVersionMatch 的三种值

语义 WatchList 使用
"" (空) 不指定匹配模式 传统 List/Watch
NotOlderThan 返回至少和 RV 一样新的数据 ✅ WatchList 使用
Exact 返回恰好等于 RV 的数据 不使用(List only)

NotOlderThan 的关键含义

  • RV="" + NotOlderThan → 返回最新一致性快照
  • RV="500" + NotOlderThan → 返回 RV≥500 的数据
  • 这确保了 WatchList 返回的数据不会比请求的 RV 更旧

11.14 WatchList 的 Feature Gate 检查链

#mermaid-svg-CMT9ddqWDyfIL1FT{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-CMT9ddqWDyfIL1FT .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-CMT9ddqWDyfIL1FT .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-CMT9ddqWDyfIL1FT .error-icon{fill:#552222;}#mermaid-svg-CMT9ddqWDyfIL1FT .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-CMT9ddqWDyfIL1FT .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-CMT9ddqWDyfIL1FT .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-CMT9ddqWDyfIL1FT .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-CMT9ddqWDyfIL1FT .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-CMT9ddqWDyfIL1FT .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-CMT9ddqWDyfIL1FT .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-CMT9ddqWDyfIL1FT .marker{fill:#333333;stroke:#333333;}#mermaid-svg-CMT9ddqWDyfIL1FT .marker.cross{stroke:#333333;}#mermaid-svg-CMT9ddqWDyfIL1FT svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-CMT9ddqWDyfIL1FT p{margin:0;}#mermaid-svg-CMT9ddqWDyfIL1FT .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-CMT9ddqWDyfIL1FT .cluster-label text{fill:#333;}#mermaid-svg-CMT9ddqWDyfIL1FT .cluster-label span{color:#333;}#mermaid-svg-CMT9ddqWDyfIL1FT .cluster-label span p{background-color:transparent;}#mermaid-svg-CMT9ddqWDyfIL1FT .label text,#mermaid-svg-CMT9ddqWDyfIL1FT span{fill:#333;color:#333;}#mermaid-svg-CMT9ddqWDyfIL1FT .node rect,#mermaid-svg-CMT9ddqWDyfIL1FT .node circle,#mermaid-svg-CMT9ddqWDyfIL1FT .node ellipse,#mermaid-svg-CMT9ddqWDyfIL1FT .node polygon,#mermaid-svg-CMT9ddqWDyfIL1FT .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-CMT9ddqWDyfIL1FT .rough-node .label text,#mermaid-svg-CMT9ddqWDyfIL1FT .node .label text,#mermaid-svg-CMT9ddqWDyfIL1FT .image-shape .label,#mermaid-svg-CMT9ddqWDyfIL1FT .icon-shape .label{text-anchor:middle;}#mermaid-svg-CMT9ddqWDyfIL1FT .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-CMT9ddqWDyfIL1FT .rough-node .label,#mermaid-svg-CMT9ddqWDyfIL1FT .node .label,#mermaid-svg-CMT9ddqWDyfIL1FT .image-shape .label,#mermaid-svg-CMT9ddqWDyfIL1FT .icon-shape .label{text-align:center;}#mermaid-svg-CMT9ddqWDyfIL1FT .node.clickable{cursor:pointer;}#mermaid-svg-CMT9ddqWDyfIL1FT .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-CMT9ddqWDyfIL1FT .arrowheadPath{fill:#333333;}#mermaid-svg-CMT9ddqWDyfIL1FT .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-CMT9ddqWDyfIL1FT .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-CMT9ddqWDyfIL1FT .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-CMT9ddqWDyfIL1FT .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-CMT9ddqWDyfIL1FT .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-CMT9ddqWDyfIL1FT .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-CMT9ddqWDyfIL1FT .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-CMT9ddqWDyfIL1FT .cluster text{fill:#333;}#mermaid-svg-CMT9ddqWDyfIL1FT .cluster span{color:#333;}#mermaid-svg-CMT9ddqWDyfIL1FT 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-CMT9ddqWDyfIL1FT .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-CMT9ddqWDyfIL1FT rect.text{fill:none;stroke-width:0;}#mermaid-svg-CMT9ddqWDyfIL1FT .icon-shape,#mermaid-svg-CMT9ddqWDyfIL1FT .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-CMT9ddqWDyfIL1FT .icon-shape p,#mermaid-svg-CMT9ddqWDyfIL1FT .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-CMT9ddqWDyfIL1FT .icon-shape .label rect,#mermaid-svg-CMT9ddqWDyfIL1FT .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-CMT9ddqWDyfIL1FT .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-CMT9ddqWDyfIL1FT .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-CMT9ddqWDyfIL1FT :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} false
true
true
false
clientfeatures.WatchListClient
Feature Gate

启用?
watchlist.DoesClientNotSupport

WatchListSemantics(lw)
Client 不支持 WatchList 语义

(如 FakeClient)
Client 支持
useWatchList = true
useWatchList = false

为什么需要 Client 级别检查? 因为单元测试通常使用 FakeClient,FakeClient 不支持 WatchList 语义(SendInitialEvents/ResourceVersionMatch)。如果强行使用,会导致测试失败。生产代码使用真实的 RESTClient,支持 WatchList。

相关推荐
IT策士2 小时前
k8s 常见面试问题
容器·面试·kubernetes
鹤落晴春2 小时前
【K8s】资源配额与访问控制
docker·容器·kubernetes
蘋天纬地2 小时前
k8s中的工作负载是什么,都有哪几种类型的工作负载
云原生·容器·kubernetes
小小龙学IT2 小时前
Go语言后端开发实战指南:构建高性能云原生服务
前端·云原生·golang
qq_452396232 小时前
第一篇:《Kubernetes 是什么?为什么它是云原生基石?》
云原生·容器·kubernetes
sbjdhjd10 小时前
Redis 主从复制、哨兵高可用与 Cluster 集群部署实验手册
运维·前端·redis·云原生·开源·bootstrap·html
ggaofeng11 小时前
glusterfs如何在k8s中使用
云原生·容器·kubernetes·glusterfs
IT策士13 小时前
第49篇 k8s之服务网格入门:Istio 简介
容器·kubernetes·istio
张忠琳16 小时前
【client-go v0.36.1】LeaderElection 深度分析(上篇)— 模块定位、结构、LeaderElector 核心逻辑
云原生·kubernetes·client-go·leaderelection