蓝鲸 CMDB 3.14.6 源码专题【左扬精讲】— CMDB #15:云区域管理:多云/混合云账号与区域管理

蓝鲸 CMDB 3.14.6 源码专题【左扬精讲】— CMDB #15:云区域管理:多云/混合云账号与区域管理

对企业 SRE 来说,最难的不是管哪一台主机,而是 "同一个 IP 在两朵云里都出现过" 这种场景:腾讯云 VPC 里有一批机器内网 IP 是 192.168.10.10,AWS VPC 里恰好也有一批机器内网 IP 是 192.168.10.10,还有内网自建的私有云十几台物理机。

如果没有 "云区域" 这一层抽象,光靠 IP 是无法定位 "一台机器到底属于哪个云、哪个 VPC、哪个账号" 的。

蓝鲸 CMDB 的 "云区域(Cloud Area)" 就是为解决这个问题设计的。 它把 bk_cloud_id 作为一级 "命名空间",让bk_host_id + bk_cloud_id成为全球唯一的主机标识;外层再叠加 "云账户(CloudAccount)""云同步任务(CloudSyncTask)",把外部云厂商的资源统一收口。

这篇博文我们不看抽象概念,而是直接看 CMDB 3.14.6 里云区域管理的真实源码:云区域 CRUD如何走事务 + 审计 + IAM 注册、内置区域保护如何防止误删默认管控区域、云账户数据模型如何做云厂商白名单校验、云区域 + 主机云区域字段如何在主机转移时一起改、云同步任务如何把外部云资源落到本地云区域。

   src/scene_server/host_server/service/cloudarea.go                   ← 云区域 CRUD 入口
    src/common/metadata/cloud.go                                       ← CloudAccount / CloudArea / CloudSyncTask 元数据
    src/common/metadata/hostserver.go                                  ← CloudAreaSearchParam / CloudAreaHostCount
    src/source_controller/coreservice/core/host/cloudarea.go           ← UpdateHostCloudAreaField / FindCloudAreaHostCount
    src/source_controller/coreservice/core/cloud/account.go            ← 云账户 CRUD(CreateAccount / SearchAccount / UpdateAccount / DeleteAccount)
    src/source_controller/coreservice/core/cloud/validator.go          ← 云账户校验(validCreateAccount)
    src/common/auditlog/cloud_area.go                                  ← 云区域审计日志
    src/common/definitions.go                                          ← BKInnerObjIDPlat / BuiltInCloudAreaIDs / UnassignedCloudAreaID
    src/scene_server/cloud_server/cloudsync/cloudsync.go               ← 云资源同步(SyncCloudResource)
    src/scene_server/cloud_server/cloudsync/hostsyncor.go              ← 云主机同步器(HostSyncor.syncCloudHost)
    src/ac/iam/adaptor.go                                              ← CloudAreaInstance → SysCloudArea IAM 映射
    src/ac/parser/cloud.go                                             ← cloudRelated 权限路由

CloudArea bk_cloud_id CloudAccount CloudVendor CloudSyncTask IAM SysCloudArea AutoRunTxn AWS=1 TencentCloud=2 v3.14.6

