跳到主要内容
最后 更新

数据血缘原理与使用

数据血缘功能可以从 Doris 支持的 DML 语句中提取表级和列级依赖,并通过 LineagePlugin 发送到外部治理系统。Doris 是血缘生产者,不保存事件、不提供血缘查询 API,也不提供血缘可视化。

该功能在 Dev 版本中可用,首次发布于 Apache Doris 4.0.6。

能力和限制

框架只会在以下语句成功后生成事件:

支持的语句事件行为
INSERT INTO ... SELECTInsert 成功后生成一个事件。
INSERT OVERWRITE TABLE ... SELECTOverwrite 成功后生成一个事件。
CREATE TABLE AS SELECT内部 Insert 成功后生成一个事件。

当前实现不会为 SELECTUPDATEDELETE、导入任务、仅包含 VALUES 的 Insert,以及目标表为 __internal_schema 的写入生成事件。部分 UPDATEDELETE 执行路径会在内部复用 Insert 命令,但原始命令类型校验会阻止这些命令提交血缘事件。

投递保证

血缘投递是异步且尽力而为的。DML 成功后,即使事件采集或插件投递失败,DML 也不会回滚。队列满时可能丢弃事件,Doris 不会重试或持久化这些事件。

工作原理

采集流程

对于受支持的语句,Doris 会在 afterAnalyze 规划 Hook 中记录 Nereids 已分析的逻辑计划。DML 成功后,系统从该逻辑计划提取血缘并将事件提交到 FE 本地队列。单个 daemon 工作线程会检查每个已加载插件,并将事件分发给 eventFilter() 返回 true 的插件。

数据血缘采集架构:受支持的 DML 成功后,由 Nereids 分析并提取为 LineageInfo,经 FE 队列和插件投递到外部治理系统。

在提取前,Doris 会对已加载插件调用 eventFilter()。如果没有插件愿意接收事件,则跳过提取。工作线程在分发前也会再次判断 eventFilter()

血缘内容

每个 LineageInfo 事件包含表级血缘、列级直接血缘、两类间接血缘和查询上下文。先通过一个完整的 SQL 示例明确源表、目标表和输出结果,后续各小节都基于该示例解释事件内容。

示例 SQL 和执行结果

以下示例需要当前用户具有创建数据库、创建表、写入和查询权限,并且 FE 已加载血缘插件。示例使用两个源表,通过 CTE、Join、过滤、聚合、窗口函数、条件表达式和排序生成客户汇总表。

CREATE DATABASE IF NOT EXISTS lineage_demo;
USE lineage_demo;

CREATE TABLE lineage_orders (
order_id BIGINT,
customer_id BIGINT,
region VARCHAR(16),
amount DECIMAL(18, 2),
status VARCHAR(16)
)
DUPLICATE KEY(order_id)
DISTRIBUTED BY HASH(order_id) BUCKETS 1
PROPERTIES ("replication_num" = "1");

CREATE TABLE lineage_customers (
customer_id BIGINT,
customer_name VARCHAR(64),
customer_level VARCHAR(16)
)
DUPLICATE KEY(customer_id)
DISTRIBUTED BY HASH(customer_id) BUCKETS 1
PROPERTIES ("replication_num" = "1");

CREATE TABLE lineage_customer_summary (
customer_id BIGINT,
region_label VARCHAR(16),
total_amount DECIMAL(18, 2),
customer_seq BIGINT,
region_group VARCHAR(16)
)
DUPLICATE KEY(customer_id)
DISTRIBUTED BY HASH(customer_id) BUCKETS 1
PROPERTIES ("replication_num" = "1");

INSERT INTO lineage_customers VALUES
(1, 'Alice', 'VIP'),
(2, 'Bob', 'VIP'),
(3, 'Carol', 'REGULAR');

INSERT INTO lineage_orders VALUES
(101, 1, 'east', 100.00, 'PAID'),
(102, 1, 'east', 50.00, 'PAID'),
(103, 2, 'west', 80.00, 'PAID'),
(104, 2, 'west', 30.00, 'CANCELLED'),
(105, 3, 'north', 200.00, 'PAID');

INSERT INTO lineage_customer_summary
WITH customer_totals AS (
SELECT
o.customer_id,
UPPER(o.region) AS region_label,
SUM(o.amount) AS total_amount
FROM lineage_orders o
JOIN lineage_customers c
ON o.customer_id = c.customer_id
WHERE o.status = 'PAID'
AND c.customer_level = 'VIP'
GROUP BY o.customer_id, UPPER(o.region)
HAVING SUM(o.amount) >= 50
)
SELECT
customer_id,
region_label,
total_amount,
ROW_NUMBER() OVER (ORDER BY customer_id) AS customer_seq,
CASE
WHEN region_label = 'EAST' THEN 'CORE'
ELSE 'OTHER'
END AS region_group
FROM customer_totals
ORDER BY total_amount DESC;

