🧠 چه زمانی از Value Objectها استفاده کنیم؟
من از Value Objectها برای حل مشکل primitive obsession و کپسولهسازی (encapsulation) قوانین دامنه (domain invariants) استفاده میکنم.
کپسولهسازی بخش مهمی از هر مدل دامنه است. شما نباید بتوانید یک Value Object را در حالت نامعتبر ایجاد کنید.
Value Object
ها همچنین type safety را فراهم میکنند. به امضای متد زیر دقت کنید:
public interface IPricingService
{
decimal Calculate(Apartment apartment, DateOnly start, DateOnly end);
}
حالا به امضای متد زیر نگاه کنید که در آن از Value Objectها استفاده شده است. میتوانید ببینید که این نسخه از IPricingService بسیار واضحتر (explicit) است. همچنین از مزیت type safety برخوردار میشوید. هنگام کامپایل کد، Value Objectها احتمال بروز خطا را کاهش میدهند.
public interface IPricingService
{
PricingDetails Calculate(Apartment apartment, DateRange period);
}
در ادامه چند نکته دیگر را ببینید که باید هنگام تصمیمگیری برای استفاده از Value Objectها در نظر بگیرید:
• پیچیدگی قوانین (invariants): اگر نیاز به اعمال قوانین پیچیده دارید، استفاده از Value Object را در نظر بگیرید.
• تعداد مقادیر اولیه (primitives): زمانی که تعداد زیادی مقدار اولیه وجود دارد، استفاده از Value Object منطقی است.
• تعداد تکرارها: اگر نیاز دارید قوانین را فقط در چند نقطه از کد اعمال کنید، میتوانید بدون Value Object هم آن را مدیریت کنید.
💾 ذخیرهسازی Value Objectها با EF Core
Value Object
ها بخشی از موجودیتهای دامنه (domain entities) هستند و باید در پایگاه داده ذخیره شوند.
در اینجا نحوه استفاده از EF Owned Types و Complex Types برای ذخیرهسازی Value Objectها آورده شده است.
🧱 Owned Types
Owned Type
ها را میتوان با فراخوانی متد OwnsOne در هنگام پیکربندی موجودیت تنظیم کرد.
این کار به EF اعلام میکند که باید Value Objectهای Address و Price را در همان جدول موجودیت Apartment ذخیره کند.
Value Object
ها با ستونهای اضافی در جدول apartments نمایش داده میشوند.
public void Configure(EntityTypeBuilder<Apartment> builder)
{
builder.ToTable("apartments");
builder.OwnsOne(property => property.Address);
builder.OwnsOne(property => property.Price, priceBuilder =>
{
priceBuilder.Property(money => money.Currency)
.HasConversion(
currency => currency.Code,
code => Currency.FromCode(code));
});
}
چند نکته در مورد Owned Typeها:
• Owned Typeها دارای یک کلید پنهان هستند.
• از نوع اختیاری (nullable) پشتیبانی نمیکنند.
• مجموعههای متعلق پشتیبانی میشوند و با OwnsMany قابل پیکربندی هستند.
•Table splitting
به شما اجازه میدهد Owned Typeها را در جدول جداگانه ذخیره کنید.
🧩 Complex Types
ویژگی جدیدی در EF هستند که در 8 NET. در دسترساند.
آنها با مقدار کلید (key value) شناسایی یا پیگیری نمیشوند.
اینها باید بخشی از نوع موجودیت (entity type) باشند.Complex Type ها برای نمایش Value Objectها در EF مناسبتر هستند.
در اینجا نحوه پیکربندی Address به عنوان یک Complex Type را میبینید:
public void Configure(EntityTypeBuilder<Apartment> builder)
{
builder.ToTable("apartments");
builder.ComplexProperty(property => property.Address);
}
چند محدودیت برای Complex Typeها وجود دارد:
🔹️ از مجموعهها (collections) پشتیبانی نمیکنند.
🔹️ از مقادیر اختیاری (nullable values) پشتیبانی نمیکنند.
🧾 نتیجهگیری
Value Object
ها به شما کمک میکنند تا یک مدل دامنه غنی طراحی کنید.
میتوانید از آنها برای حل مشکل primitive obsession و کپسولهسازی قوانین دامنه استفاده کنید.Value Objectها با جلوگیری از ایجاد اشیای دامنه نامعتبر، احتمال خطا را کاهش میدهند.
شما میتوانید برای نمایش Value Objectها از record یا کلاس پایه ValueObject استفاده کنید.انتخاب بین آنها باید بر اساس نیازها و پیچیدگی دامنه شما باشد.
من معمولاً بهصورت پیشفرض از record استفاده میکنم مگر زمانی که به ویژگیهای خاص کلاس پایه نیاز داشته باشم.
برای مثال، کلاس پایه زمانی مفید است که بخواهید اجزای برابری (equality components) را کنترل کنید.
🔖هشتگها:
#DomainDrivenDesign #ValueObject #CleanCode #EntityVsValueObject #ImmutableObjects
بسیاری از توسعهدهندگان این اشتباه را مرتکب میشوند:
آیا میدانستید که با ساخت پروژههای جانبی فقط برای پر کردن رزومه، در واقع از چیزی که سریعتر باعث رشد شما میشود، غافل میشوید؟
یادگیری واقعی، حل مسئله، و خوداتکایی.
این را امتحان کنید:
از پروژههای ساده (حتی همان برنامهی خستهکنندهی to-do!) استفاده کنید تا با تکنولوژیهای جدید آزمایش کنید و تجربهی کار با آنها را به دست آورید.
چیزی بسازید که واقعاً در آن گیر کنید، و خودتان را مجبور کنید با دیباگ کردن از آن خارج شوید — جایی که بیشترین یادگیری اتفاق میافتد.
تأمل کنید که چرا آن را ساختید و چه چیزی یاد گرفتید، نه فقط اینکه چه چیزی را تمام کردید.
این کار هیچ کمکی نمیکند اگر بگذارید هوش مصنوعی یا آموزشها تمام کار را انجام دهند.
وقتی هرگز تقلا نمیکنید، هرگز یاد نمیگیرید.
هدف، تجربهی واقعی و توسعهی مهارت است، نه فقط اضافه کردن چند خط دیگر به رزومه.
هرچند داشتن چند تیک بیشتر روی CV میتواند اثر جانبی مفیدی باشد.
آیا میدانستید که با ساخت پروژههای جانبی فقط برای پر کردن رزومه، در واقع از چیزی که سریعتر باعث رشد شما میشود، غافل میشوید؟
یادگیری واقعی، حل مسئله، و خوداتکایی.
این را امتحان کنید:
از پروژههای ساده (حتی همان برنامهی خستهکنندهی to-do!) استفاده کنید تا با تکنولوژیهای جدید آزمایش کنید و تجربهی کار با آنها را به دست آورید.
چیزی بسازید که واقعاً در آن گیر کنید، و خودتان را مجبور کنید با دیباگ کردن از آن خارج شوید — جایی که بیشترین یادگیری اتفاق میافتد.
تأمل کنید که چرا آن را ساختید و چه چیزی یاد گرفتید، نه فقط اینکه چه چیزی را تمام کردید.
این کار هیچ کمکی نمیکند اگر بگذارید هوش مصنوعی یا آموزشها تمام کار را انجام دهند.
وقتی هرگز تقلا نمیکنید، هرگز یاد نمیگیرید.
هدف، تجربهی واقعی و توسعهی مهارت است، نه فقط اضافه کردن چند خط دیگر به رزومه.
هرچند داشتن چند تیک بیشتر روی CV میتواند اثر جانبی مفیدی باشد.
🛰 الگوی Request-Response Messaging با MassTransit
ساخت برنامههای توزیعشده (Distributed Applications) در نگاه اول ساده به نظر میرسد فقط چند سرور هستند که با هم صحبت میکنند، درست است؟
اما در عمل، این نوع سیستمها مجموعهای از مشکلات بالقوه را ایجاد میکنند که باید به آنها توجه کنید.
اگر شبکه دچار اختلال شود چه؟ اگر یک سرویس بهطور غیرمنتظره کرش کند چه؟ اگر بخواهید سیستم را scale کنید و همه چیز زیر فشار از هم بپاشد چه؟
اینجاست که نحوه ارتباط سرویسها در سیستم توزیعشده اهمیت حیاتی پیدا میکند. ⚙️
ارتباطهای synchronous سنتی جایی که سرویسها مستقیماً یکدیگر را فراخوانی میکنند ذاتاً شکنندهاند.
این رویکرد باعث tight coupling میشود و در نتیجه، کل سیستم در برابر هرگونه نقطه شکست (single point of failure) آسیبپذیر میشود.
برای مقابله با این مشکل، میتوانیم از messaging توزیعشده (Distributed Messaging) استفاده کنیم.
(البته این کار خودش مجموعه جدیدی از چالشها را هم ایجاد میکند، ولی آن موضوع را میگذاریم برای مقالهای دیگر 😄)
یکی از ابزارهای قدرتمند برای انجام این کار در دنیای NET. ، کتابخانهی MassTransit است. 🚀
در این مقاله، به بررسی پیادهسازی الگوی Request-Response در MassTransit میپردازیم.
📨 مقدمهای بر الگوی Request-Response Messaging Pattern
بیایید ابتدا توضیح دهیم که این الگو چگونه کار میکند.
الگوی Request-Response بسیار شبیه به فراخوانی یک تابع معمولی است، با این تفاوت که این فراخوانی از طریق شبکه انجام میشود.
در این الگو:
• یک سرویس بهعنوان Requestor (درخواستدهنده)، یک پیام درخواست (Request Message) ارسال میکند
•و منتظر پیام پاسخ (Response Message) از سمت Responder (پاسخدهنده) میماند.
از دید Requestor، این فرآیند synchronous است.
✅ مزایا:
🔹️Loose Coupling:
سرویسها نیازی به دانستن مستقیم یکدیگر ندارند؛ تنها کافی است قرارداد پیام (message contract) را بشناسند.
این ویژگی باعث سهولت در تغییر و افزایش مقیاس (scalability) میشود.
🔹️Location Transparency:
درخواستدهنده نیازی ندارد بداند پاسخدهنده در کجا قرار دارد در نتیجه انعطافپذیری سیستم افزایش مییابد.
⚠️ معایب:
🔹️Latency:
سربار پیامرسانی باعث افزایش اندکی در زمان پاسخ میشود.
🔹️Complexity:
افزودن یک سیستم پیامرسان و مدیریت زیرساختهای مربوط به آن میتواند پیچیدگی پروژه را افزایش دهد.
🛰 پیامرسانی Request-Response با MassTransit
کتابخانهی MassTransit بهصورت پیشفرض از الگوی پیامرسانی Request-Response پشتیبانی میکند.
ما میتوانیم از یک Request Client برای ارسال درخواستها و انتظار برای پاسخ استفاده کنیم.
این Request Client بهصورت asynchronous است و از کلیدواژهی await پشتیبانی میکند.
بهصورت پیشفرض، درخواست دارای timeout سیثانیهای است تا از انتظار بیشازحد برای پاسخ جلوگیری شود.
بیایید یک سناریو را تصور کنیم که در آن سیستمی برای پردازش سفارش دارید و باید آخرین وضعیت یک سفارش را بازیابی کنید.
ما میتوانیم وضعیت را از سرویس Order Management دریافت کنیم.
با MassTransit، شما یک Request Client ایجاد میکنید تا این فرآیند را آغاز کند.
این کلاینت پیامی از نوع GetOrderStatusRequest را روی bus ارسال میکند.
public record GetOrderStatusRequest
{
public string OrderId { get; init; }
}
در سمت Order Management، یک responder (یا consumer) در حال گوش دادن به پیامهای GetOrderStatusRequest است.
این consumer درخواست را دریافت میکند، احتمالاً به پایگاه داده کوئری میزند تا وضعیت را بگیرد، و سپس پیامی از نوع GetOrderStatusResponse را دوباره روی bus ارسال میکند.
کلاینت درخواستدهنده در انتظار این پاسخ خواهد بود و میتواند آن را مطابق نیاز پردازش کند.
public class GetOrderStatusRequestConsumer : IConsumer<GetOrderStatusRequest>
{
public async Task Consume(ConsumeContext<GetOrderStatusRequest> context)
{
// Get the order status from a database.
await context.ResponseAsync<GetOrderStatusResponse>(new
{
// Set the respective response properties.
});
}
}
👥 دریافت سطح دسترسی کاربران در Modular Monolith
در اینجا یک سناریوی واقعی وجود دارد که تیم ما تصمیم گرفت این الگو را پیادهسازی کند.
ما در حال ساخت یک modular monolith بودیم و یکی از ماژولها مسئول مدیریت user permissions بود.
سایر ماژولها میتوانستند برای دریافت سطح دسترسی کاربر به ماژول Users فراخوانی انجام دهند.
و این تا زمانی که داخل یک سیستم monolith هستیم بهخوبی کار میکند.
اما در مقطعی نیاز داشتیم یکی از ماژولها را به یک سرویس مجزا استخراج کنیم.
این بدان معنا بود که ارتباط با ماژول Users از طریق method callهای ساده دیگر کار نخواهد کرد.
خوشبختانه، ما از قبل از MassTransit و RabbitMQ برای پیامرسانی درون سیستم استفاده میکردیم.
بنابراین تصمیم گرفتیم از قابلیت request-response در MassTransit برای پیادهسازی این ارتباط استفاده کنیم.
سرویس جدید یک <IRequestClient<GetUserPermissions تزریق میکند.
ما میتوانیم از آن برای ارسال پیام GetUserPermissions و انتظار برای پاسخ استفاده کنیم.
یکی از ویژگیهای قدرتمند MassTransit این است که میتوانید منتظر بیش از یک نوع پاسخ باشید.
در این مثال، ما منتظر PermissionsResponse یا Error هستیم.
این عالی است، چون راهی برای مدیریت خطاها در consumer نیز داریم.
internal sealed class PermissionService(
IRequestClient<GetUserPermissions> client)
: IPermissionService
{
public async Task<Result<PermissionsResponse>> GetUserPermissionsAsync(
string identityId)
{
var request = new GetUserPermissions(identityId);
Response<PermissionsResponse, Error> response =
await client.GetResponse<PermissionsResponse, Error>(request);
if (response.Is(out Response<Error> errorResponse))
{
return Result.Failure<PermissionsResponse>(errorResponse.Message);
}
if (response.Is(out Response<PermissionsResponse> permissionResponse))
{
return permissionResponse.Message;
}
return Result.Failure<PermissionsResponse>(NotFound);
}
}
در ماژول Users، میتوانیم بهراحتی GetUserPermissionsConsumer را پیادهسازی کنیم.
این کلاس در صورت یافتن permissions با PermissionsResponse پاسخ میدهد و در صورت خطا با Error پاسخ خواهد داد.
با پذیرش الگوهای پیامرسانی با MassTransit، شما بر روی یک پایهی بسیار مقاومتر ساختار میدهید.
سرویسهای NET. شما اکنون کمتر tightly coupled هستند و این انعطاف را دارید که آنها را بهصورت مستقل توسعه دهید و در برابر خطاهای شبکه یا service outageهای ناگهانی مقاومتر باشید.
الگوی request-response ابزاری قدرتمند در مجموعه ابزارهای پیامرسانی شماست. MassTransit پیادهسازی آن را بهطرز قابلتوجهی ساده میکند و اطمینان میدهد که درخواستها و پاسخها بهطور قابلاعتماد منتقل میشوند.
ما میتوانیم از request-response برای پیادهسازی ارتباط بین ماژولها در یک modular monolith استفاده کنیم.
با این حال، در استفاده از آن زیادهروی نکنید، زیرا ممکن است سیستم شما با latency بیشتر مواجه شود.
کوچک شروع کنید، آزمایش کنید، و ببینید که چطور قابلیت اطمینان و انعطافپذیری پیامرسانی میتواند تجربهی توسعهی شما را متحول کند.
عالی بمونید! 🚀
این کلاس در صورت یافتن permissions با PermissionsResponse پاسخ میدهد و در صورت خطا با Error پاسخ خواهد داد.
public sealed class GetUserPermissionsConsumer(
IPermissionService permissionService)
: IConsumer<GetUserPermissions>
{
public async Task Consume(ConsumeContext<GetUserPermissions> context)
{
Result<PermissionsResponse> result =
await permissionService.GetUserPermissionsAsync(
context.Message.IdentityId);
if (result.IsSuccess)
{
await context.RespondAsync(result.Value);
}
else
{
await context.RespondAsync(result.Error);
}
}
}
💭 نتیجهگیری
با پذیرش الگوهای پیامرسانی با MassTransit، شما بر روی یک پایهی بسیار مقاومتر ساختار میدهید.
سرویسهای NET. شما اکنون کمتر tightly coupled هستند و این انعطاف را دارید که آنها را بهصورت مستقل توسعه دهید و در برابر خطاهای شبکه یا service outageهای ناگهانی مقاومتر باشید.
الگوی request-response ابزاری قدرتمند در مجموعه ابزارهای پیامرسانی شماست. MassTransit پیادهسازی آن را بهطرز قابلتوجهی ساده میکند و اطمینان میدهد که درخواستها و پاسخها بهطور قابلاعتماد منتقل میشوند.
ما میتوانیم از request-response برای پیادهسازی ارتباط بین ماژولها در یک modular monolith استفاده کنیم.
با این حال، در استفاده از آن زیادهروی نکنید، زیرا ممکن است سیستم شما با latency بیشتر مواجه شود.
کوچک شروع کنید، آزمایش کنید، و ببینید که چطور قابلیت اطمینان و انعطافپذیری پیامرسانی میتواند تجربهی توسعهی شما را متحول کند.
عالی بمونید! 🚀
🔖هشتگها:
#MassTransit #RequestResponse #MessagingPattern
📌معرفی YARP
YARP (Yet Another Reverse Proxy)
یک کتابخانه reverse proxy بسیار قابل تنظیم برای NET. است. این کتابخانه طراحی شده تا یک چارچوب پروکسی قابل اعتماد، منعطف، مقیاسپذیر، امن و آسان برای استفاده ارائه دهد. YARP به توسعهدهندگان کمک میکند تا راهکارهای reverse proxy قدرتمند و بهینه متناسب با نیازهای خاص خود ایجاد کنند. 🚀
کارکرد یک Reverse Proxy
یک reverse proxy سروری است که بین دستگاههای مشتری و سرورهای بکاند قرار میگیرد. این سرور درخواستهای مشتری را به سرور مناسب منتقل میکند و سپس پاسخ سرور را به مشتری برمیگرداند. Reverse proxy چندین مزیت دارد:
🔹️Routing:
هدایت درخواستها به سرورهای مختلف بکاند بر اساس قوانین از پیش تعریف شده، مانند الگوهای URL یا هدرهای درخواست. برای مثال، درخواستهای /images، /api و /db میتوانند به سرورهای image، api و database هدایت شوند.
🔹️Load Balancing:
توزیع ترافیک ورودی بین چند سرور بکاند برای جلوگیری از بارگذاری بیش از حد یک سرور خاص. این توزیع باعث افزایش عملکرد و قابلیت اطمینان میشود.
🔹️Scalability:
با توزیع ترافیک بین چند سرور، reverse proxy به برنامه کمک میکند تا بتواند کاربران و بار بیشتر را مدیریت کند. سرورهای بکاند میتوانند بدون تأثیر بر مشتری اضافه یا حذف شوند.
🔹️SSL/TLS Termination:
بار رمزنگاری و رمزگشایی TLS را از سرورهای بکاند برداشته و بار آنها را کاهش میدهد.
🔹️Connection abstraction, decoupling and control over URL-space:
درخواستهای ورودی از مشتری و پاسخهای خروجی از بکاند مستقل هستند. این استقلال امکان:
• استفاده از نسخههای مختلف HTTP (HTTP/1.1، HTTP/2، HTTP/3) و ارتقا یا کاهش نسخهها
• مدیریت طول عمر اتصالها، مانند نگه داشتن اتصال طولانی در بکاند در حالی که اتصالات کوتاه مشتری حفظ میشوند
• کنترل URL: URLهای ورودی میتوانند قبل از ارسال به بکاند تغییر داده شوند، و نقشه داخلی خدمات میتواند بدون تأثیر بر URL خارجی تغییر کند
🔹️Security:
نقاط انتهایی داخلی میتوانند از دید خارجی مخفی بمانند و از برخی حملات سایبری مانند DDoS محافظت کنند 🔒
🔹️Caching:
منابع پر درخواست میتوانند کش شوند تا بار روی سرورهای بکاند کاهش یابد و زمان پاسخ بهبود یابد
🔹️Versioning:
نسخههای مختلف یک API با استفاده از نگاشتهای مختلف URL پشتیبانی میشوند
🔹️Simplified maintenance:
Reverse proxies
میتوانند SSL/TLS Termination و وظایف دیگر را مدیریت کنند، که پیکربندی و نگهداری سرورهای بکاند را ساده میکند. برای مثال، گواهینامهها و سیاستهای امنیتی میتوانند در سطح پروکسی مدیریت شوند به جای هر سرور جداگانه 🛠
نحوه مدیریت HTTP توسط Reverse Proxy
یک reverse proxy درخواستها و پاسخهای HTTP را به این صورت مدیریت میکند:
🔸️دریافت درخواستها: پروکسی روی پورتها و endpoints مشخص به درخواستهای HTTP مشتریان گوش میدهد.
🔸️Terminating Connections:
اتصالات HTTP ورودی در پروکسی خاتمه مییابند و اتصالات جدید برای درخواستهای خروجی ایجاد میشوند.
🔸️Routing requests:
بر اساس قوانین و تنظیمات از پیش تعریف شده، پروکسی تعیین میکند که کدام سرور بکاند یا خوشهای از سرورها باید درخواست را پردازش کنند.
🔸️Forwarding requests:
پروکسی درخواست مشتری را به سرور مناسب ارسال میکند و مسیر و هدرها را در صورت نیاز تغییر میدهد.
🔸️Connection pooling:
اتصالات خروجی به صورت pooled مدیریت میشوند تا بار اتصال کاهش یابد و از مزایای HTTP 1.1 و درخواستهای موازی HTTP/2 و HTTP/3 استفاده شود
🔸️Processing responses:
سرور بکاند درخواست را پردازش کرده و پاسخ را به پروکسی ارسال میکند
🔸️Returning responses:
پروکسی پاسخ را از سرور دریافت و به مشتری برمیگرداند و در صورت نیاز تغییرات لازم روی پاسخ اعمال میکند ✅
این روند اطمینان میدهد که مشتری با پروکسی تعامل دارد نه مستقیماً با سرورهای بکاند، و مزایای load balancing، امنیت، versioning و غیره را فراهم میکند.
چرا YARP را انتخاب کنیم؟
مزایای منحصر به فردی ارائه میدهد که آن را برای توسعهدهندگان جذاب میکند:
🔹️Customization: YARP
بسیار قابل تنظیم است و توسعهدهندگان میتوانند پروکسی را با حداقل تلاش به نیازهای خود تطبیق دهند
🔸️Integration with .NET:
بر پایه ASP.NET Core ساخته شده و به خوبی با اکوسیستم NET. یکپارچه میشود
🔹️Extensibility:
نقاط توسعهی گسترده برای افزودن منطق و ویژگیهای سفارشی با استفاده از کد #C فراهم میکند
🔸️Scalability:
قابلیت direct forwarding extensibility امکان مقیاسپذیری نام دامنه و سرورهای بکاند را فراهم میکند، چیزی که با اکثر reverse proxyها ممکن نیست
🔹️Active development: YARP
به طور فعال توسط مایکروسافت نگهداری و توسعه داده میشود
🔸️Comprehensive maintained documentation:
مستندات جامع و مثالها، شروع سریع و پیادهسازی ویژگیهای پیشرفته را آسان میکند
🔹️Open source:
و مستندات آن متنباز هستند و مشارکت، بازبینی و بازخورد پذیرفته میشود 💻
🔖هشتگها:
#YARP #ReverseProxy
🧩 الگوی Idempotent Consumer در NET. (و چرا به آن نیاز دارید)
سیستمهای توزیعشده ذاتاً غیرقابل اعتماد هستند. ⚠️
من همیشه توصیه میکنم برای درک بهتر اشتباهات رایج، مقالهی معروف Fallacies of Distributed Computing را مطالعه کنید.
یکی از چالشهای کلیدی در این سیستمها، اطمینان از این است که هر پیام دقیقاً یکبار پردازش شود که از نظر تئوری در بیشتر سیستمها غیرممکن است. 😅
در اینجا وارد مباحثی مثل CAP Theorem یا Two Generals Problem نمیشویم، اما کافی است بدانید که در دنیای واقعی:
پیامها ممکن است خارج از ترتیب برسند 🌀
پیامها ممکن است تکراری شوند 🔁
تحویل پیامها ممکن است با تأخیر انجام شود 🕒
اگر سیستم خود را طوری طراحی کنید که فرض کند هر پیام دقیقاً یکبار پردازش میشود، در واقع دارید زمینه را برای خرابی دادههای پنهان فراهم میکنید. 💥
اما میتوانیم سیستم خود را طوری طراحی کنیم که اثرات جانبی (side effects) فقط یکبار اعمال شوند با استفاده از الگوی قدرتمند Idempotent Consumer. 💡
بیایید با هم بررسی کنیم:
🔹 چه خطاهایی ممکن است رخ دهند
🔹 چگونه message brokerها در مدیریت idempotency کمک میکنند
🔹 و چطور میتوانیم یک Idempotent Consumer در NET. بسازیم
🚨 چه چیزی ممکن است هنگام انتشار پیام اشتباه پیش برود؟
فرض کنید سرویس شما زمانی که یک یادداشت جدید ایجاد میشود، یک event منتشر میکند:
await publisher.PublishAsync(new NoteCreated(note.Id, note.Title, note.Content));
در اینجا اهمیتی ندارد که publisher یا message broker شما چه پیادهسازیای دارد میتواند RabbitMQ، SQS، یا Azure Service Bus باشد.
حالا تصور کنید سناریوی زیر رخ دهد:
• Publisher
پیام را به broker ارسال میکند
• Broker
پیام را ذخیره کرده و یک ACK (تأیید دریافت) برمیگرداند
• یک اختلال شبکه باعث میشود ACK هرگز به producer نرسد 🌐
• Producer timeout
میشود و انتشار را دوباره تلاش میکند 🔁
حالا broker دو event از نوع NoteCreated دارد 😬
از دید producer، فقط یک timeout رفع شده است.
اما از دید consumer، دو پیام دریافت شده که هر دو مربوط به ایجاد یک یادداشت هستند.
و این تنها یکی از مسیرهای خرابی ممکن است!
ممکن است پیامهای تکراری به دلایل زیر هم اتفاق بیفتند:
• ارسال مجدد توسط broker
• خرابی consumer و اجرای مجدد در retryها
بنابراین حتی اگر در سمت publisher همه چیز را “درست” انجام دهید، باز هم consumer باید محافظهکارانه طراحی شود تا در برابر تکرارها مقاوم باشد. 🧱
⚙️ Publisher-Side Idempotency (بگذارید Broker آن را مدیریت کند)
بسیاری از message brokerها از قبل از طریق قابلیت message deduplication، از انتشار idempotent پشتیبانی میکنند؛ البته اگر در پیام خود یک شناسهی یکتا (unique message ID) قرار دهید.
بهعنوان مثال، Azure Service Bus میتواند پیامهای تکراری را تشخیص داده و انتشار مجدد پیامهایی با MessageId یکسان را در بازهی زمانی مشخصشده نادیده بگیرد.Amazon SQS و سایر brokerها نیز تضمینهای مشابهی ارائه میدهند. 💪
✅ شما نیازی ندارید این منطق را دوباره در برنامهی خود پیادهسازی کنید.
نکتهی کلیدی این است که برای هر پیام، یک شناسهی پایدار (stable identifier) اختصاص دهید که بهطور یکتا رویداد منطقی مورد نظر را نمایش دهد.
بهعنوان مثال، هنگام انتشار یک event از نوع NoteCreated:
var message = new NoteCreated(note.Id, note.Title, note.Content)
{
MessageId = Guid.NewGuid() // یا میتوانید از note.Id استفاده کنید
};
await publisher.PublishAsync(message);
اگر شبکه پس از ارسال پیام قطع شود 🌐، ممکن است برنامهی شما ارسال را مجدداً تلاش کند (retry).
اما زمانی که broker همان MessageId را مشاهده کند، متوجه میشود که این پیام تکراری است و آن را بهصورت ایمن نادیده میگیرد. ✅
به این ترتیب، شما deduplication را بدون نیاز به جداول ردیابی (tracking tables) یا state اضافی در سرویس خود بهدست میآورید.
این نوع idempotency در سطح broker، بخش بزرگی از مشکلات سمت producer را حل میکند مثل:
• retryهای شبکه 🔁
• خطاهای موقتی ⚠️
• انتشارهای تکراری 🌀
اما چیزی که این مکانیسم پوشش نمیدهد، retryهای سمت consumer است یعنی زمانی که پیامها مجدداً تحویل داده میشوند یا سرویس شما هنگام پردازش دچار crash میشود 💥.
اینجاست که الگوی Idempotent Consumer وارد عمل میشود. 🎯
🧠 Implementing an Idempotent Consumer in .NET
در اینجا یک نمونه از Idempotent Consumer برای eventی از نوع NoteCreated آورده شده است:
internal sealed class NoteCreatedConsumer(
TagsDbContext dbContext,
HybridCache hybridCache,
ILogger<Program> logger) : IConsumer<NoteCreated>
{
public async Task ConsumeAsync(ConsumeContext<NoteCreated> context)
{
// 1. بررسی اینکه آیا این پیام قبلاً توسط این consumer پردازش شده است
if (await dbContext.MessageConsumers.AnyAsync(c =>
c.MessageId == context.MessageId &&
c.ConsumerName == nameof(NoteCreatedConsumer)))
{
return;
}
var request = new AnalyzeNoteRequest(
context.Message.NoteId,
context.Message.Title,
context.Message.Content);
try
{
using var transaction = await dbContext.Database.BeginTransactionAsync();
// 2. پردازش قطعی (Deterministic): استخراج تگها از محتوای یادداشت
var tags = AnalyzeContentForTags(request.Title, request.Content);
// 3. ذخیرهسازی تگها در پایگاه داده
var tagEntities = tags.Select(ProjectToTagEntity(request.NoteId)).ToList();
dbContext.Tags.AddRange(tagEntities);
// 4. ثبت اینکه این پیام پردازش شده است
dbContext.MessageConsumers.Add(new MessageConsumer
{
MessageId = context.MessageId,
ConsumerName = nameof(NoteCreatedConsumer),
ConsumedAtUtc = DateTime.UtcNow
});
await dbContext.SaveChangesAsync();
await transaction.CommitAsync();
// 5. بهروزرسانی Cache
await CacheNoteTags(request, tags);
}
catch (Exception ex)
{
logger.LogError(ex, "Error analyzing note {NoteId}", request.NoteId);
throw;
}
}
}
این یک نمونهی متداول از Idempotent Consumer است که شامل چند نکتهی کلیدی است 🧩
🧱 1️⃣ The Idempotency Key
if (await dbContext.MessageConsumers.AnyAsync(c =>
c.MessageId == context.MessageId &&
c.ConsumerName == nameof(NoteCreatedConsumer)))
{
return;
}
در اینجا از موارد زیر استفاده میکنیم:
MessageId
که از transport (یعنی context.MessageId) گرفته میشود
ConsumerName
تا در صورتی که چند consumer متفاوت یک پیام را پردازش کنند، هرکدام بهصورت ایمن عمل کنند ✅
اگر یک پیام تکراری دریافت شود، پردازش کوتاه میشود و هیچ کاری انجام نمیگیرد. 🚫
نکتهی بسیار مهم این است که باید روی ستونهای (MessageId, ConsumerName) در جدول MessageConsumers یک unique constraint تعریف شود تا از race condition جلوگیری شود ⚙️
به این ترتیب حتی اگر چند پردازش همزمان از یک پیام وجود داشته باشد، فقط یکی از آنها موفق به درج رکورد خواهد شد. 💪
⚡️ 2️⃣ Atomic Side Effects + Idempotency Record
در این الگو، هم پردازش (processing) و هم ذخیرهی رکورد MessageConsumer در یک تراکنش (transaction) انجام میشود:
using var transaction = await dbContext.Database.BeginTransactionAsync();
// write tags
dbContext.Tags.AddRange(tagEntities);
// write message-consumer record
dbContext.MessageConsumers.Add(new MessageConsumer { ... });
await dbContext.SaveChangesAsync();
await transaction.CommitAsync();
چرا این مهم است؟ 🤔
اگر پردازش شکست بخورد ❌، هیچ ورودیای در MessageConsumers ثبت نمیشود، بنابراین پیام میتواند مجدداً retry شود.
اگر پردازش موفق باشد ✅، هم دادهها (مثل tags) و هم رکورد مربوط به پیام باهم commit میشوند.
در نتیجه، هیچوقت در وضعیتی قرار نمیگیرید که کار انجام شده باشد اما پیام بهعنوان پردازششده علامتگذاری نشده باشد، یا برعکس.
این اساس idempotency است:
انجام دقیق یکبار عملیات برای هر Message ID — حتی در شرایط retry. 🔁
📬 3️⃣ Handling At-Least-Once Delivery
در بیشتر سناریوهای واقعی، تحویل پیامها از نوع at-least-once است:
1️⃣ Consumer پیام را پردازش میکند
2️⃣ ACK شکست میخورد یا timeout میشود
3️⃣ Broker پیام را مجدداً تحویل میدهد
4️⃣ کد شما دوباره اجرا میشود
اما در این الگو، اجرای دوم با بررسی جدول MessageConsumers مواجه شده و خیلی سریع return میکند. ✅
نتیجه:
هیچ side effect تکراریای اتفاق نمیافتد. 🙌
البته فقط یک استثنا وجود دارد...
🔄 Deterministic vs Non-Deterministic Handlers
وقتی handler شما با سیستمهایی خارج از دیتابیس تماس میگیرد چه میشود؟
مثل:
• یک Email API ✉️
• Payment Gateway 💳
• یا یک Background Job Queue 🧵
اینها همگی side effectهای رایج هستند که باید آنها نیز idempotent باشند.
چون این تماسها خارج از محدودهی تراکنش دیتابیس انجام میشوند، ممکن است دیتابیس commit شود اما بهدلیل اختلال شبکه پاسخ از سرویس بیرونی برنگردد.
در retry بعدی، ممکن است همان ایمیل دوباره ارسال شود یا همان کارت اعتباری دوباره شارژ شود ⚠️
به این ترتیب، وارد قلمروی Non-Deterministic Handlerها میشویم عملیاتی که تکرار آنها ایمن نیست.
دو استراتژی اصلی برای مدیریت این وضعیت وجود دارد:
🧩 1. استفاده از Idempotency Key در فراخوانی خارجی
اگر سرویس خارجی از Idempotency Key پشتیبانی کند، یک شناسهی پایدار مثلاً همان MessageId پیام را در هر درخواست ارسال کنید.
بسیاری از APIها (مثل پردازشگرهای پرداخت یا پلتفرمهای ارسال ایمیل) اجازه میدهند که یک Idempotency-Key Header مشخص کنید.
در این صورت سرویس تضمین میکند که درخواستهای تکراری با کلید یکسان فقط یکبار اجرا شوند. ✅
بهعنوان مثال:
await emailService.SendAsync(new SendEmailRequest
{
To = user.Email,
Subject = "Welcome!",
Body = "Thanks for signing up.",
IdempotencyKey = context.MessageId
});
حتی اگر درخواست مجدداً ارسال شود، provider کلید را تشخیص میدهد و درخواست تکراری را نادیده میگیرد.
این سادهترین و مطمئنترین روش است، اگر وابستگی خارجی شما از آن پشتیبانی کند. 🚀
💾 2. ذخیرهی Intent بهصورت محلی
اگر سرویس خارجی از idempotency key پشتیبانی نکند، میتوانید آن را شبیهسازی کنید.
کافی است پیش از تماس با سرویس بیرونی، رکوردی از اقدام مورد نظر را در دیتابیس ذخیره کنید.
مثلاً جدولی به نام PendingEmails بسازید که نشان دهد کدام پیام باید ارسال شود بر اساس MessageId یا UserId.
سپس یک background process این رکوردها را خوانده و عملیات را تنها یکبار انجام میدهد.
این رویکرد deterministic است ولی پیچیدگی بیشتری دارد (جداول بیشتر و workerهای پسزمینه).
معمولاً تنها در موارد حیاتی یا غیرقابل بازگشت مثل پرداختها یا provisioning حسابها به این میزان از اطمینان نیاز است. 💳
این تصمیم بستگی به سطح اطمینان مورد نیاز دارد:
اگر تکرار عملیات عواقب واقعی دارد (مالی یا دادهای)، باید idempotency را صریحاً اعمال کنید.
در غیر این صورت، retry کردن عملیات ممکن است قابلقبول باشد.
همهی consumerها به سربار بررسیهای idempotency نیاز ندارند.
اگر عملیات شما بهطور طبیعی idempotent است، میتوانید از جدول اضافه و تراکنش صرفنظر کنید.
بهعنوان مثال:
بهروزرسانی projectionها 📊
تنظیم flag وضعیت ✅
یا refresh کردن cache 🧩
همگی نمونههایی از عملیات deterministic هستند که اجرای چندبارهی آنها خطری ندارد.
مثل:
«تنظیم وضعیت کاربر روی Active» یا «بازسازی Read Model» — اینها state را بازنویسی میکنند نه اینکه چیزی به آن اضافه کنند.
برخی از handlerها هم از Precondition Check برای جلوگیری از تکرار استفاده میکنند.
اگر handler در حال بهروزرسانی یک entity باشد، ابتدا میتواند بررسی کند که آیا entity در وضعیت مورد نظر هست یا نه؛ اگر هست، بهسادگی return کند.
این محافظ ساده در بسیاری موارد کافی است.
⚠️ الگوی Idempotent Consumer را بدون فکر در همهجا اعمال نکنید.
فقط در جایی از آن استفاده کنید که از آسیب واقعی (مالی یا ناسازگاری دادهای) جلوگیری کند.
برای سایر موارد، سادگی بهتر است.
سیستمهای توزیعشده ذاتاً غیرقابلپیشبینی هستند. ⚙️ Retryها، پیامهای تکراری (duplicates) و خرابیهای جزئی (partial failures) بخش طبیعی عملکرد آنها محسوب میشوند.
نمیتوانی از وقوعشان جلوگیری کنی، اما میتوانی سیستم را طوری طراحی کنی که کمترین تأثیر را از آنها بگیرد. 💪
از قابلیت Message Deduplication داخلی در Broker خود استفاده کن تا پیامهای تکراری از سمت Producer حذف شوند.
در سمت Consumer، الگوی Idempotent Consumer Pattern را اعمال کن تا مطمئن شوی Side Effectها فقط یکبار رخ میدهند حتی در صورت Retry شدن پیامها. 🔁
همیشه رکورد پیامهای پردازششده و اثر واقعی آنها را در یک تراکنش واحد ذخیره کن.
این کار کلید حفظ Consistency در سیستم توزیعشده است. 🧱
نه هر Message Handlerی نیاز به این الگو دارد.
اگر Consumer شما ذاتاً Idempotent است یا میتواند با یک Precondition ساده پردازش را زود متوقف کند، نیازی به پیچیدگی اضافی نیست. 🚫
اما هرجایی که عملیات باعث تغییر Persistent State یا فراخوانی سیستمهای خارجی میشود، Idempotency دیگر یک انتخاب نیست — بلکه تنها راه تضمین Consistency است. ✅
سیستم خود را طوری بساز که Retryها را تحمل کند.
در این صورت، سیستم توزیعشدهات بسیار قابلاعتمادتر خواهد شد. 🔐
نکتهی جالب اینجاست که وقتی این اصل را واقعاً درک میکنی، آن را در همهی سیستمهای واقعی دنیا میبینی. 🌍
امیدوارم این مطلب برات مفید بوده باشه 💙
⚖️ The Trade-Off
این تصمیم بستگی به سطح اطمینان مورد نیاز دارد:
اگر تکرار عملیات عواقب واقعی دارد (مالی یا دادهای)، باید idempotency را صریحاً اعمال کنید.
در غیر این صورت، retry کردن عملیات ممکن است قابلقبول باشد.
🧠 When Idempotent Consumer Isn’t Needed
همهی consumerها به سربار بررسیهای idempotency نیاز ندارند.
اگر عملیات شما بهطور طبیعی idempotent است، میتوانید از جدول اضافه و تراکنش صرفنظر کنید.
بهعنوان مثال:
بهروزرسانی projectionها 📊
تنظیم flag وضعیت ✅
یا refresh کردن cache 🧩
همگی نمونههایی از عملیات deterministic هستند که اجرای چندبارهی آنها خطری ندارد.
مثل:
«تنظیم وضعیت کاربر روی Active» یا «بازسازی Read Model» — اینها state را بازنویسی میکنند نه اینکه چیزی به آن اضافه کنند.
برخی از handlerها هم از Precondition Check برای جلوگیری از تکرار استفاده میکنند.
اگر handler در حال بهروزرسانی یک entity باشد، ابتدا میتواند بررسی کند که آیا entity در وضعیت مورد نظر هست یا نه؛ اگر هست، بهسادگی return کند.
این محافظ ساده در بسیاری موارد کافی است.
⚠️ الگوی Idempotent Consumer را بدون فکر در همهجا اعمال نکنید.
فقط در جایی از آن استفاده کنید که از آسیب واقعی (مالی یا ناسازگاری دادهای) جلوگیری کند.
برای سایر موارد، سادگی بهتر است.
🧭 Takeaway
سیستمهای توزیعشده ذاتاً غیرقابلپیشبینی هستند. ⚙️ Retryها، پیامهای تکراری (duplicates) و خرابیهای جزئی (partial failures) بخش طبیعی عملکرد آنها محسوب میشوند.
نمیتوانی از وقوعشان جلوگیری کنی، اما میتوانی سیستم را طوری طراحی کنی که کمترین تأثیر را از آنها بگیرد. 💪
از قابلیت Message Deduplication داخلی در Broker خود استفاده کن تا پیامهای تکراری از سمت Producer حذف شوند.
در سمت Consumer، الگوی Idempotent Consumer Pattern را اعمال کن تا مطمئن شوی Side Effectها فقط یکبار رخ میدهند حتی در صورت Retry شدن پیامها. 🔁
همیشه رکورد پیامهای پردازششده و اثر واقعی آنها را در یک تراکنش واحد ذخیره کن.
این کار کلید حفظ Consistency در سیستم توزیعشده است. 🧱
نه هر Message Handlerی نیاز به این الگو دارد.
اگر Consumer شما ذاتاً Idempotent است یا میتواند با یک Precondition ساده پردازش را زود متوقف کند، نیازی به پیچیدگی اضافی نیست. 🚫
اما هرجایی که عملیات باعث تغییر Persistent State یا فراخوانی سیستمهای خارجی میشود، Idempotency دیگر یک انتخاب نیست — بلکه تنها راه تضمین Consistency است. ✅
سیستم خود را طوری بساز که Retryها را تحمل کند.
در این صورت، سیستم توزیعشدهات بسیار قابلاعتمادتر خواهد شد. 🔐
نکتهی جالب اینجاست که وقتی این اصل را واقعاً درک میکنی، آن را در همهی سیستمهای واقعی دنیا میبینی. 🌍
امیدوارم این مطلب برات مفید بوده باشه 💙
🔖هشتگها:
#IdempotentConsumer #Idempotency #DistributedSystems #MessageBroker
⚖️ مقیاسپذیری افقی (Horizontally Scaling) در ASP.NET Core APIs با استفاده از YARP Load Balancingاپلیکیشنهای وب مدرن باید بتوانند تعداد فزایندهای از کاربران را سرویسدهی کنند و افزایش ناگهانی ترافیک را مدیریت نمایند.
زمانی که یک سرور به حد نهایی ظرفیت خود میرسد، عملکرد آن کاهش یافته و منجر به کندی پاسخها، خطاها یا حتی از کار افتادن کامل (downtime) میشود.
Load Balancing
یک تکنیک کلیدی برای مقابله با این چالشها و بهبود Scalability اپلیکیشن شما است. ⚙️
در این مقاله بررسی خواهیم کرد:
• چگونه از YARP (Yet Another Reverse Proxy) برای پیادهسازی Load Balancing استفاده کنیم
• چگونه از Horizontal Scaling برای بهبود عملکرد بهره ببریم
• چگونه از K6 بهعنوان ابزار Load Testing استفاده کنیم
در ادامه، به مفهوم Load Balancing، اهمیت آن و نحوهای که YARP این فرآیند را برای اپلیکیشنهای NET. سادهتر میکند، خواهیم پرداخت.
🧱 انواع مقیاسپذیری نرمافزار (Types of Software Scalability)
قبل از اینکه وارد جزئیات YARP و Load Balancing شویم، بیایید اصول اولیهی Scaling را مرور کنیم.
دو رویکرد اصلی برای مقیاسپذیری وجود دارد:
🔹 Vertical Scaling
در این روش، سرورهای موجود با سختافزار قویتر ارتقا مییابند — افزایش تعداد هستههای CPU، حافظه RAM و فضای ذخیرهسازی سریعتر.
اما این روش چند محدودیت دارد:
هزینهها بهسرعت افزایش مییابد و در نهایت به یک سقف عملکرد (performance ceiling) خواهید رسید.
🔹 Horizontal Scaling
در این روش، سرورهای بیشتری به زیرساخت خود اضافه میکنید و بار کاری را بهصورت هوشمندانه میان آنها توزیع میکنید.
این رویکرد پتانسیل مقیاسپذیری بسیار بیشتری دارد، زیرا میتوانید با افزودن سرورهای جدید، ترافیک بیشتر را مدیریت کنید.
اینجاست که Load Balancing وارد عمل میشود — و YARP در این زمینه بهخوبی میدرخشد. ✨
🧭 افزودن یک Reverse Proxy
YARP
یک کتابخانهی Reverse Proxy با کارایی بالا از مایکروسافت است.این ابزار برای معماریهای مدرن microservice طراحی شده است.Reverse Proxy در جلوی سرورهای backend شما قرار میگیرد و نقش مدیر ترافیک (traffic director) را ایفا میکند. 🚦
راهاندازی YARP بسیار ساده است:
• پکیج YARP NuGet را نصب میکنید،
• یک تنظیم ساده برای تعریف مقاصد backend میسازید،
• و سپس YARP middleware را فعال میکنید.
YARP
این امکان را میدهد تا قبل از رسیدن درخواستها به سرورهای backend، مسیریابی (routing) و تبدیل (transformation) روی آنها انجام دهید.
🧩 مرحلهی اول: نصب پکیج YARP
Install-Package Yarp.ReverseProxy
⚙️ مرحلهی دوم: پیکربندی سرویسها و افزودن Middleware
در این مرحله، سرویسهای موردنیاز را پیکربندی کرده و YARP middleware را به Request Pipeline معرفی میکنیم:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddReverseProxy()
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
var app = builder.Build();
app.MapReverseProxy();
app.Run();
🧾 مرحلهی سوم: افزودن تنظیمات YARP در appsettings.json
در این فایل، YARP از مفهوم Routes برای نمایش درخواستهای ورودی به Reverse Proxy و از Clusters برای تعریف سرویسهای پاییندستی (downstream services) استفاده میکند.
الگوی {**catch-all} به ما اجازه میدهد تمام درخواستهای ورودی را بهراحتی مسیردهی کنیم.
{
"ReverseProxy": {
"Routes": {
"api-route": {
"ClusterId": "api-cluster",
"Match": {
"Path": "{**catch-all}"
},
"Transforms": [{ "PathPattern": "{**catch-all}" }]
}
},
"Clusters": {
"api-cluster": {
"Destinations": {
"destination1": {
"Address": "http://api:8080"
}
}
}
}
}
}این پیکربندی، YARP را بهصورت یک Pass-through Proxy تنظیم میکند.
اما حالا بیایید آن را بهروزرسانی کنیم تا از مقیاسپذیری افقی (Horizontal Scaling) پشتیبانی کند. 🚀
⚙️ Scaling Out با YARP Load Balancing
هستهی اصلی مقیاسپذیری افقی (Horizontal Scaling) با استفاده از YARP در استراتژیهای مختلف Load Balancing آن نهفته است.YARP چندین Load Balancing Strategy مختلف ارائه میدهد که هر کدام رفتار متفاوتی در توزیع درخواستها دارند:
⚖️ PowerOfTwoChoices:
دو مقصد تصادفی را انتخاب میکند و درخواستی را به سروری ارسال میکند که کمترین تعداد درخواست تخصیص دادهشده را دارد.
🔤 FirstAlphabetical:
اولین سرور در دسترس را بر اساس ترتیب الفبایی انتخاب میکند.
🔁 LeastRequests:
درخواستها را به سرورهایی ارسال میکند که کمترین تعداد درخواست فعال دارند.
🔄 RoundRobin:
درخواستها را بهصورت یکنواخت بین تمام سرورهای backend توزیع میکند.
🎲 Random:
برای هر درخواست، بهصورت تصادفی یک سرور backend را انتخاب میکند.
شما میتوانید این استراتژیها را در فایل تنظیمات YARP پیکربندی کنید.
استراتژی Load Balancing با استفاده از ویژگی LoadBalancingPolicy در بخش Cluster مشخص میشود.
🧩 پیکربندی YARP با RoundRobin Load Balancing
در ادامه، نسخهی بهروزشدهی فایل تنظیمات YARP را میبینید که از استراتژی RoundRobin برای توزیع بار استفاده میکند:
{
"ReverseProxy": {
"Routes": {
"api-route": {
"ClusterId": "api-cluster",
"Match": {
"Path": "{**catch-all}"
},
"Transforms": [{ "PathPattern": "{**catch-all}" }]
}
},
"Clusters": {
"api-cluster": {
"LoadBalancingPolicy": "RoundRobin",
"Destinations": {
"destination1": {
"Address": "http://api-1:8080"
},
"destination2": {
"Address": "http://api-2:8080"
},
"destination3": {
"Address": "http://api-3:8080"
}
}
}
}
}
}🧭 ساختار سیستم با YARP Load Balancer
در تصویر، ساختار کلی سیستم ما با یک YARP Load Balancer و مجموعهای از Application Serverهای مقیاسپذیر افقی نشان داده شده است.
درخواستهای ورودی به API ابتدا به YARP ارسال میشوند، و سپس YARP بر اساس استراتژی انتخابشدهی Load Balancing، ترافیک را بین سرورهای اپلیکیشن توزیع میکند.
در این مثال، یک پایگاه داده (Database) داریم که به چندین نمونه از اپلیکیشن سرویسدهی میکند. 🗄
🧪 حالا نوبت تست عملکرد است (Performance Testing)
در ادامه، با استفاده از ابزار K6 به بررسی عملکرد سیستم در شرایط بار بالا خواهیم پرداخت تا اطمینان حاصل شود که استراتژی Load Balancing ما به درستی کار میکند و مقیاسپذیری بهینه حاصل شده است. 🚀
⚡️ Performance Testing با K6
برای مشاهدهی تأثیر مقیاسپذیری افقی (Horizontal Scaling) در عملکرد سیستم، باید تست بارگیری (Load Testing) انجام دهیم.
ابزار K6 یک ابزار مدرن و کاربرپسند برای Load Testing است که توسط توسعهدهندگان بهراحتی قابل استفاده است.
در این بخش، با نوشتن اسکریپتهایی در K6 ترافیک کاربر را روی اپلیکیشن شبیهسازی کرده و معیارهایی مثل میانگین زمان پاسخ (Average Response Time) و تعداد درخواستهای موفق در هر ثانیه (Requests Per Second) را مقایسه خواهیم کرد.
اپلیکیشنی که قرار است بهصورت افقی مقیاسپذیر شود، دارای دو API endpoint است:
• POST /users:
یک کاربر جدید ایجاد میکند، آن را در پایگاه دادهی PostgreSQL ذخیره کرده و شناسهی کاربر را برمیگرداند.
• GET /users/id:
اگر کاربری با شناسهی مشخص وجود داشته باشد، آن را برمیگرداند.
🧪 تست عملکرد K6 شامل مراحل زیر است:
• افزایش تدریجی تا ۲۰ کاربر مجازی (Virtual Users)
• ارسال یک درخواست POST به endpoint /users
• بررسی اینکه پاسخ 201 Created بازگردانده شود
• ارسال یک درخواست GET به endpoint /users/{id}
• بررسی اینکه پاسخ 200 OK بازگردانده شود
توجه داشته باشید که تمام درخواستهای API از طریق YARP Load Balancer عبور میکنند. ⚙️
import { check } from 'k6';
import http from 'k6/http';
export const options = {
stages: [
{ duration: '10s', target: 20 },
{ duration: '1m40s', target: 20 },
{ duration: '10s', target: 0 }
]
};
export default function () {
const proxyUrl = 'http://localhost:3000';
const response = http.post(${proxyUrl}/users);
check(response, {
'response code was 201': (res) => res.status == 201
});
const userResponse = http.get(${proxyUrl}/users/${response.body});
check(userResponse, {
'response code was 200': (res) => res.status == 200
});
}برای اینکه نتایج تست عملکرد سازگارتر باشند، میتوان منابع قابلدسترس در Docker containerها را محدود کرد — مثلاً به ۱ CPU و ۰.۵ گیگابایت RAM:
services:
api:
image: ${DOCKER_REGISTRY-}loadbalancingapi
cpus: 1
mem_limit: '0.5G'
ports:
- 5000:8080
networks:
- proxybackend
🧠 Summary
مقیاسپذیری افقی (Horizontal Scaling) در کنار Load Balancing مؤثر میتواند به شکل چشمگیری عملکرد و مقیاسپذیری اپلیکیشنهای وب را افزایش دهد.
مزایای مقیاسپذیری افقی زمانی بیشتر نمایان میشوند که حجم ترافیک بالا برود و یک سرور بهتنهایی نتواند پاسخگوی نیازها باشد.
و YARP یک Reverse Proxy قدرتمند و کاربرپسند برای اپلیکیشنهای NET. است.
با این حال، در سیستمهای بزرگ و پیچیدهی Distributed Systems، ممکن است استفاده از راهحلهای اختصاصی Load Balancer گزینهی بهتری باشد — چراکه کنترل دقیقتر و قابلیتهای پیشرفتهتری ارائه میدهند.
🔖هشتگها:
#YARP #DotNet #LoadBalancing #HorizontalScaling #PerformanceTesting #K6 #ReverseProxy