PRODСуверенная европейская платформа BaaSОткрыть панель управления →

Инженерное дело · 10 минута чтения

Рабочие области Cargo для мультисервисного бэкенда Rust

Affane Daylami · Fondateur · 12 августа 2026 г.

Вернуться в блог

Мультисервисный бэкэнд в Rust быстро вызывает один и тот же вопрос: одно депо на сервис или одно рабочее пространство Cargo? Аурабейс выбрал второй вариант. Одиннадцать сервисов, интерфейс командной строки и пять общих библиотек находятся в одном корне Cargo.toml, с одним Cargo.lock для всех. Вот как устроено это рабочее пространство, читая его построчно.

Этот текст на английском языке был создан автоматически на основе французского оригинала и еще не проверялся.
Эта страница была переведена автоматически. Английская версия является авторитетной.

Это не образовательный пример, придуманный по такому случаю. Каждый приведенный ниже фрагмент кода взят из корня Aurabase Cargo.toml и его манифестов крейта в том виде, в котором они существуют в репозитории на сегодняшний день, включая два несоответствия, которые мы обнаружили, перечитывая их для этой статьи, и которые мы документируем как есть, а не исправляем их потихоньку перед публикацией.

Самое необходимое
  • Корневая рабочая область Cargo Aurabase объявляет 18 явных членов: 11 служб, aura-cliCLI, 5 общих библиотек и aurabase-rs SDK — под resolver = "2" и один Cargo.lock.
  • [workspace.dependencies] централизует общие версии; каждый ящик наследуется с помощью { workspace = true }, а не устанавливает свой собственный номер — за исключением случаев, когда ящик переопределяет молча.
  • Папка под services/ не является автоматически членом рабочей области: список members является явным, а не glob, именно для того, чтобы можно было исключить код, не относящийся к Rust.
  • [profile.release] применяется только один раз ко всей рабочей области — вариант типа panic = "unwind" затем применяется ко всем одиннадцати службам одновременно.
#
Почему рабочее место

Одно депо на услугу или только один Cargo.lock?

Рабочая область Cargo группирует несколько ящиков в одном Cargo.lock и одном целевом каталоге — та самая функциональность, которую для этого случая предоставляет менеджер пакетов Rust. Одиннадцать отдельных двоичных файлов Rust, каждый в своем репозитории, на первый взгляд кажутся более независимыми. На практике это означает одиннадцать различных Cargo.lock, одиннадцать разрешений версий, которые могут различаться со временем, и отсутствие гарантии, что два сервиса скомпилируют одну и ту же версиюaxum или sqlx.

Рабочая область Cargo решает эту проблему на уровне менеджера пакетов, а не на уровне командной дисциплины. Все члены имеют один общий Cargo.lock в корне: общая зависимость разрешается один раз до идентичной версии для всего графа. Это также то, что делает межсервисный рефакторинг (например, изменение подписи в aura-core) видимым для одного cargo build --workspace, а не обнаруживает сервис за сервисом в производстве. Это один из вариантов, который отличает наше на 100% унифицированное ядро ​​Rust от гетерогенного стека , собранного сервисом по сервису.

#
Анатомия

Корень 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" не является косметической деталью. Резолвер v2 от Cargo изолирует функциональность build-dependencies и целевые зависимости (например,target.'cfg(windows)') от остальной части графа — они больше не просачиваются в окончательный двоичный файл. Он также унифицирует версии одной и той же зависимости для всех членов рабочей области, которые ее разделяют: одно axumтолько один раз, а не одиннадцать независимых разрешений.

#
Наследие

workspace.package: одна версия, одна редакция, в принципе общая

[workspace.package] однажды объявляет общие поля — версию, издание, авторов, лицензию — которые каждый контейнер может наследовать с помощью version.workspace = true вместо их копирования.

Cargo.toml (корневой)toml
[workspace.package]
version = "0.1.1"
edition = "2021"

# услуги/аура-шлюз/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.dependies: источник истины, если только крейт не обходит его

