RE:NODE

Hosting aplikací11 min čtení

Možnosti dotnet publish: self-contained, single-file a AOT

Co dotnet publish skutečně vytvoří: framework-dependent vs self-contained, runtime identifikátory, single-file, trimming, ReadyToRun a Native AOT a kdy co použít.

5 čtenářů

Pro webovou aplikaci nebo bota na linuxovém serveru je správný dotnet publish téměř vždy ten obyčejný: framework-dependent, konfigurace Release, výstupní složka a spuštění přes dotnet App.dll. Výstup je nejmenší, bezpečnostní záplaty runtime se projeví bez nového buildu a nemá žádné z kompatibilitních pastí těch pokročilejších režimů. Self-contained je správná volba, když na runtime na serveru nemáš vliv. Single-file, trimming, ReadyToRun a Native AOT řeší vždy jeden konkrétní problém - dobu startu, počet souborů, velikost - a každý něco stojí, takže je zapínej proto, že ten problém máš, ne proto, že ten přepínač existuje.

Tento článek vysvětluje, co která volba skutečně mění ve výstupní složce a kde se rozbije.

Stručná odpověď#

RežimPřidá do příkazuPotřebuje runtime na serveruTypické použití
Framework-dependentnicano, shodnou major verziVětšina webových aplikací a botů
Self-contained-r linux-x64 --self-containedneRuntime na serveru je špatný nebo neznámý
Single-file-p:PublishSingleFile=truepodle předchozíchJeden soubor k přenášení
Trimmed-p:PublishTrimmed=truene (jen self-contained)Důležitá velikost, kód bezpečný pro trimming
ReadyToRun-p:PublishReadyToRun=truepodle situaceRychlejší studený start
Native AOT-p:PublishAot=trueneRychlý start, nízká paměť, omezený kód

Volby se dají kombinovat. Self-contained, single-file build s ReadyToRun pro linux-x64 je naprosto běžná věc. Zbytek článku popisuje, co jednotlivé sloupce znamenají v praxi.

Co dotnet publish vytvoří#

Začni s výchozím nastavením a podívej se do složky:

bash
$ dotnet publish src/Api/Api.csproj -c Release -o out$ ls outApi  Api.deps.json  Api.dll  Api.pdb  Api.runtimeconfig.jsonappsettings.json  appsettings.Development.json  web.config  ...

Každý z těchto souborů má svůj úkol:

  • Api.dll je tvůj kód zkompilovaný do IL - mezijazyka, který JIT runtime překládá do strojového kódu při prvním volání metod.
  • Api.deps.json vypisuje všechny závislosti a kde je najít. Smaž ho a host se vrátí k hledání ve složce, což většinou funguje, dokud nefunguje.
  • Api.runtimeconfig.json říká, jaký sdílený framework načíst, třeba Microsoft.AspNetCore.App 10.0, a obsahuje nastavení runtime, například režim GC.
  • Api (na Windows Api.exe) je apphost - malý nativní spouštěč pro platformu, na které jsi build dělal. ./Api a dotnet Api.dll dělají totéž. Pokud aplikaci vždy spouštíš přes dotnet, vypni ho pomocí -p:UseAppHost=false.
  • Závislosti z NuGetu se kopírují jako samostatné DLL. Framework samotný ne.
  • web.config je pro IIS a všude jinde se ignoruje.

Od .NET 8 používá dotnet publish u projektů cílících na .NET 8 a novější ve výchozím stavu konfiguraci Release. Psát -c Release explicitně je ale pořád dobrý zvyk u starších projektů i ve build skriptech, které si někdo později přečte.

Framework-dependent a jak funguje roll-forward#

Výchozí výstup závisí na tom, že je tam, kde běží, nainstalovaný sdílený runtime .NET. Požadavek na verzi je v runtimeconfig.json:

Api.runtimeconfig.json
{  "runtimeOptions": {    "tfm": "net10.0",    "framework": {      "name": "Microsoft.AspNetCore.App",      "version": "10.0.0"    }  }}

