蓝鲸 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.go 的 FindNodePathForHost 和 src/scene_server/topo_server/service/kube/pod.go 的 FindPodPath 这两个反向追溯入口的完整实现。
本篇要点
在 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]
本篇学习重点
必须掌握
- 反查拓扑的入口:FindNodePathForHost 和 FindPodPath 的端点路由、参数校验、授权模型
- 反范式三件套:Node.HostID + SysSpec.HostID/NodeID/Node + Pod.Ref 的字段语义和写入时机
- 两条反查链路:hostIDs → cc_NodeBase.bk_host_id IN、podIDs → 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=2 和 biz=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.RefField(src/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 改写成请求方 bizID,BizName 同步更新——这是主路径
- 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 一一对应:
| 字段 | JSON | BSON | 唯一标识符前缀 |
|---|---|---|---|
| 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。这有两个目的:
- 前端简化传参:前端不需要知道"哪类资源的 id",只在上下文里赋"这些 host id 要反查",用 ids 即可
- 复用请求体:未来 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)
}
关键逻辑:sharedRel 里 rel.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 实现共同阅读 —— BKDataStatusField 是 bk_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:反查机制的核心抽象
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 表自带 SysSpec(src/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 podIDs。在 src/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 } 都内嵌在对象表里。代价是写时多查几张表(GetSysSpecInfoByCond 在 src/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 行 getHostNodeRelation:len(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 行的 buildPodPaths:ClusterID == 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.Validate:if len(h.HostIDs) > common.BKMaxLimitSize { return ...XXExceedLimit... }。Pod 反查的 PodPathOption.Validate(L109-L114)也是同一规则。BKMaxLimitSize 是 src/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 等关键文件,并详解"无锁路径"和"有锁路径"的边界。

浙公网安备 33010602011771号