ვებ აპლიკაციისთვის ან ბოტისთვის Linux სერვერზე სწორი dotnet publish თითქმის ყოველთვის უბრალოა: framework-dependent, Release კონფიგურაცია, შედეგის საქაღალდე და გაშვება dotnet App.dll-ით. ეს ყველაზე პატარა შედეგია, runtime-ის უსაფრთხოების patch-ებს ხელახალი აგების გარეშე იღებს და არ აქვს უფრო ლამაზი რეჟიმების თავსებადობის ხაფანგები. self-contained სწორი პასუხია, როცა სერვერზე runtime-ს ვერ აკონტროლებ. single-file, trimming, ReadyToRun და Native AOT თითოეული ერთ კონკრეტულ პრობლემას წყვეტს - გაშვების დროს, ფაილების რაოდენობას, ზომას - და თითოეულს თავისი ფასი აქვს, ამიტომ ჩართე ისინი იმიტომ, რომ ეს პრობლემა გაქვს, და არა იმიტომ, რომ flag არსებობს.
ეს პოსტი ხსნის, რას ცვლის თითოეული პარამეტრი რეალურად შედეგის საქაღალდეში და სად ტყდება თითოეული.
მოკლე პასუხი#
| რეჟიმი | ბრძანებას ემატება | სერვერზე runtime სჭირდება | ტიპური გამოყენება |
|---|---|---|---|
| Framework-dependent | არაფერი | კი, შესაბამისი მთავარი ვერსია | ვებ აპლიკაციებისა და ბოტების უმეტესობა |
| Self-contained | -r linux-x64 --self-contained | არა | სერვერის runtime არასწორი ან უცნობია |
| Single-file | -p:PublishSingleFile=true | დამოკიდებულია ზემოთაზე | ერთი ფაილი გადასატანად |
| Trimmed | -p:PublishTrimmed=true | არა (მხოლოდ self-contained) | ზომაზე მგრძნობიარე, trim-safe კოდი |
| ReadyToRun | -p:PublishReadyToRun=true | დამოკიდებულია | უფრო სწრაფი ცივი გაშვება |
| Native AOT | -p:PublishAot=true | არა | სწრაფი გაშვება, ცოტა მეხსიერება, შეზღუდული კოდი |
პარამეტრები ერთმანეთს ერწყმის. self-contained, single-file, ReadyToRun build linux-x64-ისთვის ჩვეულებრივი რამაა. პოსტის დანარჩენი ნაწილი იმას ეხება, რას ნიშნავს თითოეული სვეტი პრაქტიკაში.
რას ქმნის dotnet publish#
დაიწყე ნაგულისხმევით და შეხედე საქაღალდეს:
$ 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 ...ამ ფაილებიდან თითოეულს თავისი საქმე აქვს:
Api.dllშენი კოდია, IL-ში კომპილირებული - შუალედურ ენაში, რომელსაც runtime-ის JIT მანქანურ კოდად აკომპილირებს, როცა მეთოდები პირველად გამოიძახება.Api.deps.jsonჩამოთვლის ყველა დამოკიდებულებას და იმას, სად მოიძებნება. წაშალე და host საქაღალდის დათვალიერებაზე გადავა, რაც უმეტესად მუშაობს, სანამ არ შეწყვეტს.Api.runtimeconfig.jsonამბობს, რომელი shared framework ჩატვირთოს, მაგალითადMicrosoft.AspNetCore.App10.0, და ინახავს runtime-ის პარამეტრებს, როგორიცაა GC-ის რეჟიმი.Api(ანApi.exeWindows-ზე) არის apphost - პატარა native გამშვები იმ პლატფორმისთვის, რომელზეც ააგე../Apiდაdotnet Api.dllერთსა და იმავეს აკეთებს. გამორთე ის-p:UseAppHost=false-ით, თუ ყოველთვის მხოლოდdotnet-ით უშვებ.- NuGet დამოკიდებულებები საკუთარ DLL-ებად კოპირდება. თავად framework - არა.
web.configIIS-ისთვისაა და ყველგან სხვაგან იგნორირდება.
.NET 8-იდან dotnet publish ნაგულისხმევად Release კონფიგურაციას იყენებს პროექტებისთვის, რომლებიც .NET 8-ს ან უფრო ახალს მიზნად ისახავს. -c Release-ის აშკარად დაწერა მაინც კარგი ჩვევაა ძველი პროექტებისთვის და build სკრიპტებისთვის, რომლებსაც ვინმე მოგვიანებით წაიკითხავს.
Framework-dependent და როგორ მუშაობს roll-forward#
ნაგულისხმევი შედეგი დამოკიდებულია იმაზე, რომ იქ, სადაც ეშვება, shared .NET runtime იყოს დაყენებული. ვერსიის მოთხოვნა runtimeconfig.json-შია:
{ "runtimeOptions": { "tfm": "net10.0", "framework": { "name": "Microsoft.AspNetCore.App", "version": "10.0.0" } }}version მინიმუმია, და runtime roll-forward პოლიტიკას იყენებს, რომ გამოსაყენებელი ვერსია იპოვოს. ნაგულისხმევი პოლიტიკა, Minor, ორ რამეს აკეთებს: ის ყოველთვის ირჩევს მოთხოვნილი ვერსიის ყველაზე ახალ დაყენებულ patch-ს (ასე რომ 10.0.0 გაეშვება 10.0.7-ზე, თუ ის არსებობს), და თუ 10.0.x საერთოდ არ არის, იღებს უფრო გვიანდელ minor ვერსიას. ის მთავარ ვერსიას არ კვეთს: .NET 8-ისთვის აგებული აპლიკაცია არ გაეშვება მანქანაზე, სადაც მხოლოდ .NET 10-ია, და ვარდება შეტყობინებით "You must install or update .NET to run this application".
ამის შეცვლა შეგიძლია პროექტში <RollForward>Major</RollForward>-ით ან DOTNET_ROLL_FORWARD გარემოს ცვლადით, და 8-ისთვის აგებული აპლიკაცია 10-ზე გაეშვება. ეს ჩვეულებრივ მუშაობს, რადგან მთავარ ვერსიებს შორის breaking change-ები დოკუმენტირებულია და უმეტესად პატარაა, მაგრამ მაშინ ისეთ runtime-ზე მუშაობ, რომელიც არასოდეს გატესტე. პატიოსანი გამოსავალია target-ის შეცვლა და ხელახლა აგება.
framework-dependent შედეგის რეალური უპირატესობა patch-ებია. როცა ჰოსტი .NET-ის უსაფრთხოების განახლებას აყენებს, შენი აპლიკაცია მას შემდეგ restart-ზე იღებს ხელახალი აგების გარეშე. self-contained აპლიკაცია runtime-ის საკუთარ ასლს ატარებს და ინარჩუნებს იმ patch დონეს, რომლითაც აიგო, სანამ ხელახლა არ გააკეთებ publish-ს.
Self-contained და runtime identifier-ები#
self-contained build runtime-ს შედეგის საქაღალდეში აკოპირებს, ამიტომ სერვერზე .NET საერთოდ არ უნდა იყოს დაყენებული:
$ dotnet publish -c Release -r linux-x64 --self-contained -o outself-contained აპლიკაცია ერთ პლატფორმაზეა მიბმული, რომელსაც runtime identifier (RID) ასახელებს:
| RID | პლატფორმა |
|---|---|
linux-x64 | 64-ბიტიანი Intel/AMD Linux glibc-ით - სერვერების უმეტესობა |
linux-arm64 | 64-ბიტიანი ARM Linux glibc-ით |
linux-musl-x64 | Alpine და სხვა musl-ზე დაფუძნებული დისტრიბუტივები |
win-x64 | 64-ბიტიანი Windows |
osx-arm64 | Apple silicon macOS |
.NET 8-იდან SDK ნაგულისხმევად ასეთ პორტაბელურ RID-ებს იყენებს; დისტრიბუტივისთვის სპეციფიკური RID-ები, როგორიცაა ubuntu.22.04-x64, აღარ არის ის, რასაც უნდა დაუმიზნო. ასევე .NET 8-იდან მხოლოდ -r-ის გადაცემა აღარ ნიშნავს self-contained-ს - იღებ framework-dependent build-ს ამ პლატფორმისთვის. დაწერე --self-contained (ან --self-contained false) აშკარად, რომ არავის მოუწიოს დამახსოვრება, რომელმა SDK-მ რა შეცვალა.
რასაც self-contained არ შეიცავს, არის ოპერაციული სისტემის native ბიბლიოთეკები. runtime-ს მაინც სჭირდება glibc (ან musl, musl RID-ებისთვის), OpenSSL TLS-ისთვის და ICU კულტურის გათვალისწინებით სტრიქონების დამუშავებისთვის. მინიმალურ Linux image-ზე ICU-ს გარეშე .NET აპლიკაცია გაშვებისას ჩერდება შეტყობინებით "Couldn't find a valid ICU package installed on the system". თუ შენს აპლიკაციას მართლა არ სჭირდება კულტურისთვის სპეციფიკური ფორმატირება - API-ების უმეტესობას, რომლებიც JSON-ით საუბრობენ და UTC დროის ნიშნულებს ინახავენ, არ სჭირდება - დააყენე invariant globalization:
<PropertyGroup> <InvariantGlobalization>true</InvariantGlobalization></PropertyGroup>მაშინ ყველა კულტურა invariant კულტურასავით იქცევა: ToUpper თურქულ ტექსტზე და თარიღის ფორმატები de-DE-ში ადგილობრივ წესებს არ მიჰყვება. ზოგიერთ მონაცემთა ბაზის დრაივერს ამაზეც აქვს საკუთარი აზრი; Microsoft.Data.SqlClient-ის ძველი ვერსიები invariant რეჟიმში კავშირების გახსნაზე უარს ამბობდნენ, ამიტომ გატესტე შენი რეალური სტეკით.
self-contained-ის ფასი ზომაა: runtime და, ვებ აპლიკაციისთვის, ASP.NET Core ვერსიის მიხედვით დაახლოებით 70-დან 100 MB-მდე ამატებს, framework-dependent build-ის რამდენიმე მეგაბაიტის საპირისპიროდ. 5 GB დისკიან გეგმაზე ამას ნაკლები მნიშვნელობა აქვს, ვიდრე ჟღერს, მაგრამ აქვს, თუ რამდენიმე build-ს ინახავ.
Single-file#
PublishSingleFile აპლიკაციას ერთ შესრულებად ფაილში ფუთავს. მას RID სჭირდება და მუშაობს როგორც framework-dependent, ისე self-contained build-ებისთვის:
$ dotnet publish -c Release -r linux-x64 --self-contained \ -p:PublishSingleFile=true -o outსამი დეტალი აბრკოლებს ხალხს.
ზოგი ფაილი მაინც ცალკეა. managed assembly-ები bundle-ში ხვდება. native ბიბლიოთეკები შესრულებადი ფაილის გვერდით რჩება, თუ არ დაამატებ -p:IncludeNativeLibrariesForSelfExtract=true-ს, რა შემთხვევაშიც ისინი პირველი გაშვებისას დროებით დირექტორიაში იშლება (მას DOTNET_BUNDLE_EXTRACT_BASE_DIR აკონტროლებს). content ფაილები, როგორიცაა appsettings.json და wwwroot, შესრულებადი ფაილის გვერდით ქვეყნდება და არა მის შიგნით, თუ არ დააყენებ IncludeAllContentForSelfExtract-ს, რაც იშვიათად არის ის, რაც გინდა კონფიგურაციის ფაილისთვის, რომლის რედაქტირებასაც აპირებ.
`Assembly.Location` ცარიელია. კოდი, რომელიც საკუთარ საქაღალდეს typeof(Program).Assembly.Location-ით პოულობს, bundle-ის შიგნით ცარიელ სტრიქონს იღებს. გამოიყენე AppContext.BaseDirectory; კომპილატორი ამაზე IL3000-ით გაფრთხილებს, თუ analyser-ები ჩართული გაქვს.
ის არც უფრო პატარაა და არც უფრო სწრაფი. ერთი ფაილი იგივე ბაიტებია ერთ კონტეინერში. ის მოსახერხებელია command-line ხელსაწყოსთვის, რომელსაც სხვა ადამიანებს გადასცემ. Git-იდან deploy-ებული სერვერული აპლიკაციისთვის საქაღალდის გაშვება ფაილისაზე რთული არ არის, ამიტომ single-file წყვეტს პრობლემას, რომელიც არ გაქვს.
Trimming#
trimming აშორებს კოდს, რომელსაც აპლიკაცია არ იყენებს, framework-იდან და შენი დამოკიდებულებებიდან. ის მხოლოდ self-contained build-ებთან მუშაობს, რადგან shared framework-ის ერთი აპლიკაციისთვის trim ვერ მოხერხდება.
<PropertyGroup> <PublishTrimmed>true</PublishTrimmed></PropertyGroup>trimmer სტატიკური ანალიზით მუშაობს: ის შენი entry point-იდან გამოძახებებს მიჰყვება და აშორებს იმას, რასაც ვერ აღწევს. reflection მას ამარცხებს. ყველაფერი, რაც ტიპებს runtime-ში აღმოაჩენს - კლასიკური MVC controller-ების აღმოჩენა, Razor view-ები, System.Text.Json სერიალიზაცია source generation-ის გარეშე, convention-ით dependency injection-ის უმეტესობა, ძველი ORM-ები - შეიძლება დაკარგოს საჭირო კოდი, და ჩავარდნა runtime-ში ჩნდება MissingMethodException-ად ან ტიპად, რომელიც ჩუმად არაფრად დესერიალიზდება.
SDK გეუბნება, სადაა რისკი, trim გაფრთხილებებით (IL2026, IL2070 და მისთანანი). მოეპყარი მათ, როგორც შეცდომებს. თუ შენი build ათობით ასეთს აწარმოებს ბიბლიოთეკიდან, რომელსაც ვერ აკონტროლებ, trimming იმ აპლიკაციისთვის არ არის.
პრაქტიკაში: მინიმალური API source-generated JSON-ით კარგად ითრიმება. MVC აპლიკაცია Razor view-ებით, ან ნებისმიერი რამ, რაც EF Core-ს ინტენსიურად იყენებს, ცუდი კანდიდატია, და Microsoft-ის საკუთარი დოკუმენტაციაც ამას ამბობს ამ framework-ებზე. დაზოგვა რეალურია - ხშირად self-contained ზომის ნახევარი ან მეტი - მაგრამ სერვერზე დისკი იშვიათად არის ის შეზღუდვა, რომელსაც მნიშვნელობა აქვს.
ReadyToRun და tiered compilation#
ჩვეულებრივ .NET კოდს მანქანურ კოდად JIT აკომპილირებს მუშაობისას. ამიტომ გაშვება დროს კომპილაციაზე ხარჯავს, რაც ნელ CPU-ზე პირველი რამდენიმე წამის დიდი ნაწილია. ReadyToRun (R2R) წინასწარ აკომპილირებს და native კოდს IL-ის გვერდით ინახავს:
$ dotnet publish -c Release -r linux-x64 -p:PublishReadyToRun=true -o outმას RID სჭირდება, რადგან native კოდი პლატფორმისთვის სპეციფიკურია. assembly-ები იზრდება, ხშირად მათი IL ზომის ორჯერ ან სამჯერ. JIT მაინც არსებობს: tiered compilation R2R კოდს საწყის tier-ად განიხილავს და ცხელ მეთოდებს სრული ოპტიმიზაციით ხელახლა აკომპილირებს დინამიკური profile მონაცემების მიხედვით (dynamic PGO, .NET 8-იდან ნაგულისხმევად ჩართული), ამიტომ სტაბილური მდგომარეობის წარმადობა საბოლოოდ იგივეა, რაც R2R-ის გარეშე.
რასაც R2R გაძლევს, ცივი გაშვებაა. სერვერზე ნახევარი vCPU-ით, სადაც CPU-ის ლიმიტი მკაცრი შეზღუდვაა და არა რჩევა, JIT-ის სამუშაო გაშვებისას შესამჩნევად ნელია, და R2R-ს შეუძლია წამები ჩამოაჭრას დროს "პროცესი დაიწყო"-სა და "პირველ მოთხოვნას პასუხი გაეცა"-ს შორის. თუ შენი აპლიკაცია ყოველ deploy-ზე გადაიტვირთება და ეს შუალედი გაინტერესებს, ეს ამ სიაში ყველაზე იაფი მოგებაა და თავსებადობის ფასი არ აქვს. თუ აპლიკაცია კვირაში ერთხელ ეშვება, დისკად არ ღირს.
Native AOT#
Native AOT მთელ აპლიკაციას, runtime-ის ჩათვლით, ერთ native შესრულებად ფაილად აკომპილირებს, საერთოდ JIT-ის გარეშე:
<PropertyGroup> <PublishAot>true</PublishAot></PropertyGroup>შედეგები შთამბეჭდავია იქ, სადაც გამოდგება: გაშვება ათეულობით მილიწამში, trimmed self-contained-ზე პატარა binary და ნაკლები მეხსიერება უქმ მდგომარეობაში. შეზღუდვები trimming-ისაა, აბსოლუტურად ქცეული: runtime-ში კოდის გენერაცია არა, assembly-ების დინამიკური ჩატვირთვა არა, reflection მხოლოდ იქ, სადაც კომპილატორი ხედავს. ASP.NET Core მას მხარს უჭერს მინიმალური API-ებისა და gRPC-სთვის, WebApplication.CreateSlimBuilder-ით და source-generated JSON-ით; webapiaot შაბლონი ამისთვისაა მომზადებული. MVC, Razor Pages და Blazor Server მხარდაჭერილი არ არის. EF Core-ის AOT მხარდაჭერა ჯერ კიდევ შეზღუდულია და ბოლო ვერსიებში ექსპერიმენტულადაა მონიშნული.
ორი პრაქტიკული შეზღუდვა: Native AOT-ს build-ის დროს პლატფორმის native toolchain სჭირდება (Linux-ზე clang და zlib-ის development header-ები), და ის ოპერაციულ სისტემებს შორის cross-compile-ს არ აკეთებს - Linux binary-ები Linux-ზე ააგე. სერვერის საკუთარ პატარა გეგმაზე აგება ნელია და ბევრ მეხსიერებას ჭამს; ეს CI-ის საქმეა.
Native AOT მართლა კარგია პატარა, ფოკუსირებული სერვისებისა და command-line ხელსაწყოებისთვის. ტიპური ბიზნეს ვებ აპლიკაციისთვის შეზღუდვები უფრო ძვირი ჯდება, ვიდრე ის, რასაც გაშვების დრო ზოგავს.
არჩევანი პატარა სერვერისთვის#
აი, როგორ ჯდება პარამეტრები რეალურ შემთხვევაზე: ASP.NET Core API EF Core-ით, GitHub-იდან deploy-ებული 1 GB გეგმაზე ნახევარი vCPU-ით.
- დაიწყე framework-dependent-ით.
dotnet publish -c Release -o outდაdotnet out/Api.dll. ყველაზე პატარა შედეგი, runtime-თან ერთად patch-დება, სიურპრიზების გარეშე. - თუ სერვერის runtime არასწორი მთავარი ვერსიისაა, გააკეთე self-contained publish
linux-x64-ისთვის და ნუ ებრძვი roll-forward-ს. გახსოვდეს, რომ runtime-ის patch-ები ახლა შენზეა: ხელახლა ააგე, როცა .NET-ის უსაფრთხოების განახლება გამოვა. - თუ restart-ები ნელია, ყველაფერზე ადრე დაამატე ReadyToRun. მას თავსებადობის რისკი არ აქვს.
- trimming და AOT გამორთული დატოვე ამ აპლიკაციისთვის. EF Core და reflection-ზე აგებული სტეკი ისეთ გაფრთხილებებს გამოიწვევს, რომელთა უგულებელყოფაც უსაფრთხო არ არის.
- გამოტოვე single-file. ის არაფერს ამატებს, როცა deploy Git pull-ია.
სადაც ხდება build, ისევე მნიშვნელოვანია, როგორც ის, თუ როგორ. სერვერზე აგება ნიშნავს, რომ SDK, MSBuild და კომპილატორი ყოველ გაშვებაზე გეგმის მეხსიერების ლიმიტის შიგნით მუშაობს, ხოლო RE:NODE-ზე პროცესი, რომელიც მეხსიერების ლიმიტს აღწევს, ჩერდება და კონტეინერი სუფთად იწყება ხელახლა და არა swap-ში გადადის - ამიტომ დიდი build ყველაზე პატარა გეგმაზე შეიძლება შუა გზაზე ჩავარდეს. GitHub Actions-ში აგება და შედეგის deploy (ბრენჩიდან, რომელსაც სერვერი მიჰყვება) ამ სამუშაოს სერვერიდან მთლიანად აშორებს. ASP.NET Core აპლიკაციის deploy GitHub-იდან ორივე მოწყობას გადის, ხოლო .NET-ის მეხსიერება და garbage collection ხსნის, რამდენს გამოიყენებს თავად აპლიკაცია, როცა გაეშვება.
იგივე არჩევანი ვებს გარეთაც მოქმედებს: worker service ან ბოტი ზუსტად ასევე ქვეყნდება, და .NET worker service-ები და ფონური დავალებები მათი ჰოსტინგის მოდელს განიხილავს.
FAQ#
self-contained უფრო სწრაფია, ვიდრე framework-dependent?
არა. ეს იგივე runtime და იგივე JIT-ია; იცვლება მხოლოდ ის, საიდან მოდის ფაილები. გაშვების სიჩქარე ReadyToRun-იდან ან Native AOT-იდან მოდის და არა runtime-ის ჩაფუთვიდან.
რატომ ითხოვს ჩემი გამოქვეყნებული აპლიკაცია .NET-ის ვერსიას, რომელიც მეგონა, რომ მქონდა?
სერვერზე სხვა მთავარი ვერსიაა დაყენებული, ვიდრე ის, რაც runtimeconfig.json-შია, და ნაგულისხმევი roll-forward პოლიტიკა მთავარ ვერსიებს არ კვეთს. შეცვალე target, გააკეთე self-contained publish, ან შეგნებულად დააყენე RollForward.
შემიძლია publish Windows-ზე გავაკეთო და Linux-ზე გავუშვა?
კი, ყველაფრისთვის, გარდა Native AOT-ისა. framework-dependent IL პლატფორმისგან დამოუკიდებელია, ხოლო self-contained build linux-x64-ისთვის Windows-ზეც შეიძლება შეიქმნას. განსხვავდება მხოლოდ apphost, და შეგიძლია ნაცვლად მისა dotnet App.dll-ით გაუშვა.
მჭირდება SDK სერვერზე?
მხოლოდ თუ იქ აგებ. გამოქვეყნებული აპლიკაციის გაშვებას მხოლოდ runtime სჭირდება, ან საერთოდ არაფერი, თუ self-contained-ია. SDK-ის გაშვების გზიდან მოშორება CI-ში აგების ერთ-ერთი მიზეზია.
უნდა გავუკეთო commit publish-ის შედეგს Git-ში?
არა შენს მთავარ ბრენჩში. თუ წინასწარ აგებულ შედეგს deploy-ავ, შეინახე ის ცალკე ბრენჩში, რომელსაც CI წერს, რომ წყაროს ისტორია წაკითხვადი დარჩეს. .NET-ის ვერსიები და LTS მხარდაჭერა განიხილავს target framework-ის აქტუალურად შენარჩუნებას, რაც სტაბილური build-ის მეორე ნახევარია.




კომენტარები
სრულიად ანონიმურად: ანგარიშის, ელფოსტის და cookie-ის გარეშე. ინახება მხოლოდ სახელი, ტექსტი და დრო - სხვა არაფერი. ბმულების რაოდენობა ლიმიტირებულია.