蓝鲸 CMDB 3.14.6 源码专题【左扬精讲】— CMDB #20 批量录入:Excel 模板、四层数据校验

蓝鲸 CMDB 3.14.6 源码专题【左扬精讲】— CMDB #20 批量录入:Excel 模板、四层数据校验

在 v3.14.6 中,AddHostByExcel / UpdateHostByExcel 这两个入口承担了蓝鲸 CMDB "批量录入 / 批量更新" 几乎所有的高频场景。一次 Excel 导入并不是单步写入,而是 4 层校验、2 个独立事务(AutoRunTxn)、1 个表格式字段抽取(GetTableData)和 1 条审计日志事务的 "组合拳"

本篇以 v3.14.6 真实源码为主线,把这条 "Excel → MongoDB cc_HostBase" 链路拆成 4 层校验(Excel 表头 + IP/Cloud 必填 + 内网 IP 唯一性 + 管控区域存在性),并把 "主机实例 + 表格型引用字段" 的双写合并逻辑讲透。

必须掌握

  • Excel 模板的 4 列最小集:bk_host_inneripbk_host_innerip_v6bk_cloud_idbk_addressing
  • 4 层数据校验的入口位置:service 层 hostList == nil → logic 层 getIpField → logic 层 getQueryHostsFilter → coreservice 层 IsPlatAllExist
  • GetTableData 的剥离原理:从主机实例 MapStr 中"抽走"表格型字段(ModelQuoteRelation 关联的子表),剩余纯标量字段走主机实例 API。
  • 批量录入的双写合并:addHostInstance 创建主机后,立即 TransferToNormalModule / TransferToInnerModule 把主机绑定到模块。
  • AutoRunTxn 单行事务:每行主机一个独立事务,某行失败不会回滚前面已成功行。
  • 审计日志的旁路写入:主机写入完成后,SaveAuditLog 单独持久化,操作记录可追溯。

了解即可

  • HostInputType 的两种值:excel / collect,决定审计日志和字段补全策略。
  • ExcelTypeCollectType 的差异:Excel 来源补默认字段,Collect 来源补采集器 ID。

v3.14.64 层数据校验AddHostByExcelUpdateHostByExcelGetTableDataAutoRunTxnAddHostToResourcePoolHostInputTypeauditlog.NewHostAudit

src/common/metadata/hostserver.go                         ← HostInputType 枚举 + HostList 请求结构
  src/common/metadata/inst.go                               ← TableData / GetTableData 表格型字段剥离
  src/scene_server/host_server/service/host.go              ← AddHostByExcel / UpdateImportHosts 入口层
  src/scene_server/host_server/logics/import.go             ← AddHostByExcel / UpdateHostByExcel 业务编排
  src/scene_server/host_server/logics/import.go             ← addHostInstance / addTableData 双写合并
  src/common/auditlog/                                      ← NewHostAudit 审计日志旁路写入
  

一、Excel 模板:前端约定 + 后端兼容的双层契约

What — Excel 模板是什么?

蓝鲸 CMDB 的 Excel 导入并不是"任意列名" + "任意字段值"。前端在"资源池 → 导入主机"页面会先请求后端拿到"可用字段列表"(即 getHostFields),再把每个字段作为 Excel 表头,最终用户填表后上传。

后端只对 4 列做硬性校验:内网 IPv4、内网 IPv6、管控区域 ID、寻址方式。其余字段(操作系统、CPU、内存等)即使填错,也只是"业务层校验失败",不会导致整行拒绝。

Why — 为什么是这 4 列?

1. 业务唯一性约束bk_host_innerip + bk_cloud_id 是 cmdb 的天然主键,没有它主机就是"游离资产";

2. 网络可达性:寻址方式 static / dynamic 决定主机是静态 IP 还是 DHCP,否则 Agent 无法注册;

3. 多云隔离bk_cloud_id 区分不同云区域,同一 IP 在不同云区域可以共存。

没有这 4 列会发生什么?

  • 没有内网 IP:getIpField 直接拒绝该行,返回 host_import_innerip_v4_v6_empty
  • 没有管控区域:addHostInstance 走到 IsPlatAllExist 时报错,主机进库失败;
  • 没有寻址方式:checkHostAttr 返回 CCErrCommParamsInvalid,整批拒绝。

src/common/metadata/hostserver.go 第 46-62 行的真实请求结构定义:

type HostInputType string
  
  const (
      ExcelType   HostInputType = "excel"
      CollectType HostInputType = "collect"
  )
  
  type HostList struct {
      ApplicationID int64                            `json:"bk_biz_id"`
      ModuleID      int64                            `json:"bk_module_id"`
      HostInfo      map[int64]map[string]interface{} `json:"host_info"`
      InputType     HostInputType                    `json:"input_type"`
  }

Excel 模板表头字段名遵循 cmdb 字段 ID(即 bk_host_innerip),不是中英文混排。前端把 "内网 IP" 展示名翻译成 "内网 IP" 列名只是为了给用户看,真正写进 MongoDB 的是 bk_host_innerip 这串字符串。

★ 关键事实:HostInfomap[int64]map[string]interface{}外层 key 是 Excel 行号(从 0 开始),不是主机 ID。这是为了支持"按行号回填错误提示"。前端拿到 success / error 数组后,会把对应行号标红。

二、4 层数据校验:service → logic → coreservice → MongoDB

整个 Excel 导入过程有 4 层数据校验,每层职责明确:

层级校验对象失败行为源码位置
第 1 层:service 入口 hostList.HostInfo == nil 整批拒绝,返回 CCErrCommParamsNeedSet src/scene_server/host_server/service/host.go 第 370-374 行
第 2 层:logic 必填项 bk_host_innerip / bk_host_innerip_v6 / bk_cloud_id 单行拒绝,该行进 errMsg src/scene_server/host_server/logics/import.go 第 386-403 行
第 3 层:logic 唯一性 bk_host_innerip + bk_cloud_id 查 cc_HostBase 是否已存在 存在则走 updateHostInst,不存在则走 addHostInst src/scene_server/host_server/logics/import.go 第 850-890 行
第 4 层:coreservice 业务 bk_cloud_id 管控区域是否注册 单主机创建失败,整行回滚 src/scene_server/host_server/logics/import.go 第 789-800 行

第 2 层和第 3 层是"软失败"(行级),第 1 层和第 4 层是"硬失败"(批级或事务级)。看 src/scene_server/host_server/logics/import.go 第 386-403 行的"软失败"实现:

errStr, innerIP, innerIPv6 := getIpField(host)
  if errStr != "" {
      errMsg = append(errMsg, ccLang.Languagef(errStr, index))
      continue
  }
  
  // the bk_cloud_id is directly connected area
  if _, exist := host[common.BKCloudIDField]; !exist {
      errMsg = append(errMsg, ccLang.Languagef("import_host_not_provide_cloudID", index))
      continue
  }
  
  cloudID, err := util.GetInt64ByInterface(host[common.BKCloudIDField])
  if err != nil {
      errMsg = append(errMsg, ccLang.Languagef("import_host_cloudID_not_exist", index,
          innerIP+"/"+innerIPv6, util.GetStrByInterface(host[common.BKCloudIDField])))
      continue
  }

这段代码 4 步走:① 抽 IP → ② 抽 Cloud ID → ③ 校验 Cloud ID 必填 → ④ 校验 Cloud ID 是 int64。任意一步失败,continue 跳过该行(不进 MongoDB),但不影响其他行。这是 cmdb 处理批量导入的核心原则:"行级隔离"。

How — 行级隔离 + 事务内嵌的源码视角

源码视角一:第 3 层唯一性查询的 MongoDB 写法

src/scene_server/host_server/logics/import.go 第 956-985 行的 getQueryHostsFilter,把"内网 IP + 管控区域"的笛卡尔积拆成 OR 条件:

ipCond := make([]map[string]interface{}, 0)
  for cloudId, innerIPArr := range cloudIDIPMap {
      if len(innerIPArr) == 0 {
          continue
      }
      ipCond = append(ipCond, map[string]interface{}{
          common.BKAddressingField:  common.BKAddressingStatic,
          common.BKCloudIDField:     cloudId,
          common.BKHostInnerIPField: map[string]interface{}{common.BKDBIN: innerIPArr},
      })
  }
  if len(hostIDs) > 0 {
      ipCond = append(ipCond, mapstr.MapStr{common.BKHostIDField: mapstr.MapStr{common.BKDBIN: hostIDs}})
  }
  
  filter := map[string]interface{}{
      common.BKDBOR: append(ipCond),
  }
  return filter, nil