必掌握

  • 云区域 CRUD 链路CreatePlat / UpdatePlat / DeletePlat 如何把"实例操作 + 审计日志 + IAM 注册"串成原子事务
  • 内置区域保护BKDefaultDirSubArea + UnassignedCloudAreaIDBuiltInCloudAreaIDs 保护,禁止更新/删除默认区域
  • 云账户数据模型CloudAccount 必填字段校验 + SupportedCloudVendors 白名单(仅 AWS="1"、TencentCloud="2"
  • 主机云区域字段更新UpdateHostCloudAreaFieldbk_cloud_id + bk_host_innerip 唯一性校验防重复
  • 云区域主机数查询FindCloudAreaHostCount 用 goroutine 池并发查 N 个云区域的主机数

了解

  • BKInnerObjIDPlat = "plat":云区域在 CMDB 内置对象模型里的 ObjectID
  • UnassignedCloudAreaName = "未分配"UnassignedCloudAreaID = 90000001:系统兜底的"未分配"云区域
  • 云区域审计日志 CloudAreaRes 在 IAM 中的资源类型映射 SysCloudArea = "sys_cloud_area"
  • ReservedCloudAreaStartID = 90000000 ~ ReservedCloudAreaEndID = 99999999 为 CMDB 保留的云区域 ID 段

一、云区域在 CMDB 架构中的定位

What — 云区域在 CMDB 里扮演什么角色?

在蓝鲸 CMDB 中,CloudArea(管控区域 / 云区域)是bk_host_id之外最重要的"主机命名空间"。它不属于host/set/module 等业务拓扑内的资源,而是一个外层容器型抽象:每个 bk_cloud_id 代表一朵云(或一个网络平面),所有主机都挂在这个 ID 之下。对 SRE 来说,云区域相当于"主机在哪朵云"的归属标签,没有它就无法区分"两台 IP 相同的机器到底在哪朵云上"。

Why — 为什么需要云区域这一层?解决了什么问题?

问题一:同 IP 跨云冲突

不同云厂商通常都使用 10.0.0.0/8192.168.0.0/16 这些私网网段,没有云区域这层抽象,同一私网 IP 在腾讯云、AWS、私有云都会撞名。bk_host_id + bk_cloud_id 的复合主键确保每台主机全局唯一

问题二:多账号统一管理

公司通常一个云厂商有多个账号(开发账号、测试账号、生产账号)。CMDB 用 CloudAccount + bk_account_id 隔离,每个云区域绑定一个云账户,CMDB 才能用同一套 API 对接多个云厂商。

问题三:内网云区域兜底

未通过云同步进入 CMDB 的"野生主机"或纯内网机器,CMDB 默认挂到 UnassignedCloudAreaID = 90000001(未分配)的云区域下。这个内置区域不能被删除或更新,保证兜底可用。

没有云区域会发生什么?

  • 没有命名空间:主机表里只剩 bk_host_id,跨云 IP 冲突无法定位
  • 没有云厂商映射:不知道这台主机来自 AWS 还是腾讯云
  • 没有同步对接:云同步任务 CloudSyncTask 没有"目标落点"概念
  • 没有兜底区域:未同步进来的主机没有合适的归属位置
How — 从源码看云区域的元数据与对象模型

src/common/metadata/cloud.go 第 347-363 行,CloudArea 是云区域的元数据结构:

// CloudArea:管控区域,CMDB里云区域的元数据定义
    // 注意:CloudArea 本身是个 metadata 结构体,作为plat模型实例化存在
    type CloudArea struct {
        CloudID     int64     `json:"bk_cloud_id"     bson:"bk_cloud_id"`         // 云区域ID,全局唯一
        CloudName   string    `json:"bk_cloud_name"   bson:"bk_cloud_name"`       // 云区域名称(如"腾讯云-广州一区")
        Status      string    `json:"bk_status"       bson:"bk_status"`           // 云区域状态
        CloudVendor string    `json:"bk_cloud_vendor" bson:"bk_cloud_vendor"`     // 云厂商标识(AWS=1/TencentCloud=2)
        OwnerID     string    `json:"bk_supplier_account" bson:"bk_supplier_account"` // 开发商ID(多租户隔离)
        VpcID       string    `json:"bk_vpc_id"       bson:"bk_vpc_id"`           // 关联VPC ID
        VpcName     string    `json:"bk_vpc_name"     bson:"bk_vpc_name"`         // 关联VPC名称
        Region      string    `json:"bk_region"       bson:"bk_region"`           // 区域(如ap-guangzhou)
        AccountID   int64     `json:"bk_account_id"   bson:"bk_account_id"`       // 关联的云账户ID
        Creator     string    `json:"bk_creator"      bson:"bk_creator"`          // 创建者(服务端强制赋值)
        LastEditor  string    `json:"bk_last_editor"  bson:"bk_last_editor"`      // 最后修改者
        CreateTime  time.Time `json:"create_time"     bson:"create_time"`         // 创建时间(UTC)
        LastTime    time.Time `json:"last_time"       bson:"last_time"`           // 最后修改时间(UTC)
        Default     int64     `json:"default"         bson:"default"`             // 是否为默认区域标记
    }

这里 CloudID 是云区域 ID,CloudName 是云区域名称。注意它不是直接存表,CMDB 用内置对象模型 plat 来承载云区域,CloudArea 是传输用的结构体,落地存到 cc_PlatBase 集合。

src/common/definitions.go 第 132-133 行,可以确认云区域在 CMDB 内置对象模型里的 ObjectID:

// BKInnerObjIDPlat:内置对象ID"plat",云区域在CMDB中作为内置对象存储
    // 出现在 coreservice / host_server / cloud_server / api-server 等多处
    BKInnerObjIDPlat = "plat"

也就是说,所有云区域的增删改查,最终都走对象模型 plat 的实例化 API(如 CreateInstanceUpdateInstanceDeleteInstanceReadInstance)。

src/common/definitions.go 第 1820-1838 行,看内置云区域的 ID 段和"未分配"区域:

// ReservedCloudAreaIDs:保留段中已使用的ID,新增保留ID需加入此数组
    var ReservedCloudAreaIDs = []int64{UnassignedCloudAreaID}
    
    // BuiltInCloudAreaIDs:内置管控区域,删除/更新时被forbidden
    // 当前内置两个:BKDefaultDirSubArea=0(默认直连区域)、UnassignedCloudAreaID=90000001(未分配)
    var BuiltInCloudAreaIDs = []int64{BKDefaultDirSubArea, UnassignedCloudAreaID}
    
    const (
        // 保留段起始/结束ID:CMDB保留 90000000~99999999 给特殊用途
        ReservedCloudAreaStartID = 90000000
        ReservedCloudAreaEndID   = 99999999
    
        // UnassignedCloudAreaID:未分配区域的固定ID
        UnassignedCloudAreaID = 90000001
    
        // UnassignedCloudAreaName:未分配区域的名称,显示在UI上
        UnassignedCloudAreaName = "未分配"
    )

这里 BuiltInCloudAreaIDs 是云区域"内置保护"的关键,下一节会看到 UpdatePlat 用它防止误改默认区域。

源码视角总结:4 层命名空间

  • 1 层对象模型:plat(内置 ObjectID),承载所有云区域实例
  • 1 个全局 ID:bk_cloud_id 作为云区域唯一标识
  • 2 个内置保护:BKDefaultDirSubArea=0UnassignedCloudAreaID=90000001
  • 1 个保留段:[90000000, 99999999] 给 CMDB 系统保留

避坑提醒(源码视角):

  • 不要硬编码 bk_cloud_id=0 当作"内网云"BKDefaultDirSubArea=0 是默认直连区域,本身有业务含义,不要假设它等同于私有云
  • 注意 UnassignedCloudAreaID 是兜底 IDUpdateHostCloudAreaFieldsrc/source_controller/coreservice/core/host/cloudarea.go 第 484 行显式拒绝 CloudID == UnassignedCloudAreaID,避免误把有云的主机改到兜底区域

本节总结:云区域是 CMDB 的"主机命名空间"

  • 不是业务拓扑(业务/集群/模块),而是网络拓扑(云/VPC/区域)
  • 通过 bk_cloud_id 实现 bk_host_id + bk_cloud_id 全局唯一主机标识
  • 通过 BuiltInCloudAreaIDs 实现内置区域保护
  • 通过 UnassignedCloudAreaName="未分配" 给野主机提供兜底归属

二、创建链路:CreatePlatBatch 的原子操作

What — 创建链路在做什么?

云区域的创建分两个 API:CreatePlatBatch(批量创建,定义在 src/scene_server/host_server/service/cloudarea.go 第 101-211 行)和 CreatePlat(单次创建,第 216-288 行)。两者都通过 AutoRunTxn 把"实例创建 + 审计日志 + IAM 注册"打包成原子事务,唯一区别是 CreatePlatBatchCreateManyInstance 一次插入多条。

Why — 为什么创建链路要做成原子事务?

问题一:数据一致性

云区域创建涉及三个写操作:向 cc_PlatBase 集合插入实例、生成审计日志、向 IAM 注册资源创建权限。如果中间某一步失败,就会出现"云区域存在但审计缺失"或"IAM 已注册但云区域未创建"的不一致状态。

问题二:审计合规要求

CMDB 的审计日志要求"每个创建操作都有记录"。如果创建云区域成功但审计日志写入失败,从合规角度看这个操作是"半成功",运维无法追溯。

问题三:IAM 权限预注册

云区域在 IAM 里对应 SysCloudArea 资源类型(src/ac/iam/adaptor.go 第 71 行 meta.CloudAreaInstance: SysCloudArea)。创建云区域时同步注册"创建者授权",保证创建者立即拥有该云区域的操作权限。

How — CreatePlatBatch 的完整执行路径

src/scene_server/host_server/service/cloudarea.go 第 101-211 行的完整代码:

// CreatePlatBatch:批量创建云区域,定义在 service/cloudarea.go L102-211
    // 链路:JSON解码 → 填充Creator/LastEditor → AutoRunTxn原子事务(创建+审计+IAM注册)
    func (s *Service) CreatePlatBatch(ctx *rest.Contexts) {
        input := struct {
            Data []mapstr.MapStr `json:"data"`
        }{}  // 匿名结构,只接收一个data数组字段
        
        if err := ctx.DecodeInto(&input); nil != err {  // 解码JSON请求体
            ctx.RespAutoError(err)
            return
        }
        
        if len(input.Data) == 0 {  // 空数据直接报错
            ctx.RespAutoError(ctx.Kit.CCError.CCError(common.CCErrCommHTTPBodyEmpty))
            return
        }
        
        user := httpheader.GetUser(ctx.Request.Request.Header)  // 从HTTP头提取User,注入创建者字段
        for i := range input.Data {
            input.Data[i][common.BKCreator] = user       // 服务端强制覆盖bk_creator
            input.Data[i][common.BKLastEditor] = user   // 服务端强制覆盖bk_last_editor
        }
        
        instInfo := &meta.CreateManyModelInstance{Datas: input.Data}  // coreservice通用实例创建参数
        
        // 准备响应数组:每个元素对应一个创建成功的云区域
        result := make([]metadata.CreateManyCloudAreaElem, len(input.Data))
        
        // 核心:原子事务包裹"创建+审计+IAM注册"
        txnErr := s.Engine.CoreAPI.CoreService().Txn().AutoRunTxn(ctx.Kit.Ctx, ctx.Kit.Header, func() error {
            var err error
            // 第一步:调用coreservice批量创建plat实例
            res, err := s.CoreAPI.CoreService().Instance().CreateManyInstance(ctx.Kit.Ctx, ctx.Kit.Header,
                common.BKInnerObjIDPlat, instInfo)  // common.BKInnerObjIDPlat = "plat"
            if nil != err {
                return ctx.Kit.CCError.CCError(common.CCErrTopoInstCreateFailed)
            }
            
            // 检查重复创建 / 异常 / 无创建结果
            if len(res.Repeated) > 0 { return ctx.Kit.CCError.CCError(common.CCErrCommDuplicateItem) }
            if len(res.Exceptions) > 0 { return ctx.Kit.CCError.New(int(res.Exceptions[0].Code), res.Exceptions[0].Message) }
            if len(res.Created) == 0 { return ctx.Kit.CCError.CCError(common.CCErrTopoCloudNotFound) }
            
            // 第二步:组装响应,同时把platIDs传给审计/lambda闭包
            platIDs := make([]int64, len(res.Created))
            for i, created := range res.Created {
                platIDs[i] = int64(created.ID)
                result[i] = metadata.CreateManyCloudAreaElem{CloudID: int64(created.ID)}
            }
            
            // 第三步:生成审计日志(记录谁创建了哪些云区域)
            audit := auditlog.NewCloudAreaAuditLog(s.CoreAPI.CoreService())
            generateAuditParameter := auditlog.NewGenerateAuditCommonParameter(ctx.Kit, metadata.AuditCreate)
            logs, err := audit.GenerateAuditLog(generateAuditParameter, platIDs)
            if err != nil { return err }
            if err := audit.SaveAuditLog(ctx.Kit, logs...); err != nil { return err }
            
            // 第四步:向IAM注册plat类型的创建者权限(BatchRegister批量注册)
            if auth.EnableAuthorize() {
                iamInstances := make([]metadata.IamInstance, len(res.Created))
                for index, created := range res.Created {
                    iamInstances[index] = metadata.IamInstance{
                        ID:   strconv.FormatUint(created.ID, 10),  // 转为string,对应IAM要求的实例ID
                        Name: util.GetStrByInterface(input.Data[created.OriginIndex][common.BKCloudNameField]),
                    }
                }
                iamInstancesWithCreator := metadata.IamInstancesWithCreator{
                    IamInstances: metadata.IamInstances{
                        Type:      string(iam.SysCloudArea),  // IAM资源类型:sys_cloud_area
                        Instances: iamInstances,
                    },
                    Creator: user,
                }
                _, err = s.AuthManager.Authorizer.BatchRegisterResourceCreatorAction(ctx.Kit.Ctx, ctx.Kit.Header,
                    iamInstancesWithCreator)
                if err != nil { return err }
            }
            return nil
        })
        
        if txnErr != nil { ctx.RespAutoError(txnErr); return }
        ctx.RespEntity(result)
    }

这段代码的调用链非常清晰:

  1. 参数解码 + 服务端字段注入(第 107-122 行):ctx.DecodeInto 把 JSON 解码到匿名结构,循环注入 bk_creator/bk_last_editor
  2. 原子事务执行(第 129-202 行):AutoRunTxn 包裹整个创建逻辑。
  3. IAM 批量注册(第 178-198 行):auth.EnableAuthorize() 判断总开关,开启时调用 BatchRegisterResourceCreatorAction 一次注册全部新云区域。

一个关键细节:云区域的 audit log 怎么生成?

src/common/auditlog/cloud_area.go 第 27-86 行,cloudAreaAuditLog.GenerateAuditLog 会先根据 platIDs 重新查云区域文档,再为每条生成 metadata.AuditLog

// cloudAreaAuditLog.GenerateAuditLog:为云区域生成审计日志
    // 关键:先 ReadInstance 拿真实数据,再为每个 cloudID 构造一条 AuditLog
    func (h *cloudAreaAuditLog) GenerateAuditLog(parameter *generateAuditCommonParameter, platIDs []int64) (
        []metadata.AuditLog, error) {
        
        if len(platIDs) == 0 { return make([]metadata.AuditLog, 0), nil }  // 空ID直接返回
        
        // 用 IN 查询,拿到所有云区域的最新数据(重新查询避免实例刚创建的数据读不到)
        query := &metadata.QueryCondition{
            Condition: mapstr.MapStr{
                common.BKCloudIDField: mapstr.MapStr{common.BKDBIN: platIDs},  // bk_cloud_id IN platIDs
            },
        }
        res, err := h.clientSet.Instance().ReadInstance(kit.Ctx, kit.Header, common.BKInnerObjIDPlat, query)
        
        // 构建 cloudID → cloudData 映射,便于按ID查找
        mutilCloudArea := make(map[int64]mapstr.MapStr)
        for _, data := range res.Info {
            cloudID, _ := data.Int64(common.BKCloudIDField)
            mutilCloudArea[cloudID] = data
        }
        
        // 为每个云区域生成一条审计日志
        logs := make([]metadata.AuditLog, 0)
        for cloudID, cloudData := range mutilCloudArea {
            cloudName, _ := cloudData.String(common.BKCloudNameField)
            logs = append(logs, metadata.AuditLog{
                AuditType:    metadata.CloudResourceType,           // 审计类型:云资源
                ResourceType: metadata.CloudAreaRes,               // 资源类型:云区域
                Action:       parameter.action,                    // 操作类型:Create/Update/Delete
                ResourceID:   cloudID,                             // 资源ID:bk_cloud_id
                ResourceName: cloudName,                           // 资源名称:bk_cloud_name
                OperateFrom:  parameter.operateFrom,
                OperationDetail: &metadata.InstanceOpDetail{
                    BasicOpDetail: metadata.BasicOpDetail{
                        Details: parameter.NewBasicContent(cloudData),  // 完整数据快照
                    },
                    ModelID: common.BKInnerObjIDPlat,              // 对象模型ID:plat
                },
            })
        }
        return logs, nil
    }

注意 AuditType: metadata.CloudResourceTypeResourceType: metadata.CloudAreaResAuditType 是审计分类(云资源/主机/业务),ResourceType 是具体资源类型(云区域),二者组合给出"云区域操作"的完整审计维度。

本节总结:创建链路的 3 个关键设计

  • 1 个事务:AutoRunTxn 保证"创建 + 审计 + IAM 注册"原子性
  • 2 个强制字段:bk_creator / bk_last_editor 由服务端强制赋值
  • 1 批 IAM 注册:BatchRegisterResourceCreatorAction 一次注册全部新云区域

三、内置区域保护:BuiltInCloudAreaIDs 与 UnassignedCloudAreaID

What — 内置区域保护在做什么?

CMDB 把 BKDefaultDirSubArea=0(默认直连区域)和 UnassignedCloudAreaID=90000001(未分配区域)定义为"内置云区域",任何更新/删除操作都会被拒绝。这条规则集中在 BuiltInCloudAreaIDs = []int64{BKDefaultDirSubArea, UnassignedCloudAreaID},由 src/common/definitions.go 第 1824 行统一管理。

Why — 为什么需要内置区域保护?

问题一:默认区域被改会导致全量数据不显示

每台主机都必须挂在一个云区域上。如果运维误把 bk_cloud_id=0 改名或者删除,所有"默认区域下的主机"在 UI 上立刻查不到,监控告警也会断片。

问题二:未分配区域被改会导致野主机无处可归

云同步过程中如果 UnassignedCloudAreaID=90000001 被改名或删除,新拉来的主机在初始化阶段无法落地,整个云同步链路会受影响。

问题三:保留段 ID 被占用会触发拒收

[90000000, 99999999] 是 CMDB 保留给"特殊用途"的 ID 段。src/source_controller/coreservice/core/instances/instance_crud.go 第 169-179 行的 coreservice 实现中,如果调用 CreateInstance 时传入的 ID 落在保留段,coreservice 会主动拒绝创建并返回错误,避免和未来的内置 ID 撞名。

How — 内置区域的两道防线

第一道防线在 UpdatePlat

src/scene_server/host_server/service/cloudarea.go 第 374-466 行,UpdatePlat 解析 bk_cloud_id 后立刻做内置校验:

// UpdatePlat:更新云区域,定义在 service/cloudarea.go L375-466
    // 关键检查:内置云区域禁止更新(防止误改默认/未分配区域)
    func (s *Service) UpdatePlat(ctx *rest.Contexts) {
        
        // 第一步:从URL路径解析 bk_cloud_id
        platIDStr := ctx.Request.PathParameter(common.BKCloudIDField)
        platID, err := strconv.ParseInt(platIDStr, 10, 64)
        if nil != err {
            ctx.RespAutoError(ctx.Kit.CCError.CCErrorf(common.CCErrCommParamsInvalid, common.BKCloudIDField))
            return
        }
        
        // 第二步:内置云区域保护校验 ★
        // BuiltInCloudAreaIDs = []int64{BKDefaultDirSubArea, UnassignedCloudAreaID}
        if util.ContainsInt(common.BuiltInCloudAreaIDs, platID) {
            blog.Infof("UpdatePlat failed, update built in cloud area forbidden, platID:%+v, rid:%s", platID, ctx.Kit.Rid)
            ctx.RespAutoError(ctx.Kit.CCError.CCError(common.CCErrTopoUpdateBuiltInCloudForbidden))
            return
        }
        
        // 第三步:解析请求体(只接受3个字段:CloudName / CloudVendor / Region)
        input := struct {
            CloudName   string `json:"bk_cloud_name"`
            CloudVendor string `json:"bk_cloud_vendor"`
            Region      string `json:"bk_region"`
        }{}
        if err := ctx.DecodeInto(&input); err != nil { ctx.RespAutoError(err); return }
        
        // 第四步:组装 toUpdate map,只包含非空字段
        user := ctx.Kit.User
        toUpdate := mapstr.MapStr{
            common.BKLastEditor: user,  // 最后修改者强制覆盖
        }
        if len(input.CloudVendor) != 0 { toUpdate[common.BKCloudVendor] = input.CloudVendor }
        if len(input.Region) != 0      { toUpdate[common.BKRegion]      = input.Region }
        if len(input.CloudName) != 0   { toUpdate[common.BKCloudNameField] = input.CloudName }
        
        // 第五步:走 AutoRunTxn 事务执行"更新 + 审计"
        audit := auditlog.NewCloudAreaAuditLog(s.CoreAPI.CoreService())
        generateAuditParameter := auditlog.NewGenerateAuditCommonParameter(ctx.Kit,
            metadata.AuditUpdate).WithUpdateFields(toUpdate)  // 审计要记录"哪些字段被改了"
        logs, _ := audit.GenerateAuditLog(generateAuditParameter, []int64{platID})
        
        txnErr := s.Engine.CoreAPI.CoreService().Txn().AutoRunTxn(ctx.Kit.Ctx, ctx.Kit.Header, func() error {
            _, err := s.CoreAPI.CoreService().Instance().UpdateInstance(ctx.Kit.Ctx, ctx.Kit.Header,
                common.BKInnerObjIDPlat, updateOption)
            if err != nil { return err }
            if err := audit.SaveAuditLog(ctx.Kit, logs...); err != nil { return err }  // 审计持久化
            return nil
        })
        
        if txnErr != nil { ctx.RespAutoError(txnErr); return }
        ctx.RespEntity(nil)
    }

这一段最重要的是 util.ContainsInt(common.BuiltInCloudAreaIDs, platID) 这一行:

  • 如果传入的 platID 在内置列表里(090000001),立刻返回 CCErrTopoUpdateBuiltInCloudForbidden 错误
  • CMDB 拒绝所有"内置云区域被改名字 / 改云厂商 / 改区域"的请求
  • UpdatePlat 同时只允许 3 个字段被改(bk_cloud_namebk_cloud_vendorbk_region),不允许通过 API 改 bk_account_idbk_vpc_id——这些只能通过云同步任务来更新

第二道防线在 DeletePlat

src/scene_server/host_server/service/cloudarea.go 第 290-371 行,DeletePlat 的内置保护 + 主机存在校验:

// DeletePlat:删除云区域,定义在 service/cloudarea.go L291-371
    // 2道校验:(1) 防止删除默认云区域 (2) 防止删除有主机的云区域
    func (s *Service) DeletePlat(ctx *rest.Contexts) {
        
        platID, convErr := strconv.ParseInt(ctx.Request.PathParameter(common.BKCloudIDField), 10, 64)
        if nil != convErr { ctx.RespAutoError(...); return }
        
        // 第一道校验:禁止删除默认云区域(bk_cloud_id=0)
        if 0 == platID {
            blog.Errorf("DelPlat failed, can't delete default cloud area, input:%+v,rid:%s", platID, ctx.Kit.Rid)
            ctx.RespAutoError(ctx.Kit.CCError.CCError(common.CCErrDeleteDefaultCloudAreaFail))
            return
        }
        
        // 第二道校验:先查该云区域下是否还有主机
        params := new(meta.QueryInput)
        params.Fields = common.BKHostIDField
        params.Condition = map[string]interface{}{
            common.BKCloudIDField: platID,
        }
        hostRes, err := s.CoreAPI.CoreService().Host().GetHosts(ctx.Kit.Ctx, ctx.Kit.Header, params)
        if nil != err { ctx.RespAutoError(ctx.Kit.CCError.CCError(common.CCErrHostGetFail)); return }
        
        if 0 < hostRes.Count {  // 还有主机,不允许删除
            blog.Errorf("DelPlat plat [%d] has host data, can not delete,rid:%s", platID, ctx.Kit.Rid)
            ctx.RespAutoError(ctx.Kit.CCError.CCError(common.CCErrTopoHasHostCheckFailed))
            return
        }
        
        // 第三步:权限校验(IAM)
        if err := s.AuthManager.AuthorizeByPlatIDs(ctx.Kit.Ctx, ctx.Kit.Header, authmeta.Delete, platID); err != nil {
            ctx.RespAutoError(ctx.Kit.CCError.CCError(common.CCErrCommAuthorizeFailed))
            return
        }
        
        // 第四步:AutoRunTxn 事务执行"删除 + 审计"
        audit := auditlog.NewCloudAreaAuditLog(s.CoreAPI.CoreService())
        generateAuditParameter := auditlog.NewGenerateAuditCommonParameter(ctx.Kit, metadata.AuditDelete)
        logs, _ := audit.GenerateAuditLog(generateAuditParameter, []int64{platID})
        
        delCond := &meta.DeleteOption{
            Condition: mapstr.MapStr{common.BKCloudIDField: platID},
        }
        
        txnErr := s.Engine.CoreAPI.CoreService().Txn().AutoRunTxn(ctx.Kit.Ctx, ctx.Kit.Header, func() error {
            _, err := s.CoreAPI.CoreService().Instance().DeleteInstance(ctx.Kit.Ctx, ctx.Kit.Header,
                common.BKInnerObjIDPlat, delCond)
            if err != nil { return ctx.Kit.CCError.Errorf(common.CCErrTopoInstDeleteFailed) }
            if err := audit.SaveAuditLog(ctx.Kit, logs...); err != nil { return err }
            return nil
        })
        
        if txnErr != nil { ctx.RespAutoError(txnErr); return }
        ctx.RespEntity(nil)
    }

注意 DeletePlatUpdatePlat 的内置白名单校验(util.ContainsInt)之外,又单独加了 if 0 == platID 这道硬编码防线:直接拦截 bk_cloud_id=0 的删除请求,返回 CCErrDeleteDefaultCloudAreaFail

为什么 UnassignedCloudAreaID=90000001DeletePlat 里没有显式拦截?

因为 DeletePlat 的"先查后删"逻辑(GetHosts)会拦住:未分配区域默认下是没有主机的(野主机进它,但通常业务要立即迁出去)。从源码上看,DeletePlat 实际上仅仅依赖了"内置区域几乎不会有主机"这一现状前提来兜底——这是个隐含假设,并非源码显式校验。

本节总结:内置区域的两道防线

  • 更新防线:util.ContainsInt(BuiltInCloudAreaIDs, platID) 拒改内置区域
  • 删除防线:if 0 == platID 硬编码拦截默认区域 + 主机存在校验
  • 允许字段:bk_cloud_namebk_cloud_vendorbk_region(其它字段不允许 API 直改)

四、更新与删除:UpdatePlat / DeletePlat 的差异化设计

What — UpdatePlat / DeletePlat 各自在做什么?

UpdatePlatsrc/scene_server/host_server/service/cloudarea.go 第 374-466 行)只允许改 3 个字段(bk_cloud_namebk_cloud_vendorbk_region),走 AutoRunTxn 同时记录审计字段变更。DeletePlat(第 291-371 行)在删除前必须先查"该云区域下是否还有主机",走 IAM 权限校验 + 审计持久化。

