Это не образовательный пример, придуманный по такому случаю. Каждый приведенный ниже фрагмент кода взят из корня Aurabase Cargo.toml и его манифестов крейта в том виде, в котором они существуют в репозитории на сегодняшний день, включая два несоответствия, которые мы обнаружили, перечитывая их для этой статьи, и которые мы документируем как есть, а не исправляем их потихоньку перед публикацией.
- Корневая рабочая область Cargo Aurabase объявляет 18 явных членов: 11 служб,
aura-cliCLI, 5 общих библиотек иaurabase-rsSDK — под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/*.
resolver = "2" не является косметической деталью. Резолвер v2 от Cargo изолирует функциональность build-dependencies и целевые зависимости (например,target.'cfg(windows)') от остальной части графа — они больше не просачиваются в окончательный двоичный файл. Он также унифицирует версии одной и той же зависимости для всех членов рабочей области, которые ее разделяют: одно axumтолько один раз, а не одиннадцать независимых разрешений.
workspace.package: одна версия, одна редакция, в принципе общая
[workspace.package] однажды объявляет общие поля — версию, издание, авторов, лицензию — которые каждый контейнер может наследовать с помощью 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 } вместо установки собственного ограничения версии.
Это второе реальное несоответствие: рабочая область централизует 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].
Сам манифест документирует границу между ними. 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 документирует все доступные ключи в своей ссылке на профиль компиляции ; Аурабаза активирует только пять.
Выбор panic = "unwind" вместо "abort" документирован непосредственно в комментарии к файлу: паника в обработчике axum перехватывается tokio, задача возвращает 500, и процесс продолжает обслуживать другие одновременные запросы. Прирост производительностиabort не стоит потери изоляции между запросами.
Заморозьте цепочку инструментов и важные команды
Единая рабочая область устанавливает версии зависимостей, но не версию самого компилятора. rust-toolchain.tomlв корне замораживает цепочку инструментов (channel = "1.93" сегодня) для любого прямого вызова cargo или rustc в репозитории.
Этот файл существует именно потому, что произошел тихий дрейф: действие CI установило канал дня stable без чтения rust-toolchain.toml, в то время как выпускной образ Docker скомпилировался с замороженной версией. Коммит может пройти все тесты с сегодняшней стабильной версией Rust, а затем сломаться при сборке образа — это будет обнаружено после слияния, а не раньше. rustupучитывает этот файл для любой прямой команды: это было единственное необходимое исправление, не влияющее на рабочие процессы CI.
И последняя деталь для тех, кто читает манифест перед его выполнением: крейт aura-cli компилирует двоичный файл, таблица которого [[bin]] называет его aurabase, а не aura. Опубликованная оболочка npm, @aurabase/cli, предоставляет две команды — aura и aurabase, которые указывают на один и тот же скрипт. Имя [[bin]] Cargo, имя ящика и имя, предоставляемое оболочкой npm, — это три разные вещи; ни одно из двух других нельзя угадать.
Контрольный список: где что декларировать
Каждый раз, когда вы добавляете ящик в рабочую область Cargo, появляется пять решений. Вот где каждый из них объявлен на основе примера Aurabase, рассмотренного выше.
| Список участников | [рабочее пространство] участники | Корень — явный список, а не glob |
|---|---|---|
| Общая версия/издание | [рабочая область.пакет] | Корень — version.workspace = true, по ящику, необязательно. |
| Зависимость, общая для 2+ ящиков | [рабочая область.зависимости] | Корень — затем {workspace = true} в каждом ящике |
| Зависимость от одного потребителя | [зависимости] ящика | Локально, не проходя через рабочую область |
| Создать профиль | [профиль.выпуск] | Только root — применяется ко всем участникам |
Для аудита существующего рабочего пространства Cargo — вашего или проекта, который вы берете на себя — достаточно четырех проверок, чтобы обнаружить несоответствия, описанные выше:
- Сравните
[workspace.package].versionсversionкаждого ящика — другое значение не обязательно является ошибкой, но его стоит задокументировать. - Сравните
[workspace.dependencies]с зависимостями, объявленными локально каждым контейнером, — найдите имена, присутствующие на обеих сторонах разных версий. - Сравните список
membersв корнеCargo.tomlс реальными подпапками в репозитории — отсутствие папки вmembersне обязательно является ошибкой. - Проверьте фактическое имя каждого скомпилированного двоичного файла (
[[bin]] name), а не предполагайте, что оно соответствует имени крейта или имени, предоставленному возможной оболочкой npm.