Search Posts

分类: 项目

Rust开发的项目达到一定的规模时,要如何组织代码以避免危机(上篇)

Rust开发的项目达到一定的规模时,要如何组织代码,特别是package、crate和module?这些概念在项目规模增大的时候尤其重要,甚至影响项目后续的生命力。

本文是来自 参考链接[1]的Rust专家的建议的《上篇》,可以帮助避免常见的陷阱、性能问题或编译问题。

中文文章内容如下:

IC (一个开源的区块链项目)的 Rust 代码库从 2019 年 6 月的空存储库增长到 2022 年初的近 350000 行代码。这种快速增长告诉我,对于相对较小的项目来说,运作良好的决策可能会随着时间的推移开始拖累项目。本文评估了 Rust 代码组织选项,并提出了有效使用它们的方法。

Rust的重要“角色”

Rust 的一些术语容易令人困惑,例如术语crate(中文“单元包”,在下文将保留为crate,不再翻译为 单元包)就不太直观。即使是令人尊敬的 《The Rust Programming Language》一书的第一版也包含以下误导性段落:

Rust 有两个与模块系统相关的不同术语:“crate”和“module”。crate在其他语言中是“库”或“包”的同义词。因此,“Cargo”作为 Rust 包裹管理工具的名称:您将crate 与 Cargo 一起分享给其他人。Crate 可以生成可执行文件或库(.so 文件 或 .dll 等都属于动态库),具体取决于项目。

然而,库和包是不同的概念,不是吗?混淆这些概念会导致挫败感,即使你已经有几个月的 Rust 经验。工具约定也会导致混乱:如果 Rust 包定义了库 crate, cargo 则会自动从包名派生库名称。您可以覆盖此行为,但请不要这样做。

接下来让我们熟悉经常打交道的几个概念。

Rust 的 Module (模块)

Module 模块 是代码组织的单元。它是函数、类型和嵌套模块的容器。模块还指定它们定义或重新导出的名称的可见性。

Rust 的 Crate (单元包)

Crate 是编译和链接的单位。Crates是语言的一部分( crate 是一个关键字),但你在源代码中没有太多提及它们。库和可执行文件是最常见的 crate 类型。

Rust 的 Package (包)

包是软件分发的单位。包不是语言的一部分,而是 Rust 包管理器 Cargo 的工件 。一个 Package 可以包含一个或多个 crate:最多一个库和任意数量的可执行文件。

再论 Modules 与 Crates

当您将大型代码库分解为组件时,有两种极端情况:拥有几个包含大量模块的大包(但包数量不多)或具有大量小包(包拆分后数量较多)。

对于前一种情况,即拥有少量包含大量模块的软件包,具有一些优点:

  1. 添加或删除模块比添加或删除包工作量更少。

  2. 模块更加灵活。例如,同一 crate 中的模块可以形成依赖循环:模块可以使用来自模块的定义,而模块又可以使用来自其他模块如 foo bar foo 的定义。相反,包依赖项关系图必须是非循环的。

  3. 您不必每次重新排列模块时都修改 Cargo.toml 文件。

在 Rust 即时编译的理想世界中,将存储库转换为包含许多模块的庞大包将是最方便的设置。目前痛苦的现实是,Rust 需要相当长的时间来编译,而模块并不能帮助你缩短编译时间:

编译的基本单元是一个crate,而不是一个模块。您必须重新编译 crate 中的所有模块,即使您只更改一个模块。放入crate的代码越多,编译所需的时间就越长。

编译项目时,cargo对不同的crate可以并行编译,而不是在逐个crate编译。所以如果你有几个大包,你就不能充分利用多核CPU的潜力。

这两种拆分方式,是便利性和编译速度之间的权衡。Modules模块很方便,但不能帮助编译器减少工作量。Package包不太方便,但随着代码库的增长,编译速度会更好。

项目代码结构的建议

拆分依赖项中心。

有两种类型的依赖项中心:

  1. 具有大量依赖项的包。例如IC代码库中的两个示例(examples)是包含集成测试辅助代码(proptest策略,模拟和伪造组件实现,帮助程序函数等)的 test-utils replica 包,以及实例化所有组件的包。

  2. 具有大量反向依赖项的包。例如IC代码库中的示例,包含通用类型定义的 types 封装,以及指定元件接口的 interfaces 封装。

IC项目的包依赖关系图的一部分。图中 types 和 interfaces 是二类依赖中心,relica是一类依赖中心,test-utils 既是一类,又是二类依赖中心。

依赖中心是及其关键的,因为它们会对增量编译速度产生重大影响。如果您修改具有许多反向依赖项的软件包(例如图中的 types ),cargo 必须重新编译所有这些依赖项以检查您的更改。