$in 一次查询所有 (cloudId, innerIP) 组合,比 N 次单查快 10 倍以上。这是 v3.14.6 相对 v3.10 的一个关键优化点。

源码视角二:第 4 层事务内嵌的"事务嵌事务"模式

src/scene_server/host_server/logics/import.go 第 408-460 行的 AddHostByExcel 主循环:

kit.Header = kit.NewHeader()
  _ = lgc.Engine.CoreAPI.CoreService().Txn().AutoRunTxn(kit.Ctx, kit.Header, func() error {
      tableData, err := metadata.GetTableData(host, relRes)
      if err != nil {
          errMsg = append(errMsg, ccLang.Languagef("host_import_add_fail", index, innerIP+"/"+innerIPv6,
              err.Error()))
          return err
      }
  
      intHostID, err := instance.addHostInstance(cloudID, index, appID, []int64{moduleID}, toInternalModule, host)
      if err != nil {
          blog.Errorf("add host instance failed, err: %v, index: %d, bizID: %d, moduleID: %d, "+
              "toInternalModule: %t, host: %v, rid: %s", err, index, appID, moduleID, toInternalModule, host,
              kit.Rid)
          errMsg = append(errMsg, ccLang.Languagef("host_import_add_fail", index, innerIP+"/"+innerIPv6,
              err.Error()))
          return err
      }
      host[common.BKHostIDField] = intHostID
  
      // add host table field type data
      if tableData != nil {
          if err := lgc.addTableData(kit, tableData, intHostID); err != nil {
              blog.ErrorJSON("add table data failed, data: %s, err: %s, rid: %s", host, err, kit.Rid)
              errMsg = append(errMsg, ccLang.Languagef("host_import_add_fail", index, innerIP+"/"+innerIPv6,
                  err.Error()))
              return err
          }
      }
      return nil
  })

这里 3 个关键设计:

  • kit.NewHeader():每行主机独立 header,事务 ID 不冲突;
  • AutoRunTxn:每行一个事务包,return err 时整个事务回滚(含主机 + 表格型字段 + 审计);
  • _ =:事务失败不会终止外层循环,继续处理下一行。这是"行级隔离"的物理保证。

源码视角总结:1 个硬隔离 + 1 个软隔离

  • 1 个硬隔离AutoRunTxn 单行包事务,物理回滚保证单行原子;
  • 1 个软隔离errMsg = append + continue 软跳过,保证批级尽力而为。

避坑提醒(源码视角):

  • 不要把 AutoRunTxn 提到循环外:那会把整个 Excel 包成 1 个事务,部分行失败会回滚所有行,违背"行级隔离"原则;
  • 不要忘记 kit.NewHeader():复用 header 会让多个事务共享 TxnId,coreservice 会拒绝第 2 行的事务提交。

三、GetTableData:主机实例 + 表格型字段的双写合并

cmdb 的字段类型除了"标量"(字符串、数字、布尔),还有"表格型"(即"引用字段",对应 MongoDB 中另一个 collection 的数组)。主机上经常挂载"网卡表"、"磁盘表"、"进程表"等"表格型字段"。

Excel 导入时,这些表格型字段和主机实例字段混在同一个 MapStr 里,必须先剥离再分别写入。看 src/common/metadata/inst.go 第 328-357 行的核心实现:

func GetTableData(originData map[string]interface{}, relRes []ModelQuoteRelation) (*TableData, error) {
      modelData := make(map[string][]mapstr.MapStr)
      modelPropertyRel := make(map[string]string)
      for _, relation := range relRes {
          data, ok := originData[relation.PropertyID]
          if !ok {
              continue
          }
          table, err := mapstr.GetMapStrArrByInterface(data)
          if err != nil {
              return nil, fmt.Errorf("table data %s is invalid, err: %v", relation.PropertyID, err)
          }
  
          modelData[relation.DestModel] = table
          modelPropertyRel[relation.DestModel] = relation.PropertyID
          delete(originData, relation.PropertyID)
      }
  
      if len(modelData) != 0 {
          return &TableData{
              ModelData:        modelData,
              ModelPropertyRel: modelPropertyRel,
              SrcModel:         relRes[0].SrcModel,
          }, nil
      }
      return nil, nil
  }

