蓝鲸 CMDB 3.14.6 源码专题【左扬精讲】—— #10 CMDB 数据同步体系:实例间同步与外部云数据接入

蓝鲸 CMDB 3.14.6 源码专题【左扬精讲】—— #10 CMDB 数据同步体系:实例间同步与外部云数据接入

在大型企业环境中,主机资产往往分散在多个系统:云平台(AWS EC2、腾讯云 CVM)、CMDB 自身、网络监控系统、运维堡垒机。

这些系统的数据格式不统一、更新频率不同步,如何将不同来源的数据可靠地同步到 CMDB,并保证数据质量?

蓝鲸 CMDB 的数据同步体系包含两条完全独立的路径:1)synchronize_server + transfer-service 做 CMDB 主从实例间同步;2)cloud_server 从外部云平台拉取云主机数据入库。

src/scene_server/synchronize_server/service/synchronize.go         ← 外部数据源查询入口
  src/source_controller/transfer-service/sync/sync.go                ← 同步器核心结构体
  src/source_controller/transfer-service/sync/metadata/metadata.go   ← 元数据管理(角色/ID映射)
  src/source_controller/transfer-service/sync/logics/logics.go       ← 11种资源同步逻辑
  src/source_controller/transfer-service/sync/dest_full_sync.go      ← 目标端全量同步调度
  src/source_controller/transfer-service/sync/src_full_sync.go       ← 源端全量同步推送
  src/source_controller/transfer-service/sync/dest_incre_sync.go     ← 目标端增量同步拉取
  src/source_controller/transfer-service/sync/medium/client.go       ← HTTP传输介质(push/publish + pull/consume)
  src/source_controller/transfer-service/sync/watch/watch.go         ← 源端Watch监听(增量事件捕获)
  src/source_controller/transfer-service/sync/watch/token_handler.go ← 增量同步游标Token管理

★ 必须掌握

  • Find API:synchronize_server 对外暴露的数据查询接口,是目标端拉取数据的入口
  • Syncer 结构体:transfer-service 的核心同步调度器,管理 resSyncerMap 和各 goroutine
  • Metadata 角色定义:Src(源端)/ Dest(目标端)的角色配置决定同步方向
  • Master 选举 + Redis 锁:双重保障机制,确保多实例环境下只有一台机器执行同步

☆ 需要理解

  • 11 种资源类型:Biz/Set/Module/Host/ObjectInst/InstAsst/ProcessInst/HostRelation/ProcessRelation/QuotedInstance/Cli
  • ID 映射机制:通过 IDRuleMap 和 SrcInnerIDMap 实现跨环境 ID 转换
  • 全量/增量同步配合:ack 标志切换,源端 Watch + 目标端 Pull

蓝鲸 CMDBsynchronize_servertransfer-service数据同步外部数据源ID映射

1. 背景:CMDB 数据同步体系的两条路径

思考

在多云和混合云场景下,企业的主机资产分散在不同平台:

  • 云平台:AWS EC2、腾讯云 CVM(开源版本仅这两个)
  • 虚拟化平台:VMware vSphere、OpenStack
  • 配置管理系统:蓝鲸 CMDB、其他商业 CMDB
  • CMDB 内部模型:主从 CMDB 实例间同步 Biz/Set/Module/Host 等 11 种资源

这些平台的 IP 地址、主机名、规格参数各有差异。如何将这些异构数据统一接入 CMDB,同时保证 ID 映射的一致性?

蓝鲸 CMDB 的数据同步方案解决了以下核心问题:

  1. 异构数据统一:不同平台的字段映射到 CMDB 标准字段
  2. ID 映射管理:源环境 ID → 目标环境 ID 的双向映射
  3. 蓝鲸内置业务隔离:蓝鲸平台内置业务(通过 BKAppName 字段查出的 bluekingBizInfo.bizID)的资源不被同步
  4. 全量/增量同步:支持首次全量拉取和后续增量更新

2. 路径一:外部云数据接入(cloud_server)

本节详解 cloud_server 如何将 AWS EC2 / 腾讯云 CVM 的主机数据同步到 CMDB,所有结论均有源码支撑。

2.1 整体架构:为什么 cloud_server 独立于 synchronize_server?

这是最容易混淆的地方,先说清楚两者定位的根本差异:

维度cloud_serversynchronize_server + transfer-service
同步方向 外部云平台 → CMDB CMDB 实例 A → CMDB 实例 B
同步内容 云主机属性(IP/规格/状态) CMDB 内部模型 / 实例 / 关联
接入层 云厂商 SDK(AWS SDK Go / 腾讯云 SDK) HTTP REST API(Find 接口)
写库方式 直接写 MongoDB(cc_HostBaseProtect) 通过 transfer-service 中转写入
代码位置 src/scene_server/cloud_server/ src/scene_server/synchronize_server/
src/source_controller/transfer-service/
支持的云厂商 AWS、腾讯云(开源版本) 仅限 CMDB 内部模型,无云厂商概念

最权威的定义在 src/common/metadata/cloud.go 第 87-94 行:

const (
	AWS          string = "1"   // 对应属性表 bk_cloud_vendor = "1"
	TencentCloud string = "2"   // 对应属性表 bk_cloud_vendor = "2"
)

// SupportedCloudVendors:开源版本仅实现了这 2 个云厂商插件
var SupportedCloudVendors = []string{AWS, TencentCloud}

2.2 数据表结构:三大核心表

cloud_server 的运作依赖三张 MongoDB 表,理解它们的关系是理解整个同步流程的关键:

表名对应 metadata用途
cc_CloudAccount CloudAccountConf 存储云账号配置(SecretID/SecretKey/厂商类型),同步任务引用此账号
cc_CloudSyncTask CloudSyncTask 同步任务定义(账号/云厂商/VPC列表/同步周期),TaskScheduler Watch 此表
cc_HostBaseProtect CloudHost 已同步到 CMDB 的云主机数据,含云区域 ID、云账号 ID、主机属性

2.3 VendorClient 接口:适配器注册表模式

cloud_server 通过适配器模式接入多个云厂商,核心是 cloudvendor/vendorclient.go 第 22-52 行定义的接口和注册表:

// 全局适配器注册表,key 为厂商名("1"/"2"),value 为客户端实例
var vendorClients = make(map[string]VendorClient, 0)

// VendorClient 接口:所有云厂商适配器必须实现这 4 个方法
type VendorClient interface {
	NewVendorClient(secretID, secretKey string) VendorClient
	GetRegions() ([]*metadata.Region, error)                              // 获取地域列表
	GetVpcs(region string, opt *ccom.VpcOpt) (*metadata.VpcsInfo, error)  // 获取 VPC 列表
	GetInstances(region string, opt *ccom.InstanceOpt) (*metadata.InstancesInfo, error)  // 获取主机实例
	GetInstancesTotalCnt(region string, opt *ccom.InstanceOpt) (int64, error)
}

// Register:将适配器注册到全局注册表(init() 中调用)
func Register(vendorName string, client VendorClient) {
	vendorClients[vendorName] = client
}

// GetVendorClient:根据账号配置从注册表获取适配器实例
func GetVendorClient(conf metadata.CloudAccountConf) (VendorClient, error) {
	client, ok := vendorClients[conf.VendorName]
	if !ok {
		return nil, fmt.Errorf("vendor %s is not supported", conf.VendorName)
	}
	return client.NewVendorClient(conf.SecretID, conf.SecretKey), nil
}

