Caching (کش) و شتابدهی HTTP با HTTP/2 و HTTP/3
منبع: Coding Clean, Reliable, and Safe REST APIs with ASP.NET Core 8 — Anthony Giretti
اعتبار ترجمه: ترجمه با کمک هوش مصنوعی
فروش یا انتشار این ترجمه منوط به داشتن مجوز لازم از صاحب حقوق اثر است.
تکمیل JSON Streaming
نویسنده برای نمایش رفتار Streaming در یک Client جاوااسکریپتی، نمونهای ارائه کرده است که نشان میدهد آیتمها چگونه بهتدریج دریافت و نمایش داده میشوند. این مثال ادامهٔ IAsyncEnumerable بخش قبل است.
Caching (کش)
کش یعنی نگهداشتن اطلاعات پرتکرار در حافظه یا ذخیرهسازی سریعتر تا در درخواستهای بعدی مجبور نباشیم همان داده را دوباره از منبع اصلی تولید یا دریافت کنیم. این موضوع برای دادهای که از Database یا منبع خارجی میآید اهمیت زیادی دارد، چون دسترسی I/O معمولاً از خواندن Cache پرهزینهتر است.
ASP.NET Core سه نوع اصلی Cache ارائه میکند:
- HTTP/Output Cache: پاسخ روی Browser یا Proxy Cache میشود.
- In-Memory Cache: داده در RAM همان Server ذخیره میشود.
- Distributed Cache: داده در Server یا Service خارجی مشترک نگهداری میشود تا چند Instance برنامه به آن دسترسی داشته باشند.
Output Cache
Output Cache بسیار مؤثر اما محدود است. پاسخ HTTP میتواند روی Proxy یا Browser Cache شود. این مدل برای درخواستهای GET و HEAD با پاسخ موفق ۲۰۰ کاربرد دارد و Responseهایی که Cookie تولید میکنند یا Authentication دارند، بر اساس محدودیتهای بیانشده در منبع، در این سناریو Cache نمیشوند.
مزیت آن این است که ممکن است Request اصلاً به Server نرسد. همین مزیت یک محدودیت نیز هست: اگر برنامه باید هر درخواست را برای Logging، Statistics یا رفتارهای Server-side مشاهده کند، Output Cache میتواند این امکان را از بین ببرد. بنابراین نویسنده استفاده از آن را برای Endpointهایی پیشنهاد میکند که نیاز به ثبت رفتار کاربر یا Authentication ندارند.
شکل ۷-۵ — مفهوم Output Cache
Output Cache با AddOutputCache پیکربندی و با UseOutputCache فعال میشود. میتوان Base Policy سراسری و Policyهای نامدار ساخت.
var builder = WebApplication.CreateBuilder(args);
...
builder.Services.AddOutputCache(options =>
{
options.AddBasePolicy(builder =>
builder.Expire(TimeSpan.FromSeconds(30))
.SetVaryByQuery("*")
);
options.AddPolicy("5minutes", builder =>
builder.Expire(TimeSpan.FromSeconds(300))
.SetVaryByQuery("*")
);
});
var app = builder.Build();
app.UseOutputCache();
....
app.Run();
Listing 7-18 — پیکربندی Output Cache در Program.cs
Base Policy داده را ۳۰ ثانیه نگه میدارد و با SetVaryByQuery("*") برای Query Stringهای متفاوت Cache Entry جداگانه ایجاد میکند. بنابراین /countries?pageIndex=1 و /countries?pageIndex=2 یک Cache مشترک ندارند.
Policy نامدار 5minutes پنج دقیقه عمر دارد و فقط روی Endpointهایی اعمال میشود که آن را صریحاً انتخاب کنند.
app.MapGet("/cachedcountries", async (
int? pageIndex,
int? pageSize,
ICountryMapper mapper,
ICountryService countryService) => {
var countries = await countryService
.GetAllAsync(new PagingDto
{
PageIndex = pageIndex.HasValue ? pageIndex.Value : 1,
PageSize = pageSize.HasValue ? pageSize.Value : 10
});
return Results.Ok(mapper.Map(countries));
}).CacheOutput("5minutes");
Listing 7-19 — GET /cachedcountries با Policy پنجدقیقهای
در نخستین Request پاسخ عادی تولید میشود. وقتی پاسخ از Cache برگردد، Header با نام Age مدت ماندن Response در Cache را به Client نشان میدهد.
شکل ۷-۶ — پاسخ Cacheشدهٔ GET /cachedcountries و Header نوع Age
نویسنده بهدلیل محدودیتهای این روش، Output Cache را کمتر از مدلهای دادهمحور بعدی استفاده میکند.
In-Memory Cache
در In-Memory Cache فقط دادهای که برنامه انتخاب میکند Cache میشود، نه کل Response HTTP. Request همچنان به Server میرسد؛ بنابراین Logging، Statistics و سایر منطقهای Pipeline قابل اجرا هستند. برای یک API تکسروری این روش بسیار رایج است، اما Cache به همان Server وابسته است. در Server Farm هر Instance Cache جداگانه خواهد داشت؛ در چنین حالتی Distributed Cache مناسبتر است.
شکل ۷-۷ — مفهوم In-Memory Cache
برای حفظ Single Responsibility Principle (SRP)، منطق Cache مستقیماً داخل CountryService قرار نمیگیرد. بهجای آن Decorator Pattern استفاده میشود. کلاس CachedCountryService همان ICountryService را پیادهسازی میکند و یک ICountryService دیگر را Decorate میکند. این کلاس فقط مسئول افزودن رفتار Cache است و سایر عملیات را به Service اصلی Delegate میکند.
در لایهٔ BLL بستهٔ Microsoft.Extensions.Caching.Abstractions برای IMemoryCache و در API بستهٔ Microsoft.Extensions.Caching.Memory برای Implementation کامل نصب میشود.
using Domain.DTOs;
using Domain.Services;
using Microsoft.Extensions.Caching.Memory;
namespace BLL.Services;
public class CachedCountryService : ICountryService
{
private readonly ICountryService _countryService;
private readonly IMemoryCache _memoryCache;
public CachedCountryService(
ICountryService countryService,
IMemoryCache memoryCache)
{
_countryService = countryService;
_memoryCache = memoryCache;
}
public async Task<List<CountryDto>> GetAllAsync(
PagingDto paging)
{
var cachedValue = await _memoryCache.GetOrCreateAsync(
$"countries-{paging.PageIndex}-{paging.PageSize}",
async cacheEntry =>
{
cacheEntry.AbsoluteExpirationRelativeToNow =
TimeSpan.FromSeconds(30);
return await _countryService.GetAllAsync(paging);
});
return cachedValue;
}
public async Task<(byte[], string, string)> GetFileAsync()
{
return await _countryService.GetFileAsync();
}
public async Task<bool> IngestFileAsync(
Stream countryFileContent)
{
return await _countryService.IngestFileAsync(countryFileContent);
}
public async Task LongRunningQueryAsync(
CancellationToken cancellationToken)
{
await _countryService.LongRunningQueryAsync(cancellationToken);
}
}
Listing 7-20 — Decorator نوع CachedCountryService
فقط GetAllAsync رفتار Cache دارد. Key از تمام ورودیهای مؤثر بر نتیجه، یعنی PageIndex و PageSize، ساخته میشود تا هر ترکیب داده Entry مستقل داشته باشد. GetOrCreateAsync اگر Key موجود باشد مقدار Cacheشده را برمیگرداند و در غیر این صورت Service اصلی را صدا میزند و نتیجه را برای ۳۰ ثانیه با AbsoluteExpirationRelativeToNow ذخیره میکند.
نویسنده برای دادهای که ممکن است تغییر کند، Sliding Expiration را ترجیح نمیدهد؛ زیرا هر دسترسی میتواند زمان انقضا را عقب بیندازد و باعث شود دادهٔ قدیمی مدت طولانیتری زنده بماند.
var builder = WebApplication.CreateBuilder(args);
...
builder.Services.AddScoped<ICountryService, CountryService>();
builder.Services.Decorate<ICountryService,
CachedCountryService>();
builder.Services.AddMemoryCache();
...
var app = builder.Build();
...
app.MapGet("/cachedinmemorycountries", async (
ICountryMapper mapper,
ICountryService countryService) => {
var countries = await countryService
.GetAllAsync(new PagingDto
{
PageIndex = 1,
PageSize = 10
});
return Results.Ok(mapper.Map(countries));
});
...
app.Run();
Listing 7-21 — پیکربندی In-Memory Cache و Decorator Pattern
AddMemoryCache Cache را ثبت میکند و Decorate از بستهٔ Scrutor مشخص میکند CachedCountryService روی CountryService قرار گیرد. منبع اشاره میکند که هنگام نگارش، Scrutor با Preview 7 از .NET 8 مشکل شناختهشدهای داشته است.
Distributed Cache
Distributed Cache داده را خارج از Web Server، روی Server دیگری یا یک Cloud Service نگه میدارد. در نتیجه چند Instance برنامه میتوانند Cache مشترکی داشته باشند. Providerهای نمونه عبارتاند از:
- SQL Server Distributed Cache
- Redis
- In-Memory Distributed Cache
- NCache
شکل ۷-۸ — مفهوم Distributed Cache
کتاب Redis را مثال میزند؛ یک پایگاه دادهٔ In-Memory پرکاربرد برای Cache در برنامههای Distributed و پرترافیک. در BLL از IDistributedCache موجود در Microsoft.Extensions.Caching.Abstractions استفاده میشود و در API بستهٔ Microsoft.Extensions.Caching.StackExchangeRedis Implementation مربوط را ثبت میکند.
using Domain.DTOs;
using Domain.Services;
using Microsoft.Extensions.Caching.Distributed;
using System.Text.Json;
namespace BLL.Services;
internal class DistributedCachedCountryService : ICountryService
{
private readonly ICountryService _countryService;
private readonly IDistributedCache _distributedCache;
public DistributedCachedCountryService(
ICountryService countryService,
IDistributedCache distributedCache)
{
_countryService = countryService;
_distributedCache = distributedCache;
}
public async Task<List<CountryDto>>
GetAllAsync(PagingDto paging)
{
var key = $"countries-{paging.PageIndex}-{paging.PageSize}";
var cachedValue = await _distributedCache.GetStringAsync(key);
if (cachedValue == null)
{
var data = await _countryService.GetAllAsync(paging);
await _distributedCache.SetStringAsync(key,
JsonSerializer.Serialize(data),
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(30)
});
return data;
}
return JsonSerializer.Deserialize<List<CountryDto>>(cachedValue);
}
public async Task<(byte[], string, string)> GetFileAsync()
{
return await _countryService.GetFileAsync();
}
public async Task<bool> IngestFileAsync(Stream countryFileContent)
{
return await _countryService.IngestFileAsync(countryFileContent);
}
public async Task LongRunningQueryAsync(CancellationToken cancellationToken)
{
await _countryService.LongRunningQueryAsync(cancellationToken);
}
}
Listing 7-22 — Decorator نوع DistributedCachedCountryService
IDistributedCache در این نمونه String ذخیره میکند، بنابراین List کشورها با System.Text.Json Serialize و هنگام خواندن Deserialize میشود. SetStringAsync و GetStringAsync مسئول نوشتن و خواندن Redis هستند و DistributedCacheEntryOptions زمان انقضای ۳۰ ثانیه را تعیین میکند.
using Microsoft.Extensions.Caching.StackExchangeRedis;
var builder = WebApplication.CreateBuilder(args);
...
builder.Services.AddScoped<ICountryService, CountryService>();
builder.Services.Decorate<ICountryService,
DistributedCachedCountryService>();
...
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = builder.Configuration
.GetConnectionString("RedisConnectionString");
options.InstanceName = "Demo";
});
var app = builder.Build();
...
app.MapGet("/cachedinmemorycountries", async (
ICountryMapper mapper,
ICountryService countryService) => {
var countries = await countryService
.GetAllAsync(new PagingDto
{
PageIndex = 1,
PageSize = 10
});
return Results.Ok(mapper.Map(countries));
});
...
app.Run();
Listing 7-23 — پیکربندی Distributed Cache و Decorator
AddStackExchangeRedisCache Connection String Redis و InstanceName را دریافت میکند. Instance Name به Keyها Prefix اضافه میکند تا برنامههای مختلفی که از Redis مشترک استفاده میکنند تصادف Key نداشته باشند.
سریعتر کردن HTTP با HTTP/2 و HTTP/3
HTTP/1.1 پایهای است که در فصل ۱ بررسی شد. HTTP/2 بازطراحی مهمتری از پروتکل است و در شرایط مناسب میتواند ارتباط را کارآمدتر کند. ASP.NET Core از HTTP/2 پشتیبانی میکند، اما Client و Browser نیز باید آن را پشتیبانی کنند. بنابراین Server میتواند چند نسخهٔ HTTP را همزمان فعال نگه دارد.
HTTP/3 نیز نسخهٔ جدیدتری است و ASP.NET Core 8 از آن پشتیبانی میکند. بهدلیل تفاوت سطح پشتیبانی Clientها، Configuration کتاب HTTP/1.1، HTTP/2 و HTTP/3 را همزمان مجاز میکند تا Negotiation مناسب انجام شود.
"Kestrel": {
"EndpointDefaults": {
"Protocols": "Http1AndHttp2AndHttp3"
}
}
Listing 7-24 — فعالسازی HTTP/1.1، HTTP/2 و HTTP/3 در appsettings.json
جمعبندی فصل
تا اینجا API از نظر کدنویسی، معماری و Optimization پیش رفته است. فصل ۷ برنامهنویسی Async، Cancellation، Background Service و Channel، Paging، JSON Streaming، سه مدل Cache و پشتیبانی چند نسخهٔ HTTP را پوشش داد. فصل بعد از ساخت و Optimization عبور میکند و روی Observability تمرکز دارد: Logging، Metrics و Tracing برای فهم رفتار واقعی برنامه در زمان استفاده.
تصاویر منبع مرتبط با این بخش