做数据平台的这些年,我最大的感受就是:元数据这个东西,平时没人重视,一旦出了问题,全团队跟着抓瞎。业务方凌晨两点打电话问“订单表的那个字段是谁改成枚举类型的”,你翻完 Hive Metastore、翻完 BI 报表工具的字段配置、翻完调度平台的任务依赖,最后发现信息散落在六个系统里,完全拼不出一张完整视图。这也是我当年研究 DataHub 的直接原因——一个能把散落元数据统一建模、统一采集、统一检索和展示的元数据管理平台。今天这篇文章,我会从源码和实际部署经验两个维度,完整拆解 DataHub 的架构设计与核心工作原理。
DataHub 是 LinkedIn 开源的现代数据目录平台,定位是“元数据的事实来源”。它的架构核心是事件驱动:采集端将元数据变更以事件形式送入管道,服务端统一处理后写入存储、索引和图数据库,再通过 GraphQL API 对外提供查询。这个思路听起来不复杂,但真正落地时会涉及消息队列、数据库、搜索引擎、图数据库的协同,每一个环节都有值得深挖的设计考量。适合正在选型元数据平台的团队、想理解 DataHub 内部机制的开发运维同学,以及所有被“找数据、理血缘、做治理”折磨过的数据从业者。
1. 从元数据混乱到统一治理:DataHub 到底在解决什么问题
1.1 我为什么会关注 DataHub
工作里最让人头疼的不是表多,而是“没人说得清数据从哪里来、到哪里去、中间被谁改过”。传统做法是折腾 Apache Atlas,重度、配置复杂,光是 hook 和消息队列就够团队喝一壶;或者干脆自研脚本,每天定时把各系统的元数据抓一遍灌进 MySQL——问题是脚本一多,抓取逻辑各自为政,数据质量完全看天吃饭。
DataHub 给我的第一印象是“轻”和“活”。轻是说它默认用 Docker Compose 就能把整套环境拉起来,核心服务只有几个 Java 进程;活是说它的元数据建模非常灵活,不限定死你只能用哪几种实体类型,表、视图、仪表盘、机器学习模型、数据管道,都可以被建模成标准实体,并且可以自己定义新的实体类型和属性片段。这种设计让它既能快速上手,也能在后期承接团队自定义的元数据场景。
1.2 DataHub 和传统元数据工具的差异
拿 Atlas 和 Amundsen 做个简单对比,能更好理解 DataHub 的定位。
| 维度 | Atlas | Amundsen | DataHub |
|---|---|---|---|
| 元数据模型 | 基于 Type System,偏重 | 偏轻量,主要面向搜索 | Entity + Aspect 复合模型,扩展性强 |
| 变更传播 | 通过 Kafka 消息异步处理 | 依赖 Databuilder 任务流 | 事件驱动,MCE/MAE 双通道 |
| 血缘能力 | 强,支持列级血缘 | 较弱 | 强,支持列级血缘, 可选 Neo4j 图存储 |
| 搜索体验 | 一般,依赖 Solr/ES | 较好,Elasticsearch 驱动 | 好,前端 GraphQL + ES 索引 |
| 二次开发成本 | 较高,Schema 变更复杂 | 中等 | 较低,Aspect 可插拔 |
这个表格不是我随便写的,实际踩过坑才清楚。Atlas 的 Type System 虽然强大,但每加一种实体类型都要改 Schema 并管理迁移,团队没专人维护很容易烂掉。Amundsen 更像一个数据发现工具,血缘和治理能力相对薄弱。DataHub 把“变更提议”和“变更事件”分开处理,解耦了写入和消费,这是它架构设计里最值得学的一笔。
2. 整体架构:一条元数据高速公路的六个关键节点
2.1 核心组件与各自职责
要理解 DataHub 架构,先把六个关键节点记在脑子里,一条元数据从产生到被前端展示,离不开它们。
| 组件 | 角色 | 默认端口/通道 | 主要职责 |
|---|---|---|---|
| datahub-frontend | 前端服务 | 9002 | Web UI 静态资源,登录鉴权,反向代理 GraphQL 请求 |
| datahub-gms | 核心后端 | 8080 | REST + GraphQL API,实体与切面的读写、校验、版本管理 |
| datahub-mce-consumer | 元数据提议消费者 | Kafka 组 | 消费 MCE,调用 GMS 的写入接口 |
| datahub-mae-consumer | 元数据事件消费者 | Kafka 组 | 消费 MAE/日志事件,写 Elasticsearch 和 Neo4j |
| Elasticsearch | 搜索与索引 | 9200 | 存储实体对应的搜索文档,支撑检索、血缘查询、自动补全 |
| Kafka + MySQL + Neo4j | 消息与存储底座 | 9092 / 3306 / 7474 | 消息管道、主存储、图关系存储(Neo4j 可选) |
我第一次部署这套东西时,最直观的感受是“组件真多”。但多归多,职责非常清晰:GMS 是唯一的元数据写入口,任何写入都走它;Kafka 负责解耦生产者和消费者;ES 只负责查询加速和搜索;MySQL 存真相(source of truth);mae-consumer 则像一条流水线,把变更翻译成 ES 文档和图数据库里的边。
2.2 架构选型背后的关键权衡
为什么 GMS 要做唯一写入口?答案是保证一致性。如果允许采集器直连 MySQL 写库,一来要管理一堆账号权限,二来绕过了 DataHub 的校验逻辑——实体类型是否合法、Aspect 是否符合 Schema 定义、版本号是否冲突,这些问题全部依赖 GMS 层把关。DataHub 官方把写入接口收敛到/entities这一个 action 上,目的就是让校验和版本控制集中化。
为什么用 Kafka 而不是直接 HTTP 调用?因为元数据变更的峰值不可控。大促前一天批量导入几千张表、半夜定时任务全量重跑,瞬时会有大量元数据变更请求。GMS 是同步接口,扛不住突发流量时容易超时;Kafka 像缓冲区,把“变更提议”先落盘,消费者按自身的处理能力慢慢消费,削峰填谷。生产环境里我把 mce-consumer 的并发调大过,但吞吐提升有限,瓶颈往往在 MySQL 写入上,这一点后面细说。
2.3 一次请求如何穿过整个架构
我用一条最典型的链路来描述整个架构的数据流向:
采集源(MySQL / Hive / Kafka / Tableau ...) │ 通过 Ingestion Framework 读取元数据 ▼ Ingestion CLI 序列化 MCL 事件 │ datahub-rest sink 直连 GMS,或 datahub-kafka sink 发到 Kafka ▼ GMS 校验 → 写入 MySQL(版本递增)→ 发出 MetadataChangeLog 到 Kafka │ ▼ mae-consumer 消费变更日志 → 更新 Elasticsearch 文档 → 可选更新 Neo4j 图 │ ▼ 前端 GraphQL 查询 GMS → 搜索走 ES,详情走 MySQL → 页面渲染这条链路里有一个容易被忽略的点:即使采集端直连 GMS 写数据,变更日志依然会进入 Kafka,由 mae-consumer 消费后更新 ES 和 Neo4j。也就是说,GMS 只负责“记录事实”,索引和图关系是异步构建的。这个设计保证了写路径尽可能短,也把“写一次元数据”和“让元数据可以被检索、被关联”两件事彻底解耦了。
3. 核心元数据模型:Urn、Entity、Aspect 三位一体
3.1 Urn:一条数据实体的“全球身份证”
DataHub 里每个实体都有唯一标识,叫 Urn(Uniform Resource Name)。它的格式是:
urn:li:<entityType>:<id>比如 MySQL 里一个库表,Urn 长这样:
urn:li:dataset:(urn:li:dataPlatform:mysql,demo_db.orders,PROD)这个 URN 拆开看有三段:数据平台标识urn:li:dataPlatform:mysql、物理名称demo_db.orders、环境标识PROD。曾经有同事问“环境标识能不能不写”,答案是不能。同一个表在 DEV、PROD 环境都存在,如果不区分环境,两个环境的元数据会互相覆盖,血缘也会串掉。URN 的构造函数对特殊字符很敏感,字段名里有逗号、括号时必须做转义,我第一次写自定义采集器时就在这上面翻过车。
3.2 Entity 与 Aspect 的关系
实体是“身份证号码”,Aspect 则是“这个实体身上的一个个属性块”。在用户界面上看,一个 Dataset 实体包含 Schema Metadata(表结构)、Dataset Properties(描述、标签)、Ownership(负责人)、Upstream Lineage(上游血缘)等多个模块,每个模块就是一个 Aspect。
举个例子,SchemaMetadata 这个 Aspect 在系统里的核心结构类似:
{ "schemaName": "demo_db.orders", "platformSchema": { "com.linkedin.schema.MySqlDDL": { "tableSchema": "CREATE TABLE orders (id INT PRIMARY KEY, user_id INT, amount DECIMAL(10,2))" } }, "fields": [ { "fieldPath": "id", "type": { "type": { "com.linkedin.schema.NumberType": {} } }, "nativeDataType": "int" }, { "fieldPath": "amount", "type": { "type": { "com.linkedin.schema.NumberType": {} } }, "nativeDataType": "decimal(10,2)" } ] }注意字段名不是columns而是fields,每个 field 通过fieldPath唯一定位,列级血缘就是基于这个fieldPath来对齐的。很多人初次看 DataHub 的数据模型觉得绕,其实只要记住一句话:实体是稳定不变的锚点,Aspect 是可独立更新、独立版本化的属性块。
3.3 为什么要把 Aspect 拆得足够细
把属性拆成细粒度 Aspect 有两个直接好处。
第一是局部更新。假设一张表只有负责人变了,数据采集器只需要提交ownership这一个 Aspect 的变更,GMS 不会去动schemaMetadata,版本冲突的概率大幅降低。如果整个实体是一个大 JSON,每次更新都要全量覆盖,并发场景下很容易丢更新。
第二是扩展性。DataHub 本身只内置了几十个标准 Aspect,但你可以自定义 Aspect 挂到已有实体上。这个设计很像给一张信息卡片不断增加“贴纸”,而且贴纸之间互不影响。我在项目里给数据源集成加过一个customQualityScore的 Aspect,用来记录数据质量评分,全程没有改动 GMS 主代码,只加了 PDL 定义和对应的采集器。
4. 核心工作原理:一条元数据记录从采集到可见的完整旅程
4.1 采集端:Ingestion Framework 的三段式设计
DataHub 的采集框架叫 Ingestion Framework,逻辑上分三段:Source(数据源)、Transformer(转换)、Sink(输出)。
- Source:连接目标系统,读取元数据。比如 MySQL Source 会连上
information_schema获取表结构、索引、外键信息。 - Transformer:对采集结果做清洗、过滤、补字段。可以给表自动打标签,也可以过滤掉临时表。
- Sink:决定元数据去哪里。
datahub-rest直接 HTTP 调 GMS,datahub-kafka则写入 Kafka。
日常最常用的 MySQL 采集配置大概长这样:
source: type: mysql config: host_port: "localhost:3306" database: demo_db username: root password: root database_pattern: allow: - "^demo_db" - "^dw_" table_pattern: deny: - ".*_tmp$" profiling: enabled: true profile_table_level_only: true sink: type: datahub-rest config: server: "http://localhost:8080"这里的database_pattern和table_pattern强烈建议一开始就配好。我见过太多团队不加过滤直接把整个实例全量采集,结果第二天发现 ES 索引里躺着几千张备份表、临时表,搜索全是噪音,血缘图也乱成一团。另外profiling.enabled要谨慎开启,它会对表执行统计查询,生产大表上容易拖慢源库,第一次建议先只开表级统计,别开列级。
4.2 写路径:MCE 进入 GMS 后发生了什么
采集端通过datahub-rest把 MetadataChangeProposal(MCP)发给 GMS 的/entities?action=ingest接口。GMS 收到后的处理步骤,我按源码执行顺序拆给大家:
- 反序列化并校验 Urn 格式,解析实体类型。
- 加载该实体类型的 Aspect 定义,做 Schema 校验,校验不通过直接返回错误。
- 如果是更新操作,GMS 会读取当前已存储的 Aspect 版本号,然后写入新版本,版本号加 1,这一步保证了并发写同一个 Aspect 时不会互相覆盖。
- 写入 MySQL 成功后,GMS 将这次变更封装成 MetadataChangeLog,发到 Kafka 的
MetadataChangeLog_Versioned_v1topic 上。
这里有个历史概念需要说明:早期 DataHub 区分 MCE(Metadata Change Proposal,元数据变更提议)和 MAE(Metadata Change Event,元数据变更事件),后来架构演进为通过 MCP/MCL 承载类似职责,但很多文档里仍沿用 MCE/MAE 的叫法。你可以简单理解:MCE 是“希望发生什么”,MAE/MCL 是“已经发生了什么”。
4.3 读路径:MAE 到达消费者后如何建立索引与血缘
mae-consumer 消费到 MetadataChangeLog 后,会做两件事。
第一件事是更新 Elasticsearch 的搜索文档。DataHub 为每类实体建了独立的 ES 索引,Dataset 实体对应datasetindex_v2,每条文档里聚合了表名、描述、负责人、标签等可搜索字段。这也是为什么你改了表描述之后要等几秒才能在搜索框里搜到——因为 ES 更新是异步的。
第二件事是关系图更新。如果变更包含血缘、归属这类关系型 Aspect,mae-consumer 会去更新 Neo4j(默认关闭,需要在环境变量里设置GRAPH_SERVICE_IMPL=neo4j才启用)。启用 Neo4j 后,血缘查询的调用链会更清晰:前端请求 GMS 的 GraphQL 血缘接口,GMS 转发到 Neo4j 查询上下游节点和边的层级,而不是把全部血缘数据扛到内存里算,性能会好很多。
4.4 读路径:前端拿到数据后如何组织展示
前端走 GraphQL 查询 GMS,这层封装做得挺聪明。对于搜索页、首页推荐、自动补全这些场景,GMS 的 resolver 会优先查 ES,因为 ES 的倒排索引天生适合关键词匹配;对于表详情页、Schema 展示、负责人列表这种精确查询,GMS 直接查 MySQL 拿最新版 Aspect。这种“搜索走 ES、详情走 MySQL”的读写分离策略,实际上是一个很通用的元数据平台设计范式。
我还想强调一个细节:GMS 内部对 ES 和 MySQL 都做了一层缓存,默认本地缓存 Key 是 Urn + Aspect 名,TTL 比较短。生产环境如果发现修改元数据后前端迟迟看不到变化,排除网络原因后,很可能就是缓存还没过期,可以观察一下对应日志里的 cache miss 和 hit 情况。
5. 快速部署与生产环境实操要点
5.1 本地环境:十分钟起一个完整栈
本地体验 DataHub 最简单的方式是用官方 Docker Compose:
git clone https://github.com/datahub-project/datahub.git cd datahub/docker/quickstart docker compose pull docker compose up -d启动后,访问http://localhost:9002,默认账号datahub,密码datahub。整个流程正常情况下 5 到 10 分钟能把 GMS、前端、Kafka、ES、MySQL 都拉起来。我习惯在启动后先检查容器状态:
docker compose ps看到datahub-gms处于 healthy、datahub-frontend-react处于 Up,一般就可以登录了。偶尔会出现 ES 容器健康检查没通过导致 GMS 启动失败,这种时候别急着重启,先看 GMS 日志,多半是 ES 初始化分词器需要一点时间,等 ES 完全就绪后再重启 GMS 即可。
5.2 生产部署的资源规划
Docker Compose 适合本地和测试,生产环境不建议这么干,特别是 Kafka、ES、MySQL 这三个底座,最好都用独立服务或云托管产品。下面是我根据实践总结的资源评估参考:
| 规模 | 实体数量级 | GMS 节点 | 消费组节点 | ES 规格 | 备注 |
|---|---|---|---|---|---|
| 小型 | 万级以下 | 2C4G x 2 | 2C4G x 1 | 3 节点,每节点 8G | 单机房可用 |
| 中型 | 十万级 | 4C8G x 2 | 4C8G x 2 | 3 节点,每节点 16G | 建议 Kafka 用云托管 |
| 大型 | 百万级以上 | 8C16G x 4 | 8C16G x 4 | 5 节点起,每节点 32G | ES 独立集群,定期快照 |
这个表是我结合内存占用和索引增长速度给出的经验值,不是官方硬性标准。DataHub 对内存不算友好,JVM Bean 默认堆内存给得比较大,如果机器内存紧张,记得在环境变量里调低堆内存,否则 2G 内存的小机器很容易 OOM。
5.3 用 Ingestion 把 MySQL 元数据完整采集进来
本地环境跑通后,可以针对自己测试库做一次完整采集。先安装采集工具:
pip install 'acryl-datahub[mysql]' -i https://mirrors.cloud.tencent.com/pypi/simple写一个mysql_recipe.yml:
source: type: mysql config: host_port: "mysql-host:3306" database: demo_db username: read_only_user password: "your_password" database_pattern: allow: - "^demo_db$" profiling: enabled: true profile_table_level_only: true sink: type: datahub-rest config: server: "http://datahub-gms:8080"注意生产环境采集账号必须给最小权限,只需要SELECT权限和SHOW DATABASES/SHOW TABLES权限即可,千万别用管理员账号。然后执行:
datahub ingest -c mysql_recipe.yml采集完成后,登录前端页面的 Search 页,输入orders应该能看到刚采集的表。点进详情页,Schema 列表、负责人、标签、血缘 tab 都会按 Aspect 展示出来。数据量少时几秒就能搜到,如果一直搜不到,大概率是采集那一步就失败了,去 GMS 日志里捞一下错误比反复重试更高效。
5.4 部署后验证清单
我每次部署完或升级完 DataHub,都会依次做一遍验证:
- [ ] 访问
http://ip:9002/login,用管理员账号登录成功。 - [ ] 在 Search 搜索一个已知表名,能命中且详情页 Schema 正确。
- [ ] 在数据集详情页“Lineage”看到上游/下游血缘关系。
- [ ] 执行一次
datahub ingest -c recipe.yml,观察日志无 ERROR。 - [ ] 查看 Elasticsearch 索引
curl 'localhost:9200/_cat/indices/*index*?v',有数据且文档数与采集的表数量级吻合。
6. 常见问题与排查技巧实录
6.1 高频问题速查表
| 现象 | 可能原因 | 排查手段 |
|---|---|---|
| 采集完搜不到数据 | ES 索引未刷新、Ingestion 失败、URN 不匹配 | 查 GMS 日志,ES 索引文档数,确认 URN 构造 |
| 前端登录 401 | 认证开启但账号配置错误 | 检查 USER_PROPS 或 LDAP 配置,确认默认账号可登录 |
| GMS 启动失败 | MySQL 连接不上、DB 迁移失败、ES 未就绪 | 看 GMS 日志,先起依赖组件,再起 GMS |
| 血缘图空白 | 未启用 Neo4j、血缘 Aspect 未写入 | 设置 GRAPH_SERVICE_IMPL=neo4j,查看 lineage 事件 |
| Kafka 消费延迟大 | 消费者并发不足、MySQL 写入慢 | 查看 consumer lag,调整消费者线程数 |
| 元数据更新后 UI 不变化 | 本地缓存未过期、MAE 消费失败 | 观察 GMS 日志 cache 日志,检查 mae-consumer 消费状态 |
这张表是我带团队排查时常用的速查清单,绝大多数问题都能在 GMS 和 mae-consumer 的日志里找到线索,别急着删数据重来。
6.2 实操案例一:采集完成后前端搜不到表
一次帮同事排查,他跑完datahub ingest日志显示成功了,但前端搜索就是找不到demo_orders。我先去 ES 看了索引文档数,发现datasetindex_v2里确实没有对应文档。
思路转向 mae-consumer。查看它的日志,发现它在解析 MetadataChangeLog 时抛了IllegalArgumentException: unknown aspect,一查版本,原来是 GMS 版本和 mae-consumer 镜像版本不一致,新采集器提交的 Aspect 旧消费者不认识。解决方案很简单:将 mae-consumer、mce-consumer、GMS 的镜像版本统一到同一个 release 版本,然后重启消费组。这个坑很经典,升级 DataHub 时只升 GMS 不升消费者,或者反过来,都会出问题。
6.3 实操案例二:Kafka 消费延迟导致元数据长时间不更新
生产环境有一次批量刷完数据字典后,前端几个小时都没更新,不是完全没变,而是像“卡住了一样”。我登录 Kafka broker 查看消费者组状态:
kafka-consumer-groups --bootstrap-server localhost:9092 \ --group datahub-mae-consumer --describe看到LAG列一直在涨,说明 mae-consumer 消费速度跟不上生产速度。原因是有几张大表的 SchemaMetadata 特别大,单条 MCL 消息几十 KB,mae-consumer 默认抓取配置处理吃力。临时处理是重启消费者加入新节点,长期处理是在环境变量里调大消费者的max.poll.records和fetch.min.bytes,并且把采集任务改成增量采集而非全量重跑。
6.4 实操案例三:GMS 启动报数据库迁移失败
这个坑很多新手会踩。本地测试时 MySQL 里有过旧版本 DataHub 的元数据表,升级后 GMS 启动时执行 Flyway 迁移脚本报错,最常见的是重复索引或者数据不满足新约束。
我的处理办法是:非生产环境直接重建数据库DROP DATABASE datahub; CREATE DATABASE datahub;,然后重启 GMS 让它重新初始化。生产环境千万别这么干,必须先备份 schema,再逐个分析失败的迁移脚本。DataHub 的迁移脚本都在metadata-service模块的 resources 目录下,报错信息会明确告诉你哪个脚本失败、哪条 SQL 有问题,按图索骥修掉即可。没有加过自定义 Schema 的团队,生产环境很少遇到这种问题,遇到多半是版本跨得太大,建议升级前仔细阅读官方 release note。
7. 几个值得留意的生产实践心得
最后分享几条我在实际使用中沉淀下来的经验,都不是什么高深理论,但每一条都是踩过坑换来的。
第一,状态化采集要慎开。DataHub 支持 stateful ingestion,可以自动发现并移除源端已经删除的表。听起来很美好,但如果底层存储不稳定,一次误判就可能把生产环境的表元数据全清掉。我建议第一轮采集先不开remove_stale_metadata,跑稳定两周后再开启,并且开启前一定确认快照备份可用。
第二,权限模型从第一天就要规划。DataHub 有内置的 Metadata Policy,可以按用户、组、平台资源维度控制谁能看哪些数据。千万不要图省事全员管理员,到后期数据量大了再整理权限,成本远比一开始就分好角色高得多。
第三,跑批采集要有审计。给采集器单独建一个弱权限账号,并且在每次 ingest 命令后把日志落盘保存,至少保留 60 天。这样即使有人误操作、误删了元数据,也能定位到是哪一次采集、哪一份 recipe 出的问题。
第四,列级血缘的维护需要体系化配合。DataHub 本身只是血缘的载体,真正决定血缘准不准的,是上游数据平台有没有在跑任务时产出血缘信息。如果你接的是自定义 SQL 引擎,需要自己写解析器抽取 SQL 里的列映射关系,再通过datahub-rest提交upstreamLineageAspect。没有体系化的血缘生成机制,前端血缘图再漂亮也是空架子。
第五,升级前一定要先看 release note。DataHub 迭代速度很快,v0.8 到 v0.10 之间 API 变动很大,自定义 Aspect、采集器代码都可能不兼容。升级时值得同时升级 GMS、mae-consumer、mce-consumer、前端,并且先在测试环境完整跑通一遍采集、搜索、血缘验证,再上生产。
DataHub 这架构本身并不复杂,核心就是把元数据当成流式数据来治理。弄清楚 Urn、Entity、Aspect 这套模型,再顺着 MCP/MCL 链路看一遍读写流程,整个系统在你眼里就透明了。后面无论你是想给团队内部搭建统一数据目录,还是想基于 DataHub 做二次开发,心里都能有个清晰的脉络。