想象你是一家大型连锁餐厅新上任的中央厨房总厨。每天后厨要出品几百道菜,每道菜都有自己的工序:番茄要切块、蛋液要打匀、牛腩要焯水再慢炖。这些工序之间有着严格的先后关系——蛋液没打好,番茄炒蛋就没法下锅;牛腩没焯水,红烧牛腩就炖不出那个味道。更麻烦的是,这些关系并不是一条直线:切番茄和打蛋液可以同时进行,但炒蛋必须在两者都完成后才能开始。你需要在一面墙上挂一张“动线图”,标明每道工序必须等待哪些前置工序完成。这张图必须井井有条,绝不能出现“做番茄炒蛋要等蛋液、而蛋液又等番茄炒蛋出锅”的死胡同。在数据世界里,这张图的名字叫 DAG(有向无环图)。特征平台里成千上万的特征,正如这些食材与工序:它们互相依赖、按序产出、彼此牵制。而管理这一切的学问,就是特征依赖管理与 DAG 调度——理解了它,你就掌握了特征平台的心脏。

岗位关联

对 AI 从业者而言,掌握特征依赖与 DAG 调度,意味着你能设计出稳定、可追溯、能高效并行执行的特征流水线——这是从“能写特征代码”到“能搭特征基础设施”的关键一跃。

目录
  1. 可视化演示
  2. 依赖管理基础
  3. DAG核心概念
  4. 拓扑排序
  5. 特征生命周期
  6. 调度系统设计
  7. 血缘追踪运维
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

动态演示特征依赖管理与DAG调度的核心机制,通过入门/进阶/实战/优化四种模式展示关键概念与交互流程。

本节配有交互动效(进入视口后自动加载,约 3KB)。

特征依赖管理与DAG调度:特征依赖 特征依赖管理的核心概念与价值 特征依赖图的DAG建模方法 核心机制可视化 特征依赖管理与DAG调度整体架构与关键组件

依赖管理基础

一份鱼香肉丝的菜谱,除了肉丝还要配木耳、胡萝卜、泡椒,而泡椒又要提前一周泡好。总厨心中始终有一张“食材清单”:哪些是直接买来的,哪些必须先加工才能用。特征世界里同样如此——一个“用户近 30 天消费金额”的特征,依赖订单表里的支付流水;一个“用户风险分”的特征,又依赖前者和其他画像特征。这种“上游数据或上游特征 → 下游特征”的传递关系,就是特征依赖。它决定了数据如何一步步从原始形态被加工成可用特征。

特征依赖并非只在特征之间发生。按来源划分,常见三类:数据表依赖,指特征要读取某张源表或中间表,比如订单明细表;特征依赖,指一个特征的计算要使用另一个已产出的特征,比如风险分依赖消费能力分;模型任务依赖,指特征产出后要喂给模型训练与推理任务,比如推荐模型每天等待特征快照就绪。三类依赖共同构成一张前后相扣的链条,任何一环断裂,下游全部遭殃。

如果放任不管,会怎样?特征脚本各自为政,有人改了上游表的字段名,下游几百个特征静默出错;有人重复计算同一份特征,浪费了计算资源却没有统一口径。依赖管理的价值,就是把这张看不见的链条显式地记录下来,让它可查询、可验证、可调度。形式化地看,一个下游特征可以表达为上游特征的函数:

这张公式写下来,总厨就明白了:哪怕盘中餐再复杂,只要把每道菜的“食材清单”列清楚,后厨就不会乱套。依赖管理的第一步,就是把这份清单变成系统的数据资产。

fdown=g(fup1,fup2,…,fupk)f_{\text{down}} = g(f_{\text{up1}}, f_{\text{up2}}, \dots, f_{\text{upk}})
一个下游特征等于某个“加工函数”作用于若干上游特征的结果,函数 g 就是你在 SQL 或 Python 里写的那段计算逻辑。
  1. 下游特征等号左边的 f_down 是被生成的特征,例如“用户风险分”。
    fdownf_{\text{down}}
  2. 上游特征集f_up1 到 f_upk 是生成下游特征所需的 k 个前置特征。
    {fup1,fup2,…,fupk}\{f_{\text{up1}}, f_{\text{up2}}, \dots, f_{\text{upk}}\}
  3. 变换函数g 表示把上游特征组合加工成下游特征的计算逻辑,对应特征任务的 SQL 或代码。
    g(⋅)g(\cdot)

