عیب یابی گلوگاه های عملکرد در برنامه های NET 6

عیب یابی گلوگاه های عملکرد در برنامه های NET 6

مشکلات عملکرد ممکن است زمانی ایجاد شوند که انتظارش را ندارید. این می تواند عواقب منفی برای مشتریان شما داشته باشد. همانطور که پایگاه کاربر رشد می کند، برنامه شما می تواند عقب بماند زیرا قادر به پاسخگویی به تقاضا نیست. خوشبختانه، ابزارها و تکنیک هایی برای مقابله به موقع با این مسائل وجود دارد. آیا رسانه های اجتماعی در کسب و کار طراحی تاثیرگذار است

ما این مقاله را با مشارکت Site24x7 ایجاد کردیم . از شما برای حمایت از شرکای که SitePoint را ممکن می کنند، سپاسگزاریم.

در این برداشت، گلوگاه‌های عملکرد را در یک برنامه NET 6 بررسی می‌کنم. تمرکز روی یک مسئله عملکردی خواهد بود که من شخصاً در تولید دیده‌ام. هدف این است که بتوانید مشکل را در محیط توسعه دهنده محلی خود بازتولید کنید و با مشکل مقابله کنید.

نمونه کد را از GitHub دانلود کنید یا آن را دنبال کنید. راه حل دارای دو API است که نام‌های غیرقابل تخیل First.Api و Second.Api دارند. اولین API برای دریافت اطلاعات آب و هوا به API دوم فراخوانی می کند. این یک مورد استفاده رایج است، زیرا APIها می توانند به APIهای دیگر فراخوانی کنند، به طوری که منابع داده جدا باقی می مانند و می توانند به صورت جداگانه مقیاس شوند.

ابتدا، مطمئن شوید که .NET 6 SDK را نصب کرده اید روی دستگاه شما سپس، یک ترمینال یا یک پنجره کنسول را باز کنید:

> dotnet new webapi --name First.Api --use-minimal- apis --no-https --no-openapi
> dotnet new webapi --name Second.Api --use-minimal-apis --no-https --no-openapi

موارد بالا می توانند در پوشه راه حلی مانند performance-bottleneck-net6 قرار گیرند. این دو پروژه وب با حداقل API، بدون HTTPS، و بدون Swagger یا Open API ایجاد می کند. این ابزار ساختار پوشه را داربست می‌دهد، بنابراین اگر برای راه‌اندازی این دو پروژه جدید به کمک نیاز دارید، لطفاً به کد نمونه نگاه کنید.

فایل راه حل می تواند در پوشه راه حل برود. این به شما امکان می دهد کل راه حل را از طریق یک IDE مانند Rider یا Visual Studio باز کنید:

dotnet new sln --name Performance.Bottleneck.Net6
dotnet sln افزودن First.ApiFirst.Api.csproj
dotnet sln افزودن Second.ApiSecond.Api.csproj

بعد، حتماً شماره پورت‌ها را برای هر پروژه وب تنظیم کنید. در کد نمونه، من آنها را برای اولین API روی 5060 و برای دومی 5176 تنظیم کرده ام. شماره خاص مهم نیست، اما من از آنها برای ارجاع به APIها در سراسر کد نمونه استفاده خواهم کرد. بنابراین، مطمئن شوید که شماره پورت خود را تغییر می‌دهید یا آنچه را که داربست تولید می‌کند حفظ کرده و ثابت می‌مانید.

برنامه متخلف

فایل Program.cs را در API دوم باز کنید و کدی را که با داده‌های آب و هوا پاسخ می‌دهد در جای خود قرار دهید:

وار سازنده = WebApplication.CreateBuilder(args);
وار برنامه = سازنده.ساخت();
خلاصه‌های وار = جدید[]
{
 "انجماد"، "بند‌بندی"، "سرد"، " جالب"، "خفیف"، "گرم"، "بالمی"، "داغ"، " داغ"، "سوزاننده"
};