Why — 为什么 update 和 delete 的设计不同?

问题一:update 只允许非关键字段

云区域的 bk_account_idbk_vpc_idbk_vpc_name 都属于"云同步数据",由云同步任务统一管理。如果 API 允许任意改这些字段,会和 CloudSyncTask 数据不一致。UpdatePlat 只开 bk_cloud_name/bk_cloud_vendor/bk_region 三个 UI 可改字段的权限。

问题二:delete 必须先查后删

如果一个云区域下还有主机,删除云区域会导致主机失去挂载点(bk_cloud_id 指向不存在的云区域)。DeletePlat 在删除前必须 GetHosts 确认主机数为 0。

问题三:delete 必须 IAM 校验

删除云区域是高风险操作(破坏性不可逆),必须由 IAM 二次校验用户是否真的拥有删除权限。update 也走 IAM 但权限粒度更低(默认业务编辑权限即可),delete 必须精确到 platID 维度。

How — update/delete 的差异化源码细节

src/scene_server/host_server/service/cloudarea.go 第 391-427 行,update 路径只组装 3 个允许字段:

// UpdatePlat 字段白名单:bk_cloud_name / bk_cloud_vendor / bk_region
    // 禁止通过 UpdatePlat 改 bk_account_id / bk_vpc_id / bk_vpc_name 等云同步字段
    if len(input.CloudVendor) != 0 { toUpdate[common.BKCloudVendor] = input.CloudVendor }
    if len(input.Region) != 0      { toUpdate[common.BKRegion]      = input.Region }
    if len(input.CloudName) != 0   { toUpdate[common.BKCloudNameField] = input.CloudName }
    
    // 这意味着:bk_account_id / bk_vpc_id / bk_vpc_name 只能通过云同步任务修改
    // 在 CreatePlatBatch / CreatePlat 里,这些字段根本不允许传入
    // 在 UpdatePlat 里也不出现 —— 这是双层防御