DAG核心概念

有了食材清单之后,总厨还缺一样东西——挂在墙上的动线图。每个备料台是一个圆圈,每道“必须等”的关系是一条带箭头的线,箭头永远指向等待的那一方。切番茄和打蛋液之间没有箭头,因为可以并行;但炒蛋必须等两者都完成,所以有两条箭头同时指向炒蛋。

把动线图数学化,就得到 DAG(Directed Acyclic Graph,有向无环图)。它由两部分组成:顶点集 V 代表所有特征任务或数据源,边集 E 代表依赖关系。一条边 (u, v) 表示“u 完成后 v 才能开始”。用公式写就是:

“无环”是 DAG 的灵魂。所谓环,就是存在一条路径从某个顶点出发、沿着箭头方向又回到自己,例如 A→B→A。一旦出现环,A 等 B、B 等 A,调度系统将永远无法确定谁先开始。DAG 模型之所以成为特征依赖的事实标准,是因为它结构清晰、可验证、可拓扑排序,还天然支持并行调度——这就像动线图既能让总厨一眼看出哪几道菜能同时开火,也能在图纸阶段就发现“这道菜等那道菜、那道菜又等这道菜”的设计错误。

G=(V,E),E⊆V×VG = (V, E), \quad E \subseteq V \times V
图 G 由顶点集合 V 和边集合 E 构成,E 是从“前驱顶点”到“后继顶点”的有序对集合。
  1. 顶点集 V全部特征任务、数据表或模型任务的集合,是图的基本组成单元。
    V={v1,v2,…,vn}V = \{v_1, v_2, \dots, v_n\}
  2. 边集 E依赖关系的有序对集合,每一条 (u, v) 代表 u 是 v 的前置前置工序。
    (u,v)∈E  ⟺  u 完成于 v 开始之前(u, v) \in E \iff u \text{ 完成于 } v \text{ 开始之前}
  3. 子集约束E 必须是 V 与自己笛卡尔积的子集,说明边只能连接真实存在的顶点,不能悬空。
    E⊆V×V,(v,v)∉EE \subseteq V \times V, \quad (v, v) \notin E

拓扑排序

动线图挂上墙,总厨开始排工。他不会从最复杂的炖菜开始,而是先扫一眼:哪些备料台不需要等别人就能开工?把这些工序全部安排下去,等它们完工后,再释放出下一批等待者——这就是拓扑排序在日常中的样子。Kahn 算法把这件事变成了机械流程,它依赖一个叫“入度”的计数器:入度指有多少条边指向某个顶点,也就是它到底要等几个前置任务。

算法第一步,找出所有入度为 0 的顶点放入候选集合 L,它们没有任何前置依赖,可以立即开工:

拓扑排序还能回答一个关键问题:整条流水线最早什么时候能跑完?对每个顶点 v,它的完成时间等于最慢的那个前驱完成之后,再加上自身耗时:

对总厨而言,Kahn 算法就是“反复找出能开工的活儿,干完一批释放下一批”;T(v) 则是他估算整场宴席几点能开饭的依据。实践中,调度器会把每一轮同时释放的顶点归为同一个“调度批次”,同一批次内互不依赖的任务并行执行,流水线吞吐因此倍增。

L={v∈V∣indeg(v)=0}L = \{ v \in V \mid \text{indeg}(v) = 0 \}
v 必须等所有前驱都完成才能开始,所以取前驱完成时间的最大值,再累加它自身的执行时间。
  1. 前驱集合pred(v) 是 v 的全部上游节点,即所有直接指向 v 的顶点。
    pred(v)={u∈V∣(u,v)∈E}\text{pred}(v) = \{ u \in V \mid (u, v) \in E \}
  2. 取最大值v 的开工时间取决于最慢的那个前驱,体现了“木桶最短一块板”的原理。
    max⁡u∈pred(v)T(u)\max_{u \in \text{pred}(v)} T(u)
  3. 累加耗时在最长前置链的基础上加上 t(v),得到 v 的最早可完成时间。
    T(v)=max⁡u∈pred(v)T(u)+t(v)T(v) = \max_{u \in \text{pred}(v)} T(u) + t(v)

