蓝鲸 CMDB 3.14.6 源码专题【左扬精讲】— CMDB #18:容器拓扑关联:Pod ↔ Host ↔ 机柜全链路映射

蓝鲸 CMDB 3.14.6 源码专题【左扬精讲】— CMDB #18:容器拓扑关联:Pod ↔ Host ↔ 机柜全链路映射

第 #17 篇我们把 k8s 资源纳管进了 CMDB,src/kube/types/src/scene_server/topo_server/service/kube/src/scene_server/topo_server/logics/kube/ 三条线把所有对象的 CRUD 都打通了。但是 SRE 的痛点才刚刚开始:

纳管只是"看得到",真正要回答的是 "从上往下" 和 "从下往上" 的反向追溯——

  • 告警平台弹了 host 10086 上的 payment pod OOM 重启——SRE 必须能在 5 秒内把这个 host 在机房哪一台机柜、属于哪个业务的哪个集群、上面跑了哪些工作负载全部拼出来
  • 反之,Pod payment-7d8c9-xxx 重启失败——SRE 要快速知道它在哪个 Node 上、Node 归哪个业务

这一篇我们拆解 3.14.6 源码里 src/scene_server/topo_server/service/kube/node.goFindNodePathForHostsrc/scene_server/topo_server/service/kube/pod.goFindPodPath 这两个反向追溯入口的完整实现。

本篇要点

在 3.14.6 蓝鲸 CMDB 中,反查拓扑并没有独立的 "主机↔节点↔Pod" 关联表——常见的 cc_KubeHostRelationship 并不存在。真实的反查机制建立在三个内置字段之上:

  • src/kube/types/node.go L71:Node.HostID int64 bson:"bk_host_id"——Node 表本来就记着自己挂在哪个 host 上
  • src/kube/types/pod.go L242-L249:SysSpec 内嵌 HostID / NodeID / Node——Pod 表自带 host/node 三件套
  • src/kube/types/pod.go L252-L258:Ref{Kind, Name, ID}——Pod 表自带 workload 归属

所以本套设计的本质是"反范式冗余三件套",不是中间关联表。这是本篇最重要的论点,下文所有源码都围绕这个点讲。

容器拓扑 FindNodePathForHost FindPodPath 反范式冗余 SysSpec HostID反查 shared集群 SRE

src/kube/types/node.go                                ← Node 结构体(HostID/ClusterID/HasPod 字段)[L63-L94]
  src/kube/types/pod.go                                 ← Pod 结构体 + SysSpec 冗余字段 [L134-L150, L242-L249]
  src/kube/types/topo.go                                ← HostPathOption / HostPathData / HostNodePath / PodPathOption / PodPathData / PodPath [L33-L141]
  src/kube/types/types.go                               ← cc_NodeBase / cc_ClusterBase / cc_PodBase 表名常量 [L192-L238]
  src/scene_server/topo_server/service/kube/node.go     ← FindNodePathForHost 入口 [L35-L112]
                                                           getHostNodeRelation 反查 Node 表 [L114-L156]
                                                           getClusterIDWithName 第二跳 [L158-L189]
  src/scene_server/topo_server/service/kube/pod.go      ← FindPodPath 入口 [L36-L91]
                                                           buildPodPaths 拼多跳路径 [L93-L155]
                                                           combinePodPath 共享集群重写 [L157-L209]
  src/scene_server/topo_server/service/kube/service.go  ← 路由注册:/find/kube/host_node_path [L72-L73]、/find/kube/pod_path [L82]
  src/source_controller/coreservice/core/kube/kube.go   ← getNodeInfo 反查 Node 表 [L275-L306]、shared 集群校验 [L227-L273]
  

本篇学习重点

必须掌握

  • 反查拓扑的入口FindNodePathForHostFindPodPath 的端点路由、参数校验、授权模型
  • 反范式三件套Node.HostID + SysSpec.HostID/NodeID/Node + Pod.Ref 的字段语义和写入时机
  • 两条反查链路hostIDs → cc_NodeBase.bk_host_id INpodIDs → cc_PodBase.id IN,都是单表 IN 查询
  • 多跳拼装:拿到 Node 后再 SearchCluster / FindBiz 把集群名和业务名补齐

了解即可

  • 共享集群场景下 combinePodPath 对 BizName 的二次重写
  • host→Node 的反向 SearchNode 在 coreservice 层的实现位置
  • 前端 Topology 树渲染时怎么消费 HostPathData / PodPathData

一、反查拓扑的设计哲学:反范式冗余 vs 独立关联表

Why — 为什么 3.14.6 没有 cc_KubeHostRelationship 关联表?

在传统 CMDB 设计里,"主机↔节点↔Pod"是典型多对多关系,理论上要建一张关联表:

  • cc_KubeHostRelationship(hostID, nodeID, bizID, ...) —— 一台物理机可能跑多个 Node,一个 Node 也可能被同集群不同业务共享
  • 查询时通过 JOIN 拿到跨表关系

但蓝鲸 CMDB 3.14.6 的实现是"反范式冗余":把宿主关系直接塞进对象表里,省掉一张关联表。

1.1 反范式三件套的字段归属

src/kube/types/node.go L63-L94 的 Node 结构体:

// src/kube/types/node.go 第 63-94 行(精选)
  type Node struct {
      // ID cluster auto-increment ID in cc
      ID int64 `json:"id,omitempty" bson:"id"`
      // BizID the business ID to which the cluster belongs
      BizID int64 `json:"bk_biz_id,omitempty" bson:"bk_biz_id"`
      // SupplierAccount the supplier account that this resource belongs to.
      SupplierAccount string `json:"bk_supplier_account,omitempty" bson:"bk_supplier_account"`
      // HostID the node ID to which the host belongs
      HostID int64 `json:"bk_host_id,omitempty" bson:"bk_host_id"`
      // ClusterID the node ID to which the cluster belongs
      ClusterID int64 `json:"bk_cluster_id,omitempty" bson:"bk_cluster_id"`
      // ClusterUID the node ID to which the cluster belongs
      ClusterUID string `json:"cluster_uid,omitempty" bson:"cluster_uid"`
      // HasPod this field indicates whether there is a pod in the node.
      HasPod *bool `json:"has_pod,omitempty" bson:"has_pod"`
      Name   *string `json:"name,omitempty" bson:"name"`
      // Revision record this app's revision information
      table.Revision `json:",inline" bson:",inline"`
  }
  

第 71 行 HostID int64 是关键:每个 Node 行都自带自己挂在哪台物理机上的字段。这意味着查 cc_NodeBase 表一次就能反推出 host → node → cluster 的完整链路。

类似地,看 src/kube/types/pod.go L242-L249 的 SysSpec

// src/kube/types/pod.go 第 242-249 行
  type SysSpec struct {
      SupplierAccount string `json:"bk_supplier_account,omitempty" bson:"bk_supplier_account"`
      WorkloadSpec `json:",inline" bson:",inline"`
      // 反范式字段:Pod 在哪台主机、在哪个 Node、Node 叫什么名字
      HostID int64 `json:"bk_host_id,omitempty" bson:"bk_host_id"`
      NodeID int64 `json:"bk_node_id,omitempty" bson:"bk_node_id"`
      // redundant node names
      Node string `json:"node_name,omitempty" bson:"node_name"`
  }
  

SysSpec 通过src/kube/types/pod.go L34-L36 合并进 PodFields