对比 delete 的"先查主机再删除":

// DeletePlat 的"先查主机"逻辑
    params := new(meta.QueryInput)
    params.Fields = common.BKHostIDField                              // 只查主机ID字段,省IO
    params.Condition = map[string]interface{}{
        common.BKCloudIDField: platID,                                // 按云区域过滤
    }
    hostRes, err := s.CoreAPI.CoreService().Host().GetHosts(ctx.Kit.Ctx, ctx.Kit.Header, params)
    if nil != err { ctx.RespAutoError(...); return }
    
    // only empty plat could be delete
    if 0 < hostRes.Count {                                           // 还有主机
        ctx.RespAutoError(ctx.Kit.CCError.CCError(common.CCErrTopoHasHostCheckFailed))
        return
    }

注意三点设计:

  1. params.Fields = common.BKHostIDField:只查 bk_host_id 字段,避免加载完整主机数据
  2. common.BKCloudIDField: platID:精确匹配该云区域下的主机
  3. if 0 < hostRes.Count:只要 count > 0 就拒绝删除,不区分业务/资源池

关键设计原则UpdatePlat 走"白名单"(只允许 3 个字段),DeletePlat 走"前置校验"(先查后删)。两个 API 都不会被云同步任务以外的入口污染关键字段。

本节总结:update vs delete 的差异

  • update:白名单只允许改 3 个字段 + 内置保护 + 审计字段变更
  • delete:硬编码拦截默认区域 + 主机数检查 + IAM 二次校验
  • 共享:都走 AutoRunTxn 事务,都用 cloudAreaAuditLog 生成审计

五、云账户:CloudAccount 数据模型与云厂商白名单

What — 云账户在做什么?

云区域和云账户是两层正交抽象:云区域(bk_cloud_id)是"网络平面",云账户(bk_account_id)是"云厂商的身份"。一个云账户下通常承载多个云区域(CloudArea.AccountID 外键保持这个多对一关系),CMDB 用 CloudArea.AccountID 把外层云厂商身份和内层网络平面关联起来。CloudAccountsrc/common/metadata/cloud.go 第 25-37 行定义。

Why — 为什么需要独立的云账户抽象?

问题一:云厂商多账号管理

同一云厂商通常有多个账号(开发/测试/生产),每个账号下都有自己的 VPC。如果云区域只能属于一个"匿名云厂商",SRE 难以切换云资源归属。云账户显式建一层抽象,让"腾讯云生产账号下的广州一区"这种语义清晰。

问题二:SecretID / SecretKey 安全管理

对接外部云平台 API 必须用 bk_secret_id + bk_secret_key。把凭据独立成 CloudAccount 行,可以做凭据集中管理:CMDB 在 src/scene_server/cloud_server/service/account.go 第 114 行的 CreateAccount 路径里调用 s.cryptor.Encrypt(account.SecretKey) 对 SecretKey 做加密存储;删除账户前还能做"该账户下所有同步任务存在性"检查。

问题三:云厂商白名单

CMDB 只对接 AWS 和腾讯云两个云厂商(src/common/metadata/cloud.go 第 87-94 行)。其他云厂商不能凭空创建账户,否则云同步任务会找不到对应的 vendor 插件。

How — CloudAccount 数据模型与白名单校验

src/common/metadata/cloud.go 第 25-37 行,CloudAccount 数据模型:

// CloudAccount:云账户,对应外部云厂商的一个账号
    // 必填字段:bk_account_name、bk_cloud_vendor、bk_secret_id、bk_secret_key
    type CloudAccount struct {
        AccountName string    `json:"bk_account_name"     bson:"bk_account_name"`     // 账户名(业务可见)
        CloudVendor string    `json:"bk_cloud_vendor"     bson:"bk_cloud_vendor"`     // 云厂商(AWS=1/TencentCloud=2)
        AccountID   int64     `json:"bk_account_id"       bson:"bk_account_id"`       // 服务端生成的唯一ID
        SecretID    string    `json:"bk_secret_id"        bson:"bk_secret_id"`        // 访问密钥ID
        SecretKey   string    `json:"bk_secret_key"       bson:"bk_secret_key"`       // 访问密钥Key(存储加密)
        Description string    `json:"bk_description"      bson:"bk_description"`      // 描述
        OwnerID     string    `json:"bk_supplier_account" bson:"bk_supplier_account"` // 开发商ID(多租户隔离)
        Creator     string    `json:"bk_creator"          bson:"bk_creator"`          // 创建者(服务端强制赋值)
        LastEditor  string    `json:"bk_last_editor"      bson:"bk_last_editor"`      // 最后修改者
        CreateTime  time.Time `json:"create_time"         bson:"create_time"`         // 创建时间
        LastTime    time.Time `json:"last_time"           bson:"last_time"`           // 最后修改时间
    }

这里 AccountID 是服务端生成的,SecretKey 在 API 返回时会被清空(看 src/source_controller/coreservice/core/cloud/account.go 第 53/69 行)。

src/common/metadata/cloud.go 第 87-94 行,云厂商枚举和白名单:

// 云厂商常量:和属性表中的 bk_cloud_vendor 值相对应
    const (
        AWS          string = "1"  // AWS亚马逊云
        TencentCloud string = "2"  // 腾讯云
    )
    
    // SupportedCloudVendors:支持的云厂商白名单
    // 实现了相应云厂商插件的 vendor 才出现在这里
    var SupportedCloudVendors = []string{AWS, TencentCloud}