特征生命周期

一道招牌菜从后厨试菜、登上菜单、稳定出品到最后下架,每一步都有讲究:试菜要过口味关,上菜单要印新菜卡,下架则要确认没有老顾客还在点它。特征的生命周期与之同构:注册、验证、上线、监控、下线。特征注册是把特征的元数据登记进特征平台,包括名称、类型、计算逻辑、负责人和依赖清单。平台把这份登记信息当作唯一的“菜卡”,所有调度、审计、复用都以它为准。

版本管理是生命周期里最容易忽视却最要命的一环。特征逻辑每次修改,都应该生成新版本,而不是原地覆盖。形式化地,一个特征可以表示为一个元组:

质量验证在特征上线前把关:用历史数据试跑,检查分布是否合理、缺失率是否超标、口径与业务方是否确认一致。而生命周期中最容易被忽略的是“安全下线”。很多人以为下线就是删几行代码,却不知道要先回答一个问题:还有谁在用我?下游引用集合为:

回到中央厨房:下架一道菜之前,总厨要翻一遍点菜记录,确认没有固定食客每天必点,否则就是砸招牌。特征平台里,这个“翻记录”的动作由依赖图自动完成——生命周期管理的本质,就是让特征像菜品一样有始有终、有据可查。

F=(id,version,logic,owner,deps)F = (id, \text{version}, \text{logic}, \text{owner}, \text{deps})
C(v) 是所有直接依赖特征 v 的下游任务集合,只要它非空,v 就不能下线。
  1. 下游集合遍历边集 E,把所有以 v 为前驱的顶点 u 收集起来。
    C(v)={u∈V∣(v,u)∈E}C(v) = \{ u \in V \mid (v, u) \in E \}
  2. 非空即禁用如果 C(v) 不为空,说明仍有特征或模型在消费 v,直达删除会导致线上报错。
    C(v)≠∅⇒v 禁止下线C(v) \neq \varnothing \Rightarrow v \text{ 禁止下线}
  3. 先迁移再下线正确的顺序是先把下游迁移到新特征,确认引用归零后再执行下线。
    C(v)=∅⇒v 可安全下线C(v) = \varnothing \Rightarrow v \text{ 可安全下线}

调度系统设计

大型宴会有三十道菜,总厨不可能亲自盯着每口锅。他需要一张排班表和一个应急预案:谁先上灶、谁做完了通知谁、哪道菜超时了要催、哪道菜糊锅了要重做。调度系统就是特征平台里的“总厨”,它读取 DAG,按照拓扑序把任务分发给 Spark、Flink 等执行引擎,并持续跟踪每个任务的状态。

任务状态由一台状态机统一管理,它定义了任务从提交到结束的全部可能路径:

容错设计要处理三类问题:失败重试、超时熔断、数据一致性。重试的前提是幂等——同一个特征任务重复执行应当产生相同结果,否则重试会把脏数据写进特征存储,越修越乱。超时熔断则是给每个任务设一个 SLA 上限,超过就主动失败、释放资源,避免一个卡死的任务占着锅不放。数据一致性还牵涉“背压”:当下游特征消费速度跟不上上游产出速度时,调度器要主动限流或缓冲,防止生产者把消费者冲垮。

总厨的应急预案不是等糊锅了才想,而是提前写在排班表旁边。调度系统的设计同样如此:状态机让每一次状态变化都有记录,幂等让重试变得安全,背压让上下游在速度不匹配时依然有序——宴会再大,也不会乱。

