如何提高 .NET 中 Web API 的性能
由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!
在现代软件开发中,创建高性能 API 对于提供快速、稳定且响应迅速的用户体验至关重要。
无论您是构建新的 Web 应用程序还是优化现有应用程序,本文都将为您介绍 .NET 中提升性能的成熟策略。
这篇博文将带您了解 12 种可用于加快 API 速度、增强可扩展性和提高可维护性的基本技术。
我的网站antondevtips.com分享 .NET 和架构方面的最佳实践。
订阅即可成为更优秀的开发者。
1. 使用 async/await 进行异步编程
异步编程可以防止应用程序在等待 I/O 操作完成时阻塞线程。
这些操作包括:读取文件、等待数据库结果、等待 API 调用结果等等。
通过使用 async/await,.NET 可以并发处理更多请求而不会阻塞线程,从而提高可扩展性。
建议优先使用异步方法而非同步方法。
[HttpGet("items")]
public async Task<IActionResult> GetItemsAsync()
{
var items = await _itemService.GetItemsAsync();
return Ok(items);
}
2. 改进读取查询的数据访问模式(使用 EF Core 或 Dapper)
数据访问模式对API性能影响巨大。目前常用的数据库访问库有两个:EF Core和Dapper。
为了改进数据访问模式,请考虑优化查询。
添加数据库索引,优化查询,在 EF Core 中,考虑使用AsNoTracking只读查询。
// Read-only query with AsNoTracking
public async Task<List<User>> GetUsersAsync()
{
return await _dbContext.Users
.AsNoTracking()
.Where(u => u.IsActive)
.ToListAsync();
}
如果您使用 Dapper,请确保使用参数化查询、建立正确的索引,并尽量减少与数据库的往返次数。
3. 利用投影、分页和筛选减少数据传输
传输大型数据集会降低 API 的运行速度。
因此,请仅返回客户端所需的数据。
从数据库读取数据时,请考虑使用:
- 投影
- 筛选、排序
- 大型数据集的分页
投影功能允许您仅选择所需的字段,而不是检索整行数据:
var book = await context.Books
.Include(b => b.Author)
.Where(b => b.Id == id)
.Select(b => new BooksPreviewResponse
{
Title = b.Title, Author = b.Author.Name, Year = b.Year
})
.FirstOrDefaultAsync(cancellationToken);
为了提高读取查询的性能,可以考虑调整索引、过滤和排序条件。
对于大型数据集,请考虑使用分页来防止一次性向客户端返回过多数据:
[HttpGet("users")]
public async Task<IActionResult> GetUsersAsync(int pageNumber = 1,
int pageSize = 10)
{
var query = _dbContext.Users.AsNoTracking().Where(u => u.IsActive);
var users = await query
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.Select(u => new UserDto
{
Id = u.Id,
Name = u.Name
})
.ToListAsync();
return Ok(users);
}
4. 最大程度减少 JSON 序列化开销
返回 JSON 响应时,默认System.Text.Json库通常比其他库更快Newtonsoft.Json。
配置序列化选项可以去除不必要的字段,从而加快处理速度。
builder.Services
.AddControllers()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
options.JsonSerializerOptions.IgnoreNullValues = true;
});
PropertyNamingPolicy = JsonNamingPolicy.CamelCase确保 JSON 中命名的一致性。IgnoreNullValues = true:从输出中排除空属性,从而减小有效负载大小。
这虽然只是一个小小的优化,但却非常实用。
5. 添加缓存
缓存是您可以应用的最重要优化措施之一。
将频繁访问的数据存储在内存中或使用分布式缓存(例如 Redis)可以减少数据库往返次数。
我建议使用两种缓存方式:
- 输出缓存
- IDistributedCache / HybridCache (.NET 9) / Fusion Cache(存在多年的第三方库)
输出缓存会将整个 HTTP 响应存储一段时间,并在不再次执行控制器或 Minimal API 方法的情况下直接返回该响应:
builder.Services.AddOutputCache();
var app = builder.Build();
app.UseOutputCache();
[HttpGet("products")]
[OutputCache(Duration = 60)] // Cache response for 60 seconds
public async Task<IActionResult> GetProductsAsync()
{
var products = await _productService.GetProductsAsync();
return Ok(products);
}
这是最容易添加到应用程序中的缓存类型,因为它几乎不需要任何代码。
您可以使用标签使缓存失效(清除)。
OutputCache 也支持 Redis。
如果您需要更精细的缓存控制,可以考虑使用缓存库。
最近我一直在研究.NET 9 中的混合缓存,并推荐使用它。
这是一种两级缓存:内存缓存 + 分布式缓存,可以解决缓存冲突问题(即多个请求同时遇到缓存未命中,并按照缓存旁路模式调用数据库)。
以下是如何使用HybridCache:
builder.Services.AddHybridCache();
[HttpGet("orders/{id}")]
public async Task<IActionResult> GetOrderAsync(int id,
[FromServices] IHybridCache cache)
{
string cacheKey = $"Order_{id}";
var order = await cache.GetOrCreateAsync(cacheKey, async entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10);
using var context = new AppDbContext();
return await context.Orders.FindAsync(id);
});
if (order is null)
{
return NotFound();
}
return Ok(order);
}
当多个应用程序需要连接到缓存,或者需要在应用程序重启后保留缓存时,请使用像 Redis 这样的分布式缓存。
6. 启用响应压缩
使用 Brotli 或 GZIP 压缩响应可以显著减小有效载荷的大小。
更小的响应意味着更快的数据传输速度和更好的用户体验。
Brotli 和 Gzip 可以减小输出的 JSON、HTML 或静态文件数据的大小。
在流程早期添加它们可以确保较小的有效负载。
builder.Services.AddResponseCompression(options =>
{
options.EnableForHttps = true;
options.Providers.Add<BrotliCompressionProvider>();
options.Providers.Add<GzipCompressionProvider>();
});
builder.Services.Configure<BrotliCompressionProviderOptions>(options =>
{
options.Level = System.IO.Compression.CompressionLevel.Fastest;
});
builder.Services.Configure<GzipCompressionProviderOptions>(options =>
{
options.Level = System.IO.Compression.CompressionLevel.Fastest;
});
var app = builder.Build();
app.UseResponseCompression();
7. 优化中间件管道
中间件的执行顺序会影响性能。
某些中间件应该尽早运行,而其他中间件(例如路由、身份验证和授权)则应该遵循逻辑顺序。
如果您使用响应压缩,请确保在提供静态文件之前添加压缩,以获得更小的有效负载:
var app = builder.Build();
// Correct middleware ordering
app.UseResponseCompression(); // Early in the pipeline
app.UseStaticFiles(); // Serve static files
app.UseRouting();
app.UseAuthentication(); // Authentication before authorization
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
8. 启用 HTTP/2 和 HTTP/3 协议
与 HTTP/1 相比,HTTP/2 和 HTTP/3 提供了显著的性能提升,包括多路复用和更低的延迟。
配置 Kestrel 以支持多种协议非常简单:
// In Program.cs
builder.WebHost.ConfigureKestrel(options =>
{
// HTTP/1.1
options.ListenAnyIP(5000);
// HTTPS with HTTP/1.1, HTTP/2 and HTTP/3
options.ListenAnyIP(5001, listenOptions =>
{
listenOptions.UseHttps();
listenOptions.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;
});
});
var app = builder.Build();
// Rest of the configuration
支持较新协议的客户端将自动使用这些协议,而较旧的客户端可以回退到 HTTP/1.1。
9. 使用内容分发网络(CDN)
内容分发网络(CDN)是由地理位置分散的服务器组成的网络,它将内容缓存在靠近最终用户的服务器上。CDN
可以快速传输加载互联网内容所需的资源,包括 HTML 页面、JavaScript 文件、样式表、图像和视频。
当欧洲的客户端访问位于纽约的服务器所指向的网站时,可能会出现延迟问题。
当网站使用内容分发网络(CDN,例如 Cloudflare)时,距离客户端(在欧洲)最近的服务器会缓存网站内容,并将其返回给客户端,而无需向原始服务器发送请求。
通过将静态内容分发卸载到 CDN,您可以减轻自身服务器的负载并提高整体性能。
CDN 为您带来以下好处:
- 减少往返次数:CDN 从更靠近客户端的边缘节点提供内容,从而加快加载速度。
- 降低服务器负载:您的应用程序服务器专注于核心 API 功能。
- 此外,许多 CDN 提供安全防护,可抵御垃圾邮件、DDoS 攻击和机器人攻击。
10.实施速率限制和节流
速率限制可以控制资源使用并防止滥用行为。
通过限制特定时间段内的请求数量,您可以保护 API 免受过载或 DDoS 攻击。
此外,还可以实施速率限制,根据用户的订阅情况处理一定数量的请求,并限制额外的请求。
例如,ChatGPT 就是利用这种方式工作的。
以下是如何在 ASP.NET Core 中使用内置功能实现速率限制的方法:
// In Program.cs
builder.Services.AddRateLimiter(options =>
{
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, IPAddress>(context =>
{
var ipAddress = context.Connection.RemoteIpAddress;
return RateLimitPartition.GetFixedWindowLimiter(ipAddress,
_ => new FixedWindowRateLimiterOptions
{
PermitLimit = 100, // Allow 100 requests
Window = TimeSpan.FromMinutes(1), // Per 1 minute window
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 0
});
});
});
var app = builder.Build();
app.UseRateLimiter();
本示例使用 .NET 速率限制 API 将每个 IP 地址的请求数限制为每分钟 100 次。
11. 用极简 API 取代控制器
极简 API 可以减少传统 MVC 流程的一些开销。
.NET 9 中的 Minimal API 性能得到了极大的提升,每秒可处理的请求数比 .NET 8 多15%。
此外,Minimal API 的内存消耗量比以前的版本减少了93% 。
它们简洁、快速,并支持依赖注入、过滤器和其他常见的 ASP.NET Core 功能:
var app = builder.Build();
// Minimal API endpoint
app.MapGet("/customers/{id}", async (int id) =>
{
var customer = await _dbContext.Customers.FindAsync(id);
return customer is not null
? Results.Ok(customer)
: Results.NotFound();
});
app.Run();
与控制器相比,使用最小 API 几乎可以构建任何东西。
而且,微软在每个 .NET 版本中都主要关注最小 API 而不是控制器。
12. 使用 GraphQL
GraphQL 允许客户端在单个请求中从多个资源中精确查询所需字段,从而提升性能。
这与 REST API 不同,REST API 需要为每个资源发送一个新请求。
在 .NET 中实现 GraphQL 最流行且功能最丰富的库是Hot Chocolate:
请考虑以下模型:
public record Product(
int Id,
string Name,
decimal Price,
bool IsAvailable
);
public record Review(
int Id,
int ProductId,
string Content,
int Rating
);
public record Recommendation(
int Id,
string Name,
decimal Price
);
以下是 GraphQL 查询:
public record Product(int Id, string Name, decimal Price);
public class Query
{
[UseOffsetPaging(MaxPageSize = 100, IncludeTotalCount = true)]
[UseProjection]
[UseFiltering]
[UseSorting]
public IQueryable<Product> GetProducts(AppDbContext context)
{
return context.Products;
}
public Task<Product?> GetProductByIdAsync(AppDbContext context, int id)
{
return await context.Products.FirstOrDefaultAsync(p => p.Id == id);
}
public async Task<Recommendation> GetProductRecommendationsByIdAsync(
AppDbContext context,
IRecommendationService service,
int productId,
int recommendationsCount)
{
var product = await context.Products.FindAsync(productId);
if (product is null)
{
return [];
}
var recommendations = await service.GetRecommendationsForProduct(product.Name, recommendationsCount)
.ToListAsync();
return recommendations;
}
public async Task<Review> GetReviewsByProductIdAsync(
AppDbContext context, int productId)
{
var reviews = await context.Reviews
.AsNoTracking()
.Where(r => r.ProductId == productId)
.ToListAsync();
return reviews;
}
}
IQueryable<Product> GetProducts- 返回一个与 GraphQL 引擎连接的 IQueryable 对象,该对象使用以下中间件:
- 过滤
- 排序
- 分页
其他方法是您在 Web API 端点中使用控制器或最小 API 时常用的典型方法。
以下是如何在客户端 Web 应用程序中使用 GraphQL 的方法:
query {
getProducts(
where: { name: { contains: "Phone" }, price: { eq: 1000 } }
order: [{ name: ASC }
skip: 10
take: 20
) {
id
name
price
isAvailable
}
}
我们完全支持筛选、排序和分页功能,几乎无需编写任何代码;所有功能都由 Hot Chocolate 处理。
而这正是 GraphQL 相较于 REST API 的优势所在:
query {
getProductById(id: 1) {
id
name
price
isAvailable
getReviewsByProductId(productId: 1) {
id
content
rating
}
getProductRecommendationsById(productId: 1) {
id
name
price
}
}
}
在这里,我们只需向服务器发送一个 HTTP 请求,即可通过产品标识符获取产品信息、产品评价和推荐内容。
而使用 REST API 时,则需要发送三个单独的请求,或者自行实现 API 聚合器。
概括
考虑使用以下技术来显著提高 API 吞吐量和用户体验:
- 利用 async/await
- 优化数据库查询
- 利用投影、分页和过滤
- 最小化 JSON 序列化开销
- 实现缓存(输出缓存、内存缓存、Redis 等)
- 使用响应压缩
- 谨慎订购中间件
- 启用 HTTP/2 和 HTTP/3
- 通过 CDN 提供静态资源
- 增加速率限制和节流
- 在 .NET 9 中切换到最小 API
- 集成 GraphQL
逐步采用这些技巧,使用 OpenTelemetry、Benchmarks 等工具或 Application Insights 等基于云的工具来衡量改进情况,并不断改进您的 .NET Web 应用程序,以获得最佳结果。
文章来源:https://dev.to/antonmartyniuk/how-to-increase-performance-of-web-apis-in-net-4nnf我的网站antondevtips.com分享 .NET 和架构方面的最佳实践。
订阅即可成为更优秀的开发者。