注意:

  • AWS = "1"TencentCloud = "2":数字字符串作为云厂商 ID,不是整型
  • SupportedCloudVendors[]string 切片:白名单校验时用 util.InStrArr 检查
  • 新增云厂商时,需要在 SupportedCloudVendors 加常量 + 在 src/scene_server/cloud_server/cloudvendor/ 加对应插件

src/common/metadata/cloud.go 第 39-76 行,CloudAccount.Validate 校验:

// CloudAccount.Validate:云账户字段校验,失败返回 errors.RawErrorInfo
    func (c *CloudAccount) Validate() (rawError errors.RawErrorInfo) {
        if c.AccountName == "" {  // 账户名必填
            return errors.RawErrorInfo{
                ErrCode: common.CCErrCommParamsNeedSet,
                Args:    []interface{}{"bk_account_name"},
            }
        }
        if c.CloudVendor == "" {  // 云厂商必填
            return errors.RawErrorInfo{ErrCode: common.CCErrCommParamsNeedSet, Args: []interface{}{"bk_cloud_vendor"}}
        }
        if !util.InStrArr(SupportedCloudVendors, c.CloudVendor) {  // 云厂商必须在白名单内
            return errors.RawErrorInfo{ErrCode: common.CCErrCloudVendorNotSupport}  // 错误码:云厂商不支持
        }
        if c.SecretID == "" {  // 访问密钥ID必填
            return errors.RawErrorInfo{ErrCode: common.CCErrCommParamsNeedSet, Args: []interface{}{"bk_secret_id"}}
        }
        if c.SecretKey == "" {  // 访问密钥Key必填
            return errors.RawErrorInfo{ErrCode: common.CCErrCommParamsNeedSet, Args: []interface{}{"bk_secret_key"}}
        }
        return errors.RawErrorInfo{}
    }

校验有 4 个层次:账户名 → 云厂商 → 厂商白名单 → SecretID/Key 必填。ValidateRawErrorInfo 而非 CCErrorCoder,方便上游 Convert 成对应 HTTP 错误码。

src/source_controller/coreservice/core/cloud/account.go 第 27-55 行,CreateAccount 写入路径:

// CreateAccount:创建云账户,定义在 core/cloud/account.go L28-55
    func (c *cloudOperation) CreateAccount(kit *rest.Kit, account *metadata.CloudAccount) (*metadata.CloudAccount, errors.CCErrorCoder) {
        if err := c.validCreateAccount(kit, account); nil != err {  // 业务校验
            return nil, err
        }
        
        id, err := c.dbProxy.NextSequence(kit.Ctx, common.BKTableNameCloudAccount)  // 服务端生成自增ID
        if nil != err { return nil, kit.CCError.CCErrorf(common.CCErrCommGenerateRecordIDFailed) }
        account.AccountID = int64(id)
        
        ts := time.Now()
        account.OwnerID = kit.SupplierAccount  // 服务端强制覆盖,强制多租户隔离
        account.Creator = kit.User              // 服务端强制覆盖CreateUser
        account.LastEditor = kit.User           // 服务端强制覆盖LastEditor
        account.CreateTime = ts
        account.LastTime = ts
        
        err = c.dbProxy.Table(common.BKTableNameCloudAccount).Insert(kit.Ctx, account)  // 写入 cc_CloudAccount 集合
        if err != nil { return nil, kit.CCError.CCError(common.CCErrCommDBInsertFailed) }
        
        // 不返回 bk_secret_key 的值(安全)—— 关键安全设计
        account.SecretKey = ""
        return account, nil
    }

注意两个关键设计:

  1. util.InStrArr(SupportedCloudVendors, c.CloudVendor):白名单校验,拒绝任何不在 AWS / TencentCloud 中的厂商
  2. account.SecretKey = ""API 返回前清空 SecretKey,避免凭据泄露到 UI / 日志 / 客户端缓存

src/source_controller/coreservice/core/cloud/validator.go 第 26-58 行,validCreateAccount 三层防护:

// validCreateAccount:创建云账户时的业务校验
    func (c *cloudOperation) validCreateAccount(kit *rest.Kit, account *metadata.CloudAccount) errors.CCErrorCoder {
        // 第一层:云厂商白名单校验
        if !util.InStrArr(metadata.SupportedCloudVendors, string(account.CloudVendor)) {
            return kit.CCError.CCErrorf(common.CCErrCloudVendorNotSupport)
        }
        
        // 第二层:账户名唯一性校验
        cond := mapstr.MapStr{common.BKCloudAccountName: account.AccountName}
        count, err := c.countAccount(kit, cond)
        if count > 0 {
            return kit.CCError.CCError(common.CCErrCloudAccountNameAlreadyExist)  // 账户名已存在
        }
        
        // 第三层:SecretID 全局唯一性校验(一个SecretID只能绑定一个账户)
        option := &metadata.SearchCloudOption{
            Condition: mapstr.MapStr{common.BKSecretID: account.SecretID},
        }
        multiAccount, err := c.SearchAccount(kit, option)
        if len(multiAccount.Info) > 0 {
            return kit.CCError.CCErrorf(common.CCErrCloudAccountSecretIDAlreadyExist, multiAccount.Info[0].AccountName)
        }
        return nil
    }

三层校验分别是:① 云厂商白名单;② 账户名唯一;③ SecretID 全局唯一。CMDB 通过这层防护确保每个云账户的独立性

本节总结:云账户的 4 层防护

  • 1 个白名单:SupportedCloudVendors = []string{AWS, TencentCloud}
  • 1 个 Validate:字段必填 + 厂商白名单 + 凭据必填
  • 1 个服务端序列:NextSequence 自增 ID
  • 1 个安全设计:API 返回前清空 SecretKey

六、主机云区域字段更新:UpdateHostCloudAreaField 的唯一性校验

What — UpdateHostCloudAreaField 在做什么?

主机迁移场景中,经常需要把一批主机的 bk_cloud_id 从一个云区域改到另一个云区域。UpdateHostCloudAreaField 就是专门做这件事的 API,定义在 src/scene_server/host_server/service/cloudarea.go 第 469-505 行(service 层)和 src/source_controller/coreservice/core/host/cloudarea.go 第 30-60 行(coreservice 层)。它在改 bk_cloud_id 之前,会做三层校验bk_cloud_id 有效性 → 主机存在性 → 目标 bk_cloud_id + bk_host_innerip 唯一性。

Why — 为什么改一个字段要做这么复杂的校验?

问题一:避免 bk_cloud_id 指向不存在的区域

如果业务传一个不存在于 cc_PlatBasebk_cloud_id,主机表里的 bk_cloud_id 会变成野指针,后续查询会出问题。

问题二:避免内网 IP 跨云冲突

同一个 bk_host_innerip 在不同云区域下可能存在。如果跨云改 bk_cloud_id 后,新的 bk_cloud_id + bk_host_innerip 组合会和已有主机冲突。

问题三:避免把主机改到"未分配"区域

UnassignedCloudAreaID = 90000001 是兜底区域,UpdateHostCloudAreaField 显式拒绝改到这个 ID(service 层第 484 行)。

How — 三层校验的源码细节

src/scene_server/host_server/service/cloudarea.go 第 469-505 行,service 层入口:

// UpdateHostCloudAreaField:更新主机的云区域字段,service层 L470-505
    func (s *Service) UpdateHostCloudAreaField(ctx *rest.Contexts) {
        rid := ctx.Kit.Rid
        
        // 第一步:解码请求体 { BizID, HostIDs, CloudID }
        input := metadata.UpdateHostCloudAreaFieldOption{}
        if err := ctx.DecodeInto(&input); err != nil { ctx.RespAutoError(err); return }
        
        // 第二步:单次操作记录数限制(防止批量过大)
        if len(input.HostIDs) > common.BKMaxRecordsAtOnce {
            ctx.RespAutoError(ctx.Kit.CCError.CCErrorf(common.CCErrExceedMaxOperationRecordsAtOnce,
                common.BKMaxRecordsAtOnce))
            return
        }
        
        // 第三步:兜底区域校验 ★ 服务层先拒绝
        if input.CloudID == common.UnassignedCloudAreaID {
            ctx.RespAutoError(ctx.Kit.CCError.CCErrorf(common.CCErrCommParamsIsInvalid, common.BKCloudIDField))
            return
        }
        
        // 第四步:AutoRunTxn 事务调用 coreservice
        txnErr := s.Engine.CoreAPI.CoreService().Txn().AutoRunTxn(ctx.Kit.Ctx, ctx.Kit.Header, func() error {
            ccErr := s.CoreAPI.CoreService().Host().UpdateHostCloudAreaField(ctx.Kit.Ctx, ctx.Kit.Header, input)
            if ccErr != nil {
                blog.ErrorJSON("update host cloud area failed, input: %s, err: %s, rid: %s", input, ccErr.Error(), rid)
                return ccErr
            }
            return nil
        })
        
        if txnErr != nil { ctx.RespAutoError(txnErr); return }
        ctx.RespEntity(nil)
    }

src/source_controller/coreservice/core/host/cloudarea.go 第 30-152 行,coreservice 层的核心逻辑:

// UpdateHostCloudAreaField:coreservice层实现,core/host/cloudarea.go L31-60
    // 三层校验链:validCloudID → validHost → 实际更新
    func (hm *hostManager) UpdateHostCloudAreaField(kit *rest.Kit,
        input metadata.UpdateHostCloudAreaFieldOption) errors.CCErrorCoder {
        
        if len(input.HostIDs) == 0 {
            return kit.CCError.CCErrorf(common.CCErrCommParamsInvalid, "bk_host_ids")
        }
        input.HostIDs = util.IntArrayUnique(input.HostIDs)  // 去重,防止同一主机被改两次
        
        if err := validCloudID(kit, input.CloudID); err != nil {  // 第一层:目标云区域是否存在
            return err
        }
        
        if err := validHost(kit, input.CloudID, input.HostIDs); err != nil {  // 第二层:主机 + 唯一性校验
            return err
        }
        
        // 第三层:实际更新 cc_HostBase 表的 bk_cloud_id 字段
        updateFilter := map[string]interface{}{
            common.BKHostIDField: map[string]interface{}{
                common.BKDBIN: input.HostIDs,  // bk_host_id IN (input.HostIDs)
            },
        }
        updateDoc := map[string]interface{}{
            common.BKCloudIDField: input.CloudID,  // SET bk_cloud_id = input.CloudID
        }
        if err := mongodb.Client().Table(common.BKTableNameBaseHost).Update(kit.Ctx, updateFilter, updateDoc); err != nil {
            return kit.CCError.CCError(common.CCErrCommDBUpdateFailed)
        }
        return nil
    }
    
    // validCloudID:校验 bk_cloud_id 是否在 cc_PlatBase 集合中真实存在
    // 定义 core/host/cloudarea.go L62-85
    func validCloudID(kit *rest.Kit, cloudID int64) errors.CCErrorCoder {
        // 通过 hooks 触发第三方校验(可被业务方自定义扩展)
        if err := hooks.ValidHostCloudIDHook(kit, cloudID); err != nil { return err }
        
        cloudIDFiler := map[string]interface{}{common.BKCloudIDField: cloudID}
        count, err := mongodb.Client().Table(common.BKTableNameBasePlat).Find(cloudIDFiler).Count(kit.Ctx)
        if err != nil { return kit.CCError.CCError(common.CCErrCommDBSelectFailed) }
        
        if count == 0 {                                                              // 不存在
            return kit.CCError.CCErrorf(common.CCErrCommParamsInvalid, common.BKCloudIDField)
        }
        if count > 1 { return kit.CCError.CCError(common.CCErrCommGetMultipleObject) } // 多个,异常
        return nil
    }

validHost 在同一文件第 87-152 行做了最复杂的"防重复"校验:

// validHost:校验主机 + 目标云区域的 IP 唯一性
    // 定义 core/host/cloudarea.go L88-152
    func validHost(kit *rest.Kit, cloudID int64, hostIDs []int64) errors.CCErrorCoder {
        // step1. 校验 bk_host_ids 全部存在
        hostFilter := map[string]interface{}{
            common.BKHostIDField: map[string]interface{}{common.BKDBIN: hostIDs},
        }
        hostSimplify := make([]metadata.HostMapStr, 0)
        fields := []string{common.BKHostInnerIPField, common.BKHostInnerIPv6Field, common.BKCloudIDField,
            common.BKHostIDField, common.BKAddressingField}  // 仅查这5个字段,省IO
        
        err := mongodb.Client().Table(common.BKTableNameBaseHost).Find(hostFilter).Fields(fields...).All(kit.Ctx, &hostSimplify)
        
        // hostIDs 数量和实际查出来的不一致 → 有主机不存在
        if len(hostIDs) != len(hostSimplify) {
            return kit.CCError.CCErrorf(common.CCErrCommParamsInvalid, common.BKHostIDField)
        }
        
        // step2. 仅对静态 IP(addressing="static")做唯一性校验(动态 DHCP IP 无需校验)
        innerIPv4s := make([]string, 0)
        ipv4Map := make(map[string]struct{})
        innerIPv6s := make([]string, 0)
        ipv6Map := make(map[string]struct{})
        for _, item := range hostSimplify {
            addressing, ok := item[common.BKAddressingField].(string)
            if !ok { return kit.CCError.CCErrorf(common.CCErrCommParamsInvalid, common.BKAddressingField) }
            
            if addressing != common.BKAddressingStatic { continue }  // 非静态分配,跳过
            
            // 静态IP主机:检查请求体内IP是否重复(同一个 batch 内)
            ipv4, ok := item[common.BKHostInnerIPField].(string)
            if ok {
                if _, ok := ipv4Map[ipv4]; ok {
                    return kit.CCError.CCErrorf(common.CCErrCommDuplicateItem, common.BKHostInnerIPField)
                }
                innerIPv4s = append(innerIPv4s, ipv4)
                ipv4Map[ipv4] = struct{}{}
            }
            // IPv6 同样的去重逻辑
            ipv6, ok := item[common.BKHostInnerIPv6Field].(string)
            if ok {
                if _, ok := ipv6Map[ipv6]; ok {
                    return kit.CCError.CCErrorf(common.CCErrCommDuplicateItem, common.BKHostInnerIPv6Field)
                }
                innerIPv6s = append(innerIPv6s, ipv6)
                ipv6Map[ipv6] = struct{}{}
            }
        }
        
        // step3. 校验数据库内是否已有同 IP 主机在目标云区域下(即新组合会冲突)
        if err := validDuplicatedHostInDB(kit, hostIDs, cloudID, innerIPv4s, innerIPv6s); err != nil {
            return err
        }
        return nil
    }

校验链有 3 个步骤:

  1. step1:主机存在性:传入的主机 ID 必须全部在 cc_HostBase 中查得到
  2. step2:请求内 IP 去重:同一个 batch 内 IPv4/IPv6 不能重复
  3. step3:数据库内 IP 冲突validDuplicatedHostInDB(同文件第 154-202 行)查询目标云区域下是否已经存在相同 IP 的主机

validDuplicatedHostInDB 的关键构造:

// validDuplicatedHostInDB:检查目标云区域下是否已有同IP主机(除自己外)
    dbHostFilter := map[string]interface{}{
        common.BKAddressingField: common.BKAddressingStatic,                              // 只查静态IP
        common.BKHostIDField: map[string]interface{}{
            common.BKDBNIN: hostIDs,                                                     // 排除本次要改的主机
        },
        common.BKCloudIDField: cloudID,                                                  // 目标云区域
        common.BKDBOR:         ipCond,                                                   // IP 在列表内
    }

为什么用 BKDBNIN(not in)排除 hostIDs?

因为 批量场景里这些 hostIDs 就是本次要改云区域的主机,如果不排除,step2 已经去重过的 IP 还是会被自己的"未改前记录"误命中。排除掉本批次主机,step3 才是干净的"目标云区域下其它主机的 IP"查询。

本节总结:UpdateHostCloudAreaField 的 3 层校验链

  • service 层:单次记录上限校验 + 拒绝改到未分配区域
  • coreservice 层:validCloudID 校验目标云区域存在性
  • 主机校验层:validHost 校验主机存在 + 内网 IP 唯一性

七、云区域主机数查询:FindCloudAreaHostCount 的并发聚合

What — FindCloudAreaHostCount 在做什么?

云区域列表页通常需要显示每个云区域下的主机数。CMDB 提供 FindCloudAreaHostCount,让前端一次请求就能拿到 N 个云区域的主机数。它的实现路径:src/scene_server/host_server/service/cloudarea.go 第 543-572 行(service 层)+ src/source_controller/coreservice/core/host/cloudarea.go 第 204-261 行(coreservice 层)。

Why — 为什么需要并发聚合主机数?

问题一:串行查询太慢

如果串行对 N 个云区域做 Count 查询(每次都是一次独立的 MongoDB Count),前端列表页会卡顿。CMDB 用并发能压缩总耗时。关于单次 Count 耗时具体多少毫秒,受索引命中、数据量、MongoDB 压力等影响,不在本文讨论范围。

问题二:需要对结果按请求顺序排序

前端传的 bk_cloud_ids 列表,CMDB 必须按这个原始顺序返回结果(src/source_controller/coreservice/core/host/cloudarea.go 第 252-258 行的 for idx, cloudID := range input.CloudIDsinput.CloudIDs 而不是去重后的 cloudIDs 来组装结果),否则前端需要重新对齐。

问题三:避免压垮 MongoDB

无限制的 goroutine 会把 MongoDB 压垮。FindCloudAreaHostCount 用了一个 pipeline := make(chan bool, 10) 做并发上限,最多同时跑 10 个 goroutine

How — 并发聚合的实现

src/common/metadata/hostserver.go 第 826-854 行,请求/响应模型:

// CloudAreaHostCount:请求体,单次最多 50 个 bk_cloud_ids
    type CloudAreaHostCount struct {
        CloudIDs []int64 `json:"bk_cloud_ids"`  // 要查主机数的云区域ID列表
    }
    
    // Validate:云区域数校验,1-50
    func (c *CloudAreaHostCount) Validate() (rawError errors.RawErrorInfo) {
        maxLimit := 50
        if len(c.CloudIDs) == 0 || len(c.CloudIDs) > maxLimit {
            return errors.RawErrorInfo{
                ErrCode: common.CCErrArrayLengthWrong,
                Args:    []interface{}{"bk_cloud_ids", maxLimit},
            }
        }
        return errors.RawErrorInfo{}
    }
    
    // CloudAreaHostCountResult:响应体
    type CloudAreaHostCountResult struct {
        BaseResp `json:",inline"`
        Data     []CloudAreaHostCountElem `json:"data"`
    }
    
    // CloudAreaHostCountElem:单个云区域返回的元素
    type CloudAreaHostCountElem struct {
        CloudID   int64 `json:"bk_cloud_id"`  // 云区域ID(和请求顺序保持一致)
        HostCount int64 `json:"host_count"`   // 该云区域下的主机数
    }

read src/source_controller/coreservice/core/host/cloudarea.go 第 204-261 行,并发实现:

// FindCloudAreaHostCount:coreservice层实现,core/host/cloudarea.go L205-261
    func (hm *hostManager) FindCloudAreaHostCount(kit *rest.Kit, input metadata.CloudAreaHostCount) (
        []metadata.CloudAreaHostCountElem, error) {
        
        if len(input.CloudIDs) == 0 {
            return nil, kit.CCError.CCErrorf(common.CCErrCommParamsInvalid, "bk_cloud_ids")
        }
        
        cloudIDs := util.IntArrayUnique(input.CloudIDs)  // 简单去重
        
        // 关键并发原语:用 channel 当"信号量"限流
        var wg sync.WaitGroup
        var lock sync.RWMutex
        var firstErr errors.CCErrorCoder
        pipeline := make(chan bool, 10)  // ★ 信号量:最大并发 10
        cloudCountMap := make(map[int64]int64)  // cloudID → hostCount
        
        for _, cloudID := range cloudIDs {
            pipeline <- true  // ★ 进入临界区前占用槽位
            wg.Add(1)
            
            go func(cloudID int64) {
                defer func() {
                    wg.Done()
                    <-pipeline  // ★ goroutine退出时释放槽位
                }()
                
                filter := map[string]interface{}{common.BKCloudIDField: cloudID}
                hostCnt, err := mongodb.Client().Table(common.BKTableNameBaseHost).Find(filter).Count(kit.Ctx)
                if err != nil {
                    blog.ErrorJSON("UpdateHostCloudAreaField failed, db selected failed, ...",
                        common.BKTableNameBaseHost, filter, err.Error(), kit.Rid)
                    if firstErr == nil {  // 只记录第一个错误
                        firstErr = kit.CCError.CCError(common.CCErrCommDBSelectFailed)
                    }
                    return
                }
                
                lock.Lock()   // 写 map 需要互斥锁
                cloudCountMap[cloudID] = int64(hostCnt)
                lock.Unlock()
                
            }(cloudID)
        }
        
        wg.Wait()  // 等所有 goroutine 结束
        
        if firstErr != nil {  // 有错误立刻返回
            return nil, firstErr
        }
        
        // 关键:按原始请求顺序组装结果
        ret := make([]metadata.CloudAreaHostCountElem, len(input.CloudIDs))
        for idx, cloudID := range input.CloudIDs {
            ret[idx] = metadata.CloudAreaHostCountElem{
                CloudID:   cloudID,
                HostCount: cloudCountMap[cloudID],
            }
        }
        return ret, nil
    }

这段代码有 4 个关键设计:

  1. channel 信号量(第 216 行):pipeline := make(chan bool, 10)。channel 的容量 10 就是最大并发数,pipeline <- true 占槽、<-pipeline 释放槽。
  2. 只记录第一个错误(第 233-236 行):if firstErr == nil 配合 firstErr 变量,保证只有一个错误被传播。
  3. 顺序保留(第 252-258 行):遍历 input.CloudIDs 而不是 cloudIDs(去重后的),保证按请求的原始顺序返回。
  4. 默认值 0(第 256 行):HostCount: cloudCountMap[cloudID] 即使 key 不在 map 里也返回零值,不会 NPE。

Service 层(第 543-572 行)的封装非常简单——主要做 Validate + 透传:

// FindCloudAreaHostCount:service层封装,service/cloudarea.go L544-572
    func (s *Service) FindCloudAreaHostCount(ctx *rest.Contexts) {
        input := new(metadata.CloudAreaHostCount)
        if err := ctx.DecodeInto(input); nil != err { ctx.RespAutoError(err); return }
        
        rawErr := input.Validate()  // 校验:1-50个云区域
        if rawErr.ErrCode != 0 {
            ctx.RespAutoError(rawErr.ToCCError(ctx.Kit.CCError))
            return
        }
        
        // 调用coreservice
        res, err := s.CoreAPI.CoreService().Host().FindCloudAreaHostCount(ctx.Kit.Ctx, ctx.Kit.Header, *input)
        if nil != err { ctx.RespAutoError(ctx.Kit.CCError.CCError(common.CCErrCommHTTPDoRequestFailed)); return }
        if false == res.Result { ctx.RespAutoError(res.CCError()); return }
        
        ctx.RespEntity(res.Data)
    }

注意 rawErr.ToCCError(ctx.Kit.CCError)RawErrorInfo 在 service 层转换为 CCError 才能返回给客户端。

本节总结:FindCloudAreaHostCount 的 4 个设计要点

  • 1 个信号量:chan bool 容量 10,限制最大并发数
  • 1 个错误去重:firstErr 变量只记首个错误
  • 1 个顺序保留:遍历原始 input.CloudIDs 而不是去重后的
  • 1 个零值兜底:map 不存在的 key 不会出现 NPE

八、云区域管理的 5 个核心设计原则

源码视角总结:从云区域管理看 CMDB 的命名空间哲学

读完 src/scene_server/host_server/service/cloudarea.gosrc/common/metadata/cloud.gosrc/source_controller/coreservice/core/cloud/account.gosrc/source_controller/coreservice/core/host/cloudarea.go 这一组源码,可以把云区域管理的设计哲学总结为 5 个原则:

1. 内置 ID 双重保护

BKDefaultDirSubArea=0UnassignedCloudAreaID=90000001 通过 BuiltInCloudAreaIDs 白名单 + if 0 == platID 硬编码双重防线,任何 update/delete 都会被拒绝

2. 服务端字段强制覆盖

bk_creatorbk_last_editorbk_supplier_account 都由服务端强制赋值,前端传入的相应字段会被忽略。CMDB 通过这种方式保证操作可追溯。

3. 字段白名单 + 事务审计

云区域更新只能改 3 个字段(bk_cloud_name/bk_cloud_vendor/bk_region),其它字段(如 bk_account_idbk_vpc_id)只能通过云同步任务更新。所有写操作都走 AutoRunTxn,审计和 IAM 注册不会被跳过。

4. Secret 凭据安全设计

CloudAccount.SecretKey 在 API 返回前被清空(src/source_controller/coreservice/core/cloud/account.go 第 53/69 行)。SecretID 全局唯一校验(一个 SecretID 只能绑定一个账户)。CMDB 通过这两层设计防止凭据泄露。

5. 复合主键 + 并发聚合

bk_host_id + bk_cloud_id 作为全局唯一主机标识。修改云区域字段时要做 bk_cloud_id + bk_host_innerip 唯一性校验防冲突。主机数查询用 channel 信号量限制并发,最多 10 个 goroutine并发查 N 个云区域。

避坑提醒(源码视角):

  • 不要硬编码 bk_cloud_id=0 当作"内网云"BKDefaultDirSubArea=0 是默认直连区域,UnassignedCloudAreaID=90000001 才是未分配区域
  • 不要试图改 bk_account_id:它只能由云同步任务 CloudSyncTask 来更新,UpdatePlat 不接受这个字段
  • 不要在 FindCloudAreaHostCount 中传超过 50 个 bk_cloud_idValidate 会直接拒绝,最大 50
  • 不要忽略 IAM 注册失败的影响:如果 auth.EnableAuthorize() == true 时 IAM 注册失败,整个创建/批量创建事务会回滚

全篇总结:云区域管理的 5 个核心设计

  • 2 道防线:BuiltInCloudAreaIDs 白名单 + if 0 == platID 硬编码,护住内置区域
  • 3 个允许字段:bk_cloud_name / bk_cloud_vendor / bk_region,其它字段只由云同步任务更新
  • 1 个白名单:SupportedCloudVendors = []string{AWS, TencentCloud},云厂商只能在这两个里选
  • 3 层校验链:validCloudID + validHost + validDuplicatedHostInDB,护住 bk_cloud_id 字段更新
  • 1 套并发:chan bool 信号量限制 10 并发,FindCloudAreaHostCount 聚合 N 个云区域主机数

FAQ(常见 20 问)

Q1. CMDB 3.14.6 的云区域在哪个数据集合存储?

一句话结论:云区域以内置对象模型 plat 实例的形式存在,物理集合是 cc_PlatBase

src/common/definitions.go 第 132-133 行,BKInnerObjIDPlat = "plat" 是云区域的内置 ObjectID。CRUD 操作最终走 CreateInstance / UpdateInstance / DeleteInstance / ReadInstance,数据落在 coreservice 内置对象实例集合 cc_PlatBase

Q2. 云区域创建链路的关键原子操作是什么?

一句话结论:CreatePlatBatchAutoRunTxn 把"实例创建 + 审计日志 + IAM 注册"打包成原子事务。

src/scene_server/host_server/service/cloudarea.go 第 129-202 行,AutoRunTxn 包裹 CreateManyInstance(批量创建实例)、audit.SaveAuditLog(保存审计日志)、BatchRegisterResourceCreatorAction(批量注册 IAM 创建者权限)三步,任一失败全部回滚。

Q3. CloudArea 数据模型包含哪些字段?

一句话结论:包含 14 个字段,其中 bk_cloud_id/bk_cloud_name/bk_cloud_vendor/bk_account_id/bk_vpc_id/bk_region 等 6 个是核心业务字段。

src/common/metadata/cloud.go 第 347-363 行,CloudArea 结构体字段:CloudIDCloudNameStatusCloudVendorOwnerIDVpcIDVpcNameRegionAccountIDCreatorLastEditorCreateTimeLastTimeDefault

Q4. CMDB 支持哪些云厂商?

一句话结论:当前仅支持 AWS("1")和腾讯云("2")两个云厂商。

src/common/metadata/cloud.go 第 87-94 行,AWS = "1"TencentCloud = "2"SupportedCloudVendors = []string{AWS, TencentCloud}。新增云厂商需要在这两个常量 + SupportedCloudVendors 添加,并在 src/scene_server/cloud_server/cloudvendor/ 加对应插件。

