PROD欧洲主权BaaS平台打开仪表板 →

工程 · 10 最小读取值

多服务 Rust 后端的 Cargo 工作区

Affane Daylami · Fondateur · 2026年8月12日

返回博客

Rust 中的多服务后端很快就提出了同样的问题:每个服务一个仓库,还是单个 Cargo 工作区? Aurabase 决定采用第二种选择。 11 个服务、一个 CLI 和 5 个共享库位于单个 Cargo.toml 根目录中,每个人都有一个 Cargo.lock。以下是这个工作区的构建方式,逐行阅读。

该英文文本是根据法文原文自动生成的,尚未经过审查。
该页面已自动翻译。英文版具有权威性。

这不是为此而发明的教育示例。下面的每个代码片段都来自 Aurabase 根 Cargo.toml 及其板条箱清单,就像它们今天存在于存储库中一样 — 包括我们在为本文重新阅读它们时发现的两个差异,我们按原样记录它们,而不是在发布之前悄悄修复它们。

要点
  • Aurabase 根 Cargo 工作区声明了 18 个显式成员:11 个服务、aura-cliCLI、5 个共享库和 aurabase-rs SDK(位于 resolver = "2" 和单个 Cargo.lock下)。
  • [workspace.dependencies] 集中共享版本;每个板条箱都继承 { workspace = true } 而不是设置自己的编号 - 除非板条箱静默覆盖。
  • services/ 下的文件夹不会自动成为工作区的成员:members 列表是显式的,而不是 glob,正是为了能够排除非 Rust 代码。
  • [profile.release] 仅对整个工作区应用一次 - 像 panic = "unwind" 这样的选择会同时应用于所有 11 个服务。
#
为什么要有工作空间

每项服务一个仓库,还是只有一个 Cargo.lock?

工作区 Cargo 将多个 crate 分组到单个 Cargo.lock 和单个目标目录下 - 这正是 Rust 的包管理器为这种情况提供的功能。十一个独立的 Rust 二进制文件,每个都在自己的存储库中,乍一看似乎更加独立。实际上,这意味着十一个不同的 Cargo.lock,十一个版本分辨率可能会随着时间的推移而发生变化,并且不能保证两个服务编译相同版本的axum 或 sqlx。

A Cargo workspace solves this at the package manager level, not the team discipline level. All members share a single Cargo.lock at the root: a common dependency is resolved once, to an identical version, for the entire graph. This is also what makes a cross-service refactor (changing a signature in aura-core, for example) visible to a single cargo build --workspace, rather than discovered service by service in production. This is one of the choices that distinguishes our 100% unified Rust core from a heterogeneous stack assembled service by service.

#
解剖学

Cargo.toml 根:解析器和成员