[workspace.dependencies] централизует зависимости, общие для нескольких крейтов. Каждая служба обращается к ней с помощью { 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" вместо наследования — шлюз применяет ограничение скорости с помощью голого крейта governor, когда aura-auth, aura-ai, aura-db и aura-functions пройти через tower_governor в промежуточном программном обеспечении. Рабочее пространство не предотвращает расхождения: оно лишь делает их видимыми, если мы берем на себя труд сравнивать.

Однако не все зависимости заслуживают централизации. wasmtime нигде не появляется в [workspace.dependencies]: только один крейт, aura-functions, использует его для своей среды выполнения WASM, поэтому он остается объявленным локально. Правило, которое мы применяем: поднимайте зависимость до уровня рабочей области с момента, когда два или более контейнера совместно используют ее, а не раньше.

#
Внутренний график

libs/to 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. Достаточно маршрутизировать, подписать JWT для сайдкара PostgREST и экспортировать его трассировки, не трогая 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-realtime остается единственным членом рабочей области, но его Cargo.toml объявляет три отдельные таблицы [[bin]] вокруг одного и того же общего [lib].

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 путем опроса и публикует в NATS в ядре pub/sub (Client::publish_with_headers), даже не открывая сервер WebSocket — JetStream в этой же службе обслуживает исключительно KV присутствия между экземплярами, а не разветвление CDC. ws-front использует только NATS и удерживает соединения WebSocket/SSE, даже не затрагивая CDC — полная информация содержится в документации по движку реального времени. Оба двоичных файла используют один и тот же код фильтрации RLS через [lib], но развертываются и масштабируются независимо в Kubernetes. Это правильный сигнал для выбора нескольких двоичных файлов в крейте, а не нового члена рабочей области: та же внутренняя логика, разные топологии развертывания.

#
Ловушка, которую следует избегать

Файл в разделе Services/ не обязательно является членом Cargo.

Папка aura-edge-runtime существует в репозитории Aurabase. Однако он не содержит никаких Cargo.toml — только TypeScript (index.ts, envelope.ts) и не появляется нигде в списке members корневой рабочей области.

Астуце

Именно поэтому список members в Aurabase написан от руки, а не заменен общим шаблоном, таким как services/*. Глоб попытался бы включить эту папку, не относящуюся к Rust, в рабочую область, что привело бы к сбою разрешения. Явный список позволяет сосуществовать папкам, которые не говорят на одном языке, под одним родительским services/.

Урок обобщает: подсчет сервисов бэкэнда Rust путем перечисления подпапок services/ дает ложное число. Только корень Cargo.toml имеет авторитет в отношении того, что на самом деле компилируется в рабочей области.

#
Сборник

[profile.release]: единая настройка для всей рабочей области.

[profile.release], объявленный в корне рабочей области, применяется ко всем членам, скомпилированным в режиме выпуска — для настройки требуется только одно место, а не одиннадцать. Cargo документирует все доступные ключи в своей ссылке на профиль компиляции ; Аурабаза активирует только пять.

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

# Строгая проверка по всему рабочему пространству, предупреждения = ошибки.
cargo clippy --workspace --all-targets -- -D warnings

# Форматирование
cargo fmt --all

И последняя деталь для тех, кто читает манифест перед его выполнением: крейт aura-cli компилирует двоичный файл, таблица которого [[bin]] называет его aurabase, а не aura. Опубликованная оболочка npm, @aurabase/cli, предоставляет две команды — aura и aurabase, которые указывают на один и тот же скрипт. Имя [[bin]] Cargo, имя ящика и имя, предоставляемое оболочкой npm, — это три разные вещи; ни одно из двух других нельзя угадать.

#
Резюме

Контрольный список: где что декларировать

Каждый раз, когда вы добавляете ящик в рабочую область Cargo, появляется пять решений. Вот где каждый из них объявлен на основе примера Aurabase, рассмотренного выше.

Список участников[рабочее пространство] участникиКорень — явный список, а не glob
Общая версия/издание[рабочая область.пакет]Корень — version.workspace = true, по ящику, необязательно.
Зависимость, общая для 2+ ящиков[рабочая область.зависимости]Корень — затем {workspace = true} в каждом ящике
Зависимость от одного потребителя[зависимости] ящикаЛокально, не проходя через рабочую область
Создать профиль[профиль.выпуск]Только root — применяется ко всем участникам

Для аудита существующего рабочего пространства Cargo — вашего или проекта, который вы берете на себя — достаточно четырех проверок, чтобы обнаружить несоответствия, описанные выше:

  1. Сравните [workspace.package].version с version каждого ящика — другое значение не обязательно является ошибкой, но его стоит задокументировать.
  2. Сравните [workspace.dependencies] с зависимостями, объявленными локально каждым контейнером, — найдите имена, присутствующие на обеих сторонах разных версий.
  3. Сравните список members в корне Cargo.toml с реальными подпапками в репозитории — отсутствие папки в members не обязательно является ошибкой.
  4. Проверьте фактическое имя каждого скомпилированного двоичного файла ([[bin]] name), а не предполагайте, что оно соответствует имени крейта или имени, предоставленному возможной оболочкой npm.

ГОТОВЫ К РАЗВЕРТЫВАНИЮ?

Ваш бэкэнд за пять минут.

Кредитная карта не требуется · 500 МБ бесплатно · 50 000 MAU