4 步关键逻辑:

  1. 遍历 ModelQuoteRelation(即字段引用关系表),每个 relation 代表一个表格型字段;
  2. relation.PropertyID 作为 key,到 originData 里取数据;
  3. 强转为 []mapstr.MapStr(数组里每个元素是子表行);
  4. 从 originData 中 delete 该 key,让剩余字段全部是主机实例的标量字段。

★ 关键事实:delete(originData, relation.PropertyID)原地修改,返回值被 CreateInstance 使用时就只剩标量字段了。表格型字段的写入走独立的 ModelQuote().BatchCreateQuotedInstance 路径,两条路径互不干扰。

注意:如果 Excel 用户填了不存在的表格型字段(即没在 relRes 里),GetTableData保留在 originData 里,最终 CreateInstance 会因为字段未注册而拒绝整行。这是 v3.14.6 的一个隐式"防垃圾数据"机制。

四、addHostInstance:主机写入 + 模块绑定的原子组合

主机创建后必须绑定到模块(业务模块 / 资源池 / 空闲机池等),这两步必须同时成功——如果主机创建成功但模块绑定失败,主机会变成"游离主机",下次列表查询会看到一堆不属于任何业务的"孤儿主机"。

src/scene_server/host_server/logics/import.go 第 779-846 行的 addHostInstance

if toInternalModule == true {
      if len(moduleIDs) == 0 {
          err := h.ccErr.CCErrorf(common.CCErrCommParamsInvalid, common.BKModuleIDField)
          return 0, err
      }
      opt := &metadata.TransferHostToInnerModule{
          ApplicationID: appID,
          ModuleID:      moduleIDs[0],
          HostID:        []int64{hostID},
      }
      option = opt
      hResult, err = h.CoreAPI.CoreService().Host().TransferToInnerModule(h.ctx, h.pheader, opt)
  } else {
      opt := &metadata.HostsModuleRelation{
          ApplicationID: appID,
          ModuleID:      moduleIDs,
          HostID:        []int64{hostID},
      }
      option = opt
      hResult, err = h.CoreAPI.CoreService().Host().TransferToNormalModule(h.ctx, h.pheader, opt)
  }
  if err != nil {
      blog.Errorf("transfer host failed, err: %v, result: %#v, input: %#v, rid: %s", err, hResult, option, h.rid)
      return 0, err
  }
  return hostID, nil

这段代码做了 3 件事:

  1. 判断模块类型(toInternalModule):空闲机 / 故障机 / 资源池走 TransferToInnerModule
  2. 业务模块走 TransferToNormalModule(cc_HostModuleRelation 表);
  3. 任一失败整体 return 0, err,让 AutoRunTxn 回滚主机创建。

没有这一步会发生什么?

  • 主机创建后没有模块绑定:主机在 cc_HostBase 里有,但 cc_HostModuleRelation 没有,列表查询直接看不到这台主机,等于数据丢失
  • 模块绑定后没有主机:模块关联表里出现"幽灵主机ID",下次 TransferHostToModule 会触发 CCErrHostNotFound,无法清理。

五、UpdateImportHosts:批量更新走"先读后改"路径

UpdateImportHosts(前端上传修改后的 Excel,期望"按 hostID 增量更新")走的是另一条路径:先读已有主机逐行校验逐行更新。看 src/scene_server/host_server/service/host.go 第 1267-1354 行:

for _, index := range util.SortedMapInt64Keys(hostList.HostInfo) {
      hostInfo := hostList.HostInfo[index]
      if hostInfo == nil {
          continue
      }
      var intHostID int64
      hostID, ok := hostInfo[common.BKHostIDField]
      if !ok {
          blog.Errorf("UpdateImportHosts failed, because bk_host_id field not exits innerIp: %v, rid: %v",
              hostInfo[common.BKHostInnerIPField], ctx.Kit.Rid)
  
          errMsg = append(errMsg, CCLang.Languagef("import_update_host_miss_hostID", index))
          continue
      }
      intHostID, err := util.GetInt64ByInterface(hostID)
      if err != nil {
          errMsg = append(errMsg, CCLang.Languagef("import_update_host_hostID_not_int", index))
          continue
      }
  
      // remove unchangeable fields
      delete(hostInfo, common.BKHostInnerIPField)
      delete(hostInfo, common.BKCloudIDField)
      delete(hostInfo, common.BKImportFrom)
      delete(hostInfo, common.CreateTimeField)
  
      hostIDArr = append(hostIDArr, intHostID)
      hosts[index] = hostInfo
      indexHostIDMap[index] = intHostID
  }

