蓝鲸 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 的数据同步方案解决了以下核心问题:
- 异构数据统一:不同平台的字段映射到 CMDB 标准字段
- ID 映射管理:源环境 ID → 目标环境 ID 的双向映射
- 蓝鲸内置业务隔离:蓝鲸平台内置业务(通过 BKAppName 字段查出的 bluekingBizInfo.bizID)的资源不被同步
- 全量/增量同步:支持首次全量拉取和后续增量更新
2. 路径一:外部云数据接入(cloud_server)
本节详解 cloud_server 如何将 AWS EC2 / 腾讯云 CVM 的主机数据同步到 CMDB,所有结论均有源码支撑。
2.1 整体架构:为什么 cloud_server 独立于 synchronize_server?
这是最容易混淆的地方,先说清楚两者定位的根本差异:
| 维度 | cloud_server | synchronize_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.go 的 Schedule() 是整个云同步的入口,启动两个 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.go 的 TaskChanLoop() 每 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.go 的 HostSyncor.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 扩展指南:如何新增阿里云适配器
如果需要接入阿里云(或其他云厂商),只需三步,不影响现有框架:
- 新增适配器文件:在 cloudvendor/ 下新建 aliyun.go,实现 VendorClient 接口的 4 个方法:
- NewVendorClient():构造带 AK/SK 的阿里云 SDK 客户端
- GetRegions():调用阿里云 ecs.DescribeRegions
- GetVpcs():调用阿里云 vpc.DescribeVpcs
- GetInstances():调用阿里云 ecs.DescribeInstances(分页用 PageSize/PageNumber)
- 注册适配器:在 aliyun.go 的 init() 中调用 Register("3", &aliyunClient{})("3" 为新的 bk_cloud_vendor 枚举值)
- 更新白名单:在 src/common/metadata/cloud.go 的 SupportedCloudVendors 中加入 "3",在属性表 bk_cloud_vendor 的枚举值中新增对应选项
- 注册表模式:新增云厂商只需新增一个文件 + 一行 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_server 是 scene_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.resSyncerMap:map[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.go 的 PullSyncData() 发 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
}
// ...
}
}
双重保障机制
全量同步依赖两个前提:
- Master 选举:只有 Master 节点执行同步,避免多节点重复拉取
- Redis 分布式锁:即使 Master 切换,新 Master 也通过锁保证互斥
What — 这段代码在同步体系中扮演什么角色?
loopPullFullSyncData() 是目标端 CMDB 全量同步的入口 goroutine。它在 sync.go 的 NewSyncer() 末尾被调用(对应 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 映射表紊乱
看第 1071-1123 行的完整循环逻辑,每一轮同步的时序是:
- 检查 Master 身份(第 1074 行):if !s.isMaster.IsMaster(),非 master sleep 5 分钟跳到下一轮
- 申请 Redis 分布式锁(第 1081-1082 行):locker.Lock(FullSyncLockKey, time.Hour),锁的持有者才继续,否则 sleep 5 分钟重试
- 获取业务对象 ID 列表(第 52-61 行):从元数据中查询有哪些自定义对象需要同步
- 按资源类型轮询同步(第 63-80 行):遍历 11 种资源类型,每种调用 pullFullSyncData()
- 解锁(第 1122 行):locker.Unlock(),释放锁让下一轮或其他节点有机会抢锁
- 睡眠等待(第 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 行,同步启动前有两层检查:
- 第一层:IsMaster() 检查是否为主节点
- 第二层: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.go 的 loopPullIncrSyncData 从介质拉取(/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。完整链路如下(全部有源码支撑):
- 账号注册:用户在 cc_CloudAccount 表配置云账号(SecretID/SecretKey)和云厂商(bk_cloud_vendor = "1"=AWS,"2"=腾讯云)
- 任务调度:cloudsync/taskscheduler.go 的 Schedule() 通过 ZooKeeper hash ring 将任务分配给各节点,taskprocessor.go 的 TaskChanLoop() 每隔 SyncPeriodMinutes(默认 5 分钟)从数据库拉取任务推入 channel
- 类型路由:cloudsync/cloudsync.go 的 SyncCloudResource() 根据 task.ResourceType 分发,当前只识别 "host",其它类型打 error 日志忽略
- 获取云资源:cloudsync/hostsyncor.go 的 HostSyncor.Sync() 调用 logics/cloud.go 的 GetCloudHostResource(),通过 cloudvendor/vendorclient.go 的 GetVendorClient() 从注册表查到适配器,并发调用 GetVpcs() + GetInstances()
- 差异对比:getDiffHosts() 对比云端主机列表与 cc_HostBaseProtect 表中已有数据,只对新增/变更/下线的主机打标签
- 写入 CMDB:通过 AuditLog 记录变更,写入 cc_HostBaseProtect 表,更新任务状态为 cloud_sync_success 或 cloud_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 推送机制与实时事件通知

浙公网安备 33010602011771号