Q5. CMDB 内置的"未分配"云区域 ID 是多少?

一句话结论:UnassignedCloudAreaID = 90000001,名称是 "未分配"

src/common/definitions.go 第 1833-1837 行,UnassignedCloudAreaID = 90000001UnassignedCloudAreaName = "未分配"。这个 ID 在 src/scene_server/host_server/service/cloudarea.go 第 484 行被 UpdateHostCloudAreaField 显式拒绝(不允许把有云的主机改到兜底区域)。

Q6. CMDB 的云区域保留 ID 段是多少?

一句话结论:保留段是 90000000 ~ 99999999,包含 BuiltInCloudAreaIDs = {0, 90000001}

src/common/definitions.go 第 1826-1838 行,ReservedCloudAreaStartID = 90000000ReservedCloudAreaEndID = 99999999 为保留段。BuiltInCloudAreaIDs = []int64{BKDefaultDirSubArea, UnassignedCloudAreaID} 是内置保护区。

Q7. BuiltInCloudAreaIDs 是干什么用的?

一句话结论:是"内置云区域保护名单",UpdatePlat 用它判断拒绝修改内置区域。

src/common/definitions.go 第 1823-1824 行,var BuiltInCloudAreaIDs = []int64{BKDefaultDirSubArea, UnassignedCloudAreaID}src/scene_server/host_server/service/cloudarea.go 第 385 行 util.ContainsInt(common.BuiltInCloudAreaIDs, platID)UpdatePlat 中检查目标 ID。

Q8. UpdatePlat 允许改哪些字段?

一句话结论:仅允许改 bk_cloud_namebk_cloud_vendorbk_region 三字段 + bk_last_editor 四个。

src/scene_server/host_server/service/cloudarea.go 第 391-419 行,toUpdate map 只组装这 4 个字段。其它字段(bk_account_idbk_vpc_id)只能通过云同步任务修改。

Q9. DeletePlat 在删除前必须检查什么?

一句话结论:必须检查 ①不是 bk_cloud_id=0 ②该云区域下没有主机 ③用户有 IAM 删除权限。

src/scene_server/host_server/service/cloudarea.go 第 290-371 行,DeletePlat 三道前置校验:硬编码拦截默认区域(第 299 行)、调用 GetHosts 校验主机数为 0(第 320 行)、AuthorizeByPlatIDs IAM 校验(第 327 行)。

Q10. FindManyCloudArea 支持哪些查询方式?

一句话结论:支持精确查询 + name 模糊搜索(正则)+ 默认 bk_cloud_id 排序 + 可选附加同步任务 ID 信息。

src/scene_server/host_server/service/cloudarea.go 第 33-98 行,FindManyCloudArea 接收 metadata.CloudAreaSearchParam(含 IsFuzzySyncTaskIDs),按 IsFuzzy=true 时把字符串字段包成 $regex 模糊匹配。如果 SyncTaskIDs=true 会调 addPlatSyncTaskIDs 把同步任务信息附加到响应。

Q11. UpdateHostCloudAreaField 的三重校验链是什么?

一句话结论:service 层校验业务参数、validCloudID 校验目标云区域存在、validHost 校验主机存在 + 唯一性。

src/source_controller/coreservice/core/host/cloudarea.go 第 30-152 行,三层校验调用链:UpdateHostCloudAreaFieldutil.IntArrayUnique 去重 → validCloudID(第 62 行) → validHost(第 87 行) → validDuplicatedHostInDB(第 154 行)。

Q12. CloudAccount 校验哪些字段?

一句话结论:校验账户名、云厂商、厂商白名单、SecretID、SecretKey 五个必填。

src/common/metadata/cloud.go 第 39-76 行,CloudAccount.Validate 5 步:bk_account_name 必填、bk_cloud_vendor 必填、util.InStrArr(SupportedCloudVendors) 白名单、bk_secret_id 必填、bk_secret_key 必填。

Q13. 云账户的 SecretKey 在哪里被清空?

一句话结论:在 CreateAccountSearchAccount 返回前都会被清空。

src/source_controller/coreservice/core/cloud/account.go 第 53 行(CreateAccount)和第 68-70 行(SearchAccount 循环中),都执行 account.SecretKey = ""。CMDB 通过这种设计防止 SecretKey 泄露到 API 客户端和 UI。

Q14. FindCloudAreaHostCount 用什么方式做并发?

一句话结论:用 chan bool 当信号量限制最大 10 并发。

src/source_controller/coreservice/core/host/cloudarea.go 第 216 行,pipeline := make(chan bool, 10)。每个 goroutine 进入前 pipeline <- true 占槽位、退出时 <-pipeline 释放。这样避免无限制 goroutine 把 MongoDB 压垮。

Q15. FindCloudAreaHostCount 单次最多支持几个云区域?

一句话结论:最多 50 个,由 CloudAreaHostCount.Validate 强制校验。

src/common/metadata/hostserver.go 第 832-842 行,maxLimit := 50。超过 50 会返回 CCErrArrayLengthWrong 错误,超出范围的批量调用会被 coreservice 层主动拦截。

Q16. 云区域审计日志的 AuditType 是什么?

一句话结论:AuditType: metadata.CloudResourceTypeResourceType: metadata.CloudAreaRes

src/common/auditlog/cloud_area.go 第 69-82 行,cloudAreaAuditLog.GenerateAuditLog 构造 metadata.AuditLog 时用 CloudResourceType 作审计分类、CloudAreaRes 作资源类型、ModelID: common.BKInnerObjIDPlat 表示对象模型 ID。

Q17. 主机云区域和云账户的对应关系是什么?

一句话结论:CloudArea.AccountID 字段外键关联 CloudAccount.AccountID,一个云账户可被多个云区域共享。

src/common/metadata/cloud.go 第 357 行,AccountID int64CloudArea 里是外键。多对一关系:多个 bk_cloud_id 可以共享同一个 bk_account_id,CMDB 用 bk_cloud_vendor + bk_account_id 双键定位具体云厂商身份。

Q18. CloudArea 和 CloudSyncTask 的关系是什么?

一句话结论:CloudSyncTask 通过 SyncVpcs[].CloudID 字段指向具体云区域。addPlatSyncTaskIDs 反向查询补齐字段。

src/scene_server/host_server/service/cloudarea.go 第 507-541 行,addPlatSyncTaskIDs 通过 SearchSyncTask 反向查询所有同步任务,按 task.SyncVpcs[].CloudID 构建 cloudIDTasks[cloudID] = []int64{taskID} 映射,再写回响应。

Q19. 云区域创建事务里 IAM 注册的资源类型是什么?

一句话结论:iam.SysCloudArea = "sys_cloud_area",中文名"管控区域"。

src/ac/iam/types.go 第 209-210 行和 src/ac/iam/initial_resources.go 第 30 行,SysCloudArea TypeID = "sys_cloud_area"ResourceTypeIDMap[SysCloudArea] = "管控区域"src/ac/iam/adaptor.go 第 71 行 meta.CloudAreaInstance: SysCloudArea 做对象模型到 IAM 类型的映射。

Q20. BuiltInCloudAreaIDs 如何扩展新的内置 ID?

一句话结论:在 src/common/definitions.go 第 1823-1824 行的数组里追加新 ID 常量。

src/common/definitions.go 第 1823 行注释"内置管控区域,后续如果有其他新增的 id,需要添加到这个数组里"。新增内置区域需要:① 在 definitions.go 常量区加新常量;② 在 BuiltInCloudAreaIDs 数组里追加;③ 在 admin_server/upgrader/ 加数据迁移脚本(参考 src/scene_server/admin_server/upgrader/y3.13.202404221100/add_unassigned_cloud_area.go)。

FAQ 全篇总纲

本 FAQ 覆盖了 CMDB 3.14.6 云区域管理的 5 大维度:

  • 数据模型与内置常量:Q1(数据集合)、Q3(CloudArea 字段)、Q4(云厂商枚举)、Q5(未分配 ID)、Q6(保留 ID 段)、Q7(BuiltInCloudAreaIDs)
  • 创建与更新:Q2(创建原子事务)、Q8(UpdatePlat 允许字段)、Q10(查询方式)
  • 删除与修改:Q9(DeletePlat 检查)、Q11(UpdateHostCloudAreaField 校验链)、Q17(AccountID 外键)
  • 云账户与白名单:Q12(CloudAccount 校验)、Q13(SecretKey 清空)、Q16(审计日志 AuditType)、Q19(IAM SysCloudArea)
  • 并发聚合与扩展:Q14(并发信号量)、Q15(最大上限)、Q18(CloudSyncTask 关系)、Q20(BuiltInCloudAreaIDs 扩展)

Roadmap 后续预告

下一篇 #16:拓扑缓存加速 — 业务拓扑秒级查询的 Redis 缓存策略

将带你深入 src/source_controller/cacheservice/cache/topology/topology.go,看 biz-topo-cache 定时刷新业务拓扑到 Redis 的实现、Master 选举 + 分布式锁如何保证只有一个节点执行刷新、并发控制在 5 个 goroutine 的 pipeline := make(chan struct{}, 5) 设计。

后续专题预告:

  • #17:k8s 资源纳管 — Pod/Deployment/Service 统一管理
  • #18:容器拓扑关联 — Pod ↔ Host ↔ 机柜全链路映射
  • #19:主机锁定 — 并发控制与分布式锁
  • #20:批量导入导出 — Excel 模板与数据校验
posted @ 2026-07-07 00:05  左扬  阅读(11)  评论(0)    收藏  举报