version je minimum a runtime podle politiky roll-forward hledá, kterou verzi použije. Výchozí politika Minor dělá dvě věci: vždy vybere nejnovější nainstalovanou záplatu požadované verze (takže 10.0.0 poběží na 10.0.7, pokud je k dispozici) a pokud neexistuje vůbec žádné 10.0.x, přijme novější minor verzi. Přes hranici major verze nepřeskočí: aplikace sestavená pro .NET 8 se nespustí na stroji, který má jen .NET 10, a skončí hláškou „You must install or update .NET to run this application“.

Můžeš to změnit pomocí <RollForward>Major</RollForward> v projektu nebo proměnné prostředí DOTNET_ROLL_FORWARD a aplikace pro 8 pak poběží na 10. Obvykle to funguje, protože zásadní změny mezi major verzemi jsou zdokumentované a většinou malé, jenže pak běžíš na runtime, který jsi nikdy netestoval. Poctivé řešení je změnit cílovou verzi a znovu sestavit.

Skutečná výhoda framework-dependent výstupu je záplatování. Když hostitel nainstaluje bezpečnostní aktualizaci .NET, aplikace ji převezme při dalším restartu bez nového buildu. Self-contained aplikace nese vlastní kopii runtime a drží si úroveň záplat, se kterou byla sestavena, dokud znovu nepublikuješ.

Self-contained a runtime identifikátory#

Self-contained build zkopíruje runtime do výstupní složky, takže server nepotřebuje nainstalované .NET vůbec:

bash
$ dotnet publish -c Release -r linux-x64 --self-contained -o out

Self-contained aplikace je svázaná s jednou platformou, kterou určuje runtime identifikátor (RID):

RIDPlatforma
linux-x6464bitový Intel/AMD Linux s glibc - většina serverů
linux-arm6464bitový ARM Linux s glibc
linux-musl-x64Alpine a další distribuce založené na musl
win-x6464bitové Windows
osx-arm64macOS na Apple silicon

Od .NET 8 používá SDK ve výchozím stavu přenosné RID jako tyto; RID specifické pro distribuci, například ubuntu.22.04-x64, už cílit nemáš. Také od .NET 8 už samotné -r neznamená self-contained - dostaneš framework-dependent build pro danou platformu. Napiš --self-contained (nebo --self-contained false) explicitně, aby si nikdo nemusel pamatovat, které SDK co změnilo.

Self-contained nezahrnuje nativní knihovny operačního systému. Runtime pořád potřebuje glibc (u musl RID musl), OpenSSL pro TLS a ICU pro zpracování řetězců podle kultury. Na minimálním linuxovém image bez ICU se aplikace .NET zastaví při startu s hláškou „Couldn't find a valid ICU package installed on the system“. Pokud tvoje aplikace kulturně specifické formátování opravdu nepotřebuje - většina API, která mluví JSON a ukládají časy v UTC, ho nepotřebuje - zapni invariantní globalizaci:

xml
<PropertyGroup>  <InvariantGlobalization>true</InvariantGlobalization></PropertyGroup>

Pak se všechny kultury chovají jako invariantní kultura: ToUpper na tureckém textu a formáty data v de-DE nebudou dodržovat místní pravidla. Některé databázové ovladače mají k tomu také své výhrady; starší verze Microsoft.Data.SqlClient odmítaly v invariantním režimu otevřít spojení, takže to vyzkoušej se svým skutečným stackem.

Cenou za self-contained je velikost: runtime a u webové aplikace i ASP.NET Core přidají zhruba 70 až 100 MB podle verze, oproti několika megabajtům u framework-dependent buildu. Na plánu s 5 GB disku to tolik nevadí, jak to zní, ale vadí to, pokud si necháváš víc buildů najednou.

Single-file#

PublishSingleFile zabalí aplikaci do jednoho spustitelného souboru. Vyžaduje RID a funguje u framework-dependent i self-contained buildů:

bash
$ dotnet publish -c Release -r linux-x64 --self-contained \    -p:PublishSingleFile=true -o out

Na tři detaily lidé narážejí.

Některé soubory zůstávají zvlášť. Spravované assembly jdou do balíčku. Nativní knihovny zůstanou vedle spustitelného souboru, pokud nepřidáš -p:IncludeNativeLibrariesForSelfExtract=true; v tom případě se při prvním spuštění rozbalí do dočasného adresáře (řídí ho DOTNET_BUNDLE_EXTRACT_BASE_DIR). Soubory s obsahem jako appsettings.json a wwwroot se publikují vedle spustitelného souboru, ne dovnitř něj, pokud nenastavíš IncludeAllContentForSelfExtract, což u konfiguračního souboru, který chceš upravovat, málokdy chceš.