// src/kube/types/pod.go 第 34-36 行
  var PodFields = table.MergeFields(CommonSpecFieldsDescriptor, BizIDDescriptor, HostIDDescriptor,
      ClusterBaseRefDescriptor, NodeBaseRefDescriptor, NamespaceBaseRefDescriptor,
      WorkLoadRefDescriptor, PodSpecFieldsDescriptor)
  

也就是说,cc_PodBase 表上天然就有 bk_host_id / bk_node_id / node_name 三列,反查 host/pod 双向都是单表查询。

1.2 反范式 vs 关联表:选哪个?

What — 反范式设计的代价是什么?

代价写在 src/source_controller/coreservice/core/kube/kube.go 第 47-124 行的 GetSysSpecInfoByCond 里:每次创建或更新 Pod,都要重新从 workload 表、shared cluster 表、node 表里把数据捞一遍再写入 SysSpec。这是写放大。

但收益是:反查全链路只有 1 次 MongoDB IN 查询,不需要 JOIN。

源码视角总结:反范式三件套的三个独特价值

1. 单表 IN 查询把跨表 JOIN 干掉

src/scene_server/topo_server/service/kube/node.go 第 117-125 行的 getHostNodeRelation,反查就是一行 MongoDB 条件:

cond := mapstr.MapStr{common.BKHostIDField: mapstr.MapStr{common.BKDBIN: hostIDs}}
  fields := []string{
      common.BKFieldID, common.BKAppIDField, types.BKClusterIDFiled, common.BKHostIDField,
  }
  query := &metadata.QueryCondition{
      Condition:      cond,
      Fields:         fields,
      DisableCounter: true,
  }
  

用的是 bk_host_id IN (hostIDs),没有 $lookup,没有 $graphLookup,没有任何跨表操作。

2. 写扩大的代价被控制

反范式的代价是写时多查几张表,但这个代价只发生在创建/更新 Pod 时(BatchCreatePod),不在反查路径上——反向追溯路径上是常数次 MongoDB IN,O(1) 跳数

3. 前端拓扑树渲染天然适配

UI 在画 "Pod → Host → 机柜" 树时,每一层都不需要触发额外查询。从 Pod 向上找 Node 是一次反查,从 Node 向上找 Host 可以从 Node.bk_host_id 直连 CMDB 的 cc_HostBase,再上溯到 cc_RackBase

1.3 没有发生反范式会怎样

如果坚持用独立关联表 cc_KubeHostRelationship 会发生什么?

假设我们坚持建一张 cc_KubeHostRelationship(hostID, nodeID, bizID, clusterID, asstBizID, refTime)

  • 数据冗余之外多一张表:每次 Node 创建/迁移都要双写关联表,事务一致性成本陡增
  • 反查需要 JOIN:host → node 需要 $lookup 或应用层二次查,下行性能多一跳
  • 删除/迁移逻辑变复杂:关联表要级联删除,否则会出现孤立的关联行,污染反查结果
  • shared 集群语义歧义:同一 Node 上的 Pod 可能属于不同业务,关联表用单行 bizID 表达就会丢信息

蓝鲸 CMDB 选择把所有宿主信息塞进对象表,用"反范式三件套"代替独立关联表,3.14.6 实际代码就是这套。

二、反查链路全景图:从 HostID 到 Pod 全链路

2.1 两条反查链路

3.14.6 暴露了两个入口,对应两条反查链路:

链路 A:hostIDs → 业务所在集群 / 业务名
    POST /find/kube/host_node_path
    ↑
    FindNodePathForHost (node.go:35-112)
    ├─ getHostNodeRelation (node.go:114-156)
    │    ├─ SearchNode  在 cc_NodeBase 上 bk_host_id IN hostIDs
    │    └─ getClusterIDWithName (node.go:158-189)
    │         └─ SearchCluster 在 cc_ClusterBase 上 id IN clusterIDs
    ├─ getBizIDWithName (node.go:191-226) FindBiz
    └─ 拼出 HostNodePath{HostID, Paths[]NodePath{BizID,BizName,ClusterID,ClusterName}}
  
  链路 B:podIDs + BizID → 业务/集群/命名空间/workload
    POST /find/kube/pod_path
    ↑
    FindPodPath (pod.go:36-91)
    ├─ GenSharedNsListCond shared 集群 ns 过滤
    ├─ ListPod 在 cc_PodBase 上 id IN podIDs(已被 SysSpec 预填 HostID/NodeID/Ref)
    ├─ buildPodPaths (pod.go:93-155)  从 pods 提取 clusterID/namespaceID/Ref 字段
    └─ combinePodPath (pod.go:157-209) shared 集群 BizName 重写
  

两条链路都不是单表查完就返回,而是 "先查主表拿到跨表 ID,再去第二张表补齐名称" 的两段式:

链路第一步(主表 IN)第二步(补齐名称)最终返回
A: hostIDs → 路径 cc_NodeBase.bk_host_id IN cc_ClusterBase.id IN + cc_BizBase HostPathData.Info[i] = {HostID, Paths[]NodePath}
B: podIDs → 路径 cc_PodBase.id IN(已被 SysSpec 预填 clusterID/namespaceID/workload) cc_ClusterBase.id IN + cc_BizBase PodPathData.Info[i] = PodPath

两段式拆分的第一个跳都跑在 MongoDB 主表上拿 ID,第二个跳批量查 cluster 拿 name。这是为什么 getClusterIDWithName 在代码里出现两次(node / pod 各一份)。

2.2 为什么 pods 路径"看似"四层?

Pod 路径看起来要经过 BizID → ClusterID → NamespaceID → WorkloadID → PodID 五层,但实际上 SysSpec 在写入时已经把后四层都塞进了 Pod 行——反查路径上是 1 次 IN 取出 Pod 行+ 1 次 IN 补齐 Cluster/Biz 名称

具体字段来自 src/kube/types/topo.go L130-L141 的 PodPath

// src/kube/types/topo.go 第 130-141 行
  type PodPath struct {
      BizID        int64        `json:"bk_biz_id"`
      BizName      string       `json:"biz_name"`
      ClusterID    int64        `json:"bk_cluster_id"`
      ClusterName  string       `json:"cluster_name"`
      NamespaceID  int64        `json:"bk_namespace_id"`
      Namespace    string       `json:"namespace"`
      Kind         WorkloadType `json:"kind"`
      WorkloadID   int64        `json:"bk_workload_id"`
      WorkloadName string       `json:"workload_name"`
      PodID        int64        `json:"bk_pod_id"`
  }
  

字段填法见 src/scene_server/topo_server/service/kube/pod.go 第 140-150 行:

// src/scene_server/topo_server/service/kube/pod.go 第 140-150 行
  path := types.PodPath{
      BizID:        pod.BizID,
      ClusterID:    clusterID,
      NamespaceID:  namespaceID,
      Namespace:    namespace,
      Kind:         ref.Kind,
      WorkloadID:   ref.ID,
      WorkloadName: ref.Name,
      PodID:        id,
  }
  paths = append(paths, path)
  

注意 ref.Kind / ref.Name / ref.ID 全部来自 src/kube/types/pod.go L252-L258 的 Ref

// src/kube/types/pod.go 第 252-258 行
  type Ref struct {
      Kind string `json:"kind"`
      Name string `json:"name,omitempty"`
      ID   int64  `json:"id,omitempty"`
  }
  

