在拥有数十万工程师和数十亿行代码的开发环境中,代码不仅仅是资产,同样也是持续的成本来源。冗余代码、无人使用的工具、过时的库和长期未触发的测试,会占用持续集成资源、增加维护复杂性,并模糊工程师对现有系统的认知边界。Sensenmann 项目以"自动化删除死代码"为目标,在谷歌规模的单体仓库中实现了高频率、安全性的删除流程,为降低整体维护负担提供了可量化的回报。 为什么需要自动化删除代码 任何长期运行的软件工程组织都会积累大量"死代码"。有些模块从未再被链接到任何可执行产物;有些二进制只在早期迁移或一次性任务时使用;有些测试仅为历史原因保留但并不反映当前生产需求。手工清理需要大量人员成本,而且常常因优先级低被搁置。
但这些代码并非无代价:它们占用版本控制存储、让构建与测试系统执行不必要的构建、并在代码搜索、依赖迁移时制造噪声。Sensenmann 的出发点是:当规模达到一定程度时,花工程时间自动化清理,比长期人工维护更划算。 核心技术:依赖图与存活性判定 实施大规模删除的关键在于准确判断哪些目标可以安全移除。谷歌的构建系统(Blaze,开源对应为 Bazel)把目标、库、测试和二进制之间的依赖关系结构化为依赖图。通过构建这张图,可以识别出不被任何可执行产物链接的库,从而得到删除候选。 但是目标不仅仅是静态的库。
很多一次性程序、迁移工具或诊断二进制可能并不会成为其他程序的依赖,而它们的存在会把各自依赖的库"挽留"在仓库中。要判断这些程序是否仍有用,Sensenmann 引入了运行日志作为"存活性信号"。在受控环境下,每次某个内部二进制运行时都会记录一条日志,包含时间戳和对应的二进制标识。通过聚合这些日志,可以为每个二进制构建时间序列:长时间未被调用的二进制就可能是删除候选。 测试带来的挑战与强连通分量的解决方案 测试体系带来了特殊问题。持续集成会触发大量测试用例,测试目标的构建或执行不应被误认为目标本身在生产中"被使用"。
如果仅以测试执行作为存活性判定标准,许多不再用于生产的库会因其测试而永远无法被删除。为了克服这一点,Sensenmann 采用了将库与其测试视为"同生共死"的策略,通过在依赖图上构造环路,把库和测试视为强连通分量。使用 Tarjan 算法可以高效地识别这些强连通分量,并将存活性从运行的二进制向上传播 - - 任何被真实运行的二进制所依赖的组件都被标记为存活,与之互相依赖的测试也随之保留。 然而实际情况并不总是简单的一对一关系。一个测试可能同时验证两个库(例如压缩与解压库),而另一个测试可能仅依赖某些支持库但并非针对它们进行断言。结构上看这些场景可能相同,却需要不同处理。
Sensenmann 在实践中采用了若干启发式方法:鼓励工程师将某些"仅用于测试"的库作标注;使用测试与库名称之间的编辑距离等名称匹配策略;并计划将测试覆盖率数据引入,以便确定测试实际上在验证哪些代码路径,从而更精确地识别哪些库与测试应视为一个整体。 例外管理与安全开关 自动删除必须伴随细致的例外管理机制。不是所有无法观测到运行的代码都应被删除:示例代码、模板工程、仅在特殊环境下运行的二进制、或因合规与审计要求保留的实现,都需要被排除在删除名单之外。为此,Sensenmann 设计了块名单(blocklist)机制,允许有权限的团队将特定路径或目标标注为不参与自动删除流程。块名单同样允许按项目、语言或目录分层管理,确保在全自动化推动下保有必要的人为判断空间。 变更提交流程与社会化沟通 自动删除系统的技术实现只是成功的一半;另一半是工程组织的接受度。
自动化的删除请求以"批量变更(changelist)"的形式提交到代码评审流程,每周上千份变更意味着必须慎重考虑如何呈现每一次删除。高质量的变更描述是关键,需要在简洁之余提供足够背景信息,包括删除依据、时间窗口、如何回滚、相关文档链接与常见问答。配套文档需要清晰说明删除的安全理由、回退流程、以及如果发现误删应如何处理。 面对用户反馈,需要设定明确的反馈渠道与响应 SLA。工程师对自动删除的抵触常来自对代码丧失控制或潜在责任的担忧,通过透明的日志、可快速回滚的变更、以及对"误删后果最小化"的技术保障,可以显著降低阻力。Sensenmann 在运营中保留了手动审核入口和暂停机制,允许相关代码所有者或团队阻止特定变更,或在变更合并前进行补充验证。
运行时保障、度量与回报评估 任何自动删除系统都必须有完善的监控与指标来评估效果与风险。关键指标包括每周/每月提交的删除变更数、被接受与被拒绝的比例、实际删除的代码行数与文件数量、对构建时间或测试负载的影响、以及因误删导致的回滚次数和平均回滚时间。谷歌的 Sensenmann 统计显示,每周提交超过一千份删除变更,已经移除接近 5% 的 C++ 代码。这样的量级在大型单体仓库环境下,能显著降低长期维护支出,理论上的回报率远超了实现与运营成本。 在运行时还需处理误判与边缘案例。为最小化影响,删除流程可采用分阶段策略:先提交变更但不立刻合并,等待短期观察期;合并后在受控 rollout 环境中逐步触发;或者将删除与自动化测试、静态分析结合,要求删除变更必须通过一组精心设计的守护测试集才能合并。
向其他组织的可行性建议 并非所有组织都适合直接复制 Sensenmann 的做法。要实施类似能力,需先具备几项基础设施与文化条件:统一或中心化的源码仓库(单体仓库或高一致性的多仓库策略)、能够获取运行信息的日志系统与标识化机制、结构化的构建系统以生成精确的依赖图、以及成熟的 CI/CD 与代码评审流程。 在技术实践上,可以从小范围开始试点。先在某种语言或某个子模块内构建删除候选流程,验证依赖图的准确性、运行日志的覆盖率以及变更被接受的频率。逐步引入强连通分量识别和测试覆盖数据,以减少误删风险。与此同时,建立易于使用的"回退"与"阻止"渠道,保证当删除影响到真实生产或关键团队时可以迅速恢复。
治理与激励机制也很重要。清晰的代码拥有权、删除前的通知与沟通惯例、以及对积极配合清理的团队给予认可或奖励,能促进更广泛的接受。展示可量化的节省(例如减少的构建时间、降低的测试成本、以及维护工时节省)有助于让管理层与工程师看到长期回报。 未来改进方向 Sensenmann 的实践表明,自动化删除仍有大量技术可优化空间。引入测试覆盖信息能显著提升对"测试到底在验证什么"的理解,从而在复杂测试场景中做出更精确的决定。机器学习模型可以辅助识别"样板测试"与"实质性测试"之间的差别,或者通过历史变更学习哪些删除候选最容易被接受。
扩展到更多语言和构建系统需要跨团队标准化元数据,例如标注"仅示例代码"、"仅用于测试"或"运行环境受限"等属性。 结语:代码少即是福吗 代码的价值并非完全由行数衡量。长期来看,冗余代码会增加认知负担与维护成本。Sensenmann 提供了一个可复制的思路:用依赖图与运行信号识别删除候选,通过强连通分量处理测试引入的复杂性,并用周到的沟通与治理把技术能力转化为组织行为。对拥有大型单体仓库或复杂依赖关系的组织而言,自动化删除不仅能释放资源,更能成为提升工程效率与代码健康度的重要手段。随着监测能力和覆盖数据的完善,自动化删除的精度和接受度只会持续提高,最终把"减少不必要的代码"变为常态化工程实践。
。