这一切都从声明 [workspace] 及其列表 members开始。在 Aurabase,此列表是手写的,按角色(服务、工具、库)分组,而不是由 services/*这样的通用模式生成。

Cargo.tomltoml
[workspace]
resolver = "2"

members = [
    # 服务
    "services/aura-gateway",
    "services/aura-auth",
    "services/aura-db",
    # … 8 个其他服务(aura-realtime、aura-storage、aura-ai,…)
    # 工具
    "aura-cli",
    # 库
    "libs/aura-core",
    # …4 个其他库
    "aurabase-rs",
]

resolver = "2" 不是装饰细节。 Cargo 的 解析器 v2 将 build-dependencies 的功能和特定于目标的依赖项(例如target.'cfg(windows)')与图表的其余部分隔离 - 它们不再泄漏到最终的二进制文件中。它还统一了共享它的工作区所有成员之间相同依赖项的版本:单个 axum,仅一次,而不是十一个独立的解决方案。

#
遗产

workspace.package:一版一版,原则上共享

[workspace.package] 一旦声明了公共字段 - 版本、版本、作者、许可证 - 每个板条箱都可以使用 version.workspace = true 继承而不是复制它们。

Cargo.toml(根)toml
[workspace.package]
version = "0.1.1"
edition = "2021"

# 服务/aura-gateway/Cargo.toml
[package]
name = "aura-gateway"
version.workspace = true

Aurabase 的十一项服务都遵循这种模式。两个板条箱有偏差,这是在准备本文时发现的两个差异中的第一个:aura-cli 在硬拷贝中声明 version = "0.2.0",而库 libs/aura-migrations 声明 version = "0.1.0" — 两者都与工作区的 0.1.1 不同。

这意味着什么

version.workspace = true 是可选的,逐个字段,逐个箱子。没有什么可以阻止板条箱保留自己的编号——自愿(例如,单独发布的工具)或忘记。工作区审核应逐个检查此字段板条箱,而不是假定继承。

#
重复数据删除

工作区.依赖项:事实来源,除非板条箱绕过它

[workspace.dependencies] 集中了多个 crate 共享的依赖项。每个服务都使用 { workspace = true } 引用它,而不是设置自己的版本约束。

Cargo.toml(根)toml
[workspace.dependencies]
axum = { version = "0.8", features = ["ws", "multipart", "macros"] }
sqlx = { version = "0.8", default-features = false, features = […] }
tower_governor = { version = "0.8", features = ["axum"] }
governor = "0.10"

# services/aura-gateway/Cargo.toml — 不继承工作区调控器
governor = "0.8"  # 本地版本不同

这是第二个真正的差异:工作区将 governor 集中在版本 0.10中,但 aura-gateway 重新声明自己的行 governor = "0.8" 而不是继承 - 当 aura-auth、 aura-ai、 aura-db 和 aura-functions 通过时,网关对裸 governor 板条箱应用其速率限制中间件中的 tower_governor。工作区并不能防止分歧:如果我们不厌其烦地进行比较,它只会使分歧变得可见。

然而,并非所有依赖项都应该集中化。 wasmtime 没有出现在 [workspace.dependencies]中的任何位置:只有一个箱子 aura-functions将其用于 WASM 运行时,因此它仍然在本地声明。我们应用的规则:从两个或多个 crate 共享工作空间级别的那一刻起,而不是之前,就提高对工作空间级别的依赖关系。

#
内部图

libs/ 到 services/:这取决于什么

五个共享库(aura-core、 aura-crypto、 aura-db-adapters、 aura-migrations、 aura-telemetry)在 [workspace.dependencies] — aura-core = { path = "libs/aura-core" } 中声明为路径依赖项 — 然后每个服务选择它实际需要的库。

生成的图表从一个文件到另一个文件仍然可读。 aura-gateway 仅依赖于五个内部库中的三个 — aura-core、 aura-crypto 和 aura-telemetry。足以路由、为 PostgREST sidecar 签署 JWT 并导出其跟踪,而无需触及 aura-db-adapters 或 aura-migrations。 aura-provisioner添加 aura-migrations:它重播因创建项目而产生的架构迁移。

最窄的情况是 aura-migrator,它是执行 CI/CD 迁移的专用二进制文件。在 Aurabase 内部库中,其生产 [dependencies] 仅列出aura-migrations — aura-core 仅出现在其 [dev-dependencies]中,用于测试。因此,交付到生产环境的二进制文件不包含任何aura-core 代码;它仅在 cargo test期间存在。

#
具体案例

一个箱子,几个二进制文件:aura-实时除法

每次您需要新的可部署流程时,工作区不需要创建新成员。 aura-realtime 仍然是工作区的单个成员,但其 Cargo.toml 围绕同一共享 [lib]声明了三个不同的 [[bin]] 表。

Cargo.tomltoml
[lib]
name = "aura_realtime"

[[bin]]
name = "aura-realtime"

[[bin]]
name = "aura-realtime-cdc-worker"
path = "src/bin/cdc_worker.rs"

[[bin]]
name = "aura-realtime-ws-front"
path = "src/bin/ws_front.rs"

宣言本身记录了两者之间的界限。 cdc-worker 通过轮询捕获 Postgres 更改,并将其发布到 pub/sub 核心 (Client::publish_with_headers) 中的 NATS ,而无需打开 WebSocket 服务器 — JetStream 在同一服务中专门服务于跨实例存在 KV,而不是 CDC 扇出。 ws-front 仅消耗 NATS 并保持 WebSocket/SSE 连接,而无需接触 CDC - 完整详细信息位于 实时引擎文档中。两个二进制文件通过 [lib]共享相同的 RLS 过滤代码,但在 Kubernetes 中独立部署和扩展。这是在一个包中选择多个二进制文件而不是新的工作区成员的正确信号:相同的内部逻辑,不同的部署拓扑。

#
要避免的陷阱

services/ 下的文件不一定是 Cargo 会员

Aurabase 存储库中确实存在 aura-edge-runtime 文件夹。但是,它不包含任何 Cargo.toml — 仅包含 TypeScript(index.ts、 envelope.ts),并且它不会出现在根工作区的 members 列表中的任何位置。

阿斯图塞

这正是 Aurabase 的 members 列表是手写的而不是被 services/*这样的通用模式替换的原因。 glob 会尝试将这个非 Rust 文件夹包含在工作区中,从而导致解析失败。显式列表允许不同语言的文件夹在同一父级 services/下共存。

该课程概括为:通过列出 services/ 的子文件夹来计算 Rust 后端的服务会得到错误的数字。只有根 Cargo.toml 对工作区中实际编译的内容具有权威性。

#
编译

[profile.release]:整个工作区的单一设置

在工作区根目录声明的 [profile.release] 适用于在发布模式下编译的所有成员 - 只需调整一个地方,而不是十一个。 Cargo 在其 编译配置文件参考中记录了所有可用的键; Aurabase 仅激活五个。

Cargo.toml(根)toml
[profile.release]
lto = "thin"          # LTO 交叉板条箱、性能/构建时间平衡
codegen-units = 1     # 最佳整体内联
panic = "unwind"   # 由 tokio/axum → 500 隔离的恐慌,而不是全球崩溃
strip = "symbols"
opt-level = 3

panic = "unwind" 而不是 "abort" 的选择直接记录在文件注释中:axum 处理程序中的恐慌被 tokio 拦截,任务返回 500,并且该进程继续为其他并发请求提供服务。abort 的性能提升抵不上查询间隔离的损失。

#
在实践中

冻结工具链和重要的命令

统一的工作空间设置依赖项版本,但不设置编译器本身的版本。 rust-toolchain.toml在根处冻结工具链(现在是channel = "1.93" ),以便直接调用存储库中的 cargo 或 rustc 。

该文件的存在正是因为发生了无声漂移:CI 操作安装了当天的 stable 频道,但没有读取 rust-toolchain.toml,而发布的 Docker 镜像则使用冻结版本进行编译。一次提交可以通过当今稳定的 Rust 的所有测试,然后在镜像构建时中断——在合并之后而不是之前发现。 rustup尊重此文件的任何直接命令:这是唯一必要的修复,不会影响 CI 工作流程。

terminalbash
# 编译整个工作区
cargo build --workspace

# 测试单个板条箱(不是整个工作区)
cargo test -p aura-auth

# 对整个工作区进行严格的 lint,警告 = 错误
cargo clippy --workspace --all-targets -- -D warnings

# 格式化
cargo fmt --all

对于那些在执行之前阅读清单的人来说,最后一个细节是: aura-cli 板条箱编译一个二进制文件,其表 [[bin]] 将其命名为 aurabase,而不是 aura。已发布的 npm 包装器 @aurabase/cli公开了两个命令 — aura 和 aurabase 都指向同一脚本。 [[bin]] Cargo 的名称、板条箱的名称以及 npm 包装器公开的名称是三个独立的东西;从另外两个中无法猜出任何一个。

#
回顾

清单:在哪里申报什么

每次将 crate 添加到 Cargo 工作区时,都会出现五个决定。以下是基于上面介绍的 Aurabase 示例声明每个内容的位置。

成员名单[工作空间] 成员Root — 显式列表,绝不是 glob
共享版本/版本[工作空间.包]Root — version.workspace = true,通过 crate,可选
由 2 个以上 crate 共享的依赖项[工作空间.依赖项]Root — 然后在每个 crate 中使用 {workspace = true }
对单一消费者的依赖[依赖项] 板条箱在本地,无需经过工作区
建立档案[简介.发布]仅限 Root — 适用于所有成员

要审核现有的 Cargo 工作区(您的或您正在接管的项目的工作区),四项检查足以发现上面记录的差异:

  1. 将 [workspace.package].version 与每个板条箱的 version 进行比较 - 不同的值不一定是错误,但值得记录。
  2. 将 [workspace.dependencies] 与每个板条箱本地声明的依赖项进行比较 - 发现两侧都有不同版本的名称。
  3. 将根 Cargo.toml 中的 members 列表与存储库中的实际子文件夹进行比较 - members 中缺少的文件夹不一定是疏忽。
  4. 检查每个已编译二进制文件的实际名称 ([[bin]] name),而不是假设它与包名称或可能的 npm 包装器公开的名称匹配。

准备好部署了吗?

五分钟内完成您的后端。

无需信用卡 · 500 MB 免费 · 50,000 MAU