برنامه.MapGet("/weatherForecast"، ناهمگام () =>
{
 انتظار کار.تاخیر(10);
 بازگشت قابل شمارش
   .محدوده(، 1000)
   .انتخاب(index =>
     جدید پیش بینی آب و هوا
     (
       DateTime.اکنون.AddDays(index)،
       تصادفی.به اشتراک گذاشته شده.بعدی(-20، 55)،
       خلاصه ها[تصادفی.اشتراک گذاشته شده.بعدی(summaries.طول)]
     )
   )
   .ToArray()[..5 ];
});

برنامه.اجرا();

عمومی  رکورد پیش بینی آب و هوا(
 DateTime تاریخ،
 int TemperatureC،
 رشته؟ خلاصه )
{
 عمومی int TemperatureF => 32 + ( int)(TemperatureC / 0.5556);
}

ویژگی حداقل APIها در NET 6 به کوچک و مختصر نگه داشتن کد کمک می کند. این یک هزار رکورد را به چرخش در می آورد و یک تاخیر کار را برای شبیه سازی پردازش داده های ناهمگام انجام می دهد. در یک پروژه واقعی، این کد می تواند به یک حافظه پنهان توزیع شده یا یک پایگاه داده که یک عملیات محدود به IO است، فراخوانی کند.

اکنون، به فایل Program.cs در اولین API بروید و کدی را بنویسید که از این داده های آب و هوا استفاده می کند. شما به سادگی می توانید این را کپی-پیست کنید و هر چیزی را که داربست ایجاد می کند جایگزین کنید:

وار سازنده = WebApplication.CreateBuilder(args);
سازنده.خدمات.AddSingleton(_ => جدید HttpClient(
 جدید SocketsHttpHandler
 {
   PooledConnectionLifetime = TimeSpan.از دقیقه(5)
 })
{
 BaseAddress = جدید اوری("http://localhost:5176")
});

وار برنامه = سازنده.ساخت();

برنامه.MapGet("/"، ناهمگام (HttpClient کلاینت) =>
{
 var نتیجه = جدید فهرستلیست>؟>();

 برای (var i = ; i  100; i++)
 {
   نتیجه.افزودن(
     در انتظار مشتری.GetFromJsonAsync فهرستپیش بینی آب و هوا>>(
       "/weatherForecast"));
 }

 نتیجه بازگشت[تصادفی.به اشتراک گذاشته شده.بعدی(، 100)];
});

برنامه.اجرا();

عمومی  رکورد پیش بینی آب و هوا(
 DateTime تاریخ،
 int TemperatureC،
 رشته؟ خلاصه )
{
 عمومی int TemperatureF => 32 + ( int)(TemperatureC / 0.5556);
}

HttpClient به صورت تکی تزریق می شود، زیرا این باعث می شود مشتری مقیاس پذیر باشد. در دات نت، یک کلاینت جدید سوکت هایی را در سیستم عامل اصلی ایجاد می کند، بنابراین یک تکنیک خوب استفاده مجدد از آن اتصالات با استفاده مجدد از کلاس است. در اینجا، سرویس گیرنده HTTP یک طول عمر استخر اتصال را تنظیم می کند. این به مشتری امکان می دهد تا زمانی که لازم است به سوکت ها متصل شود.

آدرس پایه به سادگی به مشتری می‌گوید کجا باید برود، بنابراین مطمئن شوید که به شماره پورت صحیح تنظیم شده در API دوم اشاره می‌کند.

وقتی درخواستی وارد می‌شود، کد صد بار حلقه می‌شود، سپس به API دوم فراخوانی می‌شود. این برای شبیه سازی، به عنوان مثال، تعدادی از رکوردهای لازم برای برقراری تماس با سایر API ها است. تکرارها به صورت هاردکد هستند، اما در یک پروژه واقعی این می‌تواند فهرستی از کاربران باشد که می‌تواند بدون محدودیت با رشد کسب‌وکار رشد کند.

اکنون، توجه خود را بر روی حلقه متمرکز کنید، زیرا این امر پیامدهایی در تئوری عملکرد دارد. در تحلیل الگوریتمی، یک حلقه دارای پیچیدگی خطی Big-O یا O(n) است. اما، API دوم نیز حلقه می‌شود، که الگوریتم را به پیچیدگی درجه دوم یا O(n^2) می‌رساند. همچنین، حلقه از یک مرز IO عبور می کند تا بوت شود، که عملکرد را کاهش می دهد.