S: pending→running→success∣failed→retryS:\ \text{pending} \rightarrow \text{running} \rightarrow \text{success} \mid \text{failed} \rightarrow \text{retry}
任务先排队等待(pending),获得资源后运行(running),最终成功或失败;失败且未超重试上限则回到等待队列重试。
  1. 等待态pending 表示任务已被调度器接收,但还在排队等待计算资源。
    S0=pendingS_0 = \text{pending}
  2. 运行态running 表示任务正在执行引擎上运行,此时开始计时并监控进度。
    pending→running\text{pending} \to \text{running}
  3. 终止与重试运行结束进入 success 或 failed;failed 时若重试次数未达上限则回到 pending,否则进入终态并触发告警。
    failed→retry≤limitpending,failed→retry>limitdead\text{failed} \xrightarrow{\text{retry} \le \text{limit}} \text{pending}, \quad \text{failed} \xrightarrow{\text{retry} > \text{limit}} \text{dead}

血缘追踪运维

你是一家连锁餐厅的总厨,某天一位顾客食物中毒,接到投诉后,你要在半小时内回答:这道菜用的是什么批次的食材?供应商是谁?哪道工序可能出了问题?答案全部藏在一条贯通全链路的“食品溯源链”里。特征平台的血缘追踪(Lineage)就是这条溯源链——它记录从原始数据表到特征、再到模型输出的完整路径。

血缘带来两大利器。其一是向下影响分析:上游某张表要改字段类型,谁能立刻说出哪些特征和模型会被波及?血缘通过可达关系计算影响集合:

其二是监控体系。血缘是静态的结构,监控是动态的体检。特征平台通常监控三类指标:延迟(特征产出是否满足 SLA 时间点)、新鲜度(数据时间戳是否滞后于真实时间)、分布漂移(当前特征分布相对训练集是否发生显著偏离)。三者任何一个异常,血缘都能帮助快速缩小排查范围——是源表延迟?还是中间特征计算报错?还是模型输入格式变化?

平台落地时,血缘不是一张画完就裱起来的图,而是持续演进的活资产。新增特征要登记依赖,修改版本要更新血缘,删除任务要先验证下游。许多成熟的特征平台提供三大模块:依赖分析页面(展示 DAG 并支持点查)、自动调度配置(基于拓扑序生成执行计划)、血缘图谱(支持全链路追溯)。就像连锁餐厅把每批食材的溯源信息录进系统,消费者扫个码就能看到产地——特征平台的用户扫个码,看到的是从源表到模型的完整来路。

Path(v)={u∈V∣u⇒v}Path(v) = \{ u \in V \mid u \Rightarrow v \}
Path(v) 是所有能通过依赖路径影响到 v 的顶点集合,用来评估上游变更会波及哪些下游资产。
  1. 可达关系符号 u ⇒ v 表示存在一条从 u 出发沿箭头方向到达 v 的依赖路径。
    u⇒v  ⟺  ∃(u,…,v) 是 G 中的一条路径u \Rightarrow v \iff \exists (u, \dots, v) \text{ 是 G 中的一条路径}
  2. 影响集合Path(v) 把 v 的全部上游祖先收集起来,形成一张“波及名单”。
    Path(v)={u∈V∣u⇒v}Path(v) = \{ u \in V \mid u \Rightarrow v \}
  3. 风险预判拿到 Path(v) 后,平台可以自动列出受影响的下游特征和模型,支持灰度发布与回滚决策。
    ∣Path(v)∣ 越大,变更风险越高|Path(v)| \text{ 越大,变更风险越高}

AI 应用场景

实时特征平台的特征回填

阶段:特征工程 / 数据回填

解决的问题:新特征上线往往需要回填数周乃至数月的历史数据。手工写回填脚本时,YARN 资源怎么申请、任务之间按什么顺序执行、失败后怎么补偿,全靠人肉管理,极易漏跑或重复跑,且无法保证与线上口径一致。

具体案例:某电商平台上线“用户实时偏好”特征,需要回填 90 天历史数据。平台基于 DAG 自动编排:先执行订单表预处理任务,再执行偏好特征计算任务,最后联动刷新下游推荐模型的特征缓存。整个回填过程只消耗了一次配置——平台依据依赖图自动调度,全部任务完成后自动通知算法团队验收。

金融风控特征合规审计

