PRODSuwerenna europejska platforma BaaSOtwórz Panel →

Inżynieria · 10 min odczytu

Obszary robocze cargo dla wielousługowego backendu Rust

Affane Daylami · Fondateur · 12 sierpnia 2026

Powrót do bloga

Wielousługowy backend w Rust szybko rodzi to samo pytanie: jeden magazyn na usługę czy pojedynczy obszar roboczy Cargo? Aurabase zdecydował się na drugą opcję. Jedenaście usług, interfejs CLI i pięć bibliotek współdzielonych znajduje się w jednym katalogu głównym Cargo.toml, z jednym Cargo.lock dla wszystkich. Oto jak zbudowany jest ten obszar roboczy, czytając go linia po linii.

Ten tekst w języku angielskim został wygenerowany automatycznie na podstawie francuskiego oryginału i nie był jeszcze recenzowany.
Ta strona została przetłumaczona automatycznie. Wersja angielska jest miarodajna.

To nie jest przykład edukacyjny wymyślony na tę okazję. Każdy poniższy fragment kodu pochodzi z katalogu głównego Aurabase Cargo.toml i jego manifestów skrzynek, jakie istnieją obecnie w repozytorium — w tym dwie rozbieżności, które znaleźliśmy podczas ponownego czytania ich na potrzeby tego artykułu i które dokumentujemy w obecnym stanie, zamiast po cichu je poprawiać przed publikacją.

Najważniejsze
  • Główny obszar roboczy Cargo Aurabase deklaruje 18 jawnych elementów członkowskich: 11 usług, aura-cliCLI, 5 współdzielonych bibliotek i aurabase-rs SDK — pod resolver = "2" i pojedynczym Cargo.lock.
  • [workspace.dependencies] centralizuje udostępnione wersje; każda skrzynka dziedziczy z { workspace = true } zamiast ustawiać własny numer — z wyjątkiem sytuacji, gdy skrzynka nadpisuje się po cichu.
  • Folder pod services/ nie jest automatycznie członkiem obszaru roboczego: lista members jest jawna, a nie glob, właśnie po to, aby móc wykluczyć kod inny niż Rust.
  • [profile.release] ma zastosowanie tylko raz do całego obszaru roboczego — wybór taki jak panic = "unwind" ma wówczas zastosowanie do wszystkich jedenastu usług jednocześnie.
#
Dlaczego miejsce do pracy

Jeden magazyn na usługę czy tylko jeden Cargo.lock?

Obszar roboczy Cargo grupuje wiele skrzyń w jednym Cargo.lock i jednym katalogu docelowym — jest to właśnie funkcjonalność, którą menedżer pakietów Rusta zapewnia w tym przypadku. Jedenaście oddzielnych plików binarnych Rusta, każdy w swoim własnym repozytorium, na pierwszy rzut oka wydaje się bardziej niezależnych. W praktyce oznacza to jedenaście różnych Cargo.lock, jedenaście rozdzielczości wersji, które mogą się różnić w czasie i brak gwarancji, że dwie usługi skompilują tę samą wersjęaxum lub sqlx.

Obszar roboczy Cargo rozwiązuje ten problem na poziomie menedżera pakietów, a nie na poziomie dyscypliny zespołu. Wszyscy członkowie współdzielą pojedynczy Cargo.lock w katalogu głównym: wspólna zależność jest rozwiązywana jednorazowo do identycznej wersji dla całego wykresu. To także sprawia, że ​​refaktor międzyusługowy (na przykład zmiana podpisu w aura-core) jest widoczny dla pojedynczego cargo build --workspace, a nie odkrywany usługa po usłudze w środowisku produkcyjnym. Jest to jedna z opcji, która odróżnia nasz w 100% zunifikowany rdzeń Rust od heterogenicznej usługi złożonej w stosie według usługi.

#
Anatomia

Korzeń Cargo.toml: narzędzie rozpoznawania nazw i elementy członkowskie