SELECT customer_id, region_label, total_amount, customer_seq, region_group
FROM lineage_customer_summary
ORDER BY customer_id;

查询结果如下:

+-------------+--------------+--------------+--------------+--------------+
| customer_id | region_label | total_amount | customer_seq | region_group |
+-------------+--------------+--------------+--------------+--------------+
| 1 | EAST | 150.00 | 1 | CORE |
| 2 | WEST | 80.00 | 2 | OTHER |
+-------------+--------------+--------------+--------------+--------------+

两个仅包含 VALUES 的 Insert 不会生成血缘事件。INSERT INTO lineage_customer_summary ... SELECT 会生成一个 LineageInfo 事件,以下各小节说明该事件的具体内容。

表级血缘

表级血缘记录一个目标表和已分析逻辑计划中扫描的所有源表。例如,写入 lineage_customer_summary 的查询同时扫描 lineage_orderslineage_customers,因此这两个源表都与目标表建立表级关系。CTE 会继续解析到其底层表,UNION 的各个分支也都会计入源表集合。

该示例产生以下表级血缘:

源表目标表
lineage_orderslineage_customer_summary
lineage_customerslineage_customer_summary

列级直接血缘

列级直接血缘说明每个目标列的值由哪个源表达式产生。Doris 在解析别名和 CTE 后,按以下顺序判断类型:表达式只要包含聚合函数就是 AGGREGATION;纯源列引用是 IDENTITY;其他表达式是 TRANSFORMATION

类型含义示例
IDENTITY目标值直接取自一个源列,没有函数或运算。lineage_customer_summary.customer_id 直接来自 lineage_orders.customer_id
TRANSFORMATION目标值经过非聚合表达式转换,例如算术、字符串函数、窗口函数或条件表达式。region_label 来自 UPPER(lineage_orders.region)customer_seq 来自 ROW_NUMBER()
AGGREGATION目标值由包含聚合函数的表达式产生。即使聚合函数外还有其他表达式,仍归为该类型。total_amount 来自 SUM(lineage_orders.amount)

该示例产生以下列级直接血缘:

目标列源表达式类型
lineage_customer_summary.customer_idlineage_orders.customer_idIDENTITY
lineage_customer_summary.region_labelUPPER(lineage_orders.region)TRANSFORMATION
lineage_customer_summary.total_amountSUM(lineage_orders.amount)AGGREGATION
lineage_customer_summary.customer_seqROW_NUMBER() OVER (ORDER BY lineage_orders.customer_id)TRANSFORMATION
lineage_customer_summary.region_groupCASE WHEN UPPER(lineage_orders.region) = 'EAST' ... ENDTRANSFORMATION

数据集级间接血缘

数据集级间接血缘记录影响整个结果集、但不直接产生某个目标列值的表达式。这些依赖保存在事件级 map 中,需要时可以应用到每个目标输出列。

类型含义示例
JOINJoin 条件决定哪些源表行可以组合。lineage_orders.customer_id = lineage_customers.customer_id
FILTERWHEREHAVING 条件决定哪些行或分组进入结果。status = 'PAID'customer_level = 'VIP'HAVING SUM(amount) >= 50
GROUP_BY分组表达式决定聚合结果的粒度。customer_idUPPER(region) 分组。
SORTORDER BY 或 TopN 的排序表达式决定结果顺序。total_amount DESC 排序;别名解析后对应 SUM(amount)

该示例产生以下数据集级间接血缘:

类型事件中的源表达式示例作用
JOINlineage_orders.customer_id = lineage_customers.customer_id决定订单与客户的匹配关系。
FILTERstatus = 'PAID'customer_level = 'VIP'SUM(amount) >= 50分别来自 WHEREHAVING,共同决定进入结果的行和分组。
GROUP_BYlineage_orders.customer_idUPPER(lineage_orders.region)决定每个客户、区域的聚合粒度。
SORTtotal_amount DESC,解析后对应 SUM(lineage_orders.amount)决定插入查询结果的顺序。

输出列级间接血缘

输出列级间接血缘只挂到受影响的目标列,不会应用到其他输出列。