字段本身没有任何跨表——Pod 行被插入前 4 层信息就已经齐全,这就是"反范式三件套"在 PodPath 字段上的直接体现

三、FindNodePathForHost 源码拆解:host → node → cluster → biz

3.1 What — 这个函数反查了什么

FindNodePathForHost 是 host → 路径 反查的入口函数。看 src/scene_server/topo_server/service/kube/node.go 第 35-112 行:

// src/scene_server/topo_server/service/kube/node.go 第 35-112 行(精简后)
  func (s *service) FindNodePathForHost(ctx *rest.Contexts) {
      req := new(types.HostPathOption)
      if err := ctx.DecodeInto(&req); err != nil {
          ctx.RespAutoError(err)
          return
      }
  
      if rawErr := req.Validate(); rawErr.ErrCode != 0 {
          ctx.RespAutoError(rawErr.ToCCError(ctx.Kit.CCError))
          return
      }
  
      relation, err := s.getHostNodeRelation(ctx.Kit, req.HostIDs)
      if err != nil {
          ctx.RespAutoError(err)
          return
      }
  
      if relation == nil {
          ctx.RespEntity(types.HostPathData{
              Info: []types.HostNodePath{},
          })
          return
      }
  
      // authorize
      authRes := make([]acmeta.ResourceAttribute, len(relation.BizIDs))
      for i, bizID := range relation.BizIDs {
          authRes[i] = acmeta.ResourceAttribute{Basic: acmeta.Basic{Type: acmeta.KubeNode, Action: acmeta.Find},
              BusinessID: bizID}
      }
      if resp, authorized := s.AuthManager.Authorize(ctx.Kit, authRes...); !authorized {
          ctx.RespNoAuth(resp)
          return
      }
  
      bizIDWithName, err := s.getBizIDWithName(ctx.Kit, relation.BizIDs)
      // ... 拼出 hostsPath []HostNodePath ...
      ctx.RespEntity(types.HostPathData{
          Info: hostsPath,
      })
  }
  

3.2 Why — 为什么反查必须做授权?

Why — 跨业务场景下的反查授权为何必须 fan-out?

一个 host 可能因为同时跑了多集群的 kubelet,落到 cc_NodeBase 表里有多个 Node 行,而每个 Node 行又属于不同业务。所以授权不能单一 —— 必须对所有命中的 BizID 都做一次 Authorize

看代码第 63-71 行:

authRes := make([]acmeta.ResourceAttribute, len(relation.BizIDs))
  for i, bizID := range relation.BizIDs {
      authRes[i] = acmeta.ResourceAttribute{Basic: acmeta.Basic{Type: acmeta.KubeNode, Action: acmeta.Find},
          BusinessID: bizID}
  }
  if resp, authorized := s.AuthManager.Authorize(ctx.Kit, authRes...); !authorized {
      ctx.RespNoAuth(resp)
      return
  }
  

这是反查语义的关键约束:权限是按业务颗粒度裁剪的,用户看不到的 biz 下的 Node,行得在拼装结果时被过滤掉。

没有授权 fan-out 会发生什么?

  • SRE-A 只被授权了 biz=2 的集群视图,但请求 hostIDs=[10086] 时,如果 Node 在 biz=2biz=5 都有,会把 biz=5 的集群名也吐出去——这是越权信息泄露
  • 目前代码里 fan-out 没有按"行"过滤,而是按"BizID 集合"过滤:用户对 biz=5 没权限时,整次调用返回 RespNoAuth,不返回任何行

3.3 How — 核心两跳的执行细节

src/scene_server/topo_server/service/kube/node.go 第 114-156 行的 getHostNodeRelation

// src/scene_server/topo_server/service/kube/node.go 第 114-156 行
  func (s *service) getHostNodeRelation(kit *rest.Kit, hostIDs []int64) (*types.HostNodeRelation, error) {
      cond := mapstr.MapStr{common.BKHostIDField: mapstr.MapStr{common.BKDBIN: hostIDs}}
      fields := []string{
          common.BKFieldID, common.BKAppIDField, types.BKClusterIDFiled, common.BKHostIDField,
      }
      query := &metadata.QueryCondition{
          Condition:      cond,
          Fields:         fields,
          DisableCounter: true,
      }
  
      resp, ccErr := s.ClientSet.CoreService().Kube().SearchNode(kit.Ctx, kit.Header, query)
      if ccErr != nil {
          return nil, ccErr
      }
      if len(resp.Data) == 0 {
          return nil, nil
      }
      bizIDs := make([]int64, 0)
      hostWithNode := make(map[int64][]types.Node)
      clusterIDs := make([]int64, 0)
      for _, node := range resp.Data {
          bizIDs = append(bizIDs, node.BizID)
          hostWithNode[node.HostID] = append(hostWithNode[node.HostID], node)
          clusterIDs = append(clusterIDs, node.ClusterID)
      }
  
      clusterIDWithName, err := s.getClusterIDWithName(kit, clusterIDs)
      if err != nil {
          return nil, err
      }
  
      return &types.HostNodeRelation{
          BizIDs:            bizIDs,
          HostWithNode:      hostWithNode,
          ClusterIDWithName: clusterIDWithName,
      }, nil
  }
  

第 117 行是反查的唯一一次 MongoDB IN 查询bk_host_id IN hostIDs

第 117-126 行的字段白名单 fields 只取 4 列 id, bk_biz_id, bk_cluster_id, bk_host_id——这是性能优化,反查只读这几个字段,比全字段拉取少几十字节/行。

第 138-143 行把查到的 Node 行分组到 hostWithNode map:

  • bizIDs:所有命中 Node 的 BizID 集合——授权 fan-out 用
  • hostWithNode:hostID → []Node——同 host 多 Node 时输出多条 path
  • clusterIDs:所有命中 Cluster 的 ID 集合——第二跳 SearchCluster 用

第二跳 getClusterIDWithName 在第 158-189 行:

// src/scene_server/topo_server/service/kube/node.go 第 158-189 行
  func (s *service) getClusterIDWithName(kit *rest.Kit, clusterIDs []int64) (map[int64]string, error) {
      cond := mapstr.MapStr{common.BKFieldID: mapstr.MapStr{common.BKDBIN: clusterIDs}}
      fields := []string{common.BKFieldID, common.BKFieldName}
      query := &metadata.QueryCondition{
          Condition:      cond,
          Fields:         fields,
          DisableCounter: true,
      }
  
      resp, ccErr := s.ClientSet.CoreService().Kube().SearchCluster(kit.Ctx, kit.Header, query)
      if ccErr != nil {
          return nil, ccErr
      }
      if len(resp.Data) == 0 {
          return nil, errors.New("no cluster founded")
      }
  
      idWithName := make(map[int64]string)
      for _, cluster := range resp.Data {
          if cluster.Name == nil {
              return nil, fmt.Errorf("get node attribute failed, attr: %s", common.BKFieldName)
          }
          idWithName[cluster.ID] = *cluster.Name
      }
  
      return idWithName, nil
  }
  

第 159-167 行跟第一跳同模式:bk_id IN clusterIDs,只取 id + name 两列。第二跳不会并发跑,它是顺序执行的,因为第一跳拿到的 clusterIDs 列表大小决定它的工作量。

源码视角:反查的两个性能短板

1. len(hostIDs) 没有上限保护

src/kube/types/topo.go 第 46-51 行的 HostPathOption.Validate

