注意: 虽然 JavaScript 对于本网站不是必需的,但您与内容的互动将会受限。请开启 JavaScript 以获得完整体验。

构建 Python 代码库的依赖关系图

引言

秉承我们高频交易的根基,Hudson River Trading (HRT) 行动迅速。正如工程中的任何指标一样,速度也有其权衡。在过去五年中,由于一种偏爱“足够好”而不是“完美”的草根工程文化、鼓励团队之间代码共享的协作工作环境,以及一段加速增长时期,HRT 看到其研究导向的 Python 代码库的规模和互联性呈指数级增长。随着我们的 Python 代码库增长到数百万行,导入时间增加了一个数量级,代码更改的测试成本变得更高,并且 lint 时间远远超出了实用范围 - 我们正在经历代码“纠缠”的影响。

纠缠

代码“纠缠”是 HRT 员工从 Dropbox 发布的关于他们自己 Python 代码库的相同问题的描述中借用的概念。当代码的依赖关系图具有许多重叠的循环,并且代码库中不相关的部分通过间接和不直观的导入路径耦合在一起时,我们称之为代码“纠缠”。纠缠在任何大型代码库中都可能是一个问题(包括其他语言)!

根据我们的经验,纠缠会影响运行时导入和静态分析(例如 mypy)的性能,并导致紧密的 耦合,这会降低可靠性。在这些问题中,我们的用户认为运行时导入开销是最大的问题,因为它减慢了开发迭代循环并浪费了数据中心的 CPU 时间。对于 HRT 来说,这可能比大多数其他 Python 商店更成问题,因为短期 Python 进程占我们计算工作量的很大一部分。

纠缠的负面影响会迅速增加——一些错误的导入,突然间数百个模块耦合在一起。导入开销的影响会因纠缠而放大,因为导入循环中的任何模块最终都会传递性地导入该循环中的所有模块(及其依赖项)。

虽然某些导入速度非常快,但在许多情况下会产生很大的开销。开销偷偷引入的一种常见方式是通过文件系统访问——例如,现在已弃用的 pkg_resources 模块会爬取文件系统以查找资源。当在我们的网络文件系统上运行时,此过程尤其成问题。计算开销的另一个来源是 pandas 和 NumPy 等软件包加载的大量单体 C 扩展——甚至是专有扩展。此外,我们的一些纯 Python 模块会产生一系列昂贵的静态初始化步骤,例如检测环境特征或处理类或回调的动态注册。

单独来说,这些中的每一个都会引入可管理的导入工作负载;但是,在我们代码库中最纠缠的部分中,复合效应会导致大多数程序导入时间超过 30 秒。这种开销减慢了开发迭代循环,并浪费了我们分布式计算环境中的 CPU 时间。

依赖关系管理

在高层次上,我们解开纠缠的方法是建立并维护一个分层架构,其中较低层中的模块不从较高层中的模块导入。建立适当的分层结构有助于调用者只导入他们需要的内容。

理想情况下,我们的依赖关系图应该类似于有向无环图,其中模块按其分配的层进行拓扑排序。然而,在实践中,只要一些循环相对较小并且包含在一个(子)包中,那么这些循环是可以接受的。

过渡到更好的依赖关系管理范例需要识别当前纠缠的原因、重构代码库以重组依赖关系,并实施依赖关系验证以避免未来的回归。而所有这些工作都必须在不暂停代码库开发的情况下完成!

纠缠工具:理解纠缠

一旦我们理解了纠缠是许多开发者体验问题的根本原因,我们便开始构建一个工具包,用于分析我们代码库的依赖关系图——纠缠工具。纠缠工具分析 Python 源代码以生成整个代码库的依赖关系图(节点对应于模块,边对应于导入)。然后,我们的用户可以利用命令行和浏览器界面来发现、导航和重构依赖关系。

典型的解缠工作流程包括

  • 找到不需要的传递依赖关系

  • 从源追踪到不需要的依赖关系的导入路径

  • 计算源和依赖关系之间的流网络

  • 识别删除哪些边会减少流量

  • 重构导入以断开源与依赖关系的连接

  • 利用代码转换来自动化常见的重构(例如,将符号移动到新模块并更新现有引用)

  • 实施依赖关系验证以避免回归

  • 用户编写依赖关系规则以约束其模块的依赖关系 

  • 这些规则在我们的持续集成管道中进行检查

Rendered Summary Graph

如果没有广泛使用开源库,这一切都不可能实现!我们使用 Python 内置的 ast 库来解析我们的 Python 源代码中的导入。这项解析工作通过 stdlib 强大的 concurrent.futures 模块进行并行化,使我们能够快速处理数千个模块。在底层,我们使用 networkx 的有向图数据结构和广泛的图算法库——我们发现流算法特别有用。最后,我们使用 libcst 库执行自动源代码重构,将其编写为对具体语法树的转换。

结论

通过开发这些依赖关系管理和重构工作流程,我们能够在解开纠缠方面取得重大进展。以前,查找导入依赖关系是一个缓慢的手动过程,而重构依赖关系就像一场打地鼠游戏。现在,我们可以导航我们的完整依赖关系图,并找到有效的重构方法来解决纠缠的根本原因。

认识作者

George Farcasiu - 核心开发者

  • George Farcasiu 参与了 HRT Python 生态系统中的各种项目,包括创建 Python 静态分析和依赖关系管理工具、为分布式计算框架和环境做出贡献,以及维护构建/测试/持续集成开发人员工具。

Noah Kim - 核心开发者

  • Noah Kim 主要关注 HRT 对 CPython 解释器的使用。他还是纠缠工具的当前维护者,该工具是改进公司 Python 包生态系统的更广泛工作的一部分。

Jacob Brugh - 核心开发者

  • Jacob Brugh 最近的关注领域包括改进 HRT 的 C++ 交易库的 Python 绑定代码的性能,并开发内部工具,使我们能够进行大规模的高效静态分析。

Jiahao Li - 核心开发者

  • Jiahao Li 从事涉及分布式计算集群和构建/测试平台的各种项目。最近的项目包括全面改进 HRT 的测试环境,以提供依赖关系跟踪和更智能的测试选择。

本文 最初发表在 HRT Beat 上。