Dobrá zpráva o nasazování Rustu je, že výsledkem je jeden binární soubor bez runtime, který není potřeba nic instalovat, a ten s klidem obslouží vytížený web ve dvaceti megabajtech paměti. Horší zpráva je, že drahé je právě vyrobit ten binár, a to přesně ve zdroji, kterého má malý plán nejméně. Release build skromné webové služby chce gigabajt nebo dva RAM a několik minut CPU; hotový program pak v klidu spotřebuje zlomek toho, co srovnatelná služba v Pythonu nebo Node.
Celá otázka nasazení Rustu tedy zní: kde se build provede a co potřebuje běžící proces potom. Tento článek odpovídá na obojí a k tomu probírá nastavení, která mění dobu buildu a velikost binárky, správný bind uvnitř kontejneru, kolik worker vláken tokia ve skutečnosti chceš na zlomku jádra, a co se tak obvykle rozbije.
Co Rust služba potřebuje za běhu#
Skoro nic. Překladač slinkuje tvůj kód a závislosti do jediného spustitelného souboru. Na serveru není interpret, virtuální prostředí, node_modules ani správce balíčků. Binár ale přesto potřebuje:
- C knihovnu, proti které byl slinkován. Výchozí cíl
x86_64-unknown-linux-gnulinkuje dynamicky proti glibc, takže binár vyžaduje glibc alespoň tak novou, jako byla ta při buildu. Právě to způsobuje chybuversion 'GLIBC_2.34' not found, když zkopíruješ binár sestavený na nové distribuci na starší. Build prox86_64-unknown-linux-muslvytvoří staticky slinkovaný binár, který běží kdekoli, za cenu malého zpomalení při práci náročné na alokace. - TLS certifikáty, pokud volá ven přes HTTPS. Crates s
native-tlsčtou systémové úložiště;rustlsswebpki-rootssi nese vlastní, takže je o jednu závislost míň. - Paměť v desítkách megabajtů. Malá služba v axum s poolem databázových spojení běžně v klidu zabírá pod 20 MB rezidentní paměti a roste s počtem spojení a buffery, ne s velikostí kódu.
Jeden detail kolem glibc stojí za to znát, než tě zmate. Při souběžnosti vytváří alokátor glibc arény pro každé vlákno a uvolněnou paměť vrací operačnímu systému jen zřídka, takže rezidentní paměť může vyrůst a ustálit se na úrovni výrazně nad tím, co program skutečně drží. Nastavení MALLOC_ARENA_MAX=2 v prostředí to obvykle vyrovná, těžší cestou je přechod na jemalloc nebo mimalloc přes crate. Nehoň to, dokud nemáš graf, který to ukazuje.
axum, nebo actix-web#
Oba jsou vyspělé, oba jsou dost rychlé na to, aby brzdou byla databáze, a oba běží na tokiu. Rozdíl je v tvaru kódu.
axum stojí na hyper a tower, takže jeho middleware je middleware z toweru, který používá i zbytek ekosystému. Handlery jsou obyčejné async funkce a routování jsou data:
use axum::{routing::get, Router};use tokio::net::TcpListener;#[tokio::main]async fn main() { let app = Router::new().route("/health", get(|| async { "ok" })); let port = std::env::var("SERVER_PORT").unwrap_or_else(|_| "8000".into()); let listener = TcpListener::bind(format!("0.0.0.0:{port}")).await.unwrap(); axum::serve(listener, app).await.unwrap();}actix-web má vlastní runtime odvozený od aktorů nad tokiem, vlastní traity pro middleware a server ve stylu builderu:
HttpServer::new(|| App::new().route("/health", web::get().to(health))) .workers(2) .bind(("0.0.0.0", port))? .run() .awaitVyber axum, pokud chceš znovu použít vrstvy z toweru a ekosystém kolem hyperu, a actix-web, pokud ti sedí jeho API nebo potřebuješ něco, co umí jen on. Nevybírej ani jeden podle benchmarků; rozdíl mezi nimi je mnohem menší než rozdíl mezi jedním a dvěma dotazy do databáze.
Všimni si .workers(2) v příkladu pro actix. Výchozí chování je jeden worker na logický procesor, což je stejná past, o které se píše níž u tokia.
Build: debug, release a profil, na kterém záleží#
cargo build vyrobí debug binár: bez optimalizací, s kontrolami přetečení a za běhu často pětkrát až desetkrát pomalejší. Nikdy to není to, co nasazuješ. cargo build --release vyrobí optimalizovaný binár v target/release/<name>.
$ cargo build --release --locked$ ./target/release/api--locked způsobí, že build selže, místo aby tiše aktualizoval Cargo.lock. U aplikace - na rozdíl od knihovny - patří lock soubor do repozitáře a --locked z jeho commitnutí dělá něco, co má smysl.
Release profil se dá ladit a nastavení vyměňují dobu buildu za rychlost za běhu a velikost binárky:
[profile.release]lto = "thin"codegen-units = 1strip = "symbols"| Nastavení | Výchozí v release | Co změna udělá |
|---|---|---|
opt-level | 3 | "s" nebo "z" optimalizují na velikost místo rychlosti |
lto | false | "thin" nebo true inlinují napříč crates; pomalejší build s větší spotřebou paměti |
codegen-units | 16 | 1 dá lepší kód a pomalejší build, který žere víc paměti |
strip | false | "symbols" odstraní debug symboly, často zmenší binár na polovinu |
debug | false | 1 ponechá tabulky řádků, takže panic mají užitečný backtrace |
panic | "unwind" | "abort" je menší a rychlejší, ale jakýkoli panic zabije proces |
U panic = "abort" se vyplatí zastavit. S unwindingem panic uvnitř handleru v axum zabije task daného požadavku a proces běží dál, a vrstva catch-panic z tower_http ho umí změnit na odpověď 500. S abort jediný panic v jediném požadavku shodí celou službu a kontejner se restartuje. To není automaticky špatně - pád, po kterém následuje čistý restart, je někdy lepší než proces v neznámém stavu - ale zvol si to vědomě.
Dobré je také vědět, že inkrementální kompilace je v release profilu vypnutá, takže release build překládá tvůj crate pokaždé od nuly. Závislosti se ale dál cachují v target/, a proto je první build dlouhý a desátý krátký. V rámci nasazení nikdy nedělej cargo clean.
Doba buildu a paměť na malém plánu#
Tohle rozhoduje o tom, jaký plán potřebuješ. Překladač je program hladový po paměti a vrchol přichází při závěrečném linkování s lto a codegen-units = 1.
| Velikost projektu | První release build | Rebuild tvého crate | Špička paměti při buildu |
|---|---|---|---|
| Malá služba, ~30 crates | 1-3 minuty | 10-30 sekund | ~1 GB |
| Typická webová služba, 150-300 crates | 4-12 minut | 20-60 sekund | 1-2 GB |
| Těžký strom závislostí se zapnutým LTO | 15 minut a víc | 1-3 minuty | 2-4 GB |
Jsou to rozpětí z praxe, ne sliby - skutečné číslo závisí na stromu závislostí, na tom, kolik z něj je plné maker, a na podílu CPU, který sis koupil. Plyne z toho tři věci:
- Nejmenší tarif je na běh, ne na build. S 1 GB a půlkou jádra build čehokoli netriviálního buď trvá dvacet minut, nebo ho zastaví dosažení limitu paměti. Na RE:NODE se kontejner při dosažení limitu zastaví a čistě restartuje, místo aby mohl swapovat, takže přehnaně ambiciózní build se projeví restartem uprostřed kompilace, ne zpomalením.
- Omez paralelismus dřív než ambice.
cargo build -j 1neboCARGO_BUILD_JOBS=1kompiluje jeden crate po druhém a výrazně snižuje špičku paměti, za cenu delšího reálného času. Je to rozdíl mezi buildem, který doběhne, a tím, který ne. - LTO zapni až nakonec. Builduj s výchozím nastavením, dokud služba neběží.
lto = "thin"acodegen-units = 1stojí za pár procent rychlosti za běhu a menší binár; nestojí za server, který nejde sestavit.
Alternativou je buildovat jinde a binár poslat na server. Sestav ho lokálně nebo v CI pro správný cíl, pak spustitelný soubor nahraj přes SFTP a dej mu chmod +x. Je to legitimní způsob, jak provozovat Rust na plánu s 1 GB, a jeho cenou je, že běžící artefakt už nevyrábí server ze zdrojáků, které vidíš - udrž build reprodukovatelný a pokud to jde, nes v binárce hash commitu.
Diskové nároky té cache jsou skutečné: adresář target/ průměrné webové služby má běžně 1-3 GB, jakmile jsou v něm debug i release artefakty. Aplikační plány začínají na 5 GB disku, takže při buildu na serveru je potřeba to hlídat. Smazání target/debug je obvykle nejsnazší gigabajt, který kdy uvolníš.
Bind, porty a vlákna#
Dvě nastavení, u obou je výchozí hodnota uvnitř kontejneru špatná.
První je bind adresa. 127.0.0.1 nedosáhne na nic mimo kontejner; použij 0.0.0.0 a port si vezmi z prostředí místo natvrdo zapsané hodnoty. Na panelech odvozených od Pterodactylu přichází přidělený port jako proměnná prostředí zobrazená na záložce Startup; jinde to bývá často PORT.
Druhé je počet worker vláken tokia. Tokio dimenzuje svůj vícevláknový runtime podle available_parallelism(). Novější verze Rustu na Linuxu berou v úvahu CPU kvóty z cgroup, ale výsledek se zaokrouhluje a nikdy není menší než jedna, takže kontejner omezený na zlomek jádra může mít stále víc vláken plánovače, než dokáže využít. Explicitní nastavení stojí jeden řádek a otázku odstraní:
#[tokio::main(flavor = "multi_thread", worker_threads = 2)]async fn main() { }Jedno nebo dvě worker vlákna jsou správně pro tarify s 1-2 GB; nad nimi odpovídej podílu CPU. Víc vláken při pevné CPU kvótě nepřidá propustnost, jen přepínání kontextu a delší prodlevy, když je kontejner škrcen.
Samotná konfigurace jsou obyčejné proměnné prostředí. Na hrstku stačí std::env::var; envy nebo figment deserializují celé prostředí do struktury a při startu selžou, pokud něco chybí, což je přesně chování, které chceš. Ať použiješ cokoli, přečti každou proměnnou jednou při startu, aby chybějící hodnota zastavila proces hned, a ne ve tři ráno na vzácné cestě kódem. O tom, kam proměnné patří, je článek Proměnné prostředí a tajemství.
Za proxy: hlavičky, websockety a TLS#
Ukončovat TLS přímo ve službě jde přes axum-server a rustls a na hostingovém plánu je to obvykle špatná volba: pak ti patří obnova certifikátů, přesměrování z HTTP na HTTPS i reload při změně certifikátu. Nech to na proxy a na přidělený port obsluhuj prosté HTTP.
Co to znamená pro tvůj kód:
- Adresa klienta je v
X-Forwarded-For, ne ve spojení. V axum tiConnectInfoukáže proxy; klienta ti dá vrstva z toweru nebo ruční přečtení hlavičky. Té hlavičce věř jen proto, že víš, že ji nastavila proxy. - Schéma je v
X-Forwarded-Proto. Všude, kde skládáš absolutní URL - přesměrování, odkazy v e-mailech, OAuth callbacky - ho čti, jinak lidi pošleš nahttp://a způsobíš smyčku přesměrování. - Websockety potřebují přeposlané upgrade hlavičky a idle timeout delší než tvůj heartbeat. Celý příběh je v článku Websockety za reverzní proxy a platí stejně i pro
axum::extract::ws.
Aplikační plány zahrnují proxy slot: nasměruj A záznam na zobrazenou adresu a certifikát se vystaví a obnovuje automaticky v 21denním okně, přičemž skutečná adresa klienta přichází v X-Forwarded-For. Zbytek řetězce hlaviček vysvětluje Co dělá reverzní proxy.
Logování, panic a řádné ukončení#
Používej tracing s tracing-subscriber, filtr čti z prostředí a piš na standardní výstup, aby ho měla konzole:
RUST_LOG=info,tower_http=debug,sqlx=warnRUST_BACKTRACE=1RUST_BACKTRACE=1 nestojí nic, dokud nedojde k panicu, a pak je to rozdíl mezi užitečným hlášením a slovem „panicked“. Pokud chceš v těch backtracích čísla řádků, nech v release profilu debug = 1; zvětší to binár, ale ne používanou paměť.
Ošetři SIGTERM, protože právě ten posílá stop nebo restart. Axum má ukončení zabudované:
axum::serve(listener, app) .with_graceful_shutdown(shutdown_signal()) .await .unwrap();Uvnitř shutdown_signal počkej na tokio::signal::ctrl_c() i na stream SignalKind::terminate() a pak se vrať. Serve přestane přijímat nová spojení, nechá doběhnout rozpracované požadavky a skončí. Bez toho se proces po uplynutí odkladu zabije a všechno rozpracované se zahodí - včetně napůl odeslané odpovědi a veškeré práce v bufferech. Stranu health endpointu pokrývá článek Řádné ukončení a health checky, která je v Rustu jen route vracející konstantu a nic víc.
Databáze a pooly spojení#
Běžnou volbou jsou sqlx a tokio-postgres s deadpool. Záleží na velikosti poolu, protože pool drží svá spojení otevřená, ať je využíváš, nebo ne:
PgPoolOptions::new() .max_connections(5) .acquire_timeout(Duration::from_secs(5)) .connect(&database_url) .await?Pět je rozumná výchozí hodnota pro službu na jednom nebo dvou jádrech. Rust služba drží jeden pool na proces, ne na workera, což je skutečná výhoda oproti modelu procesu na workera u gunicornu - deset Python workerů s poolem každý je padesát spojení, zatímco ekvivalentní Rust služba jich má pět. Aritmetiku z druhé strany toho spojení najdeš v článku Pooly spojení a limity.
Jedna past specifická pro nasazení: dotazy kontrolované sqlx při kompilaci potřebují za buildu živou databázi, pokud je nepřipravíš offline. Spusť lokálně cargo sqlx prepare, commitni vygenerovaný adresář .sqlx a v prostředí buildu nastav SQLX_OFFLINE=true. Jinak ti build serveru v nejméně vhodnou chvíli skončí hláškou „set DATABASE_URL to use query macros“. Databázové sloty v panelu přicházejí s vygenerovaným hostem, uživatelem a heslem; samostatná instance PostgreSQL nebo MongoDB je plán databázového hostingu.
Co se pokazí#
`linker 'cc' not found`. Build potřebuje C toolchain pro crates s nativním kódem. Na spravovaném plánu je to součást image; na vlastním stroji nainstaluj balíček build essentials své distribuce.
Build je zabit bez chyby. Došla paměť. Přejdi na -j 1, vypni LTO, nebo builduj někde, kde je víc RAM, a binár nahraj.
`version 'GLIBC_2.34' not found`. Binár byl sestaven proti novější glibc, než má server. Builduj na starším základu, nebo cílem zvol musl pro statický binár.
`Text file busy` při výměně binárky. Přepisuješ spustitelný soubor, který běží. Nejdřív službu zastav, nebo zapiš pod novým jménem a přesuň ho na místo.
Connection refused, ale log říká, že naslouchá. Je navázaná na 127.0.0.1. Použij 0.0.0.0.
První nasazení trvá deset minut a další zase deset minut. Změnila se verze závislosti, takže cachované artefakty přestaly platit. Po aktualizaci Cargo.lock je to očekávané; pokud se to děje při každém nasazení, něco maže target/.
Paměť roste pod zátěží a už neklesne. Pravděpodobně arény alokátoru, ne únik. Zkus MALLOC_ARENA_MAX=2 a sleduj graf, než cokoli přepíšeš.
Panic nevrátí klientovi 500, ale nic. Bez vrstvy catch-panic task umře a spojení spadne. Přidej tower_http::catch_panic::CatchPanicLayer, aby klient dostal odpověď a ty panic v logu.
FAQ#
Kolik paměti potřebuje webová služba v Rustu?
Za běhu mnohem méně, než čekáš: malá služba v axum s poolem databázových spojení běžně v klidu zabírá pod 20 MB a skutečný provoz zvládne do 100 MB. Otázka paměti u Rustu se týká buildu, ne procesu - ten potřebuje gigabajt a víc.
Můžu buildovat na nejmenším plánu?
Malý projekt ano, s -j 1 a bez LTO. Služba s několika sty crates ve stromu bude při 1 GB a půlce jádra utrpením. Buď přejdi o tarif výš na plán, na kterém buildíš, nebo builduj v CI a nahraj binár.
axum, nebo actix-web?
Kterýkoli. axum sedí líp, pokud chceš middleware z toweru a ekosystém kolem hyperu; actix-web má vlastní dobře zdokumentovaný svět a je stejně připravený do produkce. Výkon není rozhodující v měřítku, ve kterém se tahle volba dělá.
Potřebuju před ním reverzní proxy?
Potřebuješ něco, co ukončí TLS a obnoví certifikát, a proxy je nejjednodušší věc, která to umí. Služba sama je schopný HTTP server, takže proxy tu je kvůli certifikátům, přeposlaným hlavičkám a limitům požadavků, ne kvůli rychlosti.
Má se Cargo.lock commitovat?
U binárky ano, vždy, a builduj s --locked, aby nasazení hlasitě selhalo, místo aby vyřešilo něco nového. U knihovny je konvence opačná, odtud ta zmatenost.
Proč konzole v panelu nic neukazuje?
Rust zapisuje na standardní výstup bez bufferu, když to není terminál, takže obvyklá příčina je, že zatím nic nelogoval - služba bez nainstalovaného subscriberu tracing nevypíše vůbec nic, ať dělá cokoli. Nainstaluj subscriber a nastav RUST_LOG.




Komentáře
Zcela anonymně: žádný účet, žádný e-mail, žádné cookie. Ukládáme jen jméno, které napíšeš, text a čas - nic dalšího. Počet odkazů je omezený a značkování se nevykresluje.