蓝鲸 CMDB 3.14.6 源码专题【左扬精讲】—— #09 主机属性自动应用:根据模块自动下发配置
蓝鲸 CMDB 3.14.6 源码专题【左扬精讲】—— #09 主机属性自动应用:根据模块自动下发配置
在 Iaas + PaaS 混合运维场景中,主机管理存在一个高频痛点:新交付的云主机加入业务模块后,需要根据模块归属手工设置对应的运维属性(如所属业务线、运行环境、SLA 等级、运维负责人),遗漏配置会导致监控告警无法路由、自动化playbook失效、变更审批流无法匹配。更棘手的是——一台主机可能同时属于多个模块(既有 PaaS 平台模块,又有 Iaas 底层模块),属性冲突怎么办?
以电商业务为例:一台 MySQL 数据库主机,既属于数据库服务模块,又属于高可用集群模块,两个模块对运行环境字段要求的值不同(一个是 prod-db,一个是 ha-proxy),系统该如何裁决?
蓝鲸 CMDB 的 Host Apply(主机属性自动应用) 正是为解决这一问题而生。它的设计思路是:给模块绑定属性规则,主机进模块时自动应用,无需人工干预;冲突时由系统按优先级裁决,确保一致性。
本文从源码出发,完整剖析其实现机制。
主机属性自动应用 HostApply 模块属性规则 冲突检测 服务模板联动 异步任务
src/scene_server/host_server/service/hostapplyrule.go ← API 入口层(scene_server)
src/common/metadata/host_apply_rule.go ← 数据结构定义(metadata)
src/common/metadata/attribute.go ← CheckAllowHostApplyOnField(字段过滤)
src/source_controller/coreservice/core/hostapplyrule/rule.go ← 规则 CRUD 核心(coreservice)
src/source_controller/coreservice/core/hostapplyrule/plan.go ← 执行计划生成 + 实际下发(coreservice)
src/common/definitions.go ← HostApplyEnabledField 常量定义
学习重点
- 必须理解:HostApplyRule 的两条关联路径——模块维度(ModuleID)与服务模板维度(ServiceTemplateID),以及它们在冲突时的优先级规则
- 必须理解:GenerateApplyPlan 是预览还是执行?两者的区别和边界在哪里
- 必须理解:ConflictResolver(冲突解决器)的设计意图——为什么冲突不一定阻止下发
- 应当了解:CheckAllowHostApplyOnField 的字段过滤逻辑,什么字段不允许参与自动应用
- 应当了解:ExecModuleHostApplyRule 中 HostApplyEnabledField 的作用——"启用"开关的来源
- 一、问题场景:主机进模块后属性配置靠人工,容易遗漏和出错
- 二、核心概念:HostApplyRule 的两条绑定路径
- 三、数据结构:从源码读出 HostApply 的完整模型
- 四、API 入口:CreateHostApplyRule 的三段式设计
- 五、规则校验:哪些主机字段可以被自动应用?
- 六、执行链路:ExecModuleHostApplyRule 的全流程
- 七、执行计划生成:GenerateApplyPlan 的冲突检测逻辑
- 八、服务模板维度:GenerateServiceTemplateApplyPlan 的特殊处理
- 九、源码视角:从源码读出三层设计思想
- 十、FAQ 20 问
- Roadmap:后续预告
一、问题场景:主机进模块后属性配置靠人工,容易遗漏和出错
想象这样一个场景:SRE 维护一个电商 IaaS/PaaS 平台,"WebServer 模块"下的所有主机需要统一设置 bk_platform_type=ecommerce、bk_sla_level=P0、bk_operator=wangwu 三个属性。手工录入时:
- 新上线 100 台主机,容易遗漏部分配置
- 多人协作时规则不统一,张三设 SLA=P1,李四设 SLA=P0
- 一台主机同时属于两个模块(Web服务模块 + 缓存模块),两个模块的规则冲突时,谁优先?
CMDB 的 Host Apply 就是要把"手工配置"变成"规则驱动"。在模块上配置好规则,主机进模块时自动应用,SRE 只需维护规则,不用管具体主机。
二、核心概念:HostApplyRule 的两条绑定路径
HostApplyRule(主机属性自动应用规则)是 CMDB 中最核心的数据结构。它描述了"哪个模块(或服务模板)下的主机,某个字段应该是什么值"。
这里有一个关键的设计点——规则可以绑定到两个维度:
读 src/common/metadata/host_apply_rule.go 第 34-51 行(HostApplyRule 结构体):
// host_apply_rule.go L34-51
type HostApplyRule struct {
ID int64 `field:"id" json:"id" bson:"id"` // 规则主键ID,全局唯一
BizID int64 `field:"bk_biz_id" json:"bk_biz_id" bson:"bk_biz_id"` // 业务ID,标识所属业务
ModuleID int64 `field:"bk_module_id" json:"bk_module_id" bson:"bk_module_id"` // 模块ID,规则绑定到具体模块
// NOCC:tosa/linelength(忽略长度)
ServiceTemplateID int64 `field:"service_template_id" json:"service_template_id" bson:"service_template_id"` // 服务模板ID,与ModuleID互斥
// `id` field of table: `cc_AsstDes`, not the same with bk_property_id
AttributeID int64 `field:"bk_attribute_id" json:"bk_attribute_id" bson:"bk_attribute_id"` // 字段属性ID,关联到具体要设置的字段
PropertyValue interface{} `field:"bk_property_value" json:"bk_property_value" bson:"bk_property_value"` // 字段值,自动应用时写入主机的该字段
// 通用字段
Creator string `field:"creator"` // 创建者
Modifier string `field:"modifier"` // 最后修改者
CreateTime time.Time `field:"create_time"` // 创建时间
LastTime time.Time `field:"last_time"` // 最后修改时间
SupplierAccount string `field:"bk_supplier_account"` // 供应商账号
}
ModuleID 与 ServiceTemplateID 的互斥关系
ModuleID 和 ServiceTemplateID 在同一条规则中互斥——要么绑定到具体模块,要么绑定到服务模板,不能同时非零。这是 src/source_controller/coreservice/core/hostapplyrule/rule.go 第 63-71 行的 validateID 函数明确校验的。
规则存储在 cc_HostApplyRule 表中(definitions.go L101-102 确认),每条规则记录:业务 ID → 模块/模板 ID → 字段 ID → 期望值。
三、数据结构:从源码读出 HostApply 的完整模型
3.1 规则的增删改查
读 src/common/metadata/host_apply_rule.go 第 58-97 行(规则操作数据结构):
// host_apply_rule.go L58-97
// 创建规则
type CreateHostApplyRuleOption struct {
ModuleID int64 `json:"bk_module_id,omitempty"`
ServiceTemplateID int64 `json:"service_template_id,omitempty"`
AttributeID int64 `json:"bk_attribute_id"`
PropertyValue interface{} `json:"bk_property_value"`
}
// 删除规则(含模块场景和服务模板场景两种校验)
type DeleteHostApplyRuleOption struct {
ModuleIDs []int64 `json:"bk_module_ids"`
ServiceTemplateIDs []int64 `json:"service_template_ids"`
RuleIDs []int64 `json:"host_apply_rule_ids"`
}
// 列表查询
type ListHostApplyRuleOption struct {
ApplicationID int64 `field:"bk_biz_id"`
ModuleIDs []int64 `json:"bk_module_ids"`
ServiceTemplateIDs []int64 `json:"service_template_ids"`
AttributeIDs []int64 `json:"bk_attribute_ids"`
Page BasePage `json:"page"`
}
// 批量创建或更新(支持冲突报告)
type BatchCreateOrUpdateApplyRuleOption struct {
Rules []CreateOrUpdateApplyRuleOption `field:"host_apply_rules"`
}
3.2 执行计划数据结构
HostApplyPlanOption 是执行计划生成逻辑的核心数据结构(host_apply_rule.go L222-235),注释写得非常详细,直接引用:
// host_apply_rule.go L222-235
// HostApplyPlanOption 主机属性自动应用执行计划生成逻辑核心数据结构
// 设计背景:该数据结构需要支持如下三种场景
// 1. 应用模块配置到主机属性
// 2. 编辑模块配置(可能未保存), 预览应用效果(查看是否有冲突)
// 3. 将主机转移到模块下前预览应用效果(查看是否有冲突)
// 字段说明
// - Rules: 主机属性应用规则,由于上述case2的存在,其中 ID 可能为0
// - HostModules: 主机所有模块信息,case3的存在,导致不能直接从db中查询主机所属模块
// - ConflictResolvers: 可选参数,用于表示主机属性应用出现冲突时,如何设置应用值,如果未设置则冲突的字段不会被更新
type HostApplyPlanOption struct {
Rules []HostApplyRule `field:"host_apply_rules"`
HostModules []Host2Modules `field:"host_modules"`
ConflictResolvers []HostApplyConflictResolver `field:"conflict_resolvers"`
}
// 单台主机的执行结果
type OneHostApplyPlan struct {
HostID int64 `field:"bk_host_id"`
CloudInfo CloudInst `field:"cloud_area"`
ModuleIDs []int64 `field:"bk_module_ids"`
// 预计执行后端主机信息
ExpectHost map[string]interface{} `field:"expect_host"`
UpdateFields []HostApplyUpdateField `field:"update_fields"`
ConflictFields []HostApplyConflictField `field:"conflicts"`
// 未解决的冲突字段数
UnresolvedConflictCount int64 `field:"unresolved_conflict_count"`
}
四、API 入口:CreateHostApplyRule 的三段式设计
主机属性自动应用规则通过 CreateHostApplyRule 接口创建,该接口位于 src/scene_server/host_server/service/hostapplyrule.go 第 35-70 行。这是整个 Host Apply 功能的 API 入口层,负责接收前端请求并转发到核心服务层处理。
整体采用参数解析 → 事务封装 → 核心调用的三段式设计,这也是 CMDB 其它 API 的通用范式。关键流程:
- 第一段:参数解析 — 从 URL 路径提取 bizID,从请求体解析 CreateHostApplyRuleOption(包含模块ID/服务模板ID、字段ID、要设置的值)
- 第二段:事务封装 — 用 AutoRunTxn 包裹核心逻辑,确保"规则写入 DB + IAM 鉴权注册"的原子性
- 第三段:核心调用 — 委托 CoreService().HostApplyRule().CreateHostApplyRule() 完成实际的规则创建、字段校验、冲突检测等业务逻辑
读源码第 35-70 行(CreateHostApplyRule API):
// hostapplyrule.go L35-70
func (s *Service) CreateHostApplyRule(ctx *rest.Contexts) { // HTTP API入口,接收POST请求创建主机属性应用规则
rid := ctx.Kit.Rid // 获取请求追踪ID,用于日志关联
bizIDStr := ctx.Request.PathParameter(common.BKAppIDField) // 从URL路径提取业务ID(如/biz/1/hostapplyrule)
bizID, err := strconv.ParseInt(bizIDStr, 10, 64) // 字符串转int64
if err != nil { // 参数校验失败
blog.Errorf("CreateHostApplyRule failed, parse biz id failed, bizIDStr: %s, err: %v,rid:%s", bizIDStr, err, rid)
ctx.RespAutoError(ctx.Kit.CCError.Errorf(common.CCErrCommParamsInvalid, common.BKAppIDField)) // 返回400参数错误
return
}
option := metadata.CreateHostApplyRuleOption{} // 定义请求体结构体,接收ModuleID/ServiceTemplateID、AttributeID、PropertyValue等参数
if err := ctx.DecodeInto(&option); err != nil { // 将JSON请求体解析到option结构体
ctx.RespAutoError(err) // 解析失败返回400
return
}
var rule metadata.HostApplyRule // 定义规则返回对象
txnErr := s.Engine.CoreAPI.CoreService().Txn().AutoRunTxn(ctx.Kit.Ctx, ctx.Kit.Header, func() error { // 开启分布式事务,保证数据一致性
var err error
rule, err = s.CoreAPI.CoreService().HostApplyRule().CreateHostApplyRule(ctx.Kit.Ctx, ctx.Kit.Header, bizID, option) // 调用核心服务层执行实际创建逻辑
if err != nil { // 核心服务返回错误
blog.ErrorJSON("CreateHostApplyRule failed, core service CreateHostApplyRule failed, bizID: %s, "+
"option: %s, err: %s, rid: %s", bizID, option, err.Error(), rid)
return err // 事务自动回滚
}
return nil // 成功,事务自动提交
})
if txnErr != nil { // 事务执行失败
ctx.RespAutoError(txnErr) // 返回500或业务错误
return
}
ctx.RespEntity(rule) // 事务成功,200返回创建的规则对象(含规则ID)
}
AutoRunTxn 包裹:规则创建的原子性保证
整个规则创建过程包裹在 AutoRunTxn 中。这是 CMDB 的标准做法——任何涉及状态变更的操作都必须事务化,保证"规则创建 + IAM 注册"要么同时成功,要么同时回滚。
底层 CreateHostApplyRule 的核心校验在 src/source_controller/coreservice/core/hostapplyrule/rule.go 第 150-213 行:
// rule.go L150-213
func (p *hostApplyRule) CreateHostApplyRule(kit *rest.Kit, bizID int64,
option metadata.CreateHostApplyRuleOption) (metadata.HostApplyRule, errors.CCErrorCoder) {
now := time.Now()
rule := metadata.HostApplyRule{
ID: 0,
BizID: bizID,
AttributeID: option.AttributeID,
ModuleID: option.ModuleID,
ServiceTemplateID: option.ServiceTemplateID,
PropertyValue: option.PropertyValue,
Creator: kit.User,
Modifier: kit.User,
CreateTime: now,
LastTime: now,
SupplierAccount: kit.SupplierAccount,
}
// Step1: 校验 ModuleID 和 ServiceTemplateID 互斥(definitions.go L101-102 表名)
if err := p.validateID(kit, bizID, rule.ModuleID, rule.ServiceTemplateID); err != nil {
return rule, err
}
// Step2: 校验属性是否存在且可被自动应用
attribute, ccErr := p.getHostAttribute(kit, bizID, rule.AttributeID)
if ccErr != nil {
return rule, ccErr
}
// Step3: 校验属性值的合法性(如枚举值范围)
rawError := attribute.Validate(kit.Ctx, option.PropertyValue, common.BKPropertyValueField)
if rawError.ErrCode != 0 {
ccErr := rawError.ToCCError(kit.CCError)
return rule, ccErr
}
// Step4: 生成 ID 并写入 cc_HostApplyRule 表
id, err := mongodb.Client().NextSequence(kit.Ctx, common.BKTableNameHostApplyRule)
rule.ID = int64(id)
if err := mongodb.Client().Table(common.BKTableNameHostApplyRule).Insert(kit.Ctx, rule); err != nil {
return rule, kit.CCError.CCError(common.CCErrCommDBInsertFailed)
}
return rule, nil
}
五、规则校验:哪些主机字段可以被自动应用?
并非所有主机字段都可以被自动应用。CMDB 通过 CheckAllowHostApplyOnField 函数(attribute.go L1745-1758)控制哪些字段可以参与规则。
读 src/common/metadata/attribute.go 第 1706-1717 行和第 1745-1758 行:
// attribute.go L1706-1717
// HostApplyFieldMap:字段级白名单/黑名单
var HostApplyFieldMap = map[string]bool{
common.BKOperatorField: true, // 允许:运维负责人
common.BKBakOperatorField: true, // 允许:备份运维负责人
"bk_state": true, // 允许:状态
"bk_sla": true, // 允许:SLA 等级
common.BKHostInnerIPField: false, // 禁止:主机内网 IP(不允许改)
common.BKHostOuterIPField: false, // 禁止:主机外网 IP
common.BKAssetIDField: false, // 禁止:资产编号
common.BKSNField: false, // 禁止:序列号
"bk_comment": false, // 禁止:备注
"bk_service_term": false, // 禁止:保修期
}
// attribute.go L1745-1758
// CheckAllowHostApplyOnField 检查字段是否能用于主机属性自动应用
func CheckAllowHostApplyOnField(field *Attribute) bool {
if !field.IsEditable {
return false // 不可编辑的字段,一律禁止
}
// 屏蔽表格字段
if field.PropertyType == common.FieldTypeInnerTable {
return false // 表格类型字段禁止
}
if allow, exist := HostApplyFieldMap[field.PropertyID]; exist == true {
return allow // 白名单/黑名单优先
}
return true // 其余可编辑字段默认可参与
}
HostApplyFieldMap 的设计意图
这个 Map 解决了一个实际问题:某些字段虽然技术上可编辑(IsEditable=true),但业务上不允许自动修改。比如主机 IP 是核心标识,改了会导致监控断路;资产编号由财务系统管理,CMDB 不应覆盖。
白名单(true)和黑名单(false)并存,兜底策略是"未声明的可编辑字段默认可参与"——这样新增自定义字段时无需改代码。
六、执行链路:ExecModuleHostApplyRule 的全流程
ExecModuleHostApplyRule 是模块维度主机属性自动应用的真正执行入口。它做了三件事:启用模块的 HostApply → 保存规则 → 立即应用。
读 src/scene_server/host_server/service/hostapplyrule.go 第 714-803 行:
// hostapplyrule.go L714-803
// ExecModuleHostApplyRule 主机自动应用规则执行入口(模块维度)
func (s *Service) ExecModuleHostApplyRule(ctx *rest.Contexts) {
rid := ctx.Kit.Rid
planReq := new(metadata.HostApplyModulesOption)
if err := ctx.DecodeInto(planReq); err != nil {
ctx.RespAutoError(err)
return
}
// 查符合条件的具体主机 ID
hostIDs, err := s.getHostIDByCondition(ctx.Kit, planReq.BizID, planReq.ModuleIDs, planReq.HostIDs)
if err != nil {
ctx.RespAutoError(err)
return
}
txnErr := s.Engine.CoreAPI.CoreService().Txn().AutoRunTxn(ctx.Kit.Ctx, ctx.Kit.Header, func() error {
// 阶段1:将模块的 host_apply_enabled 设为 true(definitions.go L378)
op := &metadata.UpdateOption{
Condition: map[string]interface{}{
common.BKModuleIDField: map[string]interface{}{common.BKDBIN: planReq.ModuleIDs}},
Data: map[string]interface{}{common.HostApplyEnabledField: true},
}
_, err := s.Engine.CoreAPI.CoreService().Instance().UpdateInstance(ctx.Kit.Ctx, ctx.Kit.Header,
common.BKInnerObjIDModule, op)
if err != nil {
return err
}
// 阶段2:批量创建或更新规则
rulesOption := make([]metadata.CreateOrUpdateApplyRuleOption, 0)
for _, rule := range planReq.AdditionalRules {
rulesOption = append(rulesOption, metadata.CreateOrUpdateApplyRuleOption{
AttributeID: rule.AttributeID,
ModuleID: rule.ModuleID,
PropertyValue: rule.PropertyValue})
}
saveRuleOp := metadata.BatchCreateOrUpdateApplyRuleOption{Rules: rulesOption}
if _, ccErr := s.CoreAPI.CoreService().HostApplyRule().BatchUpdateHostApplyRule(
ctx.Kit.Ctx, ctx.Kit.Header, planReq.BizID, saveRuleOp); ccErr != nil {
return ccErr
}
// 阶段3:删除被移除的规则
if len(planReq.RemoveRuleIDs) > 0 {
removeOp := metadata.DeleteHostApplyRuleOption{
RuleIDs: planReq.RemoveRuleIDs,
ModuleIDs: planReq.ModuleIDs}
if ccErr := s.CoreAPI.CoreService().HostApplyRule().DeleteHostApplyRule(
ctx.Kit.Ctx, ctx.Kit.Header, planReq.BizID, removeOp); ccErr != nil {
return ccErr
}
}
return nil
})
if txnErr != nil {
ctx.RespAutoError(&metadata.RespError{Msg: txnErr})
return
}
// 跳过下发的情况:changed=false、仅删除规则、或无符合条件主机
if !planReq.Changed || len(planReq.AdditionalRules) == 0 || len(hostIDs) == 0 {
ctx.RespEntity(nil)
return
}
// 阶段4:实际将属性下发到主机(不在事务内,已成功的更新无需回滚)
ctx.Kit.Header.Del(common.TransactionIdHeader)
attributes := make([]metadata.HostAttribute, 0)
for _, rule := range planReq.AdditionalRules {
attributes = append(attributes, metadata.HostAttribute{
AttributeID: rule.AttributeID,
PropertyValue: rule.PropertyValue})
}
if err = s.updateHostApplyByRule(ctx.Kit, attributes, hostIDs); err != nil {
ctx.RespAutoError(err)
return
}
ctx.RespEntity(nil)
}
为什么阶段4(属性下发)不在事务内?
代码注释里写了:"update host operation is not done in a transaction, since the successfully updated hosts need not roll back"。属性下发到主机是"幂等"的——重复下发同样值不会产生副作用。因此即使部分主机更新失败,也不应该让已成功的回滚。如果需要回滚,应当通过审计日志 + 手动补偿处理。
七、执行计划生成:GenerateApplyPlan 的冲突检测逻辑
GenerateApplyPlan(plan.go L34-169)是 Host Apply 的核心计算引擎。它接收"规则列表 + 主机-模块关系",输出每个主机的"需要更新的字段 + 冲突字段"。
这里有两个关键设计点:冲突检测和多模块优先级。
7.1 冲突检测:同一字段被多个模块要求不同值
读 src/source_controller/coreservice/core/hostapplyrule/plan.go 第 310-409 行(getOneHostApplyPlan 核心逻辑):
// plan.go L310-409(getOneHostApplyPlan 片段)
func (p *hostApplyRule) getOneHostApplyPlan(kit *rest.Kit, attrRules map[int64][]metadata.HostApplyRule, // 核心函数:为单台主机计算属性应用计划
attrMap map[int64]metadata.Attribute, hostID int64, host map[string]interface{}, moduleIDs []int64, // attrMap: 字段ID→字段定义, host: 当前主机数据, moduleIDs: 主机所在模块列表
resolverMap map[int64]interface{}) (metadata.OneHostApplyPlan, errors.CCErrorCoder) { // resolverMap: 冲突解决器映射
plan := metadata.OneHostApplyPlan{ // 初始化应用计划结构体
HostID: hostID, // 目标主机ID
ModuleIDs: moduleIDs, // 该主机所在的所有模块
ExpectHost: host, // 期望的主机属性(初始为当前值,后续会被修改)
ConflictFields: make([]metadata.HostApplyConflictField, 0), // 冲突字段列表(尚未解决的冲突)
UpdateFields: make([]metadata.HostApplyUpdateField, 0), // 待更新字段列表(需要写入的变更)
}
for attributeID, targetRules := range attrRules { // 遍历所有需要处理的字段及其关联规则
attribute, need := preCheckRules(targetRules, attributeID, attrMap, "") // 前置检查:字段是否存在、规则是否有效
if !need { // 不需要处理(如字段不存在或规则已失效)
continue
}
propertyIDField := attribute.PropertyID // 获取字段的API标识(如"bk_os_name")
originalValue, ok := host[propertyIDField] // 从主机当前数据中获取该字段的现有值
if !ok { // 字段在主机上尚未设置
originalValue = nil
}
expectValue := originalValue // 期望值初始为主机的当前值
// 遍历该主机所在所有模块的同类规则,找第一个不相等的
conflictedStillExist, needChange := false, false // conflictedStillExist: 是否存在未解决冲突, needChange: 是否需要变更
for _, rule := range targetRules { // 遍历同一字段在多个模块上的所有规则
isEqual, err := isRuleEqualOrNot(attribute.PropertyType, expectValue, rule.PropertyValue) // 比较期望值与规则值是否相等(考虑字段类型)
if err != nil {
return metadata.OneHostApplyPlan{}, err
}
if isEqual { // 期望值已等于规则值,无需处理
continue
}
needChange = true // 发现需要变更
expectValue = rule.PropertyValue // 覆盖expectValue为最新规则值
conflictedStillExist = true // 标记存在潜在冲突(多个模块规则值不一致)
if propertyValue, exist := resolverMap[attribute.ID]; exist { // 检查是否有冲突解决器
conflictedStillExist = false // 有解决器时,冲突被消除
expectValue = propertyValue // 使用解决器指定的值
}
plan.ConflictFields = append(plan.ConflictFields, metadata.HostApplyConflictField{ // 记录冲突字段信息
AttributeID: attributeID, // 冲突的字段ID
PropertyID: propertyIDField, // 字段API标识
PropertyValue: originalValue, // 主机当前的原始值
Rules: targetRules, // 触发冲突的所有规则
UnresolvedConflictExist: conflictedStillExist, // 冲突是否已解决
})
break // 找到第一个不相等的规则后跳出
}
if !needChange { // 所有规则值都等于当前值,无需变更
continue
}
if conflictedStillExist { // 存在未解决的冲突,计数+1
plan.UnresolvedConflictCount += 1
}
// 校验值合法性后再写入
plan.ExpectHost[propertyIDField] = expectValue // 将期望值写入计划的主机快照
plan.UpdateFields = append(plan.UpdateFields, metadata.HostApplyUpdateField{ // 记录需要更新的字段
AttributeID: attributeID, // 字段ID
PropertyID: propertyIDField, // 字段API标识
PropertyValue: expectValue, // 要写入的新值
})
}
return plan, nil // 返回完整的应用计划
}
7.2 多模块优先级:服务模板规则 > 模块规则
读 plan.go 第 465-567 行(getModuleIDsAndSrvTempIDs + getFinalRules):
// plan.go L465-567(优先级逻辑核心)
func getModuleIDsAndSrvTempIDs(kit *rest.Kit, modules []metadata.ModuleInst) (
enableModuleMap map[int64]struct{}, haveHostApplyModuleIDs []int64,
srvTempModulesMap map[int64][]int64, errors.CCErrorCoder) {
for _, module := range modules {
if module.ServiceTemplateID != 0 {
srvTemplateIDs = append(srvTemplateIDs, module.ServiceTemplateID)
}
moduleIDHostApplyEnabledMap[module.ModuleID] = module.HostApplyEnabled
}
// 找出服务模板中 host_apply_enabled=true 的模板
enableSrvTempMap := make(map[int64]struct{})
if len(srvTemplateIDs) > 0 {
filter := map[string]interface{}{
common.BKFieldID: map[string]interface{}{common.BKDBIN: srvTemplateIDs},
common.HostApplyEnabledField: true, // definitions.go L378
}
// ...
}
// enableModuleMap:真正需要应用规则的主机所在模块集合
// 服务模板规则优先级更高,先加入 enableModuleMap
for _, module := range modules {
if _, ok := existSrvTempIDsMap[module.ServiceTemplateID]; ok {
srvTempModulesMap[module.ServiceTemplateID] = append(...)
enableModuleMap[module.ModuleID] = struct{}{}
continue
}
// 模块维度 host_apply_enabled=true 的模块规则
if moduleIDHostApplyEnabledMap[module.ModuleID] {
enableModuleMap[module.ModuleID] = struct{}{}
}
}
// ...
}
// getFinalRules:服务模板规则 + 模块规则合并(模板规则优先)
func (p *hostApplyRule) getFinalRules(...) []metadata.HostApplyRule {
// 先拿服务模板规则
if len(serviceTemplateIDs) > 0 {
// ... 模板规则先加入 finalRules
}
// 再拿模块规则(同名属性会被模板规则覆盖)
if len(haveHostApplyIDs) > 0 {
// ... 模块规则追加到 finalRules
}
return finalRules
}
优先级结论(源码可证):
当一台主机同时属于多个模块,且这些模块对同一字段有不同规则时:服务模板规则 > 模块规则。体现在 getFinalRules 中,服务模板规则先被加入 finalRules,模块规则追加在后。后续遍历时,同一 AttributeID 的规则只取第一个匹配(即模板规则)。
八、服务模板维度:GenerateServiceTemplateApplyPlan 的特殊处理
服务模板维度的 Host Apply 遵循"模板 → 模块 → 主机"的传播链:规则配置在服务模板上,自动下发到所有绑定了该模板的模块,再应用到这些模块下的主机。
读 src/scene_server/host_server/service/hostapplyrule.go 第 1114-1196 行(generateServiceTemplateApplyPlan):
// hostapplyrule.go L1114-1196
func (s *Service) generateServiceTemplateApplyPlan(
kit *rest.Kit, option *metadata.HostApplyServiceTemplateOption) (
metadata.HostApplyPlanResult, errors.CCErrorCoder) {
// Step1: 找出服务模板对应的最终 rule(模板本身 + AdditionalRules 覆盖)
rules, err := s.findSrvTemplateRule(kit, option.BizID, option.ServiceTemplateIDs)
if err != nil {
return metadata.HostApplyPlanResult{}, err
}
templateRules := getFinalRule(rules, &option.HostApplyPlanBase)
// Step2: 找出所有绑定了这些服务模板的模块
moduleRes, err := s.getModuleRelateHostApply(kit, option.BizID, nil, option.ServiceTemplateIDs)
// 返回 ModuleInst 列表,包含每个模块的 ModuleID、HostApplyEnabled、ServiceTemplateID
// Step3: 将模板的 rule "复制" 到每个模块(生成模块维度的 finalRules)
moduleIDs := make([]int64, 0)
tempToModules := make(map[int64][]int64)
for _, module := range moduleRes {
moduleIDs = append(moduleIDs, module.ModuleID)
tempToModules[module.ServiceTemplateID] = append(tempToModules[module.ServiceTemplateID], module.ModuleID)
}
finalRules := make([]metadata.HostApplyRule, 0)
for _, rule := range templateRules {
moduleIDs, exist := tempToModules[rule.ServiceTemplateID]
if !exist {
continue
}
for _, moduleID := range moduleIDs {
rule.ModuleID = moduleID // 模板 rule 的 ModuleID 替换为具体模块 ID
finalRules = append(finalRules, rule)
}
}
// Step4: 查主机-模块关系
relationReq := &metadata.HostModuleRelationRequest{
ApplicationID: option.BizID,
ModuleIDArr: moduleIDs,
Page: metadata.BasePage{Limit: common.BKNoLimit},
}
hostRelations, _ := s.CoreAPI.CoreService().Host().GetHostModuleRelation(...)
// Step5: 复用 GenerateApplyPlan 生成执行计划
planOption := metadata.HostApplyPlanOption{
Rules: finalRules,
HostModules: hostModules,
}
planResult, ccErr := s.CoreAPI.CoreService().HostApplyRule().GenerateApplyPlan(...)
return planResult, nil
}
服务模板维度的 Host ApplyEnabled 来自服务模板本身
注意在 plan.go 的 getModuleIDsAndSrvTempIDs 中,服务模板维度的启用状态来自 ServiceTemplate.HostApplyEnabled(hostapplyrule.go L1068),而不是模块的 host_apply_enabled 。这意味着可以在服务模板层面统一开关,再通过模板传播到所有绑定模块。
九、源码视角:从源码读出三层设计思想
蓝鲸 CMDB 的主机属性自动应用体系,从源码里可以读出三个设计原则。SRE 理解这些原则后,能更准确地预判系统行为。
源码视角一:两条绑定路径 + 优先级覆盖
读 src/common/metadata/host_apply_rule.go L34-51(HostApplyRule)和 plan.go L465-567(优先级逻辑),会发现规则可以绑定到模块(ModuleID)或服务模板(ServiceTemplateID),不能同时非零。在 getFinalRules 中,服务模板规则先加入列表,模块规则后追加,遍历时同名字段只取第一个匹配——实现了"模板规则优先于模块规则"的覆盖语义。
源码视角二:预览与执行分离,冲突不阻止下发
读 src/source_controller/coreservice/core/hostapplyrule/plan.go L34-169(GenerateApplyPlan) 和 L654-704(carryOutPlan),会发现 CMDB 将"生成计划"和"执行计划"严格分离。 GenerateApplyPlan 负责冲突检测(记录哪些字段存在冲突、冲突值是什么),但不会阻止下发。ConflictResolvers 是可选的——即使不提供冲突解决器,未解决冲突的字段会留在 ConflictFields 中,但其他无冲突的字段依然可以正常下发。
源码视角三:属性下发不走事务,已成功的不回滚
读 src/scene_server/host_server/service/hostapplyrule.go L714-803(ExecModuleHostApplyRule),会发现规则创建和启用(阶段1-3)包裹在 AutoRunTxn 中,但属性实际下发(阶段4)不在事务内。代码注释明确说明:"successfully updated hosts need not roll back"——属性下发是幂等的,重复下发同样值无副作用。
源码视角四:字段级白名单,IP/SN 等核心字段禁止自动修改
读 src/common/metadata/attribute.go L1706-1758(HostApplyFieldMap + CheckAllowHostApplyOnField),会发现 CMDB 用"黑名单优先 + 默认可编辑"的方式控制哪些字段可以参与自动应用。IP、资产编号、SN 等字段明确 false,运维负责人、SLA 等字段明确 true,其余自定义字段默认参与。
源码视角总结:5 个核心设计原则
- 2 条绑定路径:ModuleID 与 ServiceTemplateID 互斥,服务模板优先
- 1 个预览-执行分离:GenerateApplyPlan 只算不写,Exec 才实际改数据
- 1 个非事务下发:属性下发不回滚,已成功的不因后续失败而撤销
- 1 个字段白名单:IP/SN/资产编号禁止,运维负责人/SLA 允许,其余默认
- 1 个异步任务标记:SyncModuleHostApplyTaskFlag=module_host_apply_sync(definitions.go L1634)
避坑提醒(源码视角):
- 不要在内置模块(空闲机/故障机)上启用 Host Apply:内置模块的 Default != 0(process.go L2109 上下文),Host Apply 主要用于业务模块,配置在空闲机上没有实际意义
- 不要跳过 CheckAllowHostApplyOnField 直接将任意字段加入规则:IP/SN/资产编号等字段禁止自动修改,强行写入规则会在 RunHostApplyOnHosts(plan.go L569)的 canUpdate hook 中被拒绝
- 不要将模块维度和模板维度的规则混淆使用:BatchUpdateHostApplyRule(rule.go L584-693)会同时创建/更新两类规则,但 getFinalRules(plan.go L526-567)会按优先级合并,混用会导致预期外的覆盖
- 主机进模块时若想预览冲突,必须用 GenerateModuleApplyPlan:直接用 ExecModuleHostApplyRule 会直接下发,无法预览冲突后再决定是否执行
十、FAQ 20 问
分组说明:以下 20 组 Q&A 覆盖了 Host Apply 从配置到触发、从冲突到校验的核心问题。
Q1. HostApplyRule 和模块的 host_apply_enabled 是什么关系?
规则本身是"配置内容",host_apply_enabled 是"启用开关"。只有当模块(或服务模板)的 host_apply_enabled=true 时,HostApplyRule 才会被实际应用到主机。规则存在但开关关闭,不会有任何效果。
Q2. 为什么需要 GenerateModuleApplyPlan?它和 ExecModuleHostApplyRule 有什么区别?
GenerateModuleApplyPlan 只计算不写入——它输出每个主机需要更新的字段和冲突情况,供 SRE 预览后决定是否执行。ExecModuleHostApplyRule 才会真正将属性下发到 cc_HostBaseAttr 表。两者是"预览"和"执行"的关系。
Q3. 一台主机同时属于两个模块,两个模块对同一字段有不同规则时,谁生效?
在 getFinalRules(plan.go L526-567)中,服务模板规则先加入 finalRules,模块规则后追加。遍历时同 AttributeID 的规则只取第一个,即服务模板规则优先于模块规则。如果两个规则都来自模块维度,则取遍历到的第一个(不确定性)。
Q4. 主机属性自动应用在哪个时机触发?
有三个触发时机:1)SRE 手动调用 ExecModuleHostApplyRule;2)主机转移进模块时(TransferHostWithAutoClearServiceInstance 会触发 RunHostApplyOnHosts,plan.go L569);3)规则变更时(UpdateModuleHostApplyRule 创建异步任务,hostapplyrule.go L684-710,任务标记为 module_host_apply_sync)。
Q5. 哪些主机字段不能参与自动应用?
HostApplyFieldMap(attribute.go L1706-1717)明确禁止:主机内网 IP(bk_host_innerip)、主机外网 IP(bk_host_outerip)、资产编号(bk_asset_id)、序列号(bk_sn)、备注(bk_comment)、保修期(bk_service_term)。禁止的原因是这些字段有外部权威来源(网络监控系统、财务系统),CMDB 自动修改会破坏数据一致性。
Q6. ConflictResolver 是怎么工作的?
ConflictResolver(host_apply_rule.go L204-208)是一个"冲突时的替代值"。当遍历规则发现 expectValue 与当前值不相等时,如果有对应 AttributeID 的 resolver,就用 resolver 的值替代,同时将 conflictedStillExist 设为 false。相当于 SRE 提前告诉系统:"如果冲突,按这个值来"。
Q7. BatchCreateOrUpdateHostApplyRule 的错误处理机制是怎样的?
BatchUpdateHostApplyRule(rule.go L584-693)对每条规则逐个尝试创建或更新(Upsert 逻辑:先查 count > 0 则更新,否则创建),错误不阻断后续规则。最终返回一个 Items 数组,每个 item 有自己的错误状态,SRE 可以通过遍历 Items 判断哪些规则失败、哪些成功。
Q8. 主机属性自动应用是否支持回滚?
不直接支持自动回滚。ExecModuleHostApplyRule(hostapplyrule.go L714)的属性下发阶段(阶段4)不在事务内,已更新成功的主机不会因为后续主机失败而回滚。但 CMDB 的 auditlog 模块会记录每次属性变更,SRE 可以通过审计日志查到变更前的值并手动补偿。
Q9. 模块的 host_apply_enabled 字段在哪里定义的?
host_apply_enabled 在 definitions.go L378 定义常量 HostApplyEnabledField = "host_apply_enabled"。这个字段存在于 cc_ModuleBase 表中,控制该模块是否启用主机属性自动应用。当 host_apply_enabled=false 时,即使有 HostApplyRule,也不会触发应用。
Q10. 服务模板维度和模块维度的 Host Apply,谁更适合生产使用?
一般推荐服务模板维度。当大量模块需要相同规则时(如 100 个 Tomcat 模块都需要 bk_sla=P0),在服务模板上配置一次,规则自动传播到所有绑定该模板的模块,避免重复配置。模块维度适合少量特殊场景的差异化配置。
Q11. UpdateModuleHostApplyRule 和 ExecModuleHostApplyRule 有什么区别?
UpdateModuleHostApplyRule(hostapplyrule.go L665)用于"更新规则并触发异步任务"——它创建 task_server 任务(task_flag=module_host_apply_sync,definitions.go L1634),在后台异步执行,主机属性变更不阻塞。ExecModuleHostApplyRule 用于"同步执行"——SRE 等待结果返回,适合小规模即时生效场景。
Q12. 为什么 HostApplyRule 的 AttributeID 是 bk_attribute_id 而不是 bk_property_id?
注释里有说明:"id field of table: cc_AsstDes, not the same with bk_property_id"(host_apply_rule.go L41-42)。bk_attribute_id 是 cc_ObjAttDes 表中的主键 ID,bk_property_id 是字段的英文名。CMDB 用 ID 而非字符串做关联键,是为了避免重命名字段时需要级联更新所有规则记录。
Q13. 如果主机已有某个字段的值和规则相同,还需要更新吗?
不需要。getOneHostApplyPlan(plan.go L342-350)会先用 isRuleEqualOrNot 比较当前值和规则期望值,只有不相等时才设置 needChange=true,才会产生 UpdateFields。如果值已经一致,该字段不会出现在更新列表中。
Q14. 主机属性自动应用会生成审计日志吗?
会。属性下发走的是 Instance().UpdateInstance(plan.go L684),CMDB 的 auditlog 中间件会拦截所有 Instance Update 操作并生成审计日志,记录变更前后的值、操作人、时间戳。
Q15. ServiceTemplate.HostApplyEnabled 和 Module.HostApplyEnabled 有什么区别?
两者控制不同维度。Module.HostApplyEnabled 控制"这个模块的规则是否生效"。ServiceTemplate.HostApplyEnabled 控制"这个服务模板的规则是否向绑定模块传播"。在 getModuleIDsAndSrvTempIDs(plan.go L465)中,会先查模板级别的 HostApplyEnabled,如果为 true 才把对应模块加入 enableModuleMap。
Q16. 为什么字段值比较需要区分 int/float/time/enum 等类型?
因为 MongoDB 存储和 Go 反序列化后的类型可能不同。isRuleEqualOrNot(plan.go L238-292)针对不同 PropertyType 做类型转换:int/float 要转成统一数值类型才能比较;time 要从 primitive.DateTime 转为 time.Time 再比较;organization 类型原生是 primitive.A 数组,需要展开逐元素比较。这确保了"字面值相同但类型不同"不会误判为相等。
Q17. 主机属性自动应用支持分页限制吗?
ListHostApplyRule(hostapplyrule.go L206-209)校验 BKMaxLimitSize(约 500 条),防止一次查询规则数过多。但主机的属性下发(updateHostPlan,hostapplyrule.go L344-428)没有显式的单次上限,因为属性下发是批量 UPDATE 语句,由 MongoDB 的写入限制自然约束。
Q18. 规则变更后,已经应用过的主机会自动重新应用吗?
需要手动触发。UpdateModuleHostApplyRule 会创建异步任务执行,但不会自动触发历史主机的重新应用。SRE 需要主动调用 GenerateModuleApplyPlan 预览后,再用 ExecModuleHostApplyRule 或 UpdateModuleHostApplyRule 触发重新应用。
Q19. 为什么 HostApplyFieldMap 里 bk_operator 允许而 bk_host_innerip 禁止?
运维负责人(bk_operator)是"业务管理属性",随模块归属变化是合理的,SRE 需要明确谁负责哪台机器。主机 IP 是"基础设施标识",修改会导致监控/告警断路,由网络管理系统权威维护,不应被 CMDB 覆盖。这个边界设计体现了"可改则改、不可改则锁"的分层控制理念。
Q20. CC_HostApplyRule 表的主键是什么?唯一索引如何保证?
主键是自增 ID(mongodb.Client().NextSequence,rule.go L197)。唯一索引由 biz_id + module_id/service_template_id + attribute_id 组合构成(DuplicateKey 时返回 CCErrCommDuplicateItem 错误,rule.go L205-207)。这保证了同一模块下同一字段只能有一条规则,避免重复。
Roadmap:后续预告
后续预告
- #10 批量导入导出:Excel 模板与数据校验——主机批量录入的高效方式
- #11 异步任务系统:批量操作的进度追踪与回滚——task_server 如何管理 Host Apply 异步任务的生命周期
全篇总结
- HostApplyRule 的两条绑定路径(ModuleID / ServiceTemplateID)互斥,服务模板规则优先
- GenerateApplyPlan 是预览,ExecModuleHostApplyRule 是执行,两者严格分离
- 冲突检测基于 isRuleEqualOrNot 分类型比较,不阻止无冲突字段下发
- 字段级白名单(HostApplyFieldMap)禁止 IP/SN/资产编号自动修改
- 属性下发不走事务,已成功的不回滚,通过审计日志支持事后追溯

浙公网安备 33010602011771号