适配器通过 init() 函数自注册(cloudvendor/aws.go 第 26-28 行):

// init() 在包加载时自动执行,将 awsClient 注册到全局注册表
func init() {
	Register(metadata.AWS, &awsClient{vendorName: metadata.AWS})
}

2.4 AWS 适配器:ec2Client + 分页拉取

AWS 适配器在 cloudvendor/aws.go,内部使用 AWS 官方的 github.com/aws/aws-sdk-go

// NewVendorClient:构造带 AK/SK 的 awsClient(持有认证凭证)
func (c *awsClient) NewVendorClient(secretID, secretKey string) VendorClient {
	return &awsClient{
		vendorName: metadata.AWS,
		secretID:   secretID,
		secretKey:  secretKey,
	}
}

// GetRegions:调用 AWS EC2 DescribeRegions(无需指定地域,用 us-west-1 做入口)
// API: https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeRegions.html
func (c *awsClient) GetRegions() ([]*metadata.Region, error) {
	sess, _ := c.newSession("us-west-1")  // 全局服务不需要 region
	ec2Svc := ec2.New(sess)
	resp, _ := ec2Svc.DescribeRegions(&ec2.DescribeRegionsInput{})
	for _, region := range resp.Regions {
		regionSet = append(regionSet, &metadata.Region{
			RegionId:   *region.RegionName,         // 如 "ap-southeast-1"
			RegionName: regionIdNameMap[*region.RegionName], // 如 "亚太区域(新加坡)"
		})
	}
	return regionSet, nil
}

// GetVpcs:调用 AWS EC2 DescribeVpcs,支持 NextToken 分页
// 单地域 VPC 配额上限 5 个(AWS 硬限制)
func (c *awsClient) GetVpcs(region string, opt *ccom.VpcOpt) (*metadata.VpcsInfo, error) {
	sess := c.newSession(region)
	ec2Svc := ec2.New(sess)
	for {
		output, err := ec2Svc.DescribeVpcs(c.newDescribeVpcsInput(opt))
		if err != nil { return nil, err }
		for _, vpc := range output.Vpcs {
			vpcsInfo.VpcSet = append(vpcsInfo.VpcSet, &metadata.Vpc{
				VpcId:   *vpc.VpcId,
				VpcName: c.getVpcName(vpc),
			})
		}
		// 分页:NextToken 为空或达到 limit 时退出
		if opt.Limit == int64(len(vpcsInfo.VpcSet)) || output.NextToken == nil { break }
		input.NextToken = output.NextToken
	}
	return vpcsInfo, nil
}

// GetInstances:调用 AWS EC2 DescribeInstances,按 VPC 过滤 + 分页遍历
// 实例过滤条件通过 InstanceOpt.Filters 构造(如 vpc-id=xxx)
func (c *awsClient) GetInstances(region string, opt *ccom.InstanceOpt) (*metadata.InstancesInfo, error) {
	sess := c.newSession(region)
	ec2Svc := ec2.New(sess)
	for {
		output, err := ec2Svc.DescribeInstances(c.newDescribeInstancesInput(opt))
		// 解析每个 Reservation 的 Instance,映射为 metadata.Instance
		for _, reservation := range output.Reservations {
			for _, instance := range reservation.Instances {
				instances = append(instances, c.ec2InstanceToInstance(instance))
			}
		}
		if output.NextToken == nil { break }
		input.NextToken = output.NextToken
	}
	return instancesInfo, nil
}

2.5 腾讯云适配器:cvmClient + 分页拉取

腾讯云适配器在 cloudvendor/tencent_cloud.go,内部使用腾讯云 SDK(github.com/tencentcloud/tencentcloud-sdk-go-intl),接口方法与 AWS 完全一致(共用 VendorClient 接口),但底层调用的 API 不同:

  • GetRegions:调用腾讯云 cvm.DescribeRegions
  • GetVpcs:调用腾讯云 vpc.DescribeVpcs(需配合 vpc.DescribeVpcEx 获取 VPC 名称)
  • GetInstances:调用腾讯云 cvm.DescribeInstances,通过 Offset/Limit 分页(单次上限 100)

2.6 调度器:Watch 任务表 + ZK hash ring 分配

cloudsync/taskscheduler.goSchedule() 是整个云同步的入口,启动两个 Watch 循环:

// Schedule:云同步主入口,启动两个 Watch goroutine
func (t *taskScheduler) Schedule(ctx context.Context) error {
	// ① Watch cc_CloudSyncTask 表:任务增/删/改事件实时更新内存 tasklist
	if err := t.watchTaskTable(ctx); err != nil { return err }
	// ② Watch ZK cloud_server 节点:节点变化时重置哈希环
	if err := t.watchServerNode(); err != nil { return err }
	return nil
}

// watchTaskTable:通过 reflector.ListWatcher 监听 MongoDB change stream
// 事件类型 → 处理器:
//   OnAdd/OnUpdate    → addTask()       任务加入内存并打散到各节点
//   OnDelete         → delTask()       从内存移除
//   OnLister         → addTask()       冷启动时全量加载已有任务
//   OnListerDone     → close(listerDone) 触发 TaskChanLoop 开始推送
func (t *taskScheduler) watchTaskTable(ctx context.Context) error {
	opts := &stypes.ListWatchOptions{
		Options: stypes.Options{
			MaxAwaitTime: 10 * time.Second,   // change stream 最大等待时间
			EventStruct:  new(taskEvent),     // event.Document 类型
			Collection:   common.BKTableNameCloudSyncTask,  // cc_CloudSyncTask
		},
	}
	capable := &reflector.Capable{
		OnChange: reflector.OnChangeEvent{
			OnAdd: t.changeOnAdd, OnUpdate: t.changeOnUpdate,
			OnDelete: t.changeOnDelete, OnLister: t.changeOnLister,
			OnListerDone: t.changeOnListerDone,
		},
	}
	return t.reflector.ListWatcher(ctx, opts, capable)
}

// watchServerNode:通过 Discovery 订阅 cloud_server 节点列表变化,
// 有节点加入/离开时,调用 setHashring() 重置一致性哈希环
func (t *taskScheduler) watchServerNode() error {
	go func() {
		for servers := range t.logics.Discovery().CloudServer().GetServersChanForHash() {
			t.setHashring(servers)
		}
	}()
	return nil
}

cloudsync/taskprocessor.goTaskChanLoop()SyncPeriodMinutes(默认 5 分钟)从数据库拉取全量任务,经哈希环分片后推入 taskChan

// SyncPeriodMinutes:默认 5 分钟同步一次(可配置,最小 5 分钟)
const SyncPeriodMinutesMin = 5