阶段:模型上线 / 合规审计

解决的问题:金融监管要求对模型特征的来源、加工过程提供完整解释。人工维护血缘文档不仅滞后,还经常与线上真实逻辑脱节,关键时刻无法通过审计。

具体案例:某银行风控团队使用血缘追踪查询“反欺诈分”的全部上游路径,发现该特征依赖一张外部征信数据表。当供应商调整表结构时,血缘系统通过影响分析自动标记出受波及的 3 个风控模型,并给出完整的依赖路径,团队据此迅速切换备用数据源,避免了线上模型静默劣化。

多团队协作特征开发

阶段:特征开发 / 版本协作

解决的问题:多个算法工程师并行开发特征时,如果缺少依赖管理和版本控制,会出现“我改了上游特征,你的下游特征悄悄变了”的口径冲突,以及大量重复计算。

具体案例:某推荐团队工程师 A 开发“用户购物意图”,依赖“用户点击序列”特征;工程师 B 同时在重构“用户点击序列”。平台检测到该依赖关系后,自动安排 B 先发布新版本,A 基于新版本重算下游特征,旧版本则保留 30 天以供线上回滚。整个协作过程被 DAG 自动编排,双方无需人工对齐进度。

Python 演示

python55 行
"""特征依赖DAG的拓扑排序与调度批次划分演示"""from collections import deque, defaultdict  class FeatureDAG:    """用邻接表管理特征依赖关系的有向无环图"""     def __init__(self):        self.nodes = set()        self.edges = defaultdict(list)  # 上游 -> [下游列表]        self.indeg = defaultdict(int)     def add_feature(self, feature):        """新增一个特征节点"""        self.nodes.add(feature)     def add_dependency(self, up, down):        """添加依赖:up 是 down 的前置特征"""        self.add_feature(up)        self.add_feature(down)        self.edges[up].append(down)        self.indeg[down] += 1        if up not in self.indeg:            self.indeg[up] = 0     def topological_schedule(self):        """Kahn算法:返回按批次划分的执行计划"""        indeg = {k: v for k, v in self.indeg.items()}        ready = deque([n for n in self.nodes if indeg[n] == 0])        batches = []        executed = 0        while ready:            batch = list(ready)            batches.append(batch)            for _ in range(len(batch)):                u = ready.popleft()                executed += 1                for v in self.edges[u]:                    indeg[v] -= 1                    if indeg[v] == 0:                        ready.append(v)        if executed != len(self.nodes):            raise ValueError("检测到环,不是合法的DAG")        return batches  if __name__ == "__main__":    dag = FeatureDAG()    dag.add_dependency("用户基础信息", "消费能力")    dag.add_dependency("用户活跃度", "消费能力")    dag.add_dependency("消费能力", "风险分")    dag.add_dependency("用户活跃度", "风险分")    plan = dag.topological_schedule()    for i, batch in enumerate(plan, 1):        print("第{}批执行: {}".format(i, batch))
目的:用 Python 实现 Kahn 拓扑排序,把特征依赖 DAG 划分为可并行执行的批次,是调度器分配计算资源的直接依据。
  1. self.edges[up].append(down)记录“上游完成后要释放哪些下游”,即邻接表。
  2. self.indeg[down] += 1每加一条依赖边,下游入度加一,入度就是它要等的任务数。
  3. ready = deque([n for n in self.nodes if indeg[n] == 0])初始候选集:所有无前置依赖、能立刻开工的特征。
  4. batches.append(batch)同一批入度为 0 的节点互不依赖,调度器可让它们并行执行。
  5. if executed != len(self.nodes)若执行节点数少于总节点数,说明存在环,抛出异常阻止调度。

常见误区

误区我理解的依赖图就是从根到叶的一棵树,每个特征只有一个父特征,照着树的层次一层层执行就行。

正解树只是 DAG 的特例。DAG 允许一个节点有任意多个父节点——比如“消费能力”同时依赖“用户基础信息”和“用户活跃度”,这就是典型的多个前驱合并。调度器必须处理“多对一”的依赖汇聚:要等待所有父节点都完成,节点才能被触发。用树形思维管理特征依赖,会漏掉共享依赖的场景,导致下游节点在上游未全部完成时就被错误触发。

