蓝鲸 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_innerip、bk_host_innerip_v6、bk_cloud_id、bk_addressing。
- 4 层数据校验的入口位置:service 层 hostList == nil → logic 层 getIpField → logic 层 getQueryHostsFilter → coreservice 层 IsPlatAllExist。
- GetTableData 的剥离原理:从主机实例 MapStr 中"抽走"表格型字段(ModelQuoteRelation 关联的子表),剩余纯标量字段走主机实例 API。
- 批量录入的双写合并:addHostInstance 创建主机后,立即 TransferToNormalModule / TransferToInnerModule 把主机绑定到模块。
- AutoRunTxn 单行事务:每行主机一个独立事务,某行失败不会回滚前面已成功行。
- 审计日志的旁路写入:主机写入完成后,SaveAuditLog 单独持久化,操作记录可追溯。
了解即可
- HostInputType 的两种值:excel / collect,决定审计日志和字段补全策略。
- ExcelType 与 CollectType 的差异: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 这串字符串。
★ 关键事实:HostInfo 是 map[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 处理批量导入的核心原则:"行级隔离"。
源码视角一:第 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 步关键逻辑:
- 遍历 ModelQuoteRelation(即字段引用关系表),每个 relation 代表一个表格型字段;
- 用 relation.PropertyID 作为 key,到 originData 里取数据;
- 强转为 []mapstr.MapStr(数组里每个元素是子表行);
- 从 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 件事:
- 判断模块类型(toInternalModule):空闲机 / 故障机 / 资源池走 TransferToInnerModule;
- 业务模块走 TransferToNormalModule(cc_HostModuleRelation 表);
- 任一失败整体 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_innerip、bk_cloud_id:内网 IP + 管控区域是 cmdb 的天然主键,不能通过 Excel 修改;
- bk_import_from:导入来源标记是 cmdb 内部维护的,前端不应该传;
- create_time:创建时间是 cmdb 写入时自动生成的,前端不应该传。
注意:这 4 个字段即使前端传了,updateHostInst 也会"静默丢弃"(不报错)。这是信任前端契约的设计哲学——但实际生产中强烈建议前端做"灰显",因为一旦用户改了这 4 个字段,Excel 预览和实际生效会不一致,排查成本极高。
六、AddHostToResourcePool:另一条批量入口
除了 AddHostByExcel,AddHostToResourcePool 是 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 个核心差异:
- 不做事务包裹:每行失败仅记录到 res.Error,不触发事务回滚(因为 Agent 回调通常是 1 次 1 台,不存在"批级回滚"语义);
- 使用 validateHostField:更严格的字段校验,错误信息直接返回 CCErrCommParamsInvalid 错误码;
- 默认进资源池:通过 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 个关键设计:
- auditLog 在 updateHostInst / 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 必须是 static 或 dynamic。其余字段(操作系统、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_innerip 和 bk_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. 表格型字段和主机字段重名时会发生什么?
以表格型字段为准,主机字段被覆盖。这是因为 GetTableData 的 delete 在前,主机 CreateInstance 的写入在后。但实际场景中字段 ID 全局唯一,几乎不会重名。
Q13. AutoRunTxn 失败会回滚哪些操作?
回滚当前行的所有写入:主机实例、模块关系、表格型字段、审计日志。事务边界由 AutoRunTxn 闭包内的所有 API 调用决定。事务外写入的(如 SaveAuditLog 单独调用)不会被回滚。
Q14. 同一批 Excel 可以混合"新增"和"更新"吗?
不行,AddHostByExcel 和 UpdateHostByExcel 是两个独立接口。前者要求请求体不包含 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. AddHostToResourcePool 和 AddHostByExcel 走同一个底层函数吗?
不,AddHostToResourcePool 直接调 addHostInstance,AddHostByExcel 调 AddHostByExcel。两者共享 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 拓扑关联:深入 InstAsst、Association、MainlineAssociation 三个模型,看拓扑递归查询如何驱动业务拓扑展示。
- #22 资源池与空闲机管理:解析"主机归还"、"主机认领"、"toInternalModule vs toNormalModule"的双路径设计。
- #23 主机转移:专题分析 TransferHostToModule、TransferHostToInnerModule 的事务边界与并发控制。

浙公网安备 33010602011771号