این یک اثر ضربی دارد، زیرا برای هر تکرار در اولین API، API دوم هزار بار حلقه می‌شود. 100 * 1000 تکرار وجود دارد. به یاد داشته باشید، این لیست ها محدود نیستند، به این معنی که با رشد مجموعه داده ها، عملکرد به طور تصاعدی کاهش می یابد.

وقتی مشتریان عصبانی به مرکز تماس هرزنامه می‌فرستند تا تجربه کاربری بهتری داشته باشند، از این ابزارها استفاده کنید تا بفهمید چه خبر است.

CURL و NBomber

اولین ابزار کمک می کند تا مشخص شود که روی کدام API تمرکز کنید. هنگام بهینه سازی کد، امکان بهینه سازی بی پایان همه چیز وجود دارد، بنابراین از بهینه سازی زودرس خودداری کنید. هدف این است که عملکرد “فقط به اندازه کافی خوب” باشد، و این امر ذهنی است و بر اساس خواسته های تجاری هدایت می شود.

ابتدا، به عنوان مثال، با استفاده از CURL، هر یک از APIها را به صورت جداگانه فراخوانی کنید تا تأخیر را احساس کنید:

> کور - i -o /dev/null -s -w %{time_total} http://localhost:5060
> curl -i -o /dev/null -s -w % {time_total} http://localhost:5176

شماره پورت 5060 متعلق به API اول و 5176 متعلق به دومی است. بررسی کنید که آیا این پورت‌ها در دستگاه شما درست هستند یا خیر.

دومین API در کسری از ثانیه پاسخ می‌دهد که به اندازه کافی خوب است و احتمالاً مقصر نیست. اما اولین API تقریباً دو ثانیه طول می کشد تا پاسخ دهد. این غیرقابل قبول است، زیرا وب سرورها می توانند درخواست هایی را که این مدت طول می کشد، از بین ببرند. همچنین، تأخیر دو ثانیه‌ای از دیدگاه مشتری بسیار کند است، زیرا تأخیر مخرب است.

در مرحله بعد، ابزاری مانند NBomber به محک زدن API مشکل ساز کمک می کند.

به کنسول برگردید و در داخل پوشه ریشه، یک پروژه آزمایشی ایجاد کنید:

dotnet new console -n NBomber.Tests
cd NBomber.Tests
dotnet افزودن بسته NBomber
dotnet افزودن بسته NBomber.Http
cd ..
dotnet sln افزودن NBomber.TestsNBomber.Tests.csproj

در فایل Program.cs، معیارها را بنویسید:

استفاده از NBomber.قراردادها;
استفاده از NBomber.CSharp;
استفاده از NBomber.افزونه ها.Http.CSharp;

var گام = مرحله.ایجاد(
 "fetch_first_api"،
 clientFactory: HttpClientFactory.ایجاد()،
 اجرا: ناهمگام زمینه =>
 {
   var درخواست = Http
     .ایجاد درخواست("GET"، "http://localhost:5060/")
     .WithHeader("پذیرش"، "application/json") ;
   var پاسخ = انتظار Http.ارسال(درخواست، context);

   بازگشت پاسخ.StatusCode == 200
     ؟ پاسخ.خوب(
       statusCode: پاسخ.StatusCode،
       sizeBytes: پاسخ.SizeBytes)
     : پاسخ.شکست();
 });

var سناریو = ScenarioBuilder
 .CreateScenario("first_http"، گام)
 .WithWarmUpDuration(TimeSpan.از ثانیه(5))
 .WithLoadSimulations(
   شبیه سازی.InjectPerSec(نرخ: 1، در طول: TimeSpan.از ثانیه(5) )،
   شبیه سازی.InjectPerSec(نرخ: 2، در طول: TimeSpan.FromSeconds(10) )،
   شبیه سازی.InjectPerSec(نرخ: 3، در طول: TimeSpan.FromSeconds(15) )
 );