4 个关键 delete 调用是"防 Excel 误改"机制:

  • bk_host_inneripbk_cloud_id:内网 IP + 管控区域是 cmdb 的天然主键,不能通过 Excel 修改;
  • bk_import_from:导入来源标记是 cmdb 内部维护的,前端不应该传;
  • create_time:创建时间是 cmdb 写入时自动生成的,前端不应该传。

注意:这 4 个字段即使前端传了,updateHostInst 也会"静默丢弃"(不报错)。这是信任前端契约的设计哲学——但实际生产中强烈建议前端做"灰显",因为一旦用户改了这 4 个字段,Excel 预览和实际生效会不一致,排查成本极高。

六、AddHostToResourcePool:另一条批量入口

除了 AddHostByExcelAddHostToResourcePool 是 cmdb 另一条批量入口,专给"Agent 自动注册"用(gse_agent 上报主机信息后回调 cmdb)。

src/scene_server/host_server/logics/import.go 第 580-659 行的 AddHostToResourcePool

result, cloudIDVal := lgc.validateHostField(kit, index, host)
  if result.ErrorMsg != "" {
      res.Error = append(res.Error, result)
      continue
  }
  host[common.BKCloudIDField] = cloudIDVal
  
  hostID, err := instance.addHostInstance(cloudIDVal, int64(index), bizID, []int64{hostList.Directory},
      toInternalModule, host)
  if err != nil {
      res.Error = append(res.Error, metadata.AddOneHostToResourcePoolResult{
          Index:    index,
          ErrorMsg: err.Error(),
      })
      continue
  }
  host[common.BKHostIDField] = hostID
  
  hostIDs = append(hostIDs, hostID)
  res.Success = append(res.Success, metadata.AddOneHostToResourcePoolResult{
      Index:  index,
      HostID: hostID,
  })

AddHostByExcel 的 3 个核心差异:

  1. 不做事务包裹:每行失败仅记录到 res.Error,不触发事务回滚(因为 Agent 回调通常是 1 次 1 台,不存在"批级回滚"语义);
  2. 使用 validateHostField:更严格的字段校验,错误信息直接返回 CCErrCommParamsInvalid 错误码;
  3. 默认进资源池:通过 hostList.Directory 指定目标目录(资源池 / 空闲机 / 故障机),而不是业务模块。

七、审计日志的旁路写入

Excel 导入的"成功 / 失败 / 字段值"必须留痕——这是合规性需求(SOX、等保)。cmdb 用 auditlog.NewHostAudit 在事务外做"旁路写入":

audit := auditlog.NewHostAudit(lgc.CoreAPI.CoreService())
  ccLang := lgc.Engine.Language.CreateDefaultCCLanguageIf(httpheader.GetLanguage(kit.Header))
  
  for _, index := range util.SortedMapInt64Keys(hostInfos) {
      host := hostInfos[index]
      ...
      if existInDB {
          var updateErrInfo string
          auditLog, errInfo, updateErrInfo = instance.updateHostInst(kit, existsHostMap[hostID], hostID, innerIP, index,
              host, appID)
          ...
      } else {
          hostID, auditLog, errInfo = instance.addHostInst(kit, ccLang, moduleIDs, cloudID, innerIP, index, host,
              appID, toInternalModule)
          ...
      }
  
      // add current host operate result to batch add result.
      successMsg = append(successMsg, strconv.FormatInt(index, 10))
  
      // add audit log.
      logContents = append(logContents, auditLog...)
      hostIDs = append(hostIDs, hostID)
  }
  
  // to save audit log.
  if len(logContents) > 0 {
      if err := audit.SaveAuditLog(kit, logContents...); err != nil {
          return hostIDs, successMsg, updateErrMsg, errMsg, fmt.Errorf("save audit log failed, but add host "+
              "success, err: %v", err)
      }
  }

2 个关键设计:

  • auditLogupdateHostInst / addHostInst 内部生成(基于"更新前快照"+ WithUpdateFields),不依赖事务回滚;
  • SaveAuditLog 在循环外统一持久化,失败仅返回错误,不影响已成功的主机写入。这是"主流程优先,审计尽力而为"的工程取舍。