Wszystko zaczyna się od deklaracji [workspace] i jej listy members. W Aurabase ta lista jest pisana odręcznie i pogrupowana według ról — usług, narzędzi, bibliotek — a nie generowana według ogólnego wzorca, takiego jak services/*.

Cargo.tomltoml
[workspace]
resolver = "2"

members = [
    # Usługi
    "services/aura-gateway",
    "services/aura-auth",
    "services/aura-db",
    # … 8 innych usług (aura-realtime, aura-storage, aura-ai, …)
    # Narzędzia
    "aura-cli",
    # Lib
    "libs/aura-core",
    # …4 inne biblioteki
    "aurabase-rs",
]

resolver = "2" to nie kosmetyczny szczegół. Narzędzie rozpoznawania nazw v2 firmy Cargo izoluje funkcjonalność build-dependencies i zależności specyficznych dla celu (na przykładtarget.'cfg(windows)') od reszty wykresu — nie wyciekają one już do końcowego pliku binarnego. Ujednolica również wersje tej samej zależności wśród wszystkich członków obszaru roboczego, którzy ją współdzielą: pojedyncze axumtylko raz, a nie jedenaście niezależnych rozwiązań.

#
Dziedzictwo

workspace.package: jedna wersja, jedno wydanie, w zasadzie wspólne

[workspace.package] raz zadeklaruje wspólne pola — wersję, wydanie, autorów, licencję — które każda skrzynka może odziedziczyć za pomocą version.workspace = true zamiast ich kopiowania.

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

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

Wszystkie jedenaście usług Aurabase opiera się na tym schemacie. Dwie skrzynki różnią się od siebie i jest to pierwsza z dwóch rozbieżności znalezionych podczas przygotowywania tego artykułu: aura-cli deklaruje version = "0.2.0" w wersji papierowej, a biblioteka libs/aura-migrations deklaruje version = "0.1.0" — obie różnią się od 0.1.1 obszaru roboczego.

Co to znaczy

version.workspace = true jest opcjonalne, pole po polu, skrzynka po skrzyni. Nic nie stoi na przeszkodzie, aby skrzynka zachowała własną numerację – dobrowolnie (np. narzędzie opublikowane osobno) lub przez zapomnienie. Audyt obszaru roboczego powinien sprawdzać tę skrzynkę po skrzynce, a nie zakładać dziedziczenie.

#
Deduplikacja

workspace.dependents: źródło prawdy, chyba że skrzynia je omija

[workspace.dependencies] centralizuje zależności współdzielone przez kilka skrzynek. Każda usługa odwołuje się do niej za pomocą { workspace = true } zamiast ustawiać własne ograniczenie wersji.

Cargo.toml (root)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 — NIE dziedziczy zarządcy obszaru roboczego
governor = "0.8"  # wersja lokalna, inna

To jest druga prawdziwa rozbieżność: obszar roboczy centralizuje governor w wersji 0.10, ale aura-gateway ponownie deklaruje własną linię governor = "0.8" zamiast dziedziczyć — bramka stosuje swoje ograniczenie szybkości z gołą skrzynią governor, gdy aura-auth, aura-ai, aura-db i aura-functions przechodzi przez tower_governor w oprogramowaniu pośrednim. Przestrzeń robocza nie zapobiega rozbieżnościom: uwidacznia je jedynie, jeśli zadamy sobie trud porównania.

Jednak nie wszystkie zależności zasługują na centralizację. wasmtime nie pojawia się nigdzie w [workspace.dependencies]: tylko jedna skrzynka, aura-functions, używa go w środowisku wykonawczym WASM, więc pozostaje zadeklarowany lokalnie. Zasada, którą stosujemy: zwiększaj zależność do poziomu obszaru roboczego od momentu współdzielenia go przez dwie lub więcej skrzynek, a nie wcześniej.

#
Wykres wewnętrzny

libs/to Services/: co zależy od czego

Pięć współdzielonych bibliotek (aura-core, aura-crypto, aura-db-adapters, aura-migrations, aura-telemetry) zadeklarowano jako zależności ścieżek w [workspace.dependencies] — aura-core = { path = "libs/aura-core" } — następnie każda usługa wybiera, których faktycznie potrzebuje.

Powstały wykres pozostaje czytelny z jednego pliku do drugiego. aura-gateway zależy tylko od trzech z pięciu wewnętrznych bibliotek — aura-core, aura-crypto i aura-telemetry. Wystarczająco, aby wyznaczyć trasę, podpisać JWT dla wózka bocznego PostgREST i wyeksportować jego ślady, bez dotykania aura-db-adapters ani aura-migrations. aura-provisionerdodaje aura-migrations: odtwarza migrację schematu wynikającą z utworzenia projektu.

Najwęższym przypadkiem jest aura-migrator, dedykowany plik binarny, który wykonuje migracje CI/CD. Wśród wewnętrznych bibliotek Aurabase, jej produkcja [dependencies] zawiera tylko listyaura-migrations — aura-core pojawia się tylko w jej [dev-dependencies]w celach testowych. Dlatego plik binarny dostarczony do produkcji nie zawiera żadnego koduaura-core; istnieje tylko podczas cargo test.

#
Konkretna sprawa

Jedna skrzynia, kilka plików binarnych: podział aury w czasie rzeczywistym

Obszar roboczy nie wymaga tworzenia nowego elementu członkowskiego za każdym razem, gdy potrzebny jest nowy proces do wdrożenia. aura-realtime pozostaje pojedynczym elementem obszaru roboczego, ale jego Cargo.toml deklaruje trzy różne tabele [[bin]] wokół tego samego współdzielonego [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"

Sam manifest dokumentuje granicę między nimi. cdc-worker przechwytuje zmiany Postgres poprzez odpytywanie i publikuje w NATS w rdzeniu pub/sub (Client::publish_with_headers), bez konieczności otwierania serwera WebSocket — JetStream w tej samej usłudze obsługuje wyłącznie KV obecności między instancjami, a nie rozgałęzianie CDC. ws-front zużywa tylko NATS i utrzymuje połączenia WebSocket/SSE, bez dotykania CDC — szczegółowe informacje znajdują się w dokumentacji silnika czasu rzeczywistego. Obydwa pliki binarne korzystają z tego samego kodu filtrującego RLS za pośrednictwem [lib], ale wdrażają i skalują niezależnie w Kubernetes. To właściwy sygnał, aby wybrać kilka plików binarnych w skrzynce zamiast nowego elementu obszaru roboczego: ta sama wewnętrzna logika, różne topologie wdrażania.

#
Pułapka, której należy unikać

Plik w Services/ niekoniecznie jest członkiem Cargo

Folder aura-edge-runtime rzeczywiście istnieje w repozytorium Aurabase. Jednakże nie zawiera żadnego Cargo.toml — tylko TypeScript (index.ts, envelope.ts) i nie pojawia się nigdzie na liście members głównego obszaru roboczego.

Astus

Właśnie dlatego lista members Aurabase jest pisana odręcznie, a nie zastępowana ogólnym wzorcem, takim jak services/*. Glob próbowałby włączyć ten folder inny niż Rust do obszaru roboczego, co skutkowałoby niepowodzeniem w rozpoznawaniu. Jawna lista umożliwia współistnienie folderów, które nie mówią tym samym językiem, pod tym samym elementem nadrzędnym services/.

Lekcja uogólnia: zliczenie usług backendu Rusta poprzez wypisanie podfolderów services/ daje fałszywą liczbę. Tylko katalog główny Cargo.toml jest autorytatywny w odniesieniu do tego, co faktycznie kompiluje się w obszarze roboczym.

#
Kompilacja

[profile.release]: jedno ustawienie dla całego obszaru roboczego

[profile.release] zadeklarowany w katalogu głównym obszaru roboczego dotyczy wszystkich elementów skompilowanych w trybie wydania — tylko jedno miejsce do dostosowania, a nie jedenaście. Cargo dokumentuje wszystkie dostępne klucze w swoim profilu kompilacji , numer referencyjny; Aurabase aktywuje tylko pięć.

Cargo.toml (root)toml
[profile.release]
lto = "thin"          # Skrzynki krzyżowe LTO, równowaga wydajności/czasu kompilacji
codegen-units = 1     # najlepszy ogólny inline
panic = "unwind"   # panika izolowana przez tokio/axum → 500, a nie globalna awaria
strip = "symbols"
opt-level = 3

Wybór panic = "unwind" zamiast "abort" jest udokumentowany bezpośrednio w komentarzu do pliku: tokio przechwytuje panikę w procedurze obsługi axum, zadanie zwraca 500, a proces w dalszym ciągu obsługuje inne współbieżne żądania. Wzrost wydajnościabort nie jest wart utraty izolacji między zapytaniami.

#
W rzeczywistości

Zablokuj łańcuch narzędzi i ważne polecenia

Ujednolicony obszar roboczy ustawia wersje zależności, ale nie wersję samego kompilatora. rust-toolchain.tomlw katalogu głównym blokuje zestaw narzędzi (dzisiajchannel = "1.93") dla dowolnego bezpośredniego wywołania cargo lub rustc w repozytorium.

Ten plik istnieje właśnie dlatego, że nastąpił cichy dryf: akcja CI zainstalowała kanał stable dnia bez odczytania rust-toolchain.toml, podczas gdy obraz Dockera w wydaniu został skompilowany z wersją zamrożoną. Zatwierdzenie mogłoby przejść wszystkie testy z dzisiejszym stabilnym Rustem, a następnie przerwać kompilację obrazu — odkryte po połączeniu, a nie wcześniej. rustupszanuje ten plik w przypadku wszelkich bezpośrednich poleceń: była to jedyna konieczna poprawka, która nie wpływała na przepływy pracy CI.

terminalbash
# Skompiluj cały obszar roboczy
cargo build --workspace

# Przetestuj pojedynczą skrzynkę (nie całą przestrzeń roboczą)
cargo test -p aura-auth

# Ścisłe kłaczki na całym obszarze roboczym, ostrzeżenia = błędy
cargo clippy --workspace --all-targets -- -D warnings

# Formatowanie
cargo fmt --all

Ostatni szczegół dla tych, którzy czytają manifest przed jego wykonaniem: skrzynia aura-cli kompiluje plik binarny, którego tabela [[bin]] nazywa go aurabase, a nie aura. Opublikowane opakowanie npm, @aurabase/cli, udostępnia dwa polecenia — aura i aurabase wskazują na ten sam skrypt. Nazwa ładunku [[bin]], nazwa skrzyni i nazwa widoczna w opakowaniu npm to trzy różne rzeczy; żadnego nie można odgadnąć na podstawie pozostałych dwóch.

#
Podsumowanie

Lista kontrolna: gdzie co zadeklarować

Za każdym razem, gdy dodajesz skrzynię do obszaru roboczego Cargo, pojawia się pięć decyzji. Tutaj jest zadeklarowany każdy z nich, w oparciu o przykład Aurabase omówiony powyżej.

Lista członków[obszar roboczy] członkówRoot — jawna lista, nigdy glob
Udostępniona wersja/wydanie[przestrzeń robocza.pakiet]Root — wersja.workspace = true, według skrzynki, opcjonalnie
Zależność współdzielona przez 2+ skrzynie[obszar roboczy.zależności]Root — następnie {workspace = true } w każdej skrzynce
Uzależnienie od jednego konsumenta[zależności] skrzynkiLokalnie, bez przechodzenia przez obszar roboczy
Zbuduj profil[wydanie.profilu]Tylko root — dotyczy wszystkich członków

Aby przeprowadzić audyt istniejącego obszaru roboczego Cargo – Twojego lub projektu, który przejmujesz – wystarczą cztery kontrole, aby znaleźć rodzaj rozbieżności udokumentowanych powyżej:

  1. Porównaj [workspace.package].version z version każdej skrzynki — inna wartość niekoniecznie oznacza błąd, ale warto ją udokumentować.
  2. Porównaj [workspace.dependencies] z zależnościami zadeklarowanymi lokalnie w każdej skrzynce — znajdź nazwy obecne po obu stronach w różnych wersjach.
  3. Porównaj listę members w katalogu głównym Cargo.toml z rzeczywistymi podfolderami w repozytorium — brak folderu w members niekoniecznie oznacza przeoczenie.
  4. Sprawdź rzeczywistą nazwę każdego skompilowanego pliku binarnego ([[bin]] name), zamiast zakładać, że pasuje do nazwy skrzynki lub nazwy ujawnionej przez możliwe opakowanie npm.

GOTOWY DO WDROŻENIA?

Twój backend w pięć minut.

Karta kredytowa nie jest wymagana · 500 MB za darmo · 50 000 MAU