NBomberRunner
.RegisterScenarios(scenario)
.اجرا();

NBomber فقط API را با سرعت یک درخواست در ثانیه ارسال می کند. سپس، در فواصل زمانی، دو بار در ثانیه برای ده ثانیه بعدی. در نهایت، سه بار در ثانیه برای 15 ثانیه بعدی. این باعث می‌شود دستگاه توسعه‌دهنده محلی با درخواست‌های بیش از حد بارگذاری نشود. NBomber همچنین از سوکت‌های شبکه استفاده می‌کند، بنابراین زمانی که API هدف و ابزار محک روی یک دستگاه اجرا می‌شوند، با احتیاط حرکت کنید.

مقاله دیگر :   قبل از خرید کولر صنعتی، هواساز، فن سانتریفیوژ، هواکش و تجهیزات تصفیه هوا اشتباه نکنید !!!

مرحله تست کد پاسخ را ردیابی می کند و آن را در مقدار بازگشتی قرار می دهد. این کار خرابی های API را پیگیری می کند. در NET، زمانی که سرور Kestrel درخواست‌های زیادی دریافت می‌کند، درخواست‌هایی را که پاسخ خرابی دارند رد می‌کند.

اکنون، نتایج را بررسی کنید و تأخیر، درخواست‌های همزمان و توان عملیاتی را بررسی کنید.

تأخیرهای P95 1.5 ثانیه را نشان می دهد، که اکثر مشتریان آن را تجربه خواهند کرد. توان عملیاتی کم می ماند، زیرا ابزار کالیبره شده بود تا تنها تا سه درخواست در ثانیه برود. در یک ماشین توسعه دهنده محلی، تشخیص همزمانی سخت است، زیرا همان منابعی که ابزار معیار را اجرا می‌کنند برای ارائه درخواست‌ها نیز ضروری هستند.

تحلیل dotTrace

بعد، ابزاری را انتخاب کنید که بتواند تجزیه و تحلیل الگوریتمی مانند dotTrace را انجام دهد. این به جداسازی بیشتر مشکل عملکرد کمک می کند.

برای انجام یک تجزیه و تحلیل، dotTrace را اجرا کنید و پس از اینکه NBomber هرزنامه‌ای را تا حد ممکن به API ارسال کرد، یک عکس فوری بگیرید. هدف این است که یک بار سنگین را شبیه سازی کنید تا مشخص شود کندی از کجا می آید. بنچمارک‌هایی که قبلاً ایجاد شده‌اند به اندازه کافی خوب هستند، بنابراین مطمئن شوید که dotTrace را همراه با NBomber اجرا می‌کنید.

بر اساس این تجزیه و تحلیل، تقریباً 85٪ از زمان صرف تماس GetFromJsonAsync می شود. نگاه کردن به ابزار نشان می دهد که این از مشتری HTTP است. این با تئوری عملکرد مرتبط است، زیرا نشان می‌دهد که حلقه غیرهمگام با پیچیدگی O(n^2) می‌تواند مشکل باشد.

ابزار معیاری که به صورت محلی اجرا می شود به شناسایی تنگناها کمک می کند. گام بعدی استفاده از ابزار نظارتی است که می‌تواند درخواست‌ها را در یک محیط تولید زنده ردیابی کند.

تحقیقات عملکرد همه در مورد جمع آوری اطلاعات است و بررسی می کنند که هر ابزار حداقل یک داستان منسجم را بیان می کند.

نظارت سایت24×7

ابزاری مانند Site24x7 می تواند به رفع مشکلات عملکرد کمک کند.

برای این برنامه، می‌خواهید روی تاخیرهای P95 در دو API تمرکز کنید. این اثر موج دار است، زیرا API ها بخشی از یک سری خدمات به هم پیوسته در یک معماری توزیع شده هستند. وقتی یک API شروع به مشکلات عملکرد می کند، سایر API های پایین دستی نیز می توانند با مشکلاتی مواجه شوند.