为何常见:数据结构课程先讲树,恰好特征血缘画出来又很像层级结构,很多初学者便把“层级”等同于“依赖”,忽略了 DAG 允许的共享依赖和交叉依赖。

误区我先算出每个特征在 DAG 里的最大深度,然后按深度 0、1、2 逐层执行,这样所有依赖都能满足。

正解依赖深度不等于调度批次。批次划分依据是“当前所有入度为 0 的节点”,而一个节点的层数只描述它距离最远祖先的长度,两个深度相同的节点可能有完全不同的前驱就绪时间。正确做法是用 Kahn 算法动态维护入度:只有当一个节点的所有前驱都执行完,入度归零,它才进入当前批次。静态分层无法处理“同一深度的节点前驱链长度不一致”的情况。

为何常见:直观上看“越靠下越晚执行”似乎成立,但层数相同不代表前驱链长度相同,静态分层忽略了依赖的就绪时序差异。

误区我只要把特征到特征的依赖管理好就行,源数据表是天然的、不用登记的,表结构变了重新跑任务就行。

正解特征依赖的源头几乎都是数据表。上游表字段改名、分区延迟、数据质量波动,都会直接传导至下游特征计算,甚至让模型静默劣化。真实平台的血缘必须覆盖“数据表 → 特征 → 模型”全链路,把表也作为 DAG 中的顶点管理。只登记特征层,等于做菜只记“番茄炒蛋依赖蛋液”,却不记蛋液依赖养鸡场——出了问题根本无从查起。

为何常见:表是团队共享的既有资源,人们默认它“一直在那里”,却没意识到表结构和数据的变更恰恰是特征事故的最大源头。

误区调度器按拓扑排序执行,顺序正确了,数据就一定正确,不需要额外的保障机制。

正解拓扑排序只保证“执行顺序合法”,不保证“数据产出正确”。任务重试如果不幂等,重复写入会让特征值翻倍或出现半成品数据;下游任务如果读到了上游写入一半的中间状态,就会拿到脏数据。因此生产级调度器必须叠加原子提交、幂等写入、SLA 监控和背压控制,顺序正确只是数据正确的必要不充分条件。

为何常见:本地脚本按顺序跑从来不出问题,让人误以为“排序”就等于“正确性”,忽略了分布式环境下可见性与瞬时状态的复杂性。

面试题

Q1. DAG 中的“有向无环”指的是什么?

单选题 · easy

  • A.所有边方向相同且不交叉
  • B.节点之间不存在任何路径
  • C.不存在能绕回起点的路径← 正确答案
  • D.每条边只能被遍历一次

答案:C。有向无环的核心约束是“无环”,即不存在从某个顶点出发、沿有向边回到该顶点的路径。A 描述的是图的布局不是定义,B 描述的是完全不连通,D 描述的是欧拉迹,都不符合 DAG 的定义。

Q2. 特征依赖公式 f_down = g(f_up1, f_up2, ……, f_upk) 中 g 表示什么?

单选题 · easy

  • A.特征数据的存储位置
  • B.加工上游特征的计算逻辑← 正确答案
  • C.特征的质量评分数值
  • D.上游特征的数量

答案:B。g 是变换函数,描述如何由 k 个上游特征计算出下游特征,对应特征任务里的 SQL 或 Python 代码。A 是存储概念,C 是监控概念,D 只是参数 k 本身,都与函数语义无关。

Q3. Kahn 算法中,初始候选集合应包含哪些顶点?

单选题 · easy

  • A.所有出度为 0 的顶点
  • B.所有入度为 0 的顶点← 正确答案
  • C.所有边数最多的顶点
  • D.随机选取的一半顶点

答案:B。入度为 0 表示没有前置依赖,可以最先执行,对应“不需要等别人就能开工的工序”。出度为 0 的是最终阶段的节点,不能作为起始集合;按边数或随机选取则完全破坏了依赖约束。

Q4. 某个特征任务的入度为 3,这意味着什么?

