3. 统一元数据模型设计:Common Information Model (CIM)
好,咱们进入第三章节。前面两章我们把多Catalog的痛点和整体架构聊清楚了。现在要动真格的了——怎么把这些乱七八糟的元数据统一起来?
答案就是:搞一个通用信息模型(CIM)。说白了,就是给所有元数据定一套通用的“普通话”。
我最早做这个的时候,心想不就是建个表嘛,有什么难的?结果被现实狠狠教育了一顿。不同数据源的元数据,结构差异大到离谱。Hive的表分区和MySQL的表分区,根本就不是一回事。你硬要往一个模子里套,最后只会得到一个四不像。
核心观点:CIM不是要消灭差异,而是要在差异之上建立统一的抽象层。就像交通规则,不关心你是轿车还是卡车,只关心你遵守红绿灯和车道线。
3.1 CIM设计三原则
我个人习惯,设计任何模型之前,先定几条铁律。CIM也不例外,我总结了三条:
- 最小化原则——只抽象所有数据源都有的共性。别贪心,别想着一次覆盖所有特性。覆盖80%的场景就够了,剩下的20%用扩展机制解决。
- 可扩展原则——模型必须能“长肉”。今天纳管Hive,明天纳管Kafka,后天纳管某个自研引擎。模型不能因为新数据源加入就推倒重来。
- 可追溯原则——每个元数据实体,必须能追溯到原始数据源。我遇到过最坑的事,就是元数据同步后,不知道这条记录是从哪儿来的,出了问题连锅都甩不出去。
我的经验:第一条“最小化原则”最容易被人忽略。很多团队一上来就想搞个大而全的模型,结果项目拖了半年,连最基本的表结构都没定下来。先做减法,再做加法。
3.2 核心实体关系建模
好,原则定好了,咱们看看CIM里到底有哪些核心实体。我把它分成三层:
- 基础设施层:数据源(DataSource)、集群(Cluster)、连接信息(Connection)
- 逻辑对象层:数据库(Database)、模式(Schema)、表(Table)、字段(Column)、分区(Partition)
- 治理对象层:标签(Tag)、分级(Classification)、血缘(Lineage)、变更历史(ChangeLog)
你想想看,不管你是Hive、MySQL、MongoDB还是Elasticsearch,最终都能映射到这套模型上。举个例子:
| 原始数据源 | DataSource类型 | Database | Table | Column |
|---|---|---|---|---|
| Hive | HIVE | default | orders | order_id, amount |
| MySQL | MYSQL | shop_db | users | id, name, email |
| Elasticsearch | ES | log_index | _doc | timestamp, message |
看到没?虽然底层千差万别,但在CIM里,它们都长一个样。这就是统一纳管的基础。
实体之间的关系也很清晰:一个DataSource下有多个Database,一个Database下有多个Table,一个Table下有多个Column。嗯,这里要注意,有些数据源(比如Elasticsearch)没有Database这个概念,我们就用默认值填充,或者干脆把Schema层当作Database用。
-- CIM核心实体关系(简化DDL示例)
CREATE TABLE cim_datasource (
id BIGINT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
ds_type VARCHAR(50) NOT NULL, -- HIVE, MYSQL, ES, KAFKA...
connection_info TEXT,
created_at TIMESTAMP
);
CREATE TABLE cim_database (
id BIGINT PRIMARY KEY,
ds_id BIGINT REFERENCES cim_datasource(id),
name VARCHAR(255) NOT NULL,
description TEXT
);
CREATE TABLE cim_table (
id BIGINT PRIMARY KEY,
db_id BIGINT REFERENCES cim_database(id),
name VARCHAR(255) NOT NULL,
table_type VARCHAR(50), -- TABLE, VIEW, EXTERNAL...
created_at TIMESTAMP
);
CREATE TABLE cim_column (
id BIGINT PRIMARY KEY,
table_id BIGINT REFERENCES cim_table(id),
name VARCHAR(255) NOT NULL,
data_type VARCHAR(100),
is_nullable BOOLEAN,
comment TEXT
);
避坑指南:我曾经在一个项目里,把DataSource的连接信息直接明文存到数据库里。结果安全审计的时候被骂得狗血淋头。连接信息一定要加密存储,或者用独立的密钥管理服务(KMS)来管理。
3.3 扩展属性机制
模型建好了,但现实世界永远比模型复杂。比如Hive表有STORED AS PARQUET属性,MySQL表有ENGINE=InnoDB属性,这些CIM核心模型里没有。怎么办?
答案就是扩展属性(Extended Attributes)。我把它设计成一个KV(键值对)结构,挂到任何实体上。
-- 扩展属性表
CREATE TABLE cim_ext_attr (
id BIGINT PRIMARY KEY,
entity_type VARCHAR(50) NOT NULL, -- TABLE, COLUMN, DATABASE...
entity_id BIGINT NOT NULL,
attr_key VARCHAR(255) NOT NULL,
attr_value TEXT,
UNIQUE KEY uk_entity_attr (entity_type, entity_id, attr_key)
);
举个例子,Hive表的分区信息:
-- 给cim_table id=1001的表添加扩展属性
INSERT INTO cim_ext_attr (entity_type, entity_id, attr_key, attr_value)
VALUES ('TABLE', 1001, 'hive.partition.columns', 'dt,region');
INSERT INTO cim_ext_attr (entity_type, entity_id, attr_key, attr_value)
VALUES ('TABLE', 1001, 'hive.storage.format', 'PARQUET');
INSERT INTO cim_ext_attr (entity_type, entity_id, attr_key, attr_value)
VALUES ('TABLE', 1001, 'hive.location', '/user/hive/warehouse/orders');
这样做的好处很明显:
- 核心模型保持稳定,不会因为新数据源加入而频繁改表结构
- 扩展属性可以按需添加,不影响已有功能
- 查询时,只需要JOIN一下扩展属性表,就能拿到完整信息
我的习惯:扩展属性的key命名,我建议用数据源类型.属性名的格式,比如hive.partition.columns、mysql.engine。这样一眼就能看出这个属性是哪个数据源特有的,避免命名冲突。
3.4 CIM整体结构图
说了这么多,咱们用一张图把CIM的整体结构串起来。下面这张SVG图,展示了核心实体、它们之间的关系,以及扩展属性如何挂载。
3.5 避坑与最佳实践
最后,分享几个我在实战中踩过的坑,以及总结出来的最佳实践:
- 不要用JSON字段代替扩展属性表——我见过有人把扩展属性直接塞到一个JSON字段里。查询时想过滤某个属性,性能直接崩了。扩展属性表一定要用独立的KV表,并且给
entity_type + entity_id + attr_key建联合索引。 - 实体ID要全局唯一——不同数据源的表,ID不能重复。我习惯用
数据源类型 + 自增ID或者直接上UUID。否则后面做血缘分析时,ID冲突会让你怀疑人生。 - 预留软删除字段——元数据同步过程中,经常会有表被删除。不要真删数据,加一个
is_deleted字段,保留历史痕迹。我曾经因为真删了数据,导致某次数据回溯完全没法做。 - 版本号控制——每次元数据变更,都带上一个版本号。这样你可以知道“这条元数据是什么时候变的”,对于排查问题非常有用。
再强调一次:CIM模型是统一纳管的基石。模型设计得不好,后面所有工作都是空中楼阁。花时间把模型讨论清楚,比急着写代码重要得多。