// TaskChanLoop:定时从 DB 拉取任务,打散推入 taskChan
func (t *taskProcessor) TaskChanLoop(taskChan chan *metadata.CloudSyncTask) {
	go func() {
		for {
			tasks, err := t.scheduler.GetTaskList()  // 从 DB 查询所有任务
			if err != nil { time.Sleep(SyncPeriodMinutes * time.Minute); continue }
			for i := range tasks {
				taskChan 

2.7 同步流程:HostSyncor.Sync() 完整链路

cloudsync/hostsyncor.goHostSyncor.Sync() 是同步的核心方法,完整流程如下(每一步都有源码对应):

// Sync:云主机同步入口,每次同步生成新的 readKit/writeKit
func (h *HostSyncor) Sync(task *metadata.CloudSyncTask) error {
	// ① 读取云账号配置(从 cc_CloudAccount 表,按 task.AccountID 查)
	accountConf, _ := h.logics.GetCloudAccountConf(h.readKit, task.AccountID)

	// ② 获取云端主机资源(按 VPC 并发调用云厂商 API)
	hostResource, _ := h.GetCloudHostResource(task, accountConf)
	//   内部调用 GetVendorClient(conf) → awsClient / tencentCloudClient
	//   并发遍历 syncVpcs,对每个 VPC 调用 GetInstances()
	//   收集 destroyedVpcs(已被云端删除的 VPC)

	// ③ 查 CMDB 中已有主机,对比差异(新增/变更/下线)
	diffHosts, _ := h.getDiffHosts(hostResource)
	//   对比 cc_HostBaseProtect 中的已有数据
	//   diffHosts = {"add": [...], "update": [...], "delete": [...]}

	// ④ 开启事务,同步差异数据
	txnErr := h.writeKit.Ctx.Database.Session().StartSession().StartTransaction()
	_, _ = h.syncDiffHosts(diffHosts, syncResult)  // 见下方详解
	// 写入 cc_HostBaseProtect 表,更新 cc_CloudSyncTask 状态

	// ⑤ 记录同步历史(cc_CloudSyncHistory)
	_, _ = h.addSyncHistory(syncResult, taskID)

	// ⑥ 更新任务状态:success / fail / in_progress
	h.updateTaskState(h.writeKit, taskID, syncResult.SyncStatus, &syncResult.StatusDescription)
	return nil
}

// syncDiffHosts:根据差异类型调用不同的写操作
func (h *HostSyncor) syncDiffHosts(diffhosts map[string][]*metadata.CloudHost,
	syncResult *metadata.SyncResult) error {
	for op, hosts := range diffhosts {
		switch op {
		case "add":
			result, _ = h.addHosts(hosts)    // Insert Many cc_HostBaseProtect
		case "update":
			result, _ = h.updateHosts(hosts) // Update cc_HostBaseProtect
		case "delete":
			// 将主机的内外网 IP 置空,状态置为已销毁(不物理删除,保持审计追溯)
			result, _ = h.deleteDestroyedHosts(hostIDs)
		}
	}
	return nil
}

2.8 分发器:10 个协程并发消费 hostChan

cloudsync/cloudsync.go 第 46-107 行展示了分发器的完整逻辑:

// syncorNum:并发协程数(hardcode 为 10)
syncorNum int = 10

// CloudSync:主入口,同时启动调度器和分发器
func CloudSync(conf *SyncConf) error {
	scheduler, _ := NewTaskScheduler(schedulerConf)
	// taskChan 为调度器→分发器的缓冲 channel(无缓冲)
	taskChan := make(chan *metadata.CloudSyncTask)
	go scheduler.Schedule(ctx)      // 调度器 Watch 任务表 + ZK
	go scheduler.TaskChanLoop(taskChan)  // 定时推送任务到 taskChan
	SyncCloudResource(taskChan, conf)    // 分发器消费
	return nil
}

// SyncCloudResource:分发器
func SyncCloudResource(taskChan chan *metadata.CloudSyncTask, conf *SyncConf) {
	hostChan := make(chan *metadata.CloudSyncTask, 10)  // host 专用 channel

	// ① 启动 10 个 HostSyncor 协程,各自阻塞从 hostChan 取任务执行
	for i := 1; i <= syncorNum; i++ {
		syncor := NewHostSyncor(conf.Logics)
		go func(syncor *HostSyncor) {
			for {
				task := 

并发模型总结:10 个 HostSyncor 协程共享一个 hostChan,调度器每 5 分钟推送所有待同步任务,同一时间最多有 10 个同步任务在执行。

2.9 扩展指南:如何新增阿里云适配器

如果需要接入阿里云(或其他云厂商),只需三步,不影响现有框架:

  1. 新增适配器文件:在 cloudvendor/ 下新建 aliyun.go,实现 VendorClient 接口的 4 个方法:
    • NewVendorClient():构造带 AK/SK 的阿里云 SDK 客户端
    • GetRegions():调用阿里云 ecs.DescribeRegions
    • GetVpcs():调用阿里云 vpc.DescribeVpcs
    • GetInstances():调用阿里云 ecs.DescribeInstances(分页用 PageSize/PageNumber
  2. 注册适配器:在 aliyun.goinit() 中调用 Register("3", &aliyunClient{})("3" 为新的 bk_cloud_vendor 枚举值)
  3. 更新白名单:在 src/common/metadata/cloud.goSupportedCloudVendors 中加入 "3",在属性表 bk_cloud_vendor 的枚举值中新增对应选项
★ cloud_server 架构设计亮点
  • 注册表模式:新增云厂商只需新增一个文件 + 一行 init() 注册,不改核心调度逻辑
  • MongoDB Change Stream Watch:任务变更实时生效,无需重启 cloud_server
  • 一致性哈希环:多节点部署时,同一任务只分配给一个节点执行,避免重复同步
  • 差异同步:每次同步不是全量覆盖,而是对比 cc_HostBaseProtect 计算出新增/变更/下线的差量,只写差量数据

3. 路径二:CMDB 实例间同步(synchronize_server + transfer-service)

3.1 synchronize_server:CMDB 实例查询入口

src/scene_server/synchronize_server/service/synchronize.go 第 26-47 行

// Find 是 synchronize_server 对外暴露的 HTTP REST 接口,供目标 CMDB 调用以查询源端数据。
// 目标端 transfer-service 的 medium 在 /api/sync/consume 回调时,底层会调这个 Find API 拉取数据。
func (s *Service) Find(req *restful.Request, resp *restful.Response) {
  // newSrvComm 从请求头提取身份信息(用户、语言、供应商账号),构造含 ctx/header/ccErr 的 srvData。
  srvData := s.newSrvComm(req.Request.Header)
  // SynchronizeFindInfoParameter 请求体:DataType(1=实例/2=模型/3=关联)、DataClassify(object_id/模型类型/关联类型)、
  // Condition(MongoDB 查询条件)、Start(分页起始)、Limit(每页条数,默认 100)。
  input := &metadata.SynchronizeFindInfoParameter{}
  // JSON 反序列化请求体。失败(非法 JSON/字段类型不匹配)返回 400。
  if err := json.NewDecoder(req.Request.Body).Decode(input); err != nil {
    blog.Errorf("FindInstance , but decode body failed, err: %v,rid:%s", err, srvData.rid)
    resp.WriteError(http.StatusBadRequest,
      &metadata.RespError{Msg: srvData.ccErr.Error(common.CCErrCommJSONUnmarshalFailed)})
    return
  }
  // 根据 DataType 分发:1=实例→findInstance()(读MongoDB),2/3=模型/关联→find()(走CoreService.Synchronize())。
  data, err := srvData.lgc.Find(srvData.ctx, input)
  if err != nil {
    blog.Errorf("FindInstance error. error: %s,input:%#v,rid:%s", err.Error(), input, srvData.rid)
    resp.WriteError(http.StatusInternalServerError, &metadata.RespError{Msg: err})
    return
  }
  // 成功返回 QueryConditionResult,BaseResp=Data=0 表示成功,Data 填充 InstDataInfo。
  resp.WriteEntity(metadata.QueryConditionResult{
    BaseResp: metadata.SuccessBaseResp,
    Data:     *data,
  })
  }

功能定位

synchronize_serverscene_server 下的一个服务组件,通过 /api/v3/synchronize/find HTTP 接口对外暴露。它本身不主动推送数据,而是充当"数据查询适配层":

  • 谁调用它:目标 CMDB 的 transfer-service 需要拉取源端数据时,通过 HTTP 调用这个接口
  • 它做什么:解析请求中的查询条件(DataType/DataClassify/Condition),从源端 MongoDB 查出对应数据返回
  • 触发方在哪侧:同步的触发逻辑在目标端,目标端决定"我要同步哪些数据",然后调 Find API 从源端拉回来

简单说:synchronize_server = "源端的数据查询服务",transfer-service = "目标端的同步调度引擎",两者配合完成跨 CMDB 的数据同步。

Why — 为什么要这样设计?

在大型多云/混合云场景中,企业往往有多个 CMDB 实例分布在不同环境:

  • 源端 CMDB:承载了真实的主机资产数据(如 AWS 云平台的 EC2 实例)
  • 目标端 CMDB:承载需要被同步填充的数据(如自研运维平台依赖的主机列表)

问题来了:谁应该主导同步?谁知道该同步哪些数据?

方案 A(推模式):源端 CMDB 主动推送数据到目标端。缺点:源端需要知道目标端地址,且推送频率难以协调。

方案 B(拉模式,即本方案):目标端按需拉取。优点:目标端最清楚自己缺什么数据,源端只提供查询接口,两者解耦。

所以 synchronize_server 被设计为"被动的查询服务",而 transfer-service 被设计为"主动的调度引擎"。

What — synchronize_server 到底暴露了哪些能力?

看源码 find.go 第 56-71 行,Logics.Find() 根据 DataType 分发到不同处理路径:

  • DataType=1(实例):调 findInstance() → 直接查源端 MongoDB 的实例表(如 cc_HostBase、cc_ModuleBase),返回原始主机/模块数据
  • DataType=2(模型):调 find() → 查源端模型定义(哪些字段、什么类型),用于同步模型结构
  • DataType=3(关联):调 find() → 查主机-模块关联关系(cc_ModuleHostConfig),保证同步后关联关系一致

这三种数据类型覆盖了 CMDB 数据同步的三个层次:模型定义 → 实例数据 → 实例关联,缺一不可。

3.2 transfer-service:跨 CMDB 数据同步

src/source_controller/transfer-service/sync/sync.go 第 44-50 行

// Syncer is cmdb data syncer
  type Syncer struct {
    enableSync   bool
    isMaster     discovery.ServiceManageInterface
    metadata     *metadata.Metadata
    resSyncerMap map[types.ResType]*resSyncer
  }
  
  // resSyncer 是每种资源类型的同步执行器
  type resSyncer struct {
    name        string
    transMedium medium.ClientI
    lgc         logics.Logics
    metadata    *metadata.Metadata
  }

src/source_controller/transfer-service/sync/sync.go 第 52-98 行

// NewSyncer new cmdb data syncer
  func NewSyncer(conf *options.Config, isMaster discovery.ServiceManageInterface, loopW stream.LoopInterface,
    cacheCli cacheservice.CacheServiceClientInterface, reg prometheus.Registerer) (*Syncer, error) {
  
    if !conf.Sync.EnableSync {
      return &Syncer{enableSync: false}, nil
    }
  
    // check if id generator is enabled, can only start syncing when id generator is enabled
    configAdminCond := map[string]interface{}{"_id": common.ConfigAdminID}
    configAdminData := make(map[string]string)
    err := mongodb.Client().Table(common.BKTableNameSystem).Find(configAdminCond).Fields(common.ConfigAdminValueField).
      One(context.Background(), &configAdminData)
    if err != nil {
      blog.Errorf("get config admin data failed, err: %v, cond: %+v", err, configAdminCond)
      return nil, err
    }
  
    if !gjson.Get(configAdminData[common.ConfigAdminValueField], "id_generator.enabled").Bool() {
      blog.Infof("config admin id generator is not enabled, do not sync cmdb data")
      return &Syncer{enableSync: false}, nil
    }
    // ...
  }

启动条件

transfer-service 启动前必须检查 id_generator.enabled 配置。只有 ID 生成器启用后,才允许开启数据同步,防止 ID 冲突。

What — Syncer 和 resSyncer 的结构是什么?

Syncer 是 transfer-service 的顶层协调器,负责整体同步调度;resSyncer 是每种资源类型的专用执行器。两者分工明确:

  • Syncer.enableSync:总开关,由配置项 conf.Sync.EnableSync 控制,关闭后整个同步功能静默禁用(返回 enableSync: false 而非报错)
  • Syncer.isMaster:master 选举接口(通过 discovery.ServiceManageInterface.IsMaster() 实现),只有 master 节点才真正执行同步,避免多个节点同时拉取同一批数据
  • Syncer.metadata:同步元数据,包含本节点角色(源/目标/双向)、内部 ID 映射配置、蓝鲸内置业务信息
  • Syncer.resSyncerMapmap[types.ResType]*resSyncer,key 是资源类型(Host/Biz/Module 等),value 是该资源类型的专用执行器。初始化时通过遍历 types.ListAllResType() 注册所有 11 种资源类型

resSyncer 的四个字段:

  • name:同步任务名称(从配置文件读取,用于日志标识)
  • transMedium:HTTP 传输介质客户端(medium.ClientI),负责与对端 CMDB 的 transfer medium 通信(push/pull 数据)
  • lgc:该资源类型的业务同步逻辑(实现了 Logics 接口),包含增删改查的差异化处理
  • metadata:该资源类型的同步元数据(角色、ID 映射规则等)

Why — 为什么要这样设计?解决了什么问题?

问题一:多节点重复同步

transfer-service 部署多个实例时,如果不加控制,每个实例都会执行同步逻辑,导致数据重复拉取、ID 映射表被多次写入。解决方案:通过 isMaster.IsMaster() 做 master 选举,只有一个节点真正执行同步(见 dest_full_sync.go 第 37 行和 src_full_sync.go 第 36 行)。

问题二:ID 冲突

源端 CMDB 和目标端 CMDB 是两个独立的 MongoDB 实例,各自的 ID 是自增的。例如源端主机 ID=100,目标端主机 ID=5000。如果不借助 ID 生成器,目标端直接用源端 ID 写入会导致 ID 冲突。解决方案:启动前强制检查 id_generator.enabled(第 70 行),只有全局 ID 生成器启用后,才允许同步。ID 生成器会为同步的数据分配一个与目标端现有 ID 空间不冲突的新 ID,并通过 IDRuleMap 维护源 ID → 目标 ID 的映射关系。

问题三:11 种资源类型的同步逻辑如何统一管理

如果每种资源类型都写一套独立的同步调度代码,代码会膨胀到难以维护。解决方案:用 resSyncerMap 注册表模式,通过 for range types.ListAllResType() 统一初始化,新增资源类型只需在 logics/logics.go 的 Map 中加一行注册代码,调度器无需改动。

我理解源码的意思是说

How — transfer-service 的初始化链路是怎么走的?

sync.go 第 52-117 行,NewSyncer 初始化链路分 6 步,每一步都有严格的顺序依赖:

第 1 步:配置开关检查(第 56-58 行)— 如果 conf.Sync.EnableSync=false,直接返回 &Syncer{enableSync: false},不做任何资源初始化,静默禁用。

第 2 步:ID 生成器检查(第 61-73 行)— 从 cc_System 表读取 configAdmin 文档,取 id_generator.enabled 字段。只有开启后才继续,防止 ID 冲突。这是强制门禁,不是警告。

第 3 步:元数据初始化(第 75-79 行)— 根据 conf.Sync.Role 创建 Metadata,确定本节点是源端、目标端还是双向同步。Role 决定了后续 Watch 模块是否启动(全量同步场景下源端才需要 Watch)。

第 4 步:传输介质初始化(第 81-85 行)— 创建 HTTP 客户端指向对端 CMDB 的 transfer medium 地址。这是数据流动的"管道",双向同步时源端和目标端各有一个 transMedium 实例,互为通信对端。

第 5 步:业务逻辑注册(第 87-112 行)— 解析 ID 映射配置(parseDestExConf),创建 11 种资源类型的 Logics 实例,然后逐一包装成 resSyncer 存入 resSyncerMap

第 6 步:启动同步循环(第 114 行)— 调用 syncer.run() 启动后台 goroutine,开始执行全量同步或增量同步。

一个关键细节enableSync 设置为 true(第 95 行)发生在所有检查和初始化完成后,表示同步功能正式启用。如果任何一步失败(ID 生成器未启用、元数据创建失败等),都会 return error 或返回 enableSync: false 的空壳,确保不会带着不完整状态启动。

3.3 双组件数据流

┌─────────────────────────────────────────────────────────────────────────┐
  │  【源端 CMDB】Source CMDB(拥有原始数据的那个实例)                         │
  │  synchronize_server → Find API(暴露查询接口给目标端调用)                  │
  │  transfer-service → Watch 模块(监听 MongoDB 变更,推送到传输介质)          │
  └─────────────────────────────────────────────────────────────────────────┘
                                 │
                                 ▼  HTTP / 传输介质
  ┌─────────────────────────────────────────────────────────────────────────┐
  │  【目标端 CMDB】Destination CMDB(需要同步数据的实例)                      │
  │  transfer-service → 目标端全量同步(loopPullFullSyncData)                 │
  │                    目标端增量同步(loopPullIncrSyncData)                   │
  │  medium/client.go → PullSyncData() 调用 /api/sync/consume                │
  └─────────────────────────────────────────────────────────────────────────┘

Why — 为什么需要双组件配合,而不是单组件完成所有事?

跨 CMDB 同步涉及两个不同职责:谁提供数据谁消费数据。如果只用一个组件,既要监听变更,又要拉取数据,就要同时担任"服务器"和"客户端"两个角色,导致职责混乱。BK-CMDB 的设计思路是:

  • 源端:作为"被动数据提供者",只暴露查询接口(Find API)和变更监听(Watch),不主动推送数据
  • 目标端:作为"主动数据消费者",按需拉取全量/增量数据,写入本地 MongoDB

这样做的好处:源端无需知道目标端是谁,扩展 N 个目标端只需要在每个目标端部署 transfer-service 即可,源端零改动。

数据流各环节详解

What — 双组件数据流中每个模块具体做什么?

源端 CMDB 的三个模块:

  • synchronize_server → Find API:源端的 HTTP REST 接口(对应 sync.go 第 754-779 行的 Find())。目标端通过 medium/client.goPullSyncData() 发 HTTP POST 请求,Find API 根据 DataType(实例/模型/关联)查询源端 MongoDB,返回 QueryConditionResult。这个接口是纯查询,无状态,目标端想查多少查多少。
  • transfer-service → Watch 模块:源端的增量同步触发器。Watch 模块(对应 sync/watch/watch.go)监听源端 MongoDB 的 Oplog(操作日志),检测到主机/业务/模块等 11 种资源的数据变更后,通过 transMedium 推送到传输介质(Redis Stream / Kafka)。Watch 不直接推给目标端,而是推给中间介质,实现解耦。
  • transfer-service → loopPushFullSyncData(全量推送):源端按配置间隔(默认 15 分钟,见 src_full_sync.go 第 33 行)全量推送数据。通过 resSyncer.pushFullSyncData() 遍历 11 种资源类型,每种类型调用 Find API 查询全量数据,再通过 transMedium 推到传输介质。

目标端 CMDB 的三个模块:

  • transfer-service → loopPullFullSyncData(全量拉取):目标端的全量同步入口(对应 dest_full_sync.go 第 33 行)。目标端主动调用 Find API 拉取全量数据,写入自己的 MongoDB。注意第 44-50 行用 Redis 分布式锁(types.FullSyncLockKey)保证同一时刻只有一个节点在执行全量同步。
  • transfer-service → loopPullIncrSyncData(增量拉取):目标端的增量同步入口(对应 dest_incre_sync.go 第 30 行)。监听源端 Watch 推送过来的变更事件(通过 medium 回调),按资源类型分别处理。拉取间隔比全量短(见第 50 行),保证增量数据尽快落地。
  • medium/client.go → PullSyncData():目标端的 HTTP 客户端封装。发起 HTTP POST 到 /api/sync/consume(源端 transfer medium 的回调接口),请求体包含 DataType/Condition/Start/Limit,响应体是源端查询结果。

中间的传输介质(HTTP / 传输介质):

在全量同步阶段,路径是"目标端 HTTP 请求 → 源端 Find API → HTTP 响应"(直接拉模式)。在增量同步阶段,路径是"源端 Watch 检测变更 → Redis Stream/Kafka → 目标端 medium 回调"(推送模式)。两种模式共用同一套 transMedium 接口,但触发方式不同。

How — 全量同步和增量同步是怎么配合的?

sync.go 第 114 行 syncer.run() 启动的逻辑,目标端会同时启动两个 goroutine:

  • 全量同步(loopPullFullSyncData):首次同步或按需触发时执行,目标端主动拉取所有历史数据。拉取时用 ack=false 表示首次,确认拉完后才 ack,保证数据不丢。
  • 增量同步(loopPullIncrSyncData):全量同步完成后持续运行,监听源端 Watch 推送的变更事件,增量拉取。全量同步完成后 ack 变为 true,增量同步每次处理完后 time.Sleep(2s) 再拉下一批。

分布式锁的保证:全量同步和增量同步都依赖 Redis 分布式锁(lock.NewLocker(redis.Client()).Lock(types.FullSyncLockKey, time.Hour)),保证多节点环境下只有一个节点真正执行同步,避免数据竞争。

Master 选举的保证:每个同步循环(loopPullFullSyncData、loopPullIncrSyncData、loopPushFullSyncData)第一件事都是 if !s.isMaster.IsMaster() 检查自己是 master 才继续,否则 sleep 后重试。这是进程级的选举,与分布式锁形成双重保险。

4. 元数据管理:角色与 ID 映射

4.1 Metadata 结构体

src/source_controller/transfer-service/sync/metadata/metadata.go 第 41-55 行

// Metadata is cmdb data syncer's metadata info
  type Metadata struct {
    role options.SyncRole
    // InnerIDInfo is inner data id info of this environment
    InnerIDInfo *options.InnerDataIDConf
    // blueking is the blueking biz info, is used to skip the resource in blueking biz for source environment
    blueking *bluekingBizInfo
  }
  
  // BluekingBizID is the blueking biz info, resource in blueking biz should not be synced
  type bluekingBizInfo struct {
    bizID         int64
    lock          sync.RWMutex
    hostModuleMap map[int64]map[int64]struct{}
  }

设计精髓

Metadata 管理了同步的两个关键元数据:

  • SyncRole:本节点在同步拓扑中的角色(源/目标/双向)
  • InnerIDInfo:本环境的内部 ID 配置,用于 ID 映射
  • bluekingBizInfo:蓝鲸内置业务的 ID,用于跳过蓝鲸平台自身的资源

4.2 蓝鲸内置业务跳过机制

src/source_controller/transfer-service/sync/metadata/metadata.go 第 72-82 行

func (m *Metadata) isHostInBluekingBiz(hostID int64) bool {
    if m.blueking == nil {
      return false
    }
  
    m.blueking.lock.RLock()
    defer m.blueking.lock.RUnlock()
  
    _, exists := m.blueking.hostModuleMap[hostID]
    return exists
  }

当同步主机资源时,会通过 isHostInBluekingBiz 检查该主机是否属于蓝鲸内置业务。如果是,则跳过同步,避免污染蓝鲸平台自身的运维数据。

5. 11种资源的同步逻辑

src/source_controller/transfer-service/sync/logics/logics.go 第 42-58 行

// New creates a new resource type to resource sync logics map
  func New(conf *LogicsConfig) map[types.ResType]Logics {
    lgcMap := map[types.ResType]Logics{
      types.Biz:             newDataWithIDLogics(conf.genResLgcConf(types.Biz), bizLgc),
      types.Set:             newDataWithIDLogics(conf.genResLgcConf(types.Set), setLgc),
      types.Module:          newDataWithIDLogics(conf.genResLgcConf(types.Module), moduleLgc),
      types.Host:            newDataWithIDLogics(conf.genResLgcConf(types.Host), hostLgc),
      types.HostRelation:    newRelationLogics(conf.genResLgcConf(types.HostRelation), hostRelLgc),
      types.ObjectInstance:  newObjInstLogics(conf.genResLgcConf(types.ObjectInstance)),
      types.InstAsst:        newDataWithIDLogics(conf.genResLgcConf(types.InstAsst), instAsstLgc),
      types.ServiceInstance: newDataWithIDLogics(conf.genResLgcConf(types.ServiceInstance), serviceInstLgc),
      types.Process:         newDataWithIDLogics(conf.genResLgcConf(types.Process), procLgc),
      types.ProcessRelation: newRelationLogics(conf.genResLgcConf(types.ProcessRelation), procRelLgc),
      types.QuotedInstance:  newDataWithIDLogics(conf.genResLgcConf(types.QuotedInstance), quotedInstLgc),
    }
  
    return lgcMap
  }
资源类型同步逻辑类型说明
Biz DataWithIDLogics 业务(带 ID 映射)
Set DataWithIDLogics 集群(带 ID 映射)
Module DataWithIDLogics 模块(带 ID 映射)
Host DataWithIDLogics 主机(核心同步对象)
HostRelation RelationLogics 主机模块关系
ObjectInstance ObjInstLogics 自定义模型实例
InstAsst DataWithIDLogics 实例关联关系
ServiceInstance DataWithIDLogics 服务实例
Process DataWithIDLogics 进程
ProcessRelation RelationLogics 进程关系
QuotedInstance DataWithIDLogics 被引用实例

6. 全量同步与增量同步机制

5.1 Master 选举与分布式锁

src/source_controller/transfer-service/sync/dest_full_sync.go 第 32-51 行

// loopPullFullSyncData loop pull full sync data
  func (s *Syncer) loopPullFullSyncData() {
    ack := false
  
    for {
      if !s.isMaster.IsMaster() {
        blog.V(4).Infof("loop pull full sync data, but not master, skip")
        time.Sleep(5 * time.Minute)
        ack = false
        continue
      }
  
      locker := lock.NewLocker(redis.Client())
      locked, err := locker.Lock(types.FullSyncLockKey, time.Hour)
      if err != nil || !locked {
        blog.Errorf("do not get %s lock, err: %v, locked: %v", types.FullSyncLockKey, err, locked)
        time.Sleep(5 * time.Minute)
        continue
      }
      // ...
    }
  }

双重保障机制

全量同步依赖两个前提:

  1. Master 选举:只有 Master 节点执行同步,避免多节点重复拉取
  2. Redis 分布式锁:即使 Master 切换,新 Master 也通过锁保证互斥

What — 这段代码在同步体系中扮演什么角色?

loopPullFullSyncData() 是目标端 CMDB 全量同步的入口 goroutine。它在 sync.goNewSyncer() 末尾被调用(对应 sync.go 第 114 行 syncer.run()),启动后永不返回,在 for {} 循环中反复执行。这个函数做了两件事:① 确保本节点有权执行同步(Master 检查 + 分布式锁),② 按资源类型轮询拉取全量数据

Why — 为什么需要双重保障?解决了什么问题?

问题一:多 transfer-service 实例同时拉取

transfer-service 部署至少 2 个实例做高可用。如果没有控制,同一时间两个实例都会去源端拉数据,相同数据被重复写入目标端 MongoDB,ID 映射表会被并发写入覆盖,导致数据不一致甚至主键冲突。

解决方案一:Master 选举

discovery/server.go 第 99-112 行,IsMaster(UUID string) bool 通过 ZooKeeper 的注册节点顺序判断——第一个注册的就是 master。每次循环第 1074 行检查 if !s.isMaster.IsMaster(),非 master 节点直接 sleep 5 分钟跳过本次执行。master 选举解决的是进程级的并发问题。

问题二:Master 节点切换时的竞态

假设 master 节点 A 正在拉取数据,突然宕机。ZooKeeper 检测到后,节点 B 成为新 master。但节点 A 宕机前可能已经拿到锁(Redis 锁 TTL=1 小时),节点 B 虽然拿到了 master 身份,但如果不检查 Redis 锁状态,就会在节点 A 的锁过期前(最多 1 小时)重复拉取数据。

解决方案二:Redis 分布式锁

第 1081-1086 行,每次循环重新获取 Redis 锁(key = "cmdb_syncer:full_sync_lock",见 pkg/synchronize/types/syncer.go 第 49 行),TTL 1 小时。锁的持有者是当前正在执行同步的那个实例。即使 master 身份切换了,新 master 也必须先抢到锁才能继续,如果没有抢到就 sleep 5 分钟重试。分布式锁解决的是节点级的竞态问题。

没有双重保障会发生什么?

  • 没有 Master 选举:两个实例同时拉取,同一主机数据写入两次,ID 映射表被并发覆盖,源端压力翻倍
  • 没有分布式锁:master 切换后新 master 不等旧锁释放就执行,数据重复拉取,目标端 ID 映射表紊乱
How — 双重保障的时序细节

看第 1071-1123 行的完整循环逻辑,每一轮同步的时序是:

  1. 检查 Master 身份(第 1074 行):if !s.isMaster.IsMaster(),非 master sleep 5 分钟跳到下一轮
  2. 申请 Redis 分布式锁(第 1081-1082 行):locker.Lock(FullSyncLockKey, time.Hour),锁的持有者才继续,否则 sleep 5 分钟重试
  3. 获取业务对象 ID 列表(第 52-61 行):从元数据中查询有哪些自定义对象需要同步
  4. 按资源类型轮询同步(第 63-80 行):遍历 11 种资源类型,每种调用 pullFullSyncData()
  5. 解锁(第 1122 行):locker.Unlock(),释放锁让下一轮或其他节点有机会抢锁
  6. 睡眠等待(第 1123 行):time.Sleep(5 * time.Minute),5 分钟后开始下一轮

一个关键细节:ack 标志

第 1071 行 ack := false 初始化,第 1120 行 ack = true 表示全量同步完成。ack 传给 pullFullSyncData(),决定本次同步是否向源端发 ack 确认。如果 ack=false,源端会认为这批数据没有被目标端确认,可能重复发送;如果 ack=true,源端才真正标记这批数据已消费。这是至少一次语义的保证。

5.2 资源类型轮询同步

src/source_controller/transfer-service/sync/dest_full_sync.go 第 63-89 行

	for _, resType := range types.ListAllResType() {
      syncer := s.resSyncerMap[resType]
  
      switch resType {
      case types.ObjectInstance:
        for _, objID := range objIDs {
          syncer.pullFullSyncData(objID, ack)
        }
      case types.InstAsst:
        for _, objID := range append(objIDs, common.BKInnerObjIDHost) {
          syncer.pullFullSyncData(objID, ack)
        }
      case types.QuotedInstance:
        for _, objID := range quotedObjIDs {
          syncer.pullFullSyncData(objID, ack)
        }
      default:
        syncer.pullFullSyncData("", ack)
      }
    }
    ack = true
  
    locker.Unlock()
    time.Sleep(5 * time.Minute)

7. ID 映射:跨环境数据关联

6.1 ID 映射的作用

在跨 CMDB 同步场景中,源环境和目标环境使用不同的自增 ID。例如:

  • 源 CMDB:主机 A 的 ID = 100
  • 目标 CMDB:主机 A 的 ID = 5000

ID 映射表记录了 (源环境, 源ID) → (目标ID) 的对应关系,确保关联数据(如主机-模块关系)的正确关联。

6.2 ID 规则配置

同步配置中通过 IDRuleMap 定义 ID 生成规则:

// LogicsConfig is the cmdb resource sync logics config
  type LogicsConfig struct {
    Metadata      *metadata.Metadata
    IDRuleMap     map[types.ResType]map[string][]options.IDRuleInfo
    SrcInnerIDMap map[string]*options.InnerDataIDConf
  }
  • IDRuleMap:定义每种资源类型的 ID 生成规则(如按业务 ID 分段)
  • SrcInnerIDMap:定义源环境的内部 ID 配置(主机池、业务 ID 范围)

8. 源码视角:设计精髓

我理解源码的意思是说

transfer-service 的设计体现了多源异构数据的统一同步思想。我们直接看三个关键设计:

源码视角一:资源类型注册表模式

logics/logics.go 第 42-58 行,会发现每种资源类型都注册了对应的同步逻辑。这种设计的好处是:新增资源类型只需要添加一行注册代码,无需修改同步调度器。

lgcMap := map[types.ResType]Logics{
      types.Biz:    newDataWithIDLogics(...),
      types.Set:    newDataWithIDLogics(...),
      types.Host:   newDataWithIDLogics(...),
      // 新增资源只需要添加一行
      types.CustomType: newCustomLogics(...),
  }

源码视角二:两层分布式锁保障

dest_full_sync.go 第 37-49 行,同步启动前有两层检查:

  1. 第一层:IsMaster() 检查是否为主节点
  2. 第二层:Redis 分布式锁 lock.Lock(types.FullSyncLockKey)

即使 Master 选举出问题,Redis 锁也能防止多节点同时同步。

源码视角三:蓝鲸业务隔离

metadata/metadata.go 第 72-82 行,isHostInBluekingBiz 通过读写锁保护 hostModuleMap。这个设计保证了蓝鲸平台自身的运维数据(如蓝鲸监控的主机)不会被同步到目标环境。

源码视角总结:3 个核心设计原则

  • 1 个注册表模式:资源类型与同步逻辑解耦,易扩展
  • 1 个双层锁:Master 选举 + Redis 分布式锁保证同步互斥
  • 1 个隔离机制:蓝鲸内置业务(`BKAppName="蓝鲸"` 查出的业务)不参与同步

9. FAQ 24 问

常见疑问

以下是关于 synchronize_server 与 transfer-service 的高频问题,每个问题都基于源码给出明确答案。

Q1. synchronize_server 和 transfer-service 有什么区别?

一句话结论:synchronize_server 是查询适配层,transfer-service 是跨环境同步引擎。synchronize_server 接收外部数据源的查询请求,transfer-service 负责将数据从源 CMDB 同步到目标 CMDB。

Q2. 为什么 transfer-service 启动前要检查 ID 生成器?

一句话结论:防止 ID 冲突。sync/sync.go 第 70 行可以看到,只有 ID 生成器启用后才允许同步,否则可能导致源环境和目标环境的 ID 冲突。

Q3. 哪些资源类型支持同步?

一句话结论:11 种核心资源类型。logics/logics.go 第 42-58 行可以看到,包括 Biz、Set、Module、Host、HostRelation、ObjectInstance、InstAsst、ServiceInstance、Process、ProcessRelation、QuotedInstance。

Q4. 全量同步和增量同步有什么区别?

一句话结论:全量同步每次拉取全部数据,增量同步只拉取变化的 ID。全量同步用于首次初始化,增量同步用于后续增量更新。

Q5. 为什么要有 Master 选举机制?

一句话结论:避免多节点重复同步。dest_full_sync.go 第 37 行可以看到,只有 Master 节点才执行同步。

Q6. Redis 分布式锁的作用是什么?

一句话结论:防止 Master 切换时的同步冲突。即使主节点切换,新 Master 也需要获取 Redis 锁才能开始同步,保证互斥。

Q7. 蓝鲸内置业务的数据会被同步吗?

一句话结论:不会。metadata/metadata.go 第 145-151 行可以看到,蓝鲸内置业务是通过 BKAppName 字段从 cc_ApplicationBase 表动态查出来的 bluekingBizInfo.bizID,然后通过 isHostInBluekingBiz 方法检查主机是否属于该业务。

Q8. ObjectInstance 同步有什么特殊处理?

一句话结论:需要遍历所有自定义模型 ID。dest_full_sync.go 第 67-70 行可以看到,ObjectInstance 需要对每个 objID 分别调用同步。

Q9. InstAsst 同步为什么要额外处理 Host?

一句话结论:因为主机也有关联关系。dest_full_sync.go 第 71-74 行可以看到,InstAsst 需要额外同步 common.BKInnerObjIDHost 类型的关联。

Q10. ID 映射是如何工作的?

一句话结论:通过 IDRuleMap 和 SrcInnerIDMap 配置。IDRuleMap 定义了 ID 生成规则,SrcInnerIDMap 定义了源环境的内部 ID 配置,两者配合实现跨环境的 ID 映射。

Q11. synchronize_server 支持哪些外部数据源?

一句话结论:synchronize_server 不对接任何云厂商,它只做 CMDB 内部模型 / 实例 / 关联的跨集群同步。它的 DataClassify 字段(src/common/metadata/core_service_synchronize.go 第 94 行)只能填 object_id(host/plat/module/proc 等 CMDB 内部模型),没有任何云厂商关键字。真正的云厂商适配器在 src/scene_server/cloud_server/cloudvendor/(见 Q21)。

Q12. 同步失败会重试吗?

一句话结论:会。dest_full_sync.go 第 54 行可以看到,使用了 util.RetryWrapper(3, ...) 包裹同步逻辑,失败时会重试 3 次。

Q13. 如何新增一种资源类型的同步支持?

一句话结论:实现 Logics 接口并在 New() 中注册。新增资源类型需要:1)实现 Logics 接口;2)在 logics/logics.go 第 42-58 行的 Map 中添加一行。

Q14. transfer-service 如何获取对象 ID 列表?

一句话结论:通过 GetCommonObjIDs() 从元数据获取。dest_full_sync.go 第 55 行可以看到,会先获取所有自定义模型的 ID。

Q15. 同步的频率是多少?

一句话结论:默认 5 分钟一次。dest_full_sync.go 第 39 行和第 87 行可以看到,同步间隔是 5 * time.Minute

Q16. QuotedInstance 是什么资源类型?

一句话结论:被其他模型引用的实例。这是 CMDB 中模型之间引用关系的体现,需要单独处理同步。

Q17. 为什么 HostRelation 和 ProcessRelation 用 RelationLogics?

一句话结论:因为它们是关联表,没有独立的 ID。关联关系依赖源实体的 ID,RelationLogics 专门处理这类数据的同步。

Q18. 增量同步的 Token 游标是如何管理的?

一句话结论:通过 MongoDB 表 cc_SrcSyncDataToken 存储每种资源类型的 Watch 游标。watch/token_handler.go 第 33 行可以看到,游标存储在 cc_SrcSyncDataToken 表中。每种资源类型(types.ResType)对应一条记录,增量同步时通过 TokenHandler 读取和更新游标位置。

Q19. 同步过程中主机属性如何处理?

一句话结论:通过 ID 映射转换。主机的 IP、规格等属性直接同步,CMDB 分配的 ID 通过映射表转换。

Q20. transfer-service 的 Watch 模块和增量同步是什么关系?

一句话结论:Watch 模块负责监听源端变更事件并推送到传输介质,增量同步负责从介质拉取并写入目标端。watch/watch.go 第 50-97 行可以看到,Watcher 通过 stream.LoopInterface 订阅源端 MongoDB 的变更(Watch 机制),将事件推送到 transfer medium(/api/sync/publish),目标端通过 dest_incre_sync.goloopPullIncrSyncData 从介质拉取(/api/sync/consume)。两者通过传输介质解耦,源端和目标端可以独立扩展。

Q21. cloud_server 和 synchronize_server 是什么关系?

一句话结论:它们是完全独立的两个服务,cloud_server 对接外部云平台,synchronize_server 对接 CMDB 实例。cloud_server(src/scene_server/cloud_server/)通过云厂商 SDK(AWS/腾讯云)拉取云主机入库;synchronize_server(src/scene_server/synchronize_server/)通过 HTTP Find API 暴露 CMDB 内部数据,供另一个 CMDB 拉取。两者的唯一共同点是都叫"synchronize",但解决的是完全不同的问题。

Q22. 外部云数据(AWS/腾讯云)是如何同步到 CMDB 的?

一句话结论:cloud_server 通过 VendorClient 注册表加载适配器,按 VPC 维度并发调用云厂商 API,将数据写入 MongoDB。完整链路如下(全部有源码支撑):

  1. 账号注册:用户在 cc_CloudAccount 表配置云账号(SecretID/SecretKey)和云厂商(bk_cloud_vendor = "1"=AWS,"2"=腾讯云)
  2. 任务调度cloudsync/taskscheduler.goSchedule() 通过 ZooKeeper hash ring 将任务分配给各节点,taskprocessor.goTaskChanLoop() 每隔 SyncPeriodMinutes(默认 5 分钟)从数据库拉取任务推入 channel
  3. 类型路由cloudsync/cloudsync.goSyncCloudResource() 根据 task.ResourceType 分发,当前只识别 "host",其它类型打 error 日志忽略
  4. 获取云资源cloudsync/hostsyncor.goHostSyncor.Sync() 调用 logics/cloud.goGetCloudHostResource(),通过 cloudvendor/vendorclient.goGetVendorClient() 从注册表查到适配器,并发调用 GetVpcs() + GetInstances()
  5. 差异对比getDiffHosts() 对比云端主机列表与 cc_HostBaseProtect 表中已有数据,只对新增/变更/下线的主机打标签
  6. 写入 CMDB:通过 AuditLog 记录变更,写入 cc_HostBaseProtect 表,更新任务状态为 cloud_sync_successcloud_sync_fail

Q23. cloud_server 支持哪些云厂商?开源版本能接入阿里云吗?

一句话结论:开源版本只支持 AWS 和腾讯云。阿里云/华为云/GCP/Azure 无任何适配器。最权威的定义在 src/common/metadata/cloud.go 第 87-94 行的 SupportedCloudVendors 白名单:

const (
	AWS          string = "1"
	TencentCloud string = "2"
)
var SupportedCloudVendors = []string{AWS, TencentCloud}

如果需要接入其他云厂商(如阿里云),只需在 cloudvendor/ 下新增一个文件(如 aliyun.go),实现 VendorClient 接口的 4 个方法(NewVendorClient / GetRegions / GetVpcs / GetInstances),在 init() 中调用 Register("aliyun", &aliyunClient{}),再将新值加入 SupportedCloudVendors 和属性表 bk_cloud_vendor 的枚举即可。

Q24. cloud_server 的云同步任务类型为什么只支持 "host"?

一句话结论:因为目前只有云主机同步器(HostSyncor)实现了。cloudsync/cloudsync.go 第 98-103 行的 switch 可以看到:

switch task.ResourceType {
case "host":
	hostChan 

其它资源类型(云硬盘/网络/安全组等)被直接忽略。如需扩展,需要新增 Syncor(如 DiskSyncor)并在 switch 中增加对应的 case 分发到对应的 channel。

全篇总纲

蓝鲸 CMDB 的 synchronize_server 和 transfer-service 实现了多源异构数据的统一同步,通过:

  • 双组件协作:synchronize_server 做查询适配,transfer-service 做跨环境同步
  • 11种资源类型:覆盖 CMDB 核心资源的全量同步
  • 双层分布式锁:Master 选举 + Redis 锁保证同步互斥
  • 蓝鲸业务隔离:蓝鲸内置业务(`BKAppName="蓝鲸"`)的资源通过 bluekingBizInfo.bizID 动态查询,不参与同步

10. 后续预告

下期预告

  • CMDB #11:数据质量保障 — 脏数据检测与告警机制
  • CMDB #12:Watch 事件订阅 — Redis Stream 推送机制与实时事件通知

posted @ 2026-07-05 22:07  左扬  阅读(24)  评论(0)    收藏  举报