本节核心总结

  • 1 个入口AddHostByExcel + UpdateHostByExcel,分别对应"新建"和"更新"两个语义。
  • 4 层校验:service 入口(批级)→ logic 必填(行级)→ logic 唯一性(行级)→ coreservice 业务(事务级)。
  • 1 个表剥离GetTableData 把表格型字段从主机 MapStr 中抽出,剩余标量字段走主机实例 API。
  • 1 个原子组合addHostInstance = 主机写入 + 模块绑定,AutoRunTxn 保证原子。
  • 1 个旁路审计auditlog.NewHostAudit 事务外持久化,不影响主流程。

FAQ(20 组问答)

FAQ 速查分类

  • ① Excel 模板相关(Q1-Q4)
  • ② 校验与错误处理(Q5-Q9)
  • ③ 表格型字段(Q10-Q12)
  • ④ 事务与并发(Q13-Q16)
  • ⑤ 审计日志(Q17-Q18)
  • ⑥ 资源池与转移(Q19-Q20)

Q1. Excel 模板最少需要哪几列?

最少 4 列:内网 IP、内网 IPv6、管控区域、寻址方式。其中内网 IP 和 IPv6 至少填一个,bk_addressing 必须是 staticdynamic。其余字段(操作系统、CPU、内存)即使空着,也能成功创建主机(值为空字符串)。

Q2. Excel 模板列名是中文还是英文?

列名是 cmdb 字段 ID(英文下划线风格),不是中文。前端把"内网 IP"展示名翻译成"内网 IP"列名只是给用户看,真正写进 MongoDB 的是 bk_host_innerip 这串字符串。前端代码在 src/ui/src/views/resource/host/import/ 下的 vue 文件里。

Q3. HostInputType 的两个值有什么区别?

excel 用于 Excel 导入,collect 用于 Agent 自动采集。两者走不同的字段补全策略:Excel 补默认字段(如 bk_import_from=excel),Collect 补采集器 ID(如 bk_agent_id)。审计日志的 action 字段也不同。

Q4. 主机表头字段名是怎么生成的?

通过 getHostFields 动态生成。前端请求 POST /api/v3/host/object/attr/search 拿到主机所有属性定义(包含属性 ID、属性名、是否必填),再翻译成 Excel 列名。属性 ID 即 bk_property_id,定义在 src/common/metadata/host.go

Q5. 第 2 层校验失败的行会怎样?

单行跳过,其他行继续处理。errMsg = append(errMsg, ...) + continue 保证行级隔离。最终返回的 success 数组只包含成功行号,error 数组包含失败行号 + 错误信息,前端会标红对应行。

Q6. 管控区域不存在时返回什么错误?

返回 CCErrTopoCloudNotFound,整行回滚。第 4 层校验在 IsPlatAllExist 实现,校验失败时 AutoRunTxn 触发事务回滚,主机不进库。这是为了避免"创建了主机但找不到云区域"的悬空数据。

Q7. bk_host_inneripbk_host_innerip_v6 都为空会怎样?

第 2 层校验直接拒绝该行。src/scene_server/host_server/logics/import.go 第 243-251 行的 getIpField,IPv4 和 IPv6 都为空时返回 host_import_innerip_v4_v6_empty 错误码,对应国际化文案"内网 IP/IPv6 至少填一个"。

Q8. 同一 Excel 里两行 IP 相同会怎样?

第 3 层校验不会发现,第 4 层会失败(主键冲突)。第 3 层 getQueryHostsFilter 查的是数据库,不查当前批次。第 4 层 CreateInstance 在 MongoDB 唯一索引上失败,返回 CCErrCommDuplicateItem。建议前端先做去重再上传。

Q9. 表格型字段填了非法 JSON 会怎样?

mapstr.GetMapStrArrByInterface 转换失败,整行拒绝。src/common/metadata/inst.go 第 338-341 行,转换失败时返回 fmt.Errorf("table data %s is invalid, err: %v", ...)AutoRunTxn 捕获后回滚该行。

Q10. 表格型字段怎么从 Excel 解析?

Excel 单元格用 JSON 字符串格式填入。例如网卡表:"[{'bk_nic_name':'eth0','bk_nic_ip':'1.1.1.1'}]"。前端在导出模板时把每个表格型字段的列名写成 table_field_name(JSON),提示用户填 JSON 数组。