مقیاس پذیری یکی دیگر از عوامل مهم است. با افزایش تعداد کاربران، برنامه می تواند شروع به تاخیر کند. ردیابی رفتار عادی و پیش‌بینی مقیاس‌پذیری برنامه در طول زمان کمک می‌کند. حلقه ناهمگام تودرتو که در این برنامه یافت می‌شود ممکن است برای N تعداد کاربر به خوبی کار کند، اما ممکن است مقیاس نباشد زیرا تعداد محدود نیست.

در نهایت، همانطور که بهینه‌سازی‌ها و بهبودها را به کار می‌گیرید، ردیابی وابستگی‌های نسخه کلیدی است. با هر تکرار، باید بتوانید بدانید کدام نسخه برای عملکرد بهتر یا بدتر است.

یک ابزار نظارتی مناسب ضروری است، زیرا تشخیص مشکلات همیشه در یک محیط توسعه دهنده محلی آسان نیست. مفروضاتی که به صورت محلی مطرح می شوند ممکن است در تولید معتبر نباشند، زیرا مشتریان شما می توانند نظر متفاوتی داشته باشند. Start your 30 day free trial of Site24x7.

A More Performant Solution

With the arsenal of tools so far, it’s time to explore a better approach.

CURL said that the first API is the one having performance issues. This means any improvements made to the second API are negligible. Even though there’s a ripple effect here, shaving off a few milliseconds from the second API won’t make much of a difference.

NBomber corroborated this story by showing the P95s were at almost two seconds in the first API. Then, dotTrace singled out the async loop, because this is where the algorithm spent most of its time. A monitoring tool like Site24x7 would have provided supporting information by showing P95 latencies, scalability over time, and versioning. Likely, the specific version that introduced the nested loop would have spiked latencies.

According to performance theory, quadratic complexity is a big concern, because the performance exponentially degrades. A good technique is to squash the complexity by reducing the number of iterations inside the loop.

One limitation in .NET is that, every time you see an await, the logic blocks and sends only one request at a time. This halts the iteration and waits for the second API to return a response. This is sad news for the performance.

One naive approach is to simply crush the loop by sending all HTTP requests at the same time:

app.MapGet("/", async (HttpClient client) =>
 (await Task.WhenAll( 
   قابل شمارش
     .Range(, 100)
     .Select(_ =>
       client.GetFromJsonAsyncListWeatherForecast>>( 
         "/weatherForecast")
     )
   )
 )
 .ToArray()[Random.Shared.Next(, 100)]);

This will nuke the await inside the loop and blocks only once. The Task.WhenAll sends everything in parallel, which smashes the loop.

This approach might work, but it runs the risk of spamming the second API with too many requests at once. The web server can reject requests, because it thinks it might be a DoS attack. A far more sustainable approach is to cut down iterations by sending only a few at a time:

var sem = new SemaphoreSlim(10); 

app.MapGet("/", async (HttpClient client) =>
 (await Task.WhenAll(
   قابل شمارش
     .Range(, 100)
     .Select(async _ =>
     {
       try
       {
         await sem.WaitAsync(); 
         return await client.GetFromJsonAsyncListWeatherForecast>>(
           "/weatherForecast");
       }
       finally
       {
         sem.Release();
       }
     })
   )
 )
 .ToArray()[Random.Shared.Next(, 100)]);

This works much like a bouncer at a club. The max capacity is ten. As requests enter the pool, only ten can enter at one time. This also allows concurrent requests, so if one request exits the pool, another can immediately enter without having to wait on ten requests.

This cuts the algorithmic complexity by a factor of ten and relieves pressure from all the crazy looping.

With this code in place, run NBomber and check the results.

 

The P95 latencies are now a third of what they used to be. A half-second response is far more reasonable than anything that takes over a second. Of course, you can keep going and optimize this further, but I think your customers will be quite pleased with this.

نتیجه گیری

Performance optimizations are a never-ending story. As the business grows, the assumptions once made in the code can become invalid over time. Therefore, you need tools to analyze, draw benchmarks, and continuously monitor the app to help quell performance woes.

منبع :

https://ifmee.ir/آیا-رسانه-های-اجتماعی-به-کسب-و-کار-طراحی/