类型含义示例
WINDOW窗口函数的 PARTITION BYORDER BY 输入影响对应输出列。customer_seqROW_NUMBER() OVER (ORDER BY customer_id) 产生,其 WINDOW 依赖是 customer_id
CONDITIONALCASEIF 的条件或 COALESCE 的候选表达式影响对应输出列。region_group 的值由 CASE WHEN region_label = 'EAST' ... 决定,其 CONDITIONAL 依赖是该判断条件。

窗口或条件表达式本身仍会出现在列级直接血缘中。例如,customer_seq 的直接类型是 TRANSFORMATION,同时具有 WINDOW 间接血缘;region_group 的直接类型也是 TRANSFORMATION,同时具有 CONDITIONAL 间接血缘。

该示例产生以下输出列级间接血缘:

目标列类型事件中的源表达式作用
customer_seqWINDOWlineage_orders.customer_id作为 ROW_NUMBER() 的窗口排序输入,只影响 customer_seq
region_groupCONDITIONALUPPER(lineage_orders.region) = 'EAST'作为 CASE WHEN 条件,只影响 region_group

其他三个目标列没有输出列级间接血缘。数据集级的 JOINFILTERGROUP_BYSORT 仍然影响整个结果集。

查询上下文

查询上下文说明这次血缘事件由谁、在什么会话中、通过哪条 DML 产生。

字段示例或含义
Source CommandInsertIntoTableCommand,表示事件来自 Insert。
Query ID 和原始 SQL本次 DML 的 Query ID,以及完整的 INSERT INTO lineage_customer_summary ... 文本。
用户和客户端 IP例如 lineage_user192.0.2.10
会话数据库和 Catalog上述示例为 lineage_demointernal;它们是会话上下文,不一定等于目标表所在位置。
执行状态成功的 DML 通常记录为 OK
时间戳和耗时事件创建时间和从查询开始到事件创建的毫秒数;无法取得时为 -1
外部 Catalog 属性记录查询涉及的外部 Catalog 的非敏感属性。密码、密钥和隐藏属性会被删除;仅使用 internal Catalog 时该 map 为空。

该示例的原始 SQL 是上述复杂 Insert,Source Command 为 InsertIntoTableCommand,会话数据库为 lineage_demo,会话 Catalog 为 internal,成功状态为 OK。Query ID、用户、客户端 IP、事件时间戳和耗时使用实际执行上下文中的值;因为示例只使用 Internal Catalog,外部 Catalog 属性 map 为空。

在插件的下游系统中确认已收到事件。首次部署时,还应检查 fe.log 是否出现 Loaded lineage plugin,以及插件自身错误或队列满 warning。

LineageInfo 事件模型:目标和源表形成直接、间接血缘,并与查询上下文一起由插件转换为下游系统事件。

提取器会在事件到达插件前解析 CTE 生产者表达式,并展开 UNION 的各分支。下游插件需要将 TableIfSlotReferenceExpression 等 Doris Java 对象转换为稳定标识或自身事件格式。

事件处理行为

lineage_event_queue_size 设置每个 FE 进程本地队列能够等待处理的最大血缘事件数,单位是事件数,不是字节数。队列满时,系统会丢弃新事件,DML 正常继续。工作线程串行调用插件,慢插件会延迟后续事件,并可能在持续负载下造成事件丢弃。插件抛出的异常会被记录,不会停止工作线程,也不会影响 DML。

插件发现和初始化只在 FE 启动时进行,不支持动态 reload 或 unload。

配置和使用

前提条件

  1. 准备外部血缘插件。Doris 不提供内置 sink。SPI 契约、JAR 打包方式和完整最小插件示例请参阅数据血缘插件开发
  2. 将插件 JAR 及所需第三方依赖 JAR 复制到每个 FE。
  3. 如果插件需要向外部治理服务发送事件,确保每个可能执行 DML 的 FE 都可以访问该服务端点。

部署插件

FE_HOME 表示包含 bin/conf/lib/ 的单个 FE 安装目录。在每个 FE 上使用以下目录结构。加载器只扫描 lineage/ 下的直接子目录,以及插件目录和其 lib/ 目录中的 JAR。

$FE_HOME/plugins/
└── lineage/
└── example-lineage/
├── example-lineage.jar
└── lib/
└── downstream-client.jar

在每个 FE 的 fe.conf 中配置。将 example-lineage 替换为插件 Factory 的 name() 返回值:

plugin_dir = /opt/apache-doris/fe/plugins
activate_lineage_plugin = example-lineage
lineage_event_queue_size = 50000
配置类型和默认值是否必填说明
plugin_dirString;$FE_HOME/plugins插件根目录。血缘加载器扫描其 lineage/ 直接子目录。使用默认目录时可以省略该配置。
activate_lineage_pluginString 数组;空建议显式填写需要实例化的 Factory 名称,以英文逗号分隔。空值表示不过滤,会实例化全部已发现的 Factory。
lineage_event_queue_size正整数;50000当前 FE 等待工作线程处理的最大血缘事件数。队列满时丢弃新事件。

三个参数都在各 FE 节点的 $FE_HOME/conf/fe.conf 中配置,作用范围是当前 FE 进程。它们都只在 FE 启动时生效,修改后必须重启对应 FE。

激活插件

在每个 FE 节点的 $FE_HOME/conf/fe.conf 中配置 activate_lineage_plugin。它控制哪些已发现的 Factory 会被实例化,不负责发现 JAR,也不控制队列容量。名称必须与 LineagePluginFactory.name() 完全一致并区分大小写;插件实现的 LineagePlugin.name() 应返回相同名称。插件目录名不参与匹配。配置多个插件时使用英文逗号分隔;FE 配置解析器会去除各项两侧的空格,但名称匹配仍区分大小写:

activate_lineage_plugin = example-lineage,governance-lineage

FE 启动时会发现内置和外部 Factory,然后只为配置中列出的名称创建插件实例。每个 FE 独立读取该配置,因此所有可能执行 DML 的 FE 都需要配置相同的插件集合。修改后必须重启 FE,不支持通过 ADMIN SET FRONTEND CONFIG 动态修改。

空值行为

当前实现中,activate_lineage_plugin 为空并不表示关闭血缘插件,而是跳过名称筛选并实例化全部已发现的 Factory。应显式填写需要启用的插件名称,不要把空值当作禁用开关。插件实例加载后,eventFilter() 仍会在提取前和分发前决定是否接收事件。

配置事件队列

在每个 FE 节点的 $FE_HOME/conf/fe.conf 中配置 lineage_event_queue_size。该参数必须是正整数,每个可能执行 DML 的 FE 都有自己的独立队列,因此需要分别配置。

该参数是 FE 启动配置,不支持通过 ADMIN SET FRONTEND CONFIG 动态修改。修改后必须重启对应 FE。增大参数只会提高待处理事件的缓冲能力和 FE 内存占用,不会增加消费线程数,也不会提高插件处理速度。出现事件丢失时,应先在 fe.log 中搜索 the lineage event queue is full,并优先降低插件 exec() 的处理延迟,再根据峰值积压量和 FE 内存容量调整队列大小。

修改其他插件配置或替换插件 JAR 后,也必须重启每个 FE。这些配置和插件目录只在 FE 启动时读取。

运维和排障

写入后没有事件

常见原因包括语句不受支持、DML 执行失败、当前 FE 没有加载插件,或者插件的 eventFilter() 返回了 false

按以下顺序检查:

  1. 确认语句是成功执行的 INSERT INTO ... SELECTINSERT OVERWRITE TABLE ... SELECTCREATE TABLE AS SELECT。仅包含 VALUES 的 Insert 不会生成事件。
  2. 确认执行该 DML 的 FE 已配置并加载插件,在 fe.log 中搜索 Loaded lineage plugin
  3. 确认插件的 eventFilter() 在查询线程和工作线程中都返回 true
  4. 检查插件日志和下游服务,确认事件不是在 FE 之外处理失败。

插件未加载

检查插件目录中是否存在 JAR,以及 JAR 中是否包含 META-INF/services/org.apache.doris.nereids.lineage.LineagePluginFactory。每个插件目录只能提供一个 Factory,Factory 名称必须全局唯一,并且应与 activate_lineage_plugin 中的名称一致。修正目录、JAR 或配置后重启对应 FE。

高负载下事件缺失

fe.log 中搜索 the lineage event queue is full 和插件异常。如果队列已满,先减少插件 exec() 中的同步阻塞,使下游投递具备幂等性和有界重试,再评估是否增大 lineage_event_queue_size。增大队列前需要评估 FE 内存,因为不同 SQL 产生的事件大小并不固定。

还应确认所有可能执行 DML 的 FE 都部署了相同插件和配置。每个 FE 使用独立的本地队列,一个 FE 上的插件和队列状态不会自动同步到其他 FE。

更新插件后未生效

插件目录只在 FE 启动时扫描,不支持动态 reload 或 unload。替换插件 JAR、依赖或配置后,重启每个相关 FE,并再次检查 Loaded lineage plugin 日志。

应使用 Query ID 和目标表等信息构造下游事件标识。这样,即使插件在框架外实现重试,治理系统也可以对事件去重。

相关文档