Q11. GetTableData 会修改原始 MapStr 吗?

会,通过 delete(originData, ...) 原地删除表格型字段。src/common/metadata/inst.go 第 345 行,删除后 originData 剩下的就是主机实例的标量字段。如果希望保留原始数据,调用前需要 mapstr.DeepCopy

Q12. 表格型字段和主机字段重名时会发生什么?

以表格型字段为准,主机字段被覆盖。这是因为 GetTableDatadelete 在前,主机 CreateInstance 的写入在后。但实际场景中字段 ID 全局唯一,几乎不会重名。

Q13. AutoRunTxn 失败会回滚哪些操作?

回滚当前行的所有写入:主机实例、模块关系、表格型字段、审计日志。事务边界由 AutoRunTxn 闭包内的所有 API 调用决定。事务外写入的(如 SaveAuditLog 单独调用)不会被回滚。

Q14. 同一批 Excel 可以混合"新增"和"更新"吗?

不行,AddHostByExcelUpdateHostByExcel 是两个独立接口。前者要求请求体不包含 bk_host_id,后者要求必须包含。混用会触发 import_update_host_miss_hostID 错误。

Q15. 导入过程中浏览器刷新会发生什么?

已成功行不会回滚,未处理行永久丢失。cmdb 不会保存"导入进度",每次导入都是独立的 host_info MapStr。建议前端在导入前先备份 Excel 文件,失败时再次上传同一文件 + 修改失败行。

Q16. 大批量导入(10 万行)会 OOM 吗?

会,host_info 一次性加载到内存。src/common/metadata/hostserver.go 第 60 行的 HostInfo 类型,是 map[int64]map[string]interface{}。建议单次 Excel 不超过 5000 行(生产经验值)。

Q17. 审计日志会记录失败行吗?

不会,SaveAuditLog 只记录成功行。失败行不进 cc_HostBase,没法生成"更新前快照"。如果需要记录"尝试导入但失败"的事件,需要走 metadata.AuditLogin 之类的操作审计,不能用 NewHostAudit

Q18. 审计日志太多会撑爆 MongoDB 吗?

会,但 cmdb 有定时清理任务。src/scene_server/admin_server/command/ 下的定时任务,默认保留 90 天。生产环境建议缩短到 30 天,敏感行业保留 1 年。

Q19. AddHostToResourcePoolAddHostByExcel 走同一个底层函数吗?

不,AddHostToResourcePool 直接调 addHostInstanceAddHostByExcelAddHostByExcel两者共享 addHostInstance 这个最底层,但上层的事务、字段剥离、模块绑定逻辑完全不同。Agent 走前者是"轻量级"路径,不做表格型字段写入。

Q20. Agent 回调失败会重试吗?

会,gse_agent 内置 3 次重试。src/scene_server/host_server/logics/import.go 第 580-659 行的 AddHostToResourcePool,每次重试会传入完整的 host_info。如果 MongoDB 已经有同名 IP(bk_host_innerip + bk_cloud_id),第 3 层会命中,主机直接更新。

本篇全章总纲

  • 主题:蓝鲸 CMDB 3.14.6 批量录入机制(Excel 模板 + 4 层数据校验)
  • 核心源码:src/common/metadata/hostserver.go + src/common/metadata/inst.go + src/scene_server/host_server/logics/import.go
  • 核心设计:4 层校验(service → logic → coreservice → MongoDB)+ GetTableData 双写合并 + AutoRunTxn 单行原子 + 审计旁路
  • 实战警示:单次 Excel ≤ 5000 行;表格型字段填 JSON;bk_host_id 不能混用 Add/Update 接口
  • 下一篇:N 实例关联查询篇 #21 拓扑关联:InstAsst 关联模型 + 拓扑递归查询

Roadmap(后续预告)

  • #21 拓扑关联:深入 InstAsstAssociationMainlineAssociation 三个模型,看拓扑递归查询如何驱动业务拓扑展示。
  • #22 资源池与空闲机管理:解析"主机归还"、"主机认领"、"toInternalModule vs toNormalModule"的双路径设计。
  • #23 主机转移:专题分析 TransferHostToModuleTransferHostToInnerModule 的事务边界与并发控制。
posted @ 2026-07-08 22:43  左扬  阅读(1)  评论(0)    收藏  举报