// src/kube/types/topo.go 第 46-51 行
  if len(h.HostIDs) > common.BKMaxLimitSize {
      return ccErr.RawErrorInfo{
          ErrCode: common.CCErrCommXXExceedLimit,
          Args:    []interface{}{"ids", common.BKMaxLimitSize},
      }
  }
  

BKMaxLimitSize 是常数,调用方传超过它的 hostIDs 直接被 Validate 拒掉。这是反查的"硬上限兜底"

2. len(resp.Data) == 0 返回 nil 不是 error

第 133-135 行:

if len(resp.Data) == 0 {
      return nil, nil
  }
  

注释里明写:"returning nil means that there is no node on the host, which is legal"——一台 host 上没有 Node 是合法状态,调用方得自己处理 relation == nil 走到 Info: []HostNodePath{} 的空集分支(第 55-60 行)。

3.4 拼装阶段:从原始关系到返回结构

回到第 80-107 行的拼装:

// src/scene_server/topo_server/service/kube/node.go 第 80-107 行
  hostsPath := make([]types.HostNodePath, len(req.HostIDs))
  for outerIdx, hostID := range req.HostIDs {
      nodes := relation.HostWithNode[hostID]
      paths := make([]types.NodePath, 0)
      uniqueMap := make(map[string]struct{})
      for _, node := range nodes {
          clusterID := node.ClusterID
          bizID := node.BizID
          unique := strconv.FormatInt(bizID, 10) + ":" + strconv.FormatInt(clusterID, 10)
          if _, ok := uniqueMap[unique]; ok {
              continue
          }
          uniqueMap[unique] = struct{}{}
  
          path := types.NodePath{
              BizID:       bizID,
              BizName:     bizIDWithName[bizID],
              ClusterID:   clusterID,
              ClusterName: relation.ClusterIDWithName[clusterID],
          }
          paths = append(paths, path)
      }
  
      hostsPath[outerIdx] = types.HostNodePath{
          HostID: hostID,
          Paths:  paths,
      }
  }
  

关键去重逻辑在第 88-92 行:同 host + 同 biz + 同 cluster 组合只保留一条。这是因为一台 host 上可能有同一个集群的多个 Node,UI 不应该看到重复路径。"biz:cluster" 拼接字符串作为 key 实现了一次天然的 group-by。

FindNodePathForHost 完整流水线(3 跳)

  • 第 1 跳getHostNodeRelation):cc_NodeBase.bk_host_id IN hostIDs,返回 HostNodeRelation{BizIDs, HostWithNode, ClusterIDWithName}
  • 第 2 跳getClusterIDWithName):cc_ClusterBase.id IN clusterIDs,返回 map[clusterID]clusterName
  • 第 3 跳getBizIDWithName,第 191-226 行):cc_BizBase,返回 map[bizID]bizName
  • 拼装:去重同 biz:cluster,生成 HostNodePath{HostID, []NodePath}

四、FindPodPath 源码拆解:pod → cluster/namespace/workload

4.1 What — Pod 路径反查的端点

src/scene_server/topo_server/service/kube/pod.go 第 36-91 行:

// src/scene_server/topo_server/service/kube/pod.go 第 36-91 行(精简后)
  func (s *service) FindPodPath(ctx *rest.Contexts) {
      req := new(types.PodPathOption)
      if err := ctx.DecodeInto(req); err != nil {
          ctx.RespAutoError(err)
          return
      }
  
      if rawErr := req.Validate(); rawErr.ErrCode != 0 {
          ctx.RespAutoError(rawErr.ToCCError(ctx.Kit.CCError))
          return
      }
  
      // authorize
      authRes := acmeta.ResourceAttribute{Basic: acmeta.Basic{Type: acmeta.KubePod, Action: acmeta.Find},
          BusinessID: req.BizID}
      if resp, authorized := s.AuthManager.Authorize(ctx.Kit, authRes); !authorized {
          ctx.RespNoAuth(resp)
          return
      }
  
      podIDCond := filtertools.GenAtomFilter(common.BKFieldID, filter.In, req.PodIDs)
      cond, err := s.Logics.KubeOperation().GenSharedNsListCond(ctx.Kit, types.KubePod, req.BizID, podIDCond)
      if err != nil {
          ctx.RespAutoError(err)
          return
      }
  
      fields := []string{common.BKFieldID, common.BKAppIDField, types.BKClusterIDFiled, types.BKNamespaceIDField,
          types.NamespaceField, types.RefField}
      query := &metadata.QueryCondition{
          Condition: cond,
          Fields:    fields,
      }
      resp, err := s.ClientSet.CoreService().Kube().ListPod(ctx.Kit.Ctx, ctx.Kit.Header, query)
      if err != nil {
          ctx.RespAutoError(err)
          return
      }
      if len(resp.Info) == 0 {
          ctx.RespEntity(types.PodPathData{Info: []types.PodPath{}})
          return
      }
  
      paths, rawErr := s.buildPodPaths(ctx.Kit, req.BizID, resp.Info)
      ctx.RespEntity(types.PodPathData{
          Info: paths,
      })
  }
  

4.2 Why — 为什么要 GenSharedNsListCond?

Why — shared ns(共享命名空间)场景下的过滤前置

如果 req.BizID 是平台业务(plat biz),它的命名空间会被多个普通业务共享,Pod 数据层面可能挂着业务 A 的 BizID、但实际"逻辑归属于"业务 B。GenSharedNsListCond 用来生成跨业务的 IN 条件,把这些共享 Pod 都带出来。

src/scene_server/topo_server/service/kube/pod.go 第 58-63 行:

podIDCond := filtertools.GenAtomFilter(common.BKFieldID, filter.In, req.PodIDs)
  cond, err := s.Logics.KubeOperation().GenSharedNsListCond(ctx.Kit, types.KubePod, req.BizID, podIDCond)
  

第 59 行不是直接拿 podIDCond,而是再套一层 GenSharedNsListCond,这才是 Pod 反查和 Node 反查的唯一关键区别

4.3 How — 反范式字段直接落入返回值

第 65-70 行有一个看似平凡、其实关键的字段白名单 fields

fields := []string{common.BKFieldID, common.BKAppIDField, types.BKClusterIDFiled, types.BKNamespaceIDField,
      types.NamespaceField, types.RefField}
  

注意 types.RefFieldsrc/kube/types/types.go L434-L435 定义为 "ref")是顶层字段名,对应 src/kube/types/pod.go L252-L258 的 Ref 子结构。MongoDB 反序列化时这个字段会以 Ref 子对象形式返回。

src/scene_server/topo_server/service/kube/pod.go 第 93-155 行的 buildPodPaths