`Assembly.Location` je prázdné. Kód, který si hledá vlastní složku přes typeof(Program).Assembly.Location, dostane uvnitř balíčku prázdný řetězec. Použij místo toho AppContext.BaseDirectory; kompilátor na to upozorní varováním IL3000, pokud máš zapnuté analyzátory.

Není menší ani rychlejší. Jeden soubor jsou stejné bajty v jedné nádobě. Hodí se pro nástroj příkazové řádky, který předáváš dalším lidem. U serverové aplikace nasazované z Gitu se složka spouští stejně snadno jako soubor, takže single-file řeší problém, který nemáš.

Trimming#

Trimming odstraní z frameworku a tvých závislostí kód, který aplikace nepoužívá. Funguje jen u self-contained buildů, protože sdílený framework nejde oříznout pro jednu aplikaci.

xml
<PropertyGroup>  <PublishTrimmed>true</PublishTrimmed></PropertyGroup>

Trimmer pracuje statickou analýzou: sleduje volání od vstupního bodu a odstraní, k čemu se nedostane. Reflexe ho oklame. Cokoli, co objevuje typy až za běhu - klasické objevování controllerů v MVC, Razor views, serializace System.Text.Json bez source generation, většina dependency injection podle konvencí, starší ORM - může přijít o kód, který potřebuje, a chyba se projeví za běhu jako MissingMethodException nebo typ, který se tiše deserializuje na nic.

SDK ukazuje, kde je riziko, pomocí varování trimmeru (IL2026, IL2070 a další). Ber je jako chyby. Pokud build vyprodukuje desítky takových varování z knihovny, kterou neovládáš, trimming pro tu aplikaci není.

V praxi: minimální API se source-generated JSON se trimuje dobře. Aplikace MVC s Razor views nebo cokoli, co hodně používá EF Core, je špatný kandidát a dokumentace Microsoftu to u těchto frameworků říká výslovně. Úspora je skutečná - často polovina velikosti self-contained buildu i víc - ale na serveru málokdy disk představuje omezení, na kterém záleží.

ReadyToRun a tiered compilation#

Běžný kód .NET překládá do strojového kódu JIT za běhu. Start proto tráví čas kompilací, což na pomalém CPU tvoří velkou část prvních sekund. ReadyToRun (R2R) kompiluje předem a ukládá nativní kód vedle IL:

bash
$ dotnet publish -c Release -r linux-x64 -p:PublishReadyToRun=true -o out

Potřebuje RID, protože nativní kód je specifický pro platformu. Assembly se zvětší, často na dvoj- až trojnásobek velikosti IL. JIT tam pořád je: tiered compilation bere kód z R2R jako výchozí úroveň a horké metody překompiluje s plnou optimalizací podle dynamických profilových dat (dynamic PGO, od .NET 8 ve výchozím stavu zapnuté), takže výkon v ustáleném stavu vyjde stejně jako bez R2R.

R2R přináší studený start. Na serveru s půl vCPU, kde je limit CPU tvrdé omezení a ne doporučení, je práce JIT při startu znatelně pomalá a R2R může ubrat sekundy z doby mezi „proces se spustil“ a „první požadavek je obsloužen“. Pokud se aplikace restartuje při každém nasazení a na té prodlevě ti záleží, je to nejlevnější výhra na tomto seznamu a nestojí nic na kompatibilitě. Pokud se aplikace spouští jednou týdně, disk za to nestojí.

Native AOT#

Native AOT zkompiluje celou aplikaci včetně runtime do jednoho nativního spustitelného souboru, bez JIT:

xml
<PropertyGroup>  <PublishAot>true</PublishAot></PropertyGroup>

Výsledky jsou tam, kde to jde použít, dramatické: start v desítkách milisekund, menší binárka než u trimmed self-contained a nižší paměť v klidu. Omezení jsou ta od trimmingu, jen absolutní: žádné generování kódu za běhu, žádné dynamické načítání assembly, reflexe jen tam, kam kompilátor dohlédne. ASP.NET Core to podporuje pro minimální API a gRPC, s WebApplication.CreateSlimBuilder a source-generated JSON; šablona webapiaot je na to připravená. MVC, Razor Pages a Blazor Server podporované nejsou. Podpora AOT v EF Core je stále omezená a v posledních verzích označená jako experimentální.

