4. 元数据采集引擎:设计可插拔的采集框架
元数据采集,说白了就是给数据目录「喂数据」的管道。没有它,目录就是个空壳子。
我见过不少团队,一开始只接了个 MySQL,后来要接 Hive,再后来要接 Kafka,每次都得改采集代码。改着改着就乱了,采集脚本散落在各个项目里,没人敢动。嗯,这就是典型的「硬编码采集」的坑。
这一章,我们就来聊聊怎么设计一个可插拔的采集框架。让新增数据源像插 U 盘一样简单。
4.1 核心设计思路:插件化与SPI
可插拔的核心,就是「面向接口编程」。Java 里最常用的机制是 SPI(Service Provider Interface)。你定义好采集接口,每个数据源自己实现,然后通过配置文件注册进来。框架启动时自动加载所有实现,互不干扰。
我习惯把采集器抽象成三层:
- 连接层:负责建立与数据源的连接,处理认证、网络、连接池。
- 抽取层:负责拉取元数据,比如表结构、分区信息、Topic 列表。
- 转换层:把不同数据源的元数据格式,统一成内部标准模型。
这样做的好处很明显——你改 Kafka 采集器,不会影响到 JDBC 采集器。各管各的,出了问题也好排查。
关键原则:采集器之间不能有共享状态。每个采集器都是独立的「黑盒」,只通过标准模型与框架通信。
4.2 采集接口定义
我们先定义一个最核心的接口。我通常会这样设计:
public interface MetadataCollector {
/**
* 采集器唯一标识,比如 "jdbc-mysql", "hive", "kafka"
*/
String type();
/**
* 初始化采集器,传入连接配置
*/
void init(Map<String, String> config);
/**
* 执行采集,返回统一元数据模型
*/
CollectResult collect(CollectContext context);
/**
* 增量采集,只返回变更的部分
*/
CollectResult collectIncremental(CollectContext context);
/**
* 测试连接是否可用
*/
boolean testConnection(Map<String, String> config);
}
你可能会问,为什么要有 collectIncremental?
我在项目中遇到过,全量采集一张大表,几万个字段,每次跑十几分钟。业务方等不及。后来加了增量采集,只抓上次采集后新增或修改的表,效率提升 80%。
4.3 三种典型采集器的实现
我们来看看三种最常用的采集器是怎么实现的。我挑几个关键点说。
4.3.1 JDBC 采集器
JDBC 是最通用的关系型数据库采集方式。核心逻辑就是查 information_schema。
public class JdbcCollector implements MetadataCollector {
private DataSource dataSource;
@Override
public void init(Map<String, String> config) {
// 用 HikariCP 创建连接池
HikariConfig hikariConfig = new HikariConfig();
hikariConfig.setJdbcUrl(config.get("url"));
hikariConfig.setUsername(config.get("username"));
hikariConfig.setPassword(config.get("password"));
this.dataSource = new HikariDataSource(hikariConfig);
}
@Override
public CollectResult collect(CollectContext context) {
CollectResult result = new CollectResult();
try (Connection conn = dataSource.getConnection()) {
DatabaseMetaData metaData = conn.getMetaData();
// 获取所有表
ResultSet tables = metaData.getTables(
null, null, "%", new String[]{"TABLE", "VIEW"}
);
while (tables.next()) {
String schema = tables.getString("TABLE_SCHEM");
String tableName = tables.getString("TABLE_NAME");
// 获取字段信息
ResultSet columns = metaData.getColumns(
null, schema, tableName, "%"
);
// ... 组装成标准模型
}
} catch (SQLException e) {
result.setSuccess(false);
result.setErrorMsg(e.getMessage());
}
return result;
}
}
这里有个坑。不同数据库的 information_schema 字段名不一样。比如 MySQL 用 TABLE_SCHEM,Oracle 用 OWNER。我建议在配置里加一个 dbType 参数,根据类型做适配。
4.3.2 Hive 采集器
Hive 的元数据其实存在 Metastore 里,通常是 MySQL 或 PostgreSQL。你可以直接连 Metastore 的库查 TBLS、COLUMNS_V2 这些表。
但我更推荐用 Hive Metastore Client 的 API。为什么呢?因为直接查库,万一 Metastore 的表结构升级了,你的采集器就挂了。用 API 更稳定。
public class HiveCollector implements MetadataCollector {
private IMetaStoreClient client;
@Override
public void init(Map<String, String> config) {
HiveMetaStoreClient.Builder builder = new HiveMetaStoreClient.Builder();
builder.setMetaStoreUris(config.get("metastore.uris"));
this.client = builder.build();
}
@Override
public CollectResult collect(CollectContext context) {
CollectResult result = new CollectResult();
try {
List<String> databases = client.getAllDatabases();
for (String db : databases) {
List<String> tables = client.getAllTables(db);
for (String table : tables) {
Table hiveTable = client.getTable(db, table);
// 提取字段、分区、存储格式等信息
// ... 组装成标准模型
}
}
} catch (TException e) {
result.setSuccess(false);
result.setErrorMsg(e.getMessage());
}
return result;
}
}
小技巧:Hive 采集时,记得把分区字段和普通字段区分开。很多目录工具把分区字段当普通字段展示,导致用户查询时总报错。
4.3.3 Kafka 采集器
Kafka 的元数据相对简单,主要是 Topic 列表、分区数、副本数、以及 Topic 的配置信息。
public class KafkaCollector implements MetadataCollector {
private AdminClient adminClient;
@Override
public void init(Map<String, String> config) {
Properties props = new Properties();
props.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, config.get("bootstrap.servers"));
this.adminClient = AdminClient.create(props);
}
@Override
public CollectResult collect(CollectContext context) {
CollectResult result = new CollectResult();
try {
ListTopicsResult topicsResult = adminClient.listTopics();
Set<String> topicNames = topicsResult.names().get();
for (String topic : topicNames) {
DescribeTopicsResult describeResult = adminClient.describeTopics(
Collections.singletonList(topic)
);
TopicDescription description = describeResult.all().get().get(topic);
// 提取分区数、副本分配等信息
// ... 组装成标准模型
}
} catch (Exception e) {
result.setSuccess(false);
result.setErrorMsg(e.getMessage());
}
return result;
}
}
Kafka 采集有个特殊点:Topic 的 Schema 信息通常不在 Kafka 本身,而在 Schema Registry 里。所以采集器需要额外对接 Schema Registry,把 Avro/Protobuf 的字段定义拉下来。
4.4 统一元数据模型
不同数据源的元数据千差万别,但最终都要映射成一套标准模型。我一般这样设计:
| 数据源类型 | 原始概念 | 统一模型映射 |
|---|---|---|
| MySQL | Database → Table → Column | Catalog → Schema → Table → Field |
| Hive | Database → Table → Column / Partition | Catalog → Schema → Table → Field + PartitionKey |
| Kafka | Cluster → Topic → Schema Field | Catalog → Schema → Table → Field |
| Elasticsearch | Cluster → Index → Mapping Field | Catalog → Schema → Table → Field |
你看,不管底层多复杂,最终都抽象成「Catalog → Schema → Table → Field」这四层。这样上层应用(比如数据地图、血缘分析)就不用关心底层是什么数据源了。
4.5 采集调度与增量策略
采集器写好了,怎么跑?总不能每次都全量扫一遍吧。
我建议采用「全量 + 增量」的混合策略:
- 首次采集:全量拉取,写入目录库。
- 定时增量:每隔 5-10 分钟,只拉取变更的部分。
- 定期全量:每天凌晨做一次全量对账,修复增量可能遗漏的数据。
增量采集怎么判断变更?不同数据源方法不同:
- JDBC:查
information_schema的UPDATE_TIME字段。 - Hive:对比 Metastore 中表的
LAST_ACCESS_TIME或CREATE_TIME。 - Kafka:通过
DescribeConfigs看配置版本号是否变化。
注意:增量采集不是万能的。有些数据源(比如老版 Hive)不支持记录变更时间。这时候只能退而求其次,缩短全量采集的周期,或者用事件通知机制(比如监听 Metastore 的 binlog)。
4.6 框架整体架构
说了这么多,我们来看一张整体架构图。它把采集框架的核心流程串起来了。
从图上你能看到,整个流程是单向的:调度器触发采集器,采集器拉取原始元数据,转换器统一格式,最后存入存储层供上层使用。每个环节都可以独立扩展。
4.7 避坑指南
最后,分享几个我踩过的坑:
- 连接泄漏:采集器用完连接一定要关闭。我曾经有个 JDBC 采集器忘了关 ResultSet,导致连接池被占满,线上数据库被打挂。后来加了 try-with-resource 才解决。
- 超时处理:有些数据源(比如 Hive)元数据查询很慢,动辄几十秒。一定要给采集设置超时时间,超时了就跳过,别卡住整个调度。
- 版本兼容:Kafka 2.x 和 3.x 的 AdminClient API 有差异。我建议在采集器里做版本检测,或者干脆维护两套实现。
- 数据一致性:增量采集时,如果数据源在采集过程中发生了变更,可能会漏掉或重复。我的做法是加一个「对账任务」,每天凌晨全量扫一遍,修复不一致的数据。
我的习惯:每个采集器上线前,先在测试环境跑一周,观察资源消耗和采集延迟。没问题了再推到生产。别一上来就全量跑,容易出事。
好了,这一章的内容就到这里。可插拔采集框架的设计,说白了就是「接口 + 实现 + 调度」的组合。把接口定义好,实现各自独立,调度统一管理,剩下的就是填坑了。嗯,填坑也是工程师的必修课。