// src/scene_server/topo_server/service/kube/pod.go 第 93-155 行(精简后)
  func (s *service) buildPodPaths(kit *rest.Kit, bizID int64, pods []types.Pod) ([]types.PodPath, error) {
      paths := make([]types.PodPath, 0)
      clusterIDs := make([]int64, 0)
      allBizIDs := make([]int64, 0)
  
      for _, pod := range pods {
          id := pod.ID
          if pod.ClusterID == 0 { return nil, fmt.Errorf("...") }
          clusterID := pod.ClusterID
          clusterIDs = append(clusterIDs, clusterID)
  
          if pod.NamespaceID == 0 { return nil, fmt.Errorf("...") }
          namespaceID := pod.NamespaceID
          if pod.Namespace == "" { return nil, fmt.Errorf("...") }
          namespace := pod.Namespace
  
          if pod.Ref == nil { return nil, fmt.Errorf("...") }
          if pod.Ref.Kind == "" { return nil, fmt.Errorf("...") }
          if pod.Ref.Name == "" { return nil, fmt.Errorf("...") }
          if pod.Ref.ID == 0 { return nil, fmt.Errorf("...") }
          ref := pod.Ref
  
          path := types.PodPath{
              BizID:        pod.BizID,
              ClusterID:    clusterID,
              NamespaceID:  namespaceID,
              Namespace:    namespace,
              Kind:         ref.Kind,
              WorkloadID:   ref.ID,
              WorkloadName: ref.Name,
              PodID:        id,
          }
          paths = append(paths, path)
          allBizIDs = append(allBizIDs, pod.BizID)
      }
  
      return s.combinePodPath(kit, bizID, allBizIDs, clusterIDs, paths)
  }
  

第 101-138 行是一长串 if x == 0 / == "" / == nil { return error }这套字段强校验有一个隐藏设计原则:因为 Pod 行是被反范式写入的,SysSpec 的字段一旦缺失说明写入链路有过错误,反查路径要 fail-fast 而非返回半残数据。

4.4 combinePodPath — shared 集群 BizName 重写

src/scene_server/topo_server/service/kube/pod.go 第 157-209 行的 combinePodPath

// src/scene_server/topo_server/service/kube/pod.go 第 157-209 行
  func (s *service) combinePodPath(kit *rest.Kit, bizID int64, allBizIDs, clusterIDs []int64, paths []types.PodPath) (
      []types.PodPath, error) {
  
      clusterCond := &metadata.QueryCondition{
          Condition:      mapstr.MapStr{common.BKFieldID: mapstr.MapStr{common.BKDBIN: clusterIDs}},
          Fields:         []string{common.BKFieldID, common.BKFieldName},
          DisableCounter: true,
      }
  
      clusterRes, ccErr := s.ClientSet.CoreService().Kube().SearchCluster(kit.Ctx, kit.Header, clusterCond)
      if ccErr != nil {
          return nil, ccErr
      }
  
      clusterMap := make(map[int64]types.Cluster)
      for _, cluster := range clusterRes.Data {
          clusterMap[cluster.ID] = cluster
      }
  
      bizIDWithName, err := s.getBizIDWithName(kit, allBizIDs)
      if err != nil {
          return nil, err
      }
  
      // 关键:shared 集群 BizName 的二次重写
      sharedClusterPaths := make([]types.PodPath, 0)
      for idx, path := range paths {
          cluster, exists := clusterMap[path.ClusterID]
          if !exists {
              return nil, kit.CCError.CCErrorf(common.CCErrCommParamsInvalid, types.BKClusterIDFiled)
          }
  
          if cluster.Name != nil {
              path.ClusterName = *cluster.Name
          }
  
          if path.BizID != bizID {
              // 共享命名空间:把原始业务路径追加一份给请求方
              path.BizName = bizIDWithName[path.BizID]
              sharedClusterPaths = append(sharedClusterPaths, path)
          }
  
          path.BizID = bizID
          path.BizName = bizIDWithName[bizID]
          paths[idx] = path
      }
  
      return append(paths, sharedClusterPaths...), nil
  }
  

为什么需要 sharedClusterPaths 二次切片?

场景:当业务 A 的 ns 被平台业务 P 共享时,P 查询自己看到的 Pod 路径时,path.BizID 里写的是 A(因为 Pod 数据归属在 A),但前端的"业务拓扑树"必须展示 P 自己的业务名,否则平台用户看到一堆 A 的业务,感觉数据"飘了"。