Dvě praktická omezení: Native AOT potřebuje při buildu nativní toolchain platformy (na Linuxu clang a vývojové hlavičky zlib) a nekompiluje napříč operačními systémy - linuxové binárky sestavuj na Linuxu. Build na malém plánu přímo na serveru je pomalý a hladový po paměti; to je práce pro CI.

Native AOT je opravdu dobrý pro malé, úzce zaměřené služby a nástroje příkazové řádky. Pro typickou firemní webovou aplikaci stojí omezení víc, než kolik ušetří zrychlení startu.

Volba pro malý server#

Takhle volby dopadnou na reálném případě: ASP.NET Core API s EF Core, nasazené z GitHubu na plán s 1 GB a půl vCPU.

  1. Začni s framework-dependent. dotnet publish -c Release -o out a dotnet out/Api.dll. Nejmenší výstup, záplatovaný spolu s runtime, žádná překvapení.
  2. Pokud má server špatnou major verzi runtime, publikuj místo bojování s roll-forward self-contained pro linux-x64. Pamatuj, že záplatování runtime je teď na tobě: znovu sestav, když vyjde bezpečnostní aktualizace .NET.
  3. Pokud jsou restarty pomalé, přidej jako první ReadyToRun. Nemá žádné riziko pro kompatibilitu.
  4. Trimming a AOT nech u této aplikace vypnuté. EF Core a stack postavený na reflexi vyprodukují varování, která nelze bezpečně ignorovat.
  5. Single-file vynech. Nic nepřidá, když je nasazení git pull.

Na tom, kde se build dělá, záleží stejně jako na tom, jak. Build na serveru znamená, že SDK, MSBuild a kompilátor při každém spuštění běží uvnitř limitu paměti plánu, a na RE:NODE se proces, který dosáhne limitu paměti, zastaví a kontejner se čistě restartuje místo swapování - takže velký build na nejmenším plánu může spadnout v půlce. Build v GitHub Actions a nasazení výstupu (z větve, kterou server sleduje) tuto práci ze serveru úplně přesune. Nasazení aplikace ASP.NET Core z GitHubu popisuje obě varianty a paměť a garbage collection v .NET vysvětluje, kolik bude aplikace sama používat, až poběží.

Stejná rozhodnutí platí i mimo web: worker služba nebo bot se publikuje úplně stejně a worker služby a úlohy na pozadí v .NET popisují hostitelský model pro ně.

FAQ#

Je self-contained rychlejší než framework-dependent?

Ne. Je to stejný runtime a stejný JIT; mění se jen to, odkud se soubory berou. Rychlost startu přináší ReadyToRun nebo Native AOT, ne přibalení runtime.

Proč moje publikovaná aplikace chce verzi .NET, o které jsem myslel, že ji mám?

Server má nainstalovanou jinou major verzi, než je v runtimeconfig.json, a výchozí politika roll-forward přes hranici major verze nepřeskočí. Změň cílovou verzi, publikuj self-contained, nebo nastav RollForward záměrně.

Můžu publikovat na Windows a spouštět na Linuxu?

Ano, u všeho kromě Native AOT. IL ve framework-dependent výstupu je nezávislé na platformě a self-contained build pro linux-x64 jde vytvořit i na Windows. Liší se jen apphost a aplikaci můžeš místo něj spouštět přes dotnet App.dll.

Potřebuju SDK na serveru?

Jen pokud tam buildíš. Ke spuštění publikované aplikace stačí runtime, nebo vůbec nic, pokud je self-contained. Držet SDK mimo běžící prostředí je jeden z důvodů, proč buildovat v CI.

Mám výstup publish commitovat do Gitu?

Ne do hlavní větve. Pokud nasazuješ předem sestavený výstup, drž ho na samostatné větvi zapisované z CI, aby historie zdrojů zůstala čitelná. Verze .NET a podpora LTS popisuje udržování aktuální cílové verze frameworku, což je druhá půlka stabilního buildu.


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.

0/2000