Rule Status 字段的设计取舍(2022)
2022 年 7 月,入职某防火墙自动化团队约一个月后,接到第一个独立功能需求:在 UI 上显示每条 Rule 的当前状态。 Rule 是用户在系统里下发的防火墙策略。每条策略在系统里会经历一系列流转节点:submit、approve、dryrun、commit、rollback、archive……这些节点对应不同的业务操作,最终需要在界面上展示"这条 Rule 现在处于哪个阶段"。 这是一个看起来不大的需求,但它要求先回答一个设计问题:Rule 的状态存在哪里? 两种方案 方案一:SQL 聚合 不新增字段。系统里本来就有 rule_dryrun、rule_commit 等操作记录表,查询当前状态时通过 JOIN 这些表、判断哪些记录存在来推算出状态。 改动量最小,现有表结构不动,在查询层加逻辑就够了。 方案二:状态归属 在 rules 表增加 status 字段,每次业务状态发生变更时在对应模块里写入新状态。Rule 的状态就存在 Rule 这里,查询时直接读取。 初期需要找到所有状态变更的节点,在分散在多个模块的代码里补齐“更新 Rule 状态”的逻辑,改动范围比方案一大。 讨论 两种方案都在组内讨论过。支持方案一的理由有几个:改动文件少,风险低;项目当时还不确定做多久,不必做太完善;另外我是新入职,对 Python 代码库不熟悉,方案一可以减少上手压力。 但是…我更倾向方案二,核心判断是:Rule 的状态是 Rule 本身的属性,有独立的业务名称和语义,它应该和 Rule 在一起,而不是分散在各操作记录表里通过 JOIN 聚合出来。方案一把复杂度延后了——每增加一个新的查询场景,都要重新写一次聚合逻辑;状态维度越多,维护代价越高。 讨论中有一个具体的质疑:如果用户对一条 Rule 操作三次,rules 表就要被修改三次,会不会太频繁?我的回应是:操作了三次,状态就变了三次,写入三次和业务行为是完全对应的,代码逻辑和业务逻辑是一致的。 讨论下来,同事们接受了这个方向。其中一位熟悉代码库的同事专门指导我:Rule 的状态会在哪些业务节点发生变化,应该在哪些地方插入状态更新代码。这个指导让我能在不熟悉代码库的情况下快速定位到所有需要修改的地方。 执行 功能在一个 sprint(约一个月)内完成开发、测试、上线。改动涉及多个模块,但每个模块的改动量不大,主要工作是把状态抽象出来、并在正确的节点写入。 上线前还准备了一份 migration.sql,对存量的历史 Rule 记录做 backfill——需要根据这些 Rule 在各操作表里的记录,推算出它们当前应该处于哪个状态。这份 SQL 写起来需要复杂的跨表 JOIN,刚好印证了方案一如果长期维护会是什么样子。 回看 这个字段上线后,后续三年里所有 Rule 状态机相关的功能——新的流转流程、状态过滤、统计报表、列表查询里的复杂过滤条件——大多都直接依赖这个 status 字段,没有因为最初的设计需要绕路。后来又陆续引入了更多流程,让 Rule 在更多角色之间流转;这些变化本质上都是状态转移,没有对应的操作历史记录。如果当初用聚合方案,这些新状态根本没有操作记录可以聚合,届时无论如何都要补加 status 字段,区别只是改动量更大、时机更被动。 ...