有时可以消除依赖关系中心。例如,包 test-utils 是一些独立实用程序的联合。我们可以按它们所属的测试组件对这些实用程序进行分组,并将对应的实用程序代码分解到多个 -test-utils 包中。

但是,更常见的是,依赖中心将不得不保留。某些 types 类型是普遍存在的。包含这些类型的包注定是二类依赖项中心。连接所有组件的 replica 包注定是一类依赖中心。您能做的最好的事情就是本地化连结并使它们小而稳定。

请考虑使用泛型和关联类型来消除依赖项。

这个建议需要一个例子,所以请耐心等待。

types 、 interfaces 和 replicated_state 是 IC 代码库中的首批封装之一。该 types 包,含有通用类型定义,interfaces包定义软件组件的特征,replicated_state 包定义 IC 的复制状态机数据结构, ReplicatedState 类型位于根目录。

但是为什么我们需要这个 types 包呢?既然Types是接口的一个组成部分,那为什么不在interfaces 包内部定义Types呢?

原因是某些接口引用了该 ReplicatedState 类型。 replicated_state 包依赖于types包中的类型定义。如果所有类型都存在于 interfaces 包中,可能导致 replicated_state 和 interfaces 之间存在循环依赖关系。

如图,types、 interfaces 和 replicated_state 包的依赖关系图。

当我们需要打破循环依赖时,我们可以将公共定义移动到新包中或合并一些包。 replicated_state 包很重;我们不想将其内容合并入interfaces包。因此,我们采用了第一个选项:将不同interface 和replicated_state 包之间共享的类型移动到 types 包中。

interfaces 包的特征定义有个特点:特征仅取决于 ReplicatedState 类型名称。这些特征不需要知道 ReplicatedState 的定义。

trait StateManager {
  fn get_latest_state(&self) -ReplicatedState;

  fn commit_state(&self, state: ReplicatedState, version: Version);
}

这段代码是interfaces包的特征定义的示例,它依赖于ReplicatedState类型。

interfaces 包中有个例子演示了依赖于 ReplicatedState 类型的特征定义。

此属性允许我们打破interfaces 与 replicated_state 之间的 直接依赖关系。我们只需要用泛型类型参数替换确切的类型。

trait StateManager {
  type State; //< We turned a specific type into an associated type.

  fn get_latest_state(&self) -> State;

  fn commit_state(&self, state: State, version: Version);
}

不依赖于 ReplicatedState 的 StateManager的特征定义的通用版本。

基于此,我们不再需要在每次向复制状态添加新字段时重新编译 interfaces 包及其众多依赖项。

运行时多态性是首选。

我们设计的考量之一是如何连接软件组件。我们应该像Arc的方式把组件的实例以运行时多态性传递,还是作为泛型类型参数(编译时多态性)传递 ?

pub struct Consensus {
  artifact_pool: Arc,
  state_manager: Arc,
}

上段代码是使用运行时多态性组合组件。

pub struct Consensus {
  artifact_pool: AP,
  state_manager: SM,
}

上段代码使用编译时多态性组合组件。

编译时多态性是必不可少的工具,更是重量级的工具。运行时多态性需要更少的代码,且有助于更少的二进制膨胀。大多数团队成员也发现该 dyn 版本(即上述第一段代码)更易于阅读。

首选显式依赖项。

新开发人员在开发频道上最常问的问题之一是“为什么我们要显式传递loggers?全局的loggers似乎也能工作得很好”。这是个好问题。如果回到2019年我也会问同样的问题!

全局变量很糟糕,但我以前的经验表明,日志对象loggers和指标接收器(metric sinks)很特殊。哦,好吧,其实也没有那么特殊。

隐式状态依赖的常见问题在 Rust 中尤为突出。

大多数 Rust 库不依赖于真正的全局变量。传递隐式状态的常用方法是使用线程局部变量,当您生成新线程时,这可能会成为问题。新线程倾向于继承并保留线程局部变量的意外值。

默认情况下,Cargo 在测试二进制文件中并行运行测试。如果不小心通过调用堆栈对loggers进行线程处理,测试输出可能会变得无形的混乱。当后台线程需要访问日志时,通常会出现此问题。而通过显式传递loggers的方式则可以消除该问题。

在多线程环境中,对依赖于隐式状态代码的测试很困难甚至不可能。记录指标的代码就是代码。它也值得测试。

如果使用依赖于隐式状态的库,则在依赖于不同包中不兼容的库版本时,可能会引入细微的BUG。

对于这个观点,迫切需要一个例子。这里有一个小合适的故事作为映证:

我们使用普罗米修斯软件包进行指标记录。此包可以将指标注册表保留在全局变量中。

突然有一天,我们遇到了一个错误:我们无法看到某些组件的指标。我们的代码看起来是正确的,但指标却缺失了。

其中一个软件包依赖于普罗米修斯版本 0.9 ,而所有其他软件包都使用 0.10 。根据semver的说法,这些版本是不兼容的,因此cargo将两个版本链接到二进制文件中,引入了两个隐式注册表。我们仅通过 HTTP 接口公开 0.10 版本注册表。正如您正确猜测的那样,缺少的组件将指标记录到注册表中 0.9 。

而传递loggers、指标注册表和异步运行时的方式会显式地将运行时 bug 转换为编译时错误。切换到显式传递指标注册表帮助我找到并修复了该错误。

古老的 slog 包的官方文档还建议明确传递loggers:

原因是:手动传递 Logger 提供了最大的灵活性。使用slog_scope 将日志记录数据结构绑定到堆栈跟踪,这与软件的逻辑结构不同。特别是库应该向用户展示充分的灵活性,而不是使用隐式日志记录行为。

通常 Logger 实例非常适合表示的代码中的资源的数据结构,因此在构造函数中传递它们并在任何地方使用,并不难,像这样:

info!(self.log,
查看余下内容

互联网的商业规律:梅特卡夫效应、网络效应、规模效应

互联网的一个商业模式规律:梅特卡夫效应

什么是梅特卡夫效应?

梅特卡夫效应(the Metcalfe’s Law)是一种用于描述网络效应(network effect)的经济学理论。它的基本思想是,随着网络中参与者的数量的增加,网络的价值会呈现出指数级的增长。
具体地说,梅特卡夫效应指出,一个网络的价值与其用户数的平方成正比。换句话说,如果一个网络有n个用户,那么它的价值就是n²。这是因为一个用户加入网络后,不仅可以享受到其他用户已经存在的服务和资源,还可以为网络增加新的服务和资源,从而提高整个网络的价值。
梅特卡夫效应在现代经济学中被广泛应用于研究网络产业和技术产业的发展趋势和商业模式。例如,在互联网领域,许多公司都利用梅特卡夫效应来促进用户增长和市场份额扩大。


什么是网络效应?

网络效应(network effect),也叫网络外部性(network externality),是指当一个产品或服务的使用者数量增加时,该产品或服务的价值也会相应地增加。简单来说,就是用户数量的增加会带来更多的好处,而这种好处不仅仅是单纯的数量增加,而是价值、效益等方面的提高。
网络效应通常出现在网络、社交媒体、电子商务等领域,其中最典型的例子是互联网。在互联网上,当一个网站、应用或社交网络平台的用户数量增加时,其价值也随之增加,因为更多的用户意味着更多的内容、更多的互动、更多的数据等等,从而吸引更多的用户加入其中,形成良性循环。
网络效应是现代经济学研究的重要领域之一,它对于企业的创新、市场竞争和商业模式的发展具有重要的指导意义。许多知名的公司、产品和服务都依赖于网络效应,如Facebook、谷歌、Uber等。


什么是规模效应?

规模效应(economies of scale)指的是在生产过程中,随着生产规模的增加,单位成本会逐渐降低的现象。简单来说,就是随着产量的增加,单位成本会逐渐减少。
规模效应可以发生在生产任何种类的商品或服务中,包括制造业、零售业、金融业等。实现规模效应的主要方式是通过生产工艺和管理效率的优化,以及原材料、劳动力等资源的更有效利用,从而降低生产成本。
规模效应可以带来多方面的好处,包括提高生产效率、降低成本、扩大市场份额、增强竞争力等。然而,规模效应也有一些限制,如难以管理的规模、生产线的复杂性等,这些因素可能导致成本上升,抵消规模效应带来的好处。
总之,规模效应是企业在生产过程中的一种经济效益,可以帮助企业提高效率、降低成本、扩大市场规模等。


梅特卡夫效应 属于 网络效应和还是规模效应?

梅特卡夫效应属于网络效应(network effect)的范畴,而不是规模效应(economies of scale)。网络效应指的是当一个产品或服务的价值随着使用该产品或服务的用户数量的增加而增加时,就会出现网络效应。而规模效应则是指在生产过程中,随着产量的增加,单位成本会逐渐降低的现象。尽管这两种效应都与数量的增长和价值的变化有关,但它们所描述的现象和效应机制是不同的。… 查看余下内容

加好友请备注:chinaoss
您可以在微信公众号联系我们
我们将24小时内回复。
取消