单选题 · mid

  • A.它有 3 个下游任务要执行
  • B.需要 3 个上游依赖都完成← 正确答案
  • C.它已经被重复执行过 3 次
  • D.它同时占用 3 个计算资源

答案:B。入度是“指向该节点的边的数量”,即前置依赖数。3 个上游任务全部完成后,入度才会降到 0,该任务才能被调度。A 说的是出度,C 是执行次数,D 是资源占用,均与入度含义无关。

Q5. 特征下线前必须执行哪项检查?

单选题 · mid

  • A.删除特征定义与其全部元数据
  • B.关闭该特征的全部监控告警
  • C.检查下游依赖集合是否为空← 正确答案
  • D.清空全部历史特征值存储

答案:C。C(v) 非空说明还有下游特征或模型引用,直接下线会导致链路断裂。正确流程是先迁移下游引用、确认引用归零再下线。A、D 会直接造成线上事故,B 只是掩盖问题,都不是下线的合法前提。

Q6. 调度状态机中 failed 状态进入 retry 的条件是什么?

单选题 · mid

  • A.任务代码被回滚到旧版本
  • B.重试次数未超过配置上限← 正确答案
  • C.上游任务被全部重新执行
  • D.用户手动触发一次重跑

答案:B。自动重试的前提是失败次数未超过最大重试上限,超过后任务进入终态并触发告警。A 是代码管理动作,C 是上游重跑,D 是人工干预,都不是状态机自动转移的依据。

Q7. T(v) = max(T(pred(v))) + t(v) 中取 max 的工程含义是什么?

单选题 · hard

  • A.选择最慢的前驱作为唯一依赖
  • B.必须等待所有前驱中最慢的完成← 正确答案
  • C.与所有前驱同时并行执行计算
  • D.取全部前驱耗时的平均值估算

答案:B。v 必须等所有前驱都完成后才能开工,所以最早开始时间取决于最慢的那个前驱。max 体现了“木桶原理”,也解释了为何单个长尾上游任务会拖慢整条特征流水线。A 错在“唯一依赖”,C 方向反了,D 用均值无法保证依赖满足。

Q8. 某平台成功执行 5 个节点后,剩余 2 个节点未执行且已无就绪节点,这说明什么?

单选题 · hard

  • A.依赖图中存在环导致拓扑排序失败← 正确答案
  • B.剩余节点权重较低需要等待调度
  • C.调度器需要扩容计算资源来提高
  • D.剩余节点正在等待锁资源的释放

答案:A。Kahn 算法正常结束时会执行全部节点。若还有节点未执行却没有入度为 0 的节点,唯一解释是剩余节点构成环,互相等待永远无法归零。B、C、D 都是工程类猜测,但问题出在 DAG 结构本身而非资源或调度策略。

Q9. 特征依赖管理中,血缘追踪(Lineage)能解决哪些问题?

多选题 · mid

  • A.定位特征数据质量问题的根源← 正确答案
  • B.评估上游表结构变更的影响范围← 正确答案
  • C.自动生成特征计算的 SQL 代码
  • D.为合规审计提供特征来源凭证← 正确答案

答案:ABD。血缘记录“表 → 特征 → 模型”的完整路径,A 可借助路径定位脏数据起点,B 是影响分析的直接应用,D 是金融审计的典型场景。C 错误:血缘只描述依赖关系和来源,不负责生成计算逻辑,不能保证生成代码语义正确。

Q10. 关于特征生命周期与调度一致性,下列说法正确的有哪些?

多选题 · hard

  • A.特征任务重试时必须保证幂等性← 正确答案
  • B.同一批次节点必然全部互不依赖← 正确答案
  • C.特征版本变更后旧版本必须立即删除
  • D.下游任务需避免读到上游的中间态数据← 正确答案

答案:ABD。A 正确,幂等才能安全重试;B 正确,Kahn 算法把同轮入度为 0 的节点归为同一批,它们互相无依赖;D 正确,需要原子提交或可见性控制避免脏读。C 错误,版本管理应保留旧版本用于回滚,而不是立即删除。