ASP.NET Core ერთ-ერთი ყველაზე სწრაფი ვებ სტეკია, და პატარა სერვერზე მისი შენელება მაინც ადვილია. პრობლემა იშვიათად framework-შია. ნახევარ ბირთვსა და გიგაბაიტ მეხსიერებაზე ფასს გახდევინებს async კოდზე დაბლოკვა, თითო მოთხოვნაზე მეგაბაიტების allocation, შეუკუმშავი JSON-ის გაგზავნა, ერთი და იმავე პასუხის წუთში ათასჯერ ხელახლა გამოთვლა და მოთხოვნების შეუზღუდავი რიგის დაგროვების დაშვება, ნაცვლად იმისა, რომ ზედმეტზე უარი თქვა. გაასწორე ეს ხუთი და ერთი პატარა ინსტანსი იმაზე მეტ ტრაფიკს გაუმკლავდება, ვიდრე პროექტების უმეტესობა ოდესმე ნახავს. ეს პოსტი თითოეულს გადის პარამეტრებით, მათი ნაგულისხმევი მნიშვნელობებით და იმით, როგორ გაიგო, რომელი გტკენს რეალურად, სანამ რამეს შეცვლი.
რას ცვლის პატარა სერვერი#
დეველოპერის ლეპტოპზე მორგებული .NET აპლიკაცია კონტეინერში ორ მყარ ლიმიტს ხვდება: CPU-ს კვოტას და მეხსიერების ლიმიტს. ორივე ცვლის, როგორ იქცევა runtime.
runtime CPU-ს კვოტას კონტეინერის cgroup-იდან კითხულობს და მისგან Environment.ProcessorCount-ს ადგენს, ზემოთ დამრგვალებით. ნახევარი vCPU-ის გეგმაზე აპლიკაცია ფიქრობს, რომ ერთი პროცესორი აქვს, რაც გავლენას ახდენს იმაზე, რამდენ garbage collector heap-ს ქმნის, რამდენი thread pool thread-ით იწყებს და რა პარალელიზმს ირჩევენ ბიბლიოთეკები ნაგულისხმევად. კვოტა მაინც მყარი შემზღუდველია: როცა პროცესი დაგეგმვის პერიოდში თავის წილს გამოიყენებს, ის შემდეგს ელოდება, რაც შეცდომად კი არა, დაყოვნების ნახტომებად ჩანს.
მეხსიერება მეორე ჭერია. garbage collector კონტეინერის ლიმიტს კითხულობს და heap-ის ბიუჯეტს მის მიხედვით ზომავს, ამიტომ აპლიკაცია უსაზღვროდ არ გაიზრდება, მაგრამ ყოველი allocation მაინც CPU-ს დროს ჯდება მოგვიანებით, collection-ებში. RE:NODE-ზე სერვერს, რომელიც მეხსიერების ლიმიტს აღწევს, kernel აჩერებს და სუფთად რესტარტავს და არა swap-ში ტოვებს, ამიტომ მეხსიერების ნახტომი გათიშვაა და არა შენელება. .NET-ის მეხსიერება და garbage collection collector-ის პარამეტრებს სიღრმისეულად განიხილავს; ეს პოსტი იმას ფარავს, რას აკეთებს აპლიკაცია ამ წნევის შესაქმნელად.
| გეგმის ზომა | რას ატარებს თავისუფლად | სად ეწურება პირველად |
|---|---|---|
| 0.5 vCPU, 1 GB | API ან პატარა საიტი, წამში ათობით მოთხოვნა | CPU ნახტომებისა და GC-ის დროს |
| 1 vCPU, 2 GB | დატვირთული API, Blazor Server რამდენიმე ათეული მომხმარებლისთვის | მეხსიერება კავშირზე, CPU სერიალიზაციაში |
| 1.5-2 vCPU, 4 GB | პროდუქტი რეალური ტრაფიკით და ფონური ამოცანებით | მონაცემთა ბაზასთან მიმოსვლები და არა აპლიკაცია |
| 3 vCPU, 8 GB | რამდენიმე აპლიკაცია ან მძიმე in-process ქეშირება | ამ დროისთვის ჩვეულებრივ დიზაინის პრობლემა გაქვს |
ეს რიგები უხეშია. აპლიკაცია, რომელიც server-side გვერდებს დიდი მოდელებით არენდერებს ან დიდ ქეშებს მეხსიერებაში ინახავს, ერთი რიგით ზემოთ ადის; თხელი JSON API კარგად ინდექსირებულ მონაცემთა ბაზაზე ერთით ქვემოთ ჩადის.
ჯერ გაზომე, მერე ააწყვე#
ქვემოთ მოცემულ ყოველ ნაბიჯს თავისი ფასი აქვს, და პატარა აპლიკაციებზე წარმადობის სამუშაოს უმეტესობა არასწორ ფენაზე იხარჯება. ჯერ სამი რიცხვი მოიპოვე:
- სად მიდის დრო ერთ ნელ მოთხოვნაში? დალოგე თითო მოთხოვნაზე გასული დრო (Serilog-ის request logging ამას ერთ ხაზში აკეთებს) და, ნელებისთვის, რამდენი დასჭირდა მონაცემთა ბაზის ნაწილს. EF Core თითო ბრძანების ხანგრძლივობას
Information-ზე ლოგავსMicrosoft.EntityFrameworkCore.Database.Command-ში; მოკლე დროით ჩართე. - არის CPU დატვირთვისას თავის ლიმიტზე? პანელის CPU გრაფიკი გეგმის ლიმიტთან შედარებით ამაზე პასუხობს. ჭერზე ბრტყელი ხაზი CPU-ზე დამოკიდებულებას ნიშნავს; დაბალი CPU მაღალი დაყოვნებით ნიშნავს, რომ რაღაცას ელოდები - მონაცემთა ბაზას, გარე API-ს ან thread pool-ს.
- როგორ იქცევა კონკურენტულობისას? გაუშვი დატვირთვის ტესტი სხვა მანქანიდან
k6-ით,bombardier-ით ანwrk-ით მოთხოვნების რეალისტური ნარევით და უყურე დაყოვნების პერცენტილებს და არა საშუალოს.
შენს მანქანაზე dotnet-counters runtime-ის ხედს გაძლევს, სანამ დატვირთვის ტესტი მუშაობს:
$ dotnet tool install --global dotnet-counters$ dotnet-counters monitor --name MyApp System.Runtime Microsoft.AspNetCore.Hostingმნიშვნელოვანი მრიცხველებია thread pool-ის რიგის სიგრძე, thread pool-ის thread-ების რაოდენობა, GC heap-ის ზომა და GC-ში გატარებული დრო, და მოთხოვნები წამში. მზარდი thread pool-ის რიგი დაბალი CPU-თი .NET-ის წარმადობის ყველაზე გავრცელებული ხარვეზის ნიშანია, რომელიც შემდეგია. მრიცხველების ზუსტი სახელები .NET 8-სა და .NET 9-ს შორის შეიცვალა, როცა runtime ახალ System.Runtime meter-ზე გადავიდა, ამიტომ წაიკითხე ისინი ინსტრუმენტის გამოტანიდან და არა ძველი ბლოგ პოსტიდან.
Thread pool starvation და sync-over-async#
ASP.NET Core მოთხოვნებს thread pool-ის thread-ებზე ემსახურება. async მეთოდი, რომელიც I/O-ს ელოდება, ლოდინის დროს თავის thread-ს აბრუნებს, ამიტომ მცირე რაოდენობის thread-ებს ათასობით ერთდროული მოთხოვნის მომსახურება შეუძლია. კოდი, რომელიც async სამუშაოზე იბლოკება - .Result, .Wait(), .GetAwaiter().GetResult(), ან სინქრონული მონაცემთა ბაზის ან HTTP გამოძახება - thread-ს მთელი ლოდინის განმავლობაში იკავებს.
// Blocks a thread pool thread for the full round tripvar user = db.Users.FirstOrDefaultAsync(u => u.Id == id).Result;// Hands the thread back while waitingvar user = await db.Users.FirstOrDefaultAsync(u => u.Id == id);როცა საკმარისი thread-ია დაბლოკილი, ახალი მოთხოვნები რიგში დგება. pool მართლა ამატებს thread-ებს, მაგრამ შეგნებულად ნელა - მისი injection-ის ევრისტიკა ისეა შექმნილი, რომ ზედმეტად არ რეაგირებდეს, და წამში რამდენიმე thread-ის დამატება ახლოსაც არ არის საკმარისი, როცა ნახტომი მოდის. შედეგი starvation-ის კლასიკური სურათია: CPU დაბალია, დაყოვნება წამებამდე იზრდება, შემდეგ timeout-ები, შემდეგ კი აღდგენა, როცა ნახტომი გადის. ერთბირთვიან გეგმაზე pool ძალიან ცოტა thread-ით იწყება, ამიტომ ამის გამოსაწვევად გასაკვირად ცოტა დაბლოკვაა საკმარისი.
გამოსწორება გზის ბოლომდე async-ად ქცევაა. ჩვეულებრივი დამნაშავეები:
- სინქრონული EF Core გამოძახებები (
ToList(),SaveChanges(),First()) მოთხოვნის handler-ებში. გამოიყენეAsyncვერსიები. HttpClient-ის გამოძახებები.Result-ში შეფუთული, რადგან გამომძახებელი async არ იყო. გახადე გამომძახებელი async.- მოთხოვნის body-ს სინქრონული წაკითხვა. Kestrel მათ ნაგულისხმევად უარყოფს (
AllowSynchronousIOარისfalse)InvalidOperationException-ით, ხოლო ამ პარამეტრის ჩართვა შეცდომის გასაჩუმებლად ზუსტად ის გზაა, რომლითაც starvation-ს ხელახლა შემოიტან. - Lock-ები async სამუშაოს გარშემო. გამოიყენე
SemaphoreSlim.WaitAsync()და არაlock, რომელიცawait-ს მაინც ვერ შეიცავს.
ThreadPool.SetMinThreads thread-ების საწყის რაოდენობას ზრდის და ლეგიტიმური დროებითი გამოსავალია, სანამ დამბლოკველ გამოძახებებს მოაშორებ, მაგრამ ეს მხოლოდ დროებითია: მეტი დაბლოკილი thread ერთ ბირთვზე მაინც მეტ მეხსიერებასა და მეტ კონტექსტის გადართვას ნიშნავს.
Kestrel-ის ლიმიტები, რომლებიც პატარა სერვერს იცავს#
Kestrel-ის ნაგულისხმევი მნიშვნელობები გულუხვია, რადგან ყველა ზომის სერვერისთვისაა დაწერილი. პატარა სერვერს სარგებელს აძლევს ადრე უარის თქმა, ნაცვლად იმ სამუშაოს მიღებისა, რომელსაც ვერ დაასრულებს.
| პარამეტრი | ნაგულისხმევი | რატომ შეიძლება შეცვალო |
|---|---|---|
MaxConcurrentConnections | შეუზღუდავი | შეზღუდე ღია კავშირები, რომ ნაკადი proxy-ზე დადგეს რიგში და არა შენს მეხსიერებაში |
MaxConcurrentUpgradedConnections | შეუზღუდავი | იგივე WebSocket-ებისა და SignalR-ისთვის |
MaxRequestBodySize | 30,000,000 bytes | შეამცირე API-სთვის, რომელიც ატვირთვებს არასოდეს იღებს |
KeepAliveTimeout | 130 წამი | უფრო მოკლე უქმ კავშირებს ადრე ათავისუფლებს |
RequestHeadersTimeout | 30 წამი | უფრო მოკლე ზღუდავს ნელი header-ების შეტევებს |
MinRequestBodyDataRate | 240 bytes/s 5 წამიანი შეღავათის შემდეგ | აგდებს კლიენტებს, რომლებიც body-ს წვეთ-წვეთად აგზავნიან კავშირის შესანარჩუნებლად |
MaxRequestHeadersTotalSize | 32 KB | შეცვლა იშვიათად სჭირდება |
builder.WebHost.ConfigureKestrel(options =>{ options.Limits.MaxConcurrentConnections = 500; options.Limits.MaxConcurrentUpgradedConnections = 200; options.Limits.MaxRequestBodySize = 2 * 1024 * 1024; options.Limits.KeepAliveTimeout = TimeSpan.FromSeconds(60);});იგივე მნიშვნელობები შეიძლება კონფიგურაციაში იყოს Kestrel:Limits-ის ქვეშ, რაც მათ გარემოს ცვლადით შეცვლის საშუალებას გაძლევს, მაგალითად Kestrel__Limits__MaxConcurrentConnections=500, ხელახალი build-ის ნაცვლად. ცალკეულ endpoint-ს, რომელიც ატვირთვებს მართლა იღებს, body-ს ლიმიტის საკუთარი თავისთვის გაზრდა შეუძლია [RequestSizeLimit]-ით ან IHttpMaxRequestBodySizeFeature-ით.
თუმცა კავშირების ლიმიტები მიმდინარე სამუშაოს არ ზღუდავს. ამისთვის rate limiting middleware-ს (ყუთში .NET 7-დან) აქვს concurrency limiter, რაც პატარა ინსტანსისთვის ყველაზე სასარგებლო ცალკეული დაცვაა: ის ერთდროულად მოთხოვნების ფიქსირებულ რაოდენობას უშვებს, რამდენიმეს რიგში აყენებს და დანარჩენს 503-ით უარყოფს, ნაცვლად იმისა, რომ დაყოვნებას უსაზღვროდ გაზრდის საშუალება მისცეს.
builder.Services.AddRateLimiter(options =>{ options.RejectionStatusCode = StatusCodes.Status503ServiceUnavailable; options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(_ => RateLimitPartition.GetConcurrencyLimiter("global", _ => new ConcurrencyLimiterOptions { PermitLimit = 64, QueueLimit = 32 }));});app.UseRateLimiter();კლიენტზე ლიმიტები (fixed window, sliding window, token bucket) იმავე middleware-ში ცხოვრობს და სწორი ინსტრუმენტია ერთი ხმაურიანი კლიენტის წინააღმდეგ, თუ forwarded header-ები დაკონფიგურირებულია ისე, რომ partition-ის გასაღები კლიენტის რეალური მისამართი იყოს. Rate limit-ები და ბოროტად გამოყენება რიცხვების არჩევის ლოგიკას შეიცავს.
პასუხების შეკუმშვა#
JSON და HTML თავისი ზომის მცირე ნაწილამდე იკუმშება, ხოლო პატარა სერვერზე შემზღუდველი იშვიათად არის გამტარუნარიანობა - შემზღუდველი კლიენტისთვის დაყოვნებაა. შეკუმშვა ცოტა CPU-ს ცვლის ხაზზე გაცილებით ნაკლებ დროზე.
builder.Services.AddResponseCompression(options =>{ options.EnableForHttps = true; options.Providers.Add<BrotliCompressionProvider>(); options.Providers.Add<GzipCompressionProvider>();});builder.Services.Configure<BrotliCompressionProviderOptions>(o => o.Level = CompressionLevel.Fastest);app.UseResponseCompression();სამი სიფრთხილე. EnableForHttps ნაგულისხმევად გამორთულია, რადგან TLS-ზე იმ პასუხების შეკუმშვა, რომლებიც საიდუმლოებებს თავდამსხმელის მიერ კონტროლირებად შეყვანასთან ურევს, CRIME და BREACH ოჯახის შეტევების წინაპირობაა; ის უსაფრთხოა საჯარო JSON-ისა და გვერდებისთვის, რომლებშიც საიდუმლოებები არ არის, ხოლო ტოკენების შემცველ პასუხებზე ჩართვამდე უნდა დაფიქრდე. CompressionLevel.Fastest სწორი დონეა დინამიკური პასუხებისთვის პატარა CPU-ზე; optimal დონემ შეიძლება რამდენჯერმე მეტი CPU დაჯდეს გამოტანის რამდენიმე პროცენტით შემცირებისთვის. და თუ აპლიკაციის წინ მდგომი reverse proxy უკვე კუმშავს, ორჯერ ნუ შეკუმშავ: ეს CPU-ს ტყუილად ხარჯავს, და proxy ჩვეულებრივ შენს შეკუმშულ body-ს მაინც უცვლელად გაატარებს. შეამოწმე პასუხის header-ები - Content-Encoding: br ან gzip - აპლიკაციაში გამორთული შეკუმშვით, და გაიგებ, აკეთებს თუ არა ამას proxy.
სტატიკური ფაილები ჯობს ერთხელ შეიკუმშოს build-ის დროს, ვიდრე ყოველ მოთხოვნაზე. MapStaticAssets() .NET 9-სა და უფრო ახალში ამას build-ის დროს ცნობილი ფაილებისთვის აკეთებს და წინასწარ შეკუმშულ ვერსიებს ემსახურება fingerprint-იანი სახელებით და ქეშის ხანგრძლივი ვადით.
Output caching და response caching#
ყველაზე იაფი მოთხოვნა ის არის, რომელსაც არ ითვლი. ASP.NET Core-ს ქეშირების ორი middleware აქვს და მათ ხშირად ურევენ:
- Response caching (
AddResponseCaching) HTTP ქეშირების header-ებს მიჰყვება. ის მხოლოდ იმას ქეშავს, რასაც header-ები უშვებს, ხოლო ბრაუზერი, რომელიცCache-Control: no-cache-ს აგზავნის, მას გვერდს უვლის. ის ძირითადად სასარგებლოა ქვემოთ მდგომი ქეშებისთვის header-ების სწორად დასაყენებლად. - Output caching (
AddOutputCache, .NET 7 და უფრო ახალი) არის სერვერის მხარის ქეში, რომელსაც შენ აკონტროლებ. კლიენტს მისი გვერდის ავლა არ შეუძლია, ჩანაწერებს შეიძლება ტეგები მიენიჭოს და გამოიდევნოს, ხოლო ერთსა და იმავე დაუქეშავ ჩანაწერზე ერთდროული მოთხოვნები ერთ გამოთვლად ერთიანდება, რაც პატარა სერვერს ნახტომისგან იცავს.
builder.Services.AddOutputCache(options =>{ options.AddBasePolicy(b => b.Expire(TimeSpan.FromSeconds(10))); options.AddPolicy("Products", b => b.Expire(TimeSpan.FromMinutes(5)).Tag("products"));});app.UseOutputCache();app.MapGet("/products", GetProducts).CacheOutput("Products");app.MapPost("/products", async (Product p, IOutputCacheStore cache, CancellationToken ct) =>{ // ... save await cache.EvictByTagAsync("products", ct);});ნაგულისხმევად output caching ინახავს მხოლოდ GET და HEAD პასუხებს 200 სტატუსით და გამოტოვებს ავთენტიფიცირებულ მოთხოვნებს და პასუხებს, რომლებიც cookie-ებს აყენებს, რაც ერთი მომხმარებლის გვერდს მეორისთვის მიწოდებისგან იცავს. ნაგულისხმევი საცავი მეხსიერებაშია 100 MB-იანი ზომის ლიმიტით; 1 GB-იან გეგმაზე SizeLimit უფრო დაბლა დააყენე. რამდენიმე ინსტანსისთვის, რომლებიც ერთ ქეშს იზიარებს, .NET 8-მა Redis-ის პროტოკოლის საცავი დაამატა (Microsoft.AspNetCore.OutputCaching.StackExchangeRedis, რეგისტრირდება AddStackExchangeRedisOutputCache-ით), რომელიც Valkey-სთანაც მუშაობს. Valkey-ს ქეშირების პატერნები განიხილავს, რა დააქეშო და როგორ მოაძველო.
დატვირთულ endpoint-ზე output caching-ის ათი წამიც კი წუთში ასობით მონაცემთა ბაზის query-ს ექვსად აქცევს. ეს ჩვეულებრივ ყველაზე დიდი ცალკეული მოგებაა, რაც ხელმისაწვდომია, და რამდენიმე ხაზი ღირს.
Allocation-ები, სერიალიზაცია და მონაცემთა ბაზა#
პატარა სერვერზე garbage collector შენს მოთხოვნებს ერთსა და იმავე CPU-სთვის ეჯიბრება, ამიტომ allocation-ის სიჩქარე გამტარუნარიანობაა. ჩვეულებრივი წყაროები, დაახლოებით იმის მიხედვით, რამდენად ხშირად აქვს მნიშვნელობა:
- მონაცემთა ბაზიდან საჭიროზე მეტის ჩატვირთვა.
ToListAsync()გაუფილტრავ query-ზე, ან entity-ები ყველა სვეტით, როცა გვერდი სამს აჩვენებს. გააკეთე პროექციაSelect-ით პატარა record-ში, დაყავი გვერდებადSkip-ითა დაTake-ით და წაკითხვისთვის გამოიყენეAsNoTracking()- change tracking ყოველი ჩატვირთული entity-ის snapshot-ს ინახავს. - N+1 query-ები. ციკლი, რომელიც თითო რიგზე relation-ს lazy-ად ტვირთავს, ერთ მოთხოვნას ას მიმოსვლად აქცევს. გამოიყენე
Includeან პროექცია და წაიკითხე SQL, რომელსაც EF Core რეალურად აგენერირებს. - დიდი პასუხების buffer-ში შენახვა. ორმოცდაათი ათასი რიგის
List<T>-ის დაბრუნება მთელ სიას და შემდეგ მთელ JSON-ს მეხსიერებაში აწყობს. დაყავი გვერდებად ან დააბრუნეIAsyncEnumerable<T>, რომელსაცSystem.Text.Jsonstream-ად გადასცემს. - ახალი `HttpClient` თითო მოთხოვნაზე. გამოიყენე
IHttpClientFactoryან ერთი ხანგრძლივად მცხოვრები კლიენტი. თითო მოთხოვნაზე კლიენტების შექმნა socket-ებს ამოწურავს და ტყუილად აკეთებს allocation-ს. - Reflection-ზე დაფუძნებული სერიალიზაცია ცხელ გზებზე.
System.Text.Json-ის source generation (JsonSerializerContext) reflection-ს და allocation-ის ნაწილს აშორებს, და trimmed ან Native AOT build-ებისთვის ისედაც აუცილებელია. - სტრიქონების აწყობა ციკლებში და დიდი `byte[]` buffer-ები. ამისთვის არსებობს
StringBuilder,ArrayPool<T>.SharedდაSpan<T>.
მონაცემთა ბაზა ჩვეულებრივ ყველაზე ნელი კომპონენტია პატარა deploy-ში, და ამ პოსტში არაფერი ეხმარება query-ს, რომელსაც ინდექსი აკლია. connection pool, რომლის ზომაც ბევრად აღემატება იმას, რისი მომსახურებაც მონაცემთა ბაზას შეუძლია, ასევე უფრო ავნებს, ვიდრე ეხმარება; Connection pool-ები და ლიმიტები ხსნის, რატომ არის სწორი pool იმაზე პატარა, ვიდრე ხალხი ელის.
გაშვება, GC რეჟიმი და publish-ის პარამეტრები#
build-ისა და runtime-ის რამდენიმე პარამეტრი კონკრეტულად პატარა მანქანებზეა მნიშვნელოვანი:
<PropertyGroup> <InvariantGlobalization>true</InvariantGlobalization> <TieredPGO>true</TieredPGO> <PublishReadyToRun>true</PublishReadyToRun></PropertyGroup>InvariantGlobalization ICU-ს კულტურის მონაცემებს აგდებს, რაც მეხსიერებასა და გაშვების დროს ზოგავს კულტურაზე დამოკიდებული ფორმატირებისა და დახარისხების ხარჯზე - API-ების უმეტესობისთვის კარგია, არასწორია აპლიკაციისთვის, რომელიც თარიღებსა და ვალუტებს თითო მომხმარებლის მიხედვით აფორმატებს. TieredPGO .NET 8-დან უკვე ნაგულისხმევად ჩართულია, ამიტომ მისი ჩამოწერა მხოლოდ განზრახვას აფიქსირებს. PublishReadyToRun კოდს წინასწარ აკომპილირებს, რომ restart-ის შემდეგ პირველი მოთხოვნები JIT-მა არ შეანელოს, რაც მნიშვნელოვანია, როცა deploy აპლიკაციას ტრაფიკის ქვეშ რესტარტავს; publish-ის დროს runtime identifier სჭირდება და გამოტანას აზრდის. dotnet publish და runtime-ის პარამეტრები RID-ებს, self-contained build-ებს და trimming-ს განიხილავს.
ASP.NET Core აპლიკაციები ნაგულისხმევად Server GC-ს იყენებს. ერთი ხილული პროცესორით runtime მაინც workstation GC-ს იყენებს, ხოლო .NET 9-დან Server GC რთავს DATAS-ს, რომელიც heap-ების რაოდენობას დატვირთვას უსადაგებს და მეხსიერებას გაცილებით დაბლა ინახავს, ვიდრე ძველი Server GC. თუ აპლიკაცია 2-ბირთვიან გეგმაზე იმაზე მაღალ საბაზისო მეხსიერებაზე ზის, ვიდრე შეგიძლია გაიღო, <ServerGarbageCollector>false</ServerGarbageCollector> გამტარუნარიანობის ნაწილს უფრო პატარა ნაკვალევზე ცვლის; გადაწყვეტამდე ორივე გაზომე.
FAQ#
წამში რამდენ მოთხოვნას გაუმკლავდება პატარა ASP.NET Core სერვერი?
მარტივი JSON endpoint-ებისთვის, რომლებიც ქეშს ან სწრაფ query-ს მიმართავს, ერთ ბირთვზე ათასობითს. endpoint-ებისთვის, რომლებიც ყოველ ჯერზე მონაცემთა ბაზას მიმართავს, რიცხვს მონაცემთა ბაზა ადგენს, ჩვეულებრივ ასობით. framework იშვიათად არის ლიმიტი; ლიმიტი შენი ყველაზე ნელი დამოკიდებულება და allocation-ის სიჩქარეა.
უსაფრთხოა Kestrel-ის გამოჩენა reverse proxy-ს გარეშე?
Kestrel მხარდაჭერილი edge სერვერია და TLS-სა და HTTP/2-ს თავად ამუშავებს. წინ proxy მაინც სასარგებლოა სერტიფიკატებისთვის, სტაბილური დომენისა და header-ების დამუშავებისთვის, და ნელ კლიენტებს შთანთქავს, სანამ ისინი შენს აპლიკაციამდე მიაღწევს. თუ Kestrel-ს პირდაპირ აჩენ, ამ პოსტის ლიმიტები შეგნებულად დააყენე.
პასუხების შეკუმშვა აპლიკაციაში ჩავრთო თუ proxy-ზე?
ერთ-ერთზე, ორივეზე არა. თუ შენს წინ მდგომი proxy კუმშავს, იქ დატოვე. თუ არა, ჩართე აპლიკაციაში Brotli-თი და gzip-ით ყველაზე სწრაფ დონეზე და დაფიქრდი EnableForHttps-ზე პასუხებისთვის, რომლებიც საიდუმლოებებს შეიცავს.
რატომ ნელდება ჩემი აპლიკაცია დატვირთვისას, როცა CPU დაბალი რჩება?
თითქმის ყოველთვის thread pool starvation async კოდზე დაბლოკვის გამო, ან გაჯერებული მონაცემთა ბაზის connection pool-ის ლოდინი. მზარდი thread pool-ის რიგი dotnet-counters-ში პირველს ადასტურებს; ნელი query-ების ლოგები ან pool-ის timeout შეცდომები - მეორეს.
აქვს აზრი Native AOT-ს პატარა სერვერზე?
ის უფრო სწრაფ გაშვებასა და მეხსიერების უფრო მცირე ნაკვალევს იძლევა, მაგრამ ყველა ბიბლიოთეკა მას არ უჭერს მხარს, და .NET 8-სა და უფრო ახალში ASP.NET Core-ის მხოლოდ ნაწილია თავსებადი - minimal API-ები და gRPC, არა MVC ან Blazor Server. პატარა, დამოუკიდებელი API-სთვის შეიძლება ღირდეს; ტიპური აპლიკაციისთვის EF Core-ითა და MVC-ით ReadyToRun და ზემოთ მოცემული გამოსწორებები უკეთესი გაცვლაა.




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