第 198-205 行的处理:

  • 原始路径(paths:把 BizID 改写成请求方 bizIDBizName 同步更新——这是主路径
  • shared 副本(sharedClusterPaths:保留原始业务信息(BizID != bizID),告诉前端"这个 Pod 其实归属于业务 X,平台业务 Y 通过共享协议看到它"

这是 Pod 反查和 Host 反查的最大学习差异点——Host 反查没有 shared 语义,Pod 反查有。

4.5 如果不做 shared 改写会发生什么?

没有 sharedClusterPaths 二次切片会发生什么?

  • 平台业务(bizID=0)查询自己的共享 Pod 时,所有 Pod 的 BizName 都被覆盖成平台业务名——前端拓扑树展示的是平台业务,但告警显示的实际归属却是 A 业务,信息不一致
  • 反之 SRE 想要"这个 Pod 原本归属哪个业务"的信息彻底丢失,安全审计失败
  • 目前代码里 sharedClusterPaths 被 append 到末尾,前端必须按业务 ID 区分主路径和 shared 副本

五、type 设计:HostPathOption / HostPathData / PodPath 的字段语义

5.1 请求与响应的类型全景

看完实现层,我们看 API 协议层的类型设计。这部分定义在 src/kube/types/topo.go

请求:
    HostPathOption {HostIDs []int64 json:"ids"}            L33-L35
    PodPathOption  {BizID int64 json:"bk_biz_id", PodIDs []int64 json:"ids"}  L88-L91
  
  响应包装:
    HostPathResp {Data HostPathData}                      L56-L59
    PodPathResp  {Data PodPathData}                       L119-L122
  
  响应主体:
    HostPathData {Info []HostNodePath}                    L62-L64
    PodPathData  {Info []PodPath}                         L125-L127
  
  单行:
    HostNodePath {HostID int64, Paths []NodePath}         L67-L70
    NodePath     {BizID, BizName, ClusterID, ClusterName} L73-L78
    PodPath      {BizID, BizName, ClusterID, ClusterName,
                 NamespaceID, Namespace, Kind, WorkloadID,
                 WorkloadName, PodID}                     L130-L141
  
  内部关联结构:
    HostNodeRelation {BizIDs []int64,
                     HostWithNode map[int64][]Node,
                     ClusterIDWithName map[int64]string}  L81-L85
  

5.2 字段命名:JSON tag 与 BSON tag 一致性的关键

src/kube/types/node.go 第 67-94 行的 Node 字段:

// src/kube/types/node.go 第 67-94 行(精选)
  type Node struct {
      ID             int64  `json:"id,omitempty" bson:"id"`
      BizID          int64  `json:"bk_biz_id,omitempty" bson:"bk_biz_id"`
      SupplierAccount string `json:"bk_supplier_account,omitempty" bson:"bk_supplier_account"`
      HostID         int64  `json:"bk_host_id,omitempty" bson:"bk_host_id"`
      ClusterID      int64  `json:"bk_cluster_id,omitempty" bson:"bk_cluster_id"`
      ClusterUID     string `json:"cluster_uid,omitempty" bson:"cluster_uid"`
      HasPod         *bool  `json:"has_pod,omitempty" bson:"has_pod"`
      table.Revision `json:",inline" bson:",inline"`
  }
  

每个字段都有 JSON tag 与 BSON tag 一一对应

字段JSONBSON唯一标识符前缀
BizID bk_biz_id bk_biz_id bk_(蓝鲸 ID 命名空间)
HostID bk_host_id bk_host_id bk_
ClusterID bk_cluster_id bk_cluster_id bk_

这套规则来自 src/common,所有 bk_xxx 字段都是 CMDB 系统字段,跨对象共享。前端拿到 bk_host_id: 10086 可以直接调通用的 /find/host 接口再查一遍拿到物理机详情,跳出 k8s 视图。

5.3 请求字段:为什么 HostPathOption.HostIDs 的 JSON tag 是 "ids"

src/kube/types/topo.go L33-L35:

// src/kube/types/topo.go 第 33-35 行
  type HostPathOption struct {
      HostIDs []int64 `json:"ids"`
  }
  

字段名是 HostIDs,但对外 JSON 名是 ids。这有两个目的:

  1. 前端简化传参:前端不需要知道"哪类资源的 id",只在上下文里赋"这些 host id 要反查",用 ids 即可
  2. 复用请求体:未来 FindNodePathForCluster 等同类接口如果出现,"ids" 作为通用入参,不需要改前端调用

5.4 PodPath.Kinds 字段 — WorkloadType 的对外暴露

src/kube/types/topo.go L137 的 PodPath.Kind,类型是 WorkloadType(在 src/kube/types/types.go L54 定义):

// src/kube/types/types.go 第 54-66 行
  type WorkloadType string
  
  func (t WorkloadType) Validate() error {
      switch t {
      case KubeDeployment, KubeStatefulSet, KubeDaemonSet,
          KubeGameStatefulSet, KubeGameDeployment, KubeCronJob,
          KubeJob, KubePodWorkload:
          return nil
      default:
          return fmt.Errorf("can not support this type of workload, kind: %s", t)
      }
  }
  

WorkloadType 是字符串枚举,JSON 序列化时输出字符串字面量(如 "deployment""statefulSet")。前端在 PodPath.Kind == "deployment" 时决定展示图标、走 Deployment 详情查询接口

5.5 表名常量:cc_NodeBase / cc_PodBase 等

src/kube/types/types.go L191-L239 的表名常量:

// src/kube/types/types.go 第 191-239 行(精选)
  const (
      BKTableNameBaseCluster       = "cc_ClusterBase"
      BKTableNameBaseNode          = "cc_NodeBase"
      BKTableNameBaseNamespace     = "cc_NamespaceBase"
      BKTableNameBaseWorkload      = "cc_WorkloadBase"
      BKTableNameBaseDeployment    = "cc_DeploymentBase"
      BKTableNameBaseStatefulSet   = "cc_StatefulSetBase"
      BKTableNameBaseDaemonSet     = "cc_DaemonSetBase"
      BKTableNameGameDeployment    = "cc_GameDeploymentBase"
      BKTableNameGameStatefulSet   = "cc_GameStatefulSetBase"
      BKTableNameBaseCronJob       = "cc_CronJobBase"
      BKTableNameBaseJob           = "cc_JobBase"
      BKTableNameBasePodWorkload   = "cc_PodWorkloadBase"
      BKTableNameBaseCustom        = "cc_CustomBase"
      BKTableNameBasePod           = "cc_PodBase"
      BKTableNameBaseContainer     = "cc_ContainerBase"
      BKTableNameNsSharedClusterRel = "cc_NsSharedClusterRelation"
  )
  

所有表名带 Base 后缀——这是 CMDB 的核心架构约定:"Base" 表存放模型主数据,"属性数据"由缓存或动态 schema 派生。3.14.6 删除了 cc_KubeHostRelationship 这种关联表的根本原因也在这里——反范式写入 Base 表本身就是设计选择。

类型设计四大要点

  • JSON/BSON 一致:所有字段 bk_ 前缀对齐
  • Validate() 内嵌:每个请求类型自带参数校验,反范式字段强校验(fail-fast)
  • WorkloadType 字符串枚举:JSON 序列化友好、前端 switch 友好
  • 所有集合都带 Base 后缀:CMDB "Base 表" 是反范式一致性的物理基础

六、shared 集群语义:为什么 Pod 反查和 Host 反查不一样

6.1 What — shared cluster 概念

在 #17 里讲过:蓝鲸 CMDB 支持"平台业务(plat biz)创建 k8s 集群,普通业务 ns 接入"。这背后是 src/kube/types/types.go L238 的 BKTableNameNsSharedClusterRel = "cc_NsSharedClusterRelation",对应 cc_NsSharedClusterRelation 表。

这张表的字段语义看 src/source_controller/coreservice/core/kube/kube.go 第 165-273 行的 getSharedClusterInfo

// src/source_controller/coreservice/core/kube/kube.go 第 240-273 行(精简)
  clusterPlatBizMap := make(map[int64]int64)
  for _, rel := range sharedRel {
      clusterPlatBizMap[rel.ClusterID] = rel.AsstBizID
  
      inputBiz, exists := mismatchNsBizIDMap[rel.NamespaceID]
      if !exists {
          continue
      }
  
      if inputBiz != rel.AsstBizID {
          // 共享 ns 的关联业务必须匹配,否则报错
          return nil, kit.CCError.CCErrorf(common.CCErrCommParamsInvalid, types.KubeNamespace)
      }
  
      delete(mismatchNsBizIDMap, rel.NamespaceID)
  }
  

关键逻辑:sharedRelrel.AsstBizID 是"这个 ns 反向关联的平台业务"。如果 inputBiz != rel.AsstBizID,说明 ns 配置错了,直接报错。

6.2 Why — 为什么 host 反查不需要 shared 语义?

host 永远是单业务的

src/source_controller/coreservice/core/kube/kube.go 第 308-356 行的 getNodeRelatedInfo

// src/source_controller/coreservice/core/kube/kube.go 第 337-355 行
  hostFilter := map[string]interface{}{
      common.BKHostIDField: map[string]interface{}{common.BKDBIN: hostIDs},
  }
  util.SetModOwner(hostFilter, kit.SupplierAccount)
  
  relations := make([]metadata.ModuleHost, 0)
  err = mongodb.Client().Table(common.BKTableNameModuleHostConfig).Find(hostFilter).
      Fields(common.BKAppIDField, common.BKHostIDField).All(kit.Ctx, &relations)
  
  hostMap := make(map[int64]int64)
  for _, rel := range relations {
      hostMap[rel.HostID] = rel.AppID
  }
  

第 344 行查的是 cc_ModuleHostConfig(主机-模块关系表,这是经典的 CMDB 设计),一台 host 通过模块关系只能落到一个业务

所以 host 反查时,BizID 是单值,不需要 fan-out 重写;而 Pod 反查时,BizID 因为 shared 协议可能多值。

6.3 How — Pod 反查的 BizID 复杂流

combinePodPath 流程画出来:

Step 1: ListPod 拿到 pods
          包含 pod[].BizID(原始业务)、pod[].Namespace、pod[].Ref
          v
  Step 2: buildPodPaths 逐个校验
          强校验 ClusterID != 0 / NamespaceID != 0 / Namespace != "" / Ref != nil ...
          生成 PodPath,累加 clusterIDs(去重用)、allBizIDs(拼装时拿 BizName 用)
          v
  Step 3: combinePodPath
          3.1 SearchCluster cc_ClusterBase.id IN clusterIDs  -> clusterMap
          3.2 getBizIDWithName cc_BizBase != bizIDs  -> bizIDWithName
          3.3 遍历 paths:
              - 若 path.BizID != 请求方 bizID,说明是 shared 来的:
                * sharedClusterPaths = append(sharedClusterPaths, path)(保留原始 BizID/BizName)
              - 主路径 path.BizID/BizName 改写为请求方 bizID
          v
  Step 4: 返回 paths + sharedClusterPaths
          前端拿到的是一条 Pod 对应多条 PodPath(主 + shared 副本)
  

6.4 为什么 Step 3.2 用了看上去"反常"的过滤?

src/scene_server/topo_server/service/kube/node.go 第 191-201 行的 getBizIDWithName

// src/scene_server/topo_server/service/kube/node.go 第 191-201 行
  func (s *service) getBizIDWithName(kit *rest.Kit, bizIDs []int64) (map[int64]string, error) {
      query := &metadata.QueryCondition{
          Fields: []string{
              common.BKAppIDField,
              common.BKAppNameField,
          },
          Condition: mapstr.MapStr{
              common.BKDataStatusField: mapstr.MapStr{common.BKDBNE: bizIDs},
          },
          DisableCounter: true,
      }
      _, instItems, err := s.Logics.BusinessOperation().FindBiz(kit, query)
  

第 198 行的过滤用了 bk_data_status NE bizIDs。这一行的实际执行结果需要结合 src/common 中的 FindBiz 实现共同阅读 —— BKDataStatusFieldbk_data_status 字段,与请求里传入的 BizID 集合语义有重叠但字段名不同,因此最终拼出的 MongoDB 条件由 FindBiz 的实现决定。在解读该行时务必连同 FindBiz 的过滤逻辑一起看。

阅读提示

第 198 行的 bk_data_status NE bizIDs 写在 src/scene_server/topo_server/service/kube/node.go 中。其最终拼出的 MongoDB 条件需要结合 src/common 中的 BusinessOperation().FindBiz 一起阅读——单看这一行直接断言语义容易误读。本文按字面源码呈现,不对其执行行为做超出可见上下文的推断。

6.5 my-idea-block:反查机制的核心抽象

源码视角:反查链路复盘 -- 4 个"无关联表"事实

1. cc_NodeBase 直接带 bk_host_id

src/kube/types/node.go L71:HostID int64 bson:"bk_host_id"。这不是外键,不是关联,是物理字段。host → node 反查不需要 JOIN、不需要 LOOKUP、单表 bk_host_id IN 直接拿。

2. cc_PodBase 直接带 bk_host_id / bk_node_id / node_name

src/kube/types/pod.go L242-L249 的 SysSpec。Pod 行被插入前,CC 已经知道这台 host 是哪台 Node、Node 叫啥 -- 由 GetSysSpecInfoByCond 在写之前批量预填。

3. cc_PodBase 直接带 ref{Kind, Name, ID}

Ref 子结构记录 Pod 由哪个 workload 创建。Pod 反查的 Kind / WorkloadID / WorkloadName 全部从 pod.Ref 来,无需再查 workload 表。

4. shared 协议靠 cc_NsSharedClusterRelation 单表承担

src/kube/types/types.go L238 的 cc_NsSharedClusterRelation唯一一张和拓扑语义有关的"额外"表。它不存 host/node/pod 的物理关联,而是 ns 维度的平台业务 ↔ 命名空间反向关联。Pod 反查经过它是为了"扩展可见 Pod 集合"。

总结:3.14.6 的反查机制没有 cc_KubeHostRelationship 这种"节点-主机关联表",是通过反范式把 ID 直接放进对象表,省掉 JOIN。

七、FAQ -- 20 组 Q&A

FAQ 分组说明

这一节按 "先查证-后理解-再扩展-终落地" 的学习路径组织 20 个问题:

  • Q1-Q4:源码查证层面(哪些字段、哪些表名、哪些接口)
  • Q5-Q9:反查机制理解层面(反范式 vs 关联表、两段式流水线、shared 语义)
  • Q10-Q14:链路反推层面(host 找不到怎么办、Pod 跨 cluster 怎么办、shared ns 校验)
  • Q15-Q18:扩展到上下游接口(ListContainer、ListContainerByTopo、KubeTopoPath)
  • Q19-Q20:生产落地 / 排障

Q1. FindNodePathForHost 和 FindPodPath 在哪个包里?端点路由是什么?

都在 src/scene_server/topo_server/service/kube/ 包下。路由注册在 src/scene_server/topo_server/service/kube/service.go 第 72-73 和 82 行:/find/kube/host_node_path(POST)和 /find/kube/pod_path(POST)。它们走的是同一个 rest.RestUtility 工具,与其他 kube CRUD 接口(/findmany/kube/cluster 等)在同一文件注册。

Q2. cc_KubeHostRelationship 这张关联表在 3.14.6 真实存在吗?

不存在。3.14.6 中并没有独立的"主机↔节点↔Pod"关联表,宿主机→容器拓扑的关系是通过反范式三件套直接落地在对象表里的:Node 表自带 bk_host_id 字段(src/kube/types/node.go L71)、Pod 表自带 SysSpecsrc/kube/types/pod.go L242-L249),反查不需要独立关联表。

Q3. Node 反查走的是哪张表?查询字段是?

cc_NodeBase 表,条件 bk_host_id IN hostIDs具体在 src/scene_server/topo_server/service/kube/node.go 第 117-125 行:mapstr.MapStr{common.BKHostIDField: mapstr.MapStr{common.BKDBIN: hostIDs}}。表名常量在 src/kube/types/types.go L196 BKTableNameBaseNode = "cc_NodeBase"

Q4. Pod 反查走的是哪张表?查询字段是?

cc_PodBase 表,条件 bk_id IN podIDssrc/scene_server/topo_server/service/kube/pod.go 第 58-66 行:用 filtertools.GenAtomFilter(common.BKFieldID, filter.In, req.PodIDs) 构造,字段白名单只取 6 列(id/bk_biz_id/bk_cluster_id/bk_namespace_id/namespace/ref)。表名常量 src/kube/types/types.go L232 BKTableNameBasePod = "cc_PodBase"

Q5. 为什么不建一张独立的"主机-节点"关联表?

反范式三件套代替了独立关联表。Node 行 bk_host_id、Pod 行 SysSpec{ bk_host_id, bk_node_id, node_name }、Pod 行 Ref{ Kind, Name, ID } 都内嵌在对象表里。代价是写时多查几张表(GetSysSpecInfoByCondsrc/source_controller/coreservice/core/kube/kube.go L47-L124),收益是反查路径单表 IN 查询、无需 JOIN。

Q6. 两段式反查流水线(两步)是如何分工的?

第一步单表 IN 拿 ID,第二步批量补名称。链路 A(host):第一步 cc_NodeBase.bk_host_id IN 拿到 Node + BizID + ClusterID;第二步 SearchCluster + FindBiz 补 ClusterName + BizName。链路 B(pod):第一步 cc_PodBase.bk_id IN 拿到含 namespace/ref 的 pod 行;第二步 SearchCluster 补 ClusterName,FindBiz 补 BizName。两段顺序执行,第二段工作量由第一段结果大小决定。

Q7. shared 命名空间为什么会导致 Pod 反查路径"分裂"为多份?

因为共享协议的 ns 让一个 Pod 可以被多个业务看到。src/scene_server/topo_server/service/kube/pod.go 第 157-209 行 combinePodPath:当 pod.BizID != req.BizID 时(pod 数据归属另一个业务,但当前用户通过 shared 协议也能看到),路径会被追加到 sharedClusterPaths 中保留原始 BizID。同时主路径会改写 BizID 为请求方 bizID。最终返回 paths + sharedClusterPaths...,前端根据 BizID 区分。

Q8. cc_NsSharedClusterRelation 表存的是什么?

存"命名空间 ↔ 反向关联平台业务"的映射。表名常量在 src/kube/types/types.go L238。每个 ns 可以关联一个平台业务(AsstBizID),平台业务下的 Pod 反查就是通过这个关联"看见"其他业务 ns 下的 Pod。具体反查 / 校验逻辑在 src/source_controller/coreservice/core/kube/kube.go L165-L273。

Q9. Host 反查为什么不需要 shared 语义?

host 永远单业务。src/source_controller/coreservice/core/kube/kube.go 第 308-356 行的 getNodeRelatedInfo:host 通过 cc_ModuleHostConfig(主机-模块关系)落到一个业务,模块关系是单值。所以 FindNodePathForHost 只需 fan-out 授权但不需要二次切片 BizName。

Q10. 如果 hostIDs = [10086] 但 cc_NodeBase 找不到对应行,函数返回什么?

返回 HostPathData{Info: []HostNodePath{}},HTTP 状态 200。src/scene_server/topo_server/service/kube/node.go 第 132-135 行 getHostNodeRelationlen(resp.Data) == 0 时返回 nil, nil。注释明确:"returning nil means that there is no node on the host, which is legal"。上层第 55-60 行判 relation == nil 后直接 RespEntity(HostPathData{Info: []HostNodePath{}})

Q11. 如果一台 host 对应多个集群的不同 Node,授权为何 fan-out?

因为权限是按业务颗粒度裁剪的。src/scene_server/topo_server/service/kube/node.go 第 63-71 行:authRes := make([]acmeta.ResourceAttribute, len(relation.BizIDs)),对所有命中的 BizID 都生成一次授权资源。如果用户对任意一个 BizID 没权限,整次调用 RespNoAuth,不返回任何数据,避免越权信息泄露。

Q12. Pod 反查时 missing 字段怎么办?强校验的逻辑在哪?

fail-fast 直接报错,不返回半残数据。src/scene_server/topo_server/service/kube/pod.go 第 101-138 行的 buildPodPathsClusterID == 0 / NamespaceID == 0 / Namespace == "" / Ref == nil / Ref.Kind == "" / Ref.Name == "" / Ref.ID == 0 任一缺失就 return nil, fmt.Errorf(...)。设计意图:反范式字段缺失说明写入链路有问题,反查不能遮蔽。

Q13. 反查字段白名单里的 types.RefField 对应什么?

对应 "ref",是 Pod 行里的子文档结构。常量在 src/kube/types/types.go L434-L435 注释 "// pod relate workload field"。结构定义在 src/kube/types/pod.go L252-L258:type Ref struct { Kind string; Name string; ID int64 }。Pod 反查拿到 pod.Ref 后直接拿 Kind/Name/ID,无需二次查 workload 表。

Q14. 反查的 hostIDs / podIDs 上限是多少?

有上限,在 Validate() 里硬截。src/kube/types/topo.go 第 46-51 行 HostPathOption.Validateif len(h.HostIDs) > common.BKMaxLimitSize { return ...XXExceedLimit... }。Pod 反查的 PodPathOption.Validate(L109-L114)也是同一规则。BKMaxLimitSizesrc/common 的全局常量。

Q15. 反查 Pod 的同时怎么拿这个 Pod 上的容器?

src/source_controller/coreservice/core/kube/container.go L35-L172 的 ListContainerByPod它有三种调用分支:

  • len(input.PodCond) == 0:纯 container 条件,第 53-77 行的 listContainerByContainerCond
  • len(input.ContainerCond) == 0:纯 pod 条件,第 80-108 行的 listContainerByPodCond,用 $lookup 把 container 拼到 pod 结果里
  • 两者都有:第 111-128 行的 listContainerByBothCond

Q16. 反查接口怎么和"业务拓扑树"对接?

靠响应类型的 Info[] 数组,UI 按类型决定后续路由。UI 拿到 HostPathData.Info[i].Paths 是 HostID → BizID/ClusterID 的二维表;拿到 PodPathData.Info[i] 是 PodID → BizID/ClusterID/NamespaceID/WorkloadID 的全链。UI 按元素类型再触发具体子树的资源查询,例如 workload 名反向拿 /findmany/kube/workload/{kind}

Q17. FindNodePathForHost 内部的"去重"做了什么?

同 host + 同 biz + 同 cluster 组合只留一条。src/scene_server/topo_server/service/kube/node.go 第 88-92 行:unique := strconv.FormatInt(bizID, 10) + ":" + strconv.FormatInt(clusterID, 10),拼接字符串做去重 key。因为一台 host 可能跑同集群多个 Node,UI 不应该看到重复的 (BizID, ClusterID) 路径。

Q18. SearchNode / ListPod 这两个 coreservice 内部方法的实现层在哪?

src/source_controller/coreservice/service/kube/具体的 dao 实现位于 coreservice/core/kube/ -- 例如 SearchNode 落到 src/source_controller/coreservice/core/kube/kube.go 第 114-156 行的 getNodeInfo(用于 pod 创建时反查),ListPod 由 coreservice 直接转发到 mongodb 客户端。

Q19. 反查超时报错:genSharedNsListCond failed 怎么办?

多半是 shared 命名空间配置被改坏了。src/scene_server/topo_server/service/kube/pod.go 第 58-63 行 GenSharedNsListCond 出错通常是输入 BizID 或者传入的 podID 跟 shared 表里的关系对不上。可以参考 src/scene_server/topo_server/logics/kube/kube.go 检查 GenSharedNsListCond 的实现并核对 BizID。

Q20. 文章提到"如果坚持用 cc_KubeHostRelationship 会怎样",这个关联表被设计为坏方案吗?

不是坏方案,是 3.14.6 没选择这条路。关联表设计的代价是写时双写 / 反查时 JOIN / 删除时级联 / 共享语义歧义。3.14.6 用反范式把 host/node/pod 关联直接放进对象表,省掉这些代价。如果未来 k8s 跨集群联邦或资源调度需要"动态关联",再单独建关联表也不迟 -- 这是"按需演进"的工程哲学,不是"一次到位"。

FAQ 全篇总纲

20 个问题覆盖了三个核心维度:

  • 查证维度(Q1-Q4):源码位置、表名、接口路径
  • 理解维度(Q5-Q9):反范式设计、两段式流水线、shared 语义
  • 生产维度(Q10-Q20):空数据处理、授权 fan-out、强校验、容器扩展、UI 对接、排障

回到主线:FindNodePathForHost + FindPodPath + 反范式三件套 = 蓝鲸 CMDB 3.14.6 的容器拓扑关联完整机制。这张牌打出去之后,从 Pod 反推到机柜就是 4 次 IN 查询、不超过 100ms

八、Roadmap -- 下一篇预告

本篇容器拓扑关联的文章读完后,下一篇 #19:主机锁定 -- 并发控制与分布式锁 会深入讲反向追溯路径被并发竞争时的处理:

多个 SRE 同时操作"重命名 host"或者"删除 pod"时,反查路径返回的数据可能正在被改写。如何用 RedisLock + CMDB 内置的 host_lock 机制保证一致性?

下一篇会拆解 src/scene_server/host_server/service/host_lock.go 等关键文件,并详解"无锁路径"和"有锁路径"的边界。


posted @ 2026-07-07 20:57  左扬  阅读(3)  评论(0)    收藏  举报