• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    公众号

ASP.NET Core 高性能开发最佳实践

原作者: [db:作者] 来自: [db:来源] 收藏 邀请

from:https://docs.microsoft.com/zh-cn/aspnet/core/performance/performance-best-practices?view=aspnetcore-3.1

Mike Rousos

本文提供了有关 ASP.NET Core 的性能最佳做法的准则。

主动缓存

响应缓存在 ASP.NET Core。

了解热代码路径

热代码路径通常限制应用向外缩放和性能,并将在本文档的几个部分中进行讨论。

避免阻止调用

线程可以处理另一请求,而不是等待长时间运行的同步任务完成。

线程池不足并降低响应时间。

请勿:

  • task.来阻止异步执行。
  • 当构建为并行运行代码时,ASP.NET Core 应用程序的性能最高。
  • 即使计划的代码会阻止线程,任务也不会阻止。

建议做法:

  • 热代码路径处于异步状态。
  • 任务。运行以使 synchronus API 成为异步。
  • async/await模式,整个调用堆栈是异步的。

Microsoft-Windows-DotNETRuntime/ThreadPoolWorkerThread/Start 事件表示添加到线程池中的线程。

最小化大型对象分配

频繁分配和取消分配大型对象会导致性能不一致。

建议:

  • 缓存大型对象会阻止开销较高的分配。
  • ArrayPool<t >来存储大型数组,从而对缓冲区进行缓冲。
  • 热代码路径上分配很多生存期较短的大型对象。

PerfView中的垃圾回收(GC)统计信息并进行检查来诊断内存问题,例如前面的问题:

  • 垃圾回收暂停时间。
  • 垃圾回收所用的处理器时间百分比。
  • 第0代、第1代和第2代垃圾回收量。

垃圾回收和性能。

优化数据访问和 i/o

有效读取和写入数据对于良好的性能至关重要。

建议:

  • 请以异步方式调用所有数据访问 api。
  • 编写查询以仅返回当前 HTTP 请求所必需的数据。
  • 响应缓存在 ASP.NET Core。
  • 目标是使用单个调用而不是多个调用来检索所需数据。
  • EF Core 可以更有效地返回非跟踪查询的结果。
  • 筛选和聚合 LINQ 查询(例如,使用 .Where.Select或 .Sum 语句),以便数据库执行筛选。
  • 客户端评估性能问题。
  • 相关子查询的优化。

EF 高性能,了解可提高大规模应用程序性能的方法:

已编译查询的额外复杂性可能不会提高性能。

大多数数据库还提供有关频繁执行的查询的统计信息。

与 HttpClientFactory 建立池 HTTP 连接

它处理池 HTTP 连接以优化性能和可靠性。

建议:

  • 不要直接创建和释放 HttpClient 实例。
  • 使用 HttpClientFactory 实现复原 HTTP 请求。

快速保持通用代码路径

您希望所有的代码都是快速的,通常称为代码路径是最重要的,可进行优化:

  • 这些组件会对性能产生很大的影响。
  • 例如,自定义日志记录、授权处理程序或暂时性服务的初始化。

建议:

  • 不要将自定义中间件组件用于长时间运行的任务。
  • 热代码路径。

在 HTTP 请求之外完成长时间运行的任务

对于涉及长时间运行的任务的某些请求,最好将整个请求响应过程设为异步处理。

建议:

  • 请不要等待长时间运行的任务在普通的 HTTP 请求处理过程中完成。
  • 在进程外完成工作对于 CPU 密集型任务特别有用。
  • SignalR)以异步方式与客户端进行通信。

缩小客户端资产

可以通过以下方式改善初始负载请求的性能:

  • 绑定,将多个文件合并到一个文件中。
  • 缩小,它通过删除空白和注释来减小文件大小。

建议:

  • 内置支持,以便对客户端资产进行捆绑和缩小。
  • Webpack),以实现复杂的客户端资产管理。

压缩响应

响应压缩。

使用最新 ASP.NET Core 版本

如果性能是优先考虑的,请考虑升级到 ASP.NET Core 的当前版本。

最小化异常

因此,不应使用异常来控制正常的程序流。

建议:

  • 热代码路径中。
  • 在应用程序中包括逻辑,以检测和处理会导致异常的情况。
  • 引发或捕获异常或意外情况的异常。

应用诊断工具(如 Application Insights)可帮助识别应用中可能影响性能的常见异常。

性能和可靠性

以下各节提供了性能提示以及已知的可靠性问题和解决方案。

避免 HttpRequest/Httpresponse.cache 正文上的同步读取或写入

阻塞线程可能会导致线程池不足。

通过异步同步的示例。

复制
 
public class BadStreamReaderController : Controller
{
    [HttpGet("/contoso")]
    public ActionResult<ContosoData> Get()
    {
        var json = new StreamReader(Request.Body).ReadToEnd();

        return JsonSerializer.Deserialize<ContosoData>(json);
    }
}

应用通过异步同步,因为 Kestrel不支持同步读取。

ReadToEndAsync,在读取时不会阻止线程。

复制
 
public class GoodStreamReaderController : Controller
{
    [HttpGet("/contoso")]
    public async Task<ActionResult<ContosoData>> Get()
    {
        var json = await new StreamReader(Request.Body).ReadToEndAsync();

        return JsonSerializer.Deserialize<ContosoData>(json);
    }

}

前面的代码异步将整个 HTTP 请求正文读入内存中。

 警告

避免将大型请求正文或响应正文读入内存中。

执行以下操作: 下面的示例使用非缓冲请求正文完全异步:

复制
 
public class GoodStreamReaderController : Controller
{
    [HttpGet("/contoso")]
    public async Task<ActionResult<ContosoData>> Get()
    {
        return await JsonSerializer.DeserializeAsync<ContosoData>(Request.Body);
    }
}

前面的代码将请求正文异步反序列化为C#对象。

首选 ReadFormAsync over 请求。窗体

仅在以下情况下,才能安全地读取 HttpContext.Request.Form

  • 已通过调用 ReadFormAsync读取了窗体,且
  • 正在使用读取缓存的窗体值 HttpContext.Request.Form

通过异步使用同步,可能会导致线程池不足。

复制
 
public class BadReadController : Controller
{
    [HttpPost("/form-body")]
    public IActionResult Post()
    {
        var form =  HttpContext.Request.Form;

        Process(form["id"], form["name"]);

        return Accepted();
    }

执行以下操作: 下面的示例使用 HttpContext.Request.ReadFormAsync 以异步方式读取窗体体。

复制
 
public class GoodReadController : Controller
{
    [HttpPost("/form-body")]
    public async Task<IActionResult> Post()
    {
       var form = await HttpContext.Request.ReadFormAsync();

        Process(form["id"], form["name"]);

        return Accepted();
    }

避免将大型请求正文或响应正文读入内存

大型对象的开销很大:

  • CLR 确保清除所有新分配对象的内存。
  • Gen2 集合。

博客文章简单介绍了问题:

但一定不要用于高性能的 web 服务器,在这种情况下,需要少量的大内存缓冲区来处理平均 web 请求(从套接字读取、解压缩、解码 JSON & 更多)。

将大型请求或响应正文存储到单个 byte[] 或 string中的 Naively:

  • 可能会导致 LOH 中的空间快速耗尽。
  • 可能导致应用程序出现性能问题,因为正在运行完全 Gc。

使用同步数据处理 API

JSON.NET)时:

  • 将数据异步缓冲到内存中,然后将其传递给序列化程序/反序列化程序。

 警告

避免将大型请求正文或响应正文读入内存中。

System.Text.Json:

  • 以异步方式读取和写入 JSON。
  • 针对 UTF-8 文本进行了优化。
  • 通常比 Newtonsoft.Json 性能更高。

不要在字段中存储 IHttpContextAccessor

IHttpContextAccessor.HttpContext不应存储在字段或变量中。

请勿执行此操作: 下面的示例将 HttpContext 存储在字段中,然后稍后尝试使用它。

复制
 
public class MyBadType
{
    private readonly HttpContext _context;
    public MyBadType(IHttpContextAccessor accessor)
    {
        _context = accessor.HttpContext;
    }

    public void CheckAdmin()
    {
        if (!_context.User.IsInRole("admin"))
        {
            throw new UnauthorizedAccessException("The current user isn't an admin");
        }
    }
}

前面的代码在构造函数中频繁捕获 null 或不正确的 HttpContext

执行以下操作: 下面的示例:

  • IHttpContextAccessor 存储在字段中。
  • 在正确的时间使用 HttpContext 字段并检查 null
复制
 
public class MyGoodType
{
    private readonly IHttpContextAccessor _accessor;
    public MyGoodType(IHttpContextAccessor accessor)
    {
        _accessor = accessor;
    }

    public void CheckAdmin()
    {
        var context = _accessor.HttpContext;
        if (context != null && !context.User.IsInRole("admin"))
        {
            throw new UnauthorizedAccessException("The current user isn't an admin");
        }
    }
}

不要从多个线程访问 HttpContext

并行访问来自多个线程的 HttpContext 可能会导致未定义的行为,如挂起、崩溃和数据损坏。

可以从多个线程访问请求路径,可能会并行进行。

复制
 
public class AsyncBadSearchController : Controller
{       
    [HttpGet("/search")]
    public async Task<SearchResults> Get(string query)
    {
        var query1 = SearchAsync(SearchEngine.Google, query);
        var query2 = SearchAsync(SearchEngine.Bing, query);
        var query3 = SearchAsync(SearchEngine.DuckDuckGo, query);

        await Task.WhenAll(query1, query2, query3);

        var results1 = await query1;
        var results2 = await query2;
        var results3 = await query3;

        return SearchResults.Combine(results1, results2, results3);
    }       

    private async Task<SearchResults> SearchAsync(SearchEngine engine, string query)
    {
        var searchResults = _searchService.Empty();
        try
        {
            _logger.LogInformation("Starting search query from {path}.", 
                                    HttpContext.Request.Path);
            searchResults = _searchService.Search(engine, query);
            _logger.LogInformation("Finishing search query from {path}.", 
                                    HttpContext.Request.Path);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed query from {path}", 
                             HttpContext.Request.Path);
        }

        return await searchResults;
    }

执行以下操作: 下面的示例在发出三个并行请求之前复制传入请求中的所有数据。

复制
 
public class AsyncGoodSearchController : Controller
{       
    [HttpGet("/search")]
    public async Task<SearchResults> Get(string query)
    {
        string path = HttpContext.Request.Path;
        var query1 = SearchAsync(SearchEngine.Google, query,
                                 path);
        var query2 = SearchAsync(SearchEngine.Bing, query, path);
        var query3 = SearchAsync(SearchEngine.DuckDuckGo, query, path);

        await Task.WhenAll(query1, query2, query3);

        var results1 = await query1;
        var results2 = await query2;
        var results3 = await query3;

        return SearchResults.Combine(results1, results2, results3);
    }

    private async Task<SearchResults> SearchAsync(SearchEngine engine, string query,
                                                  string path)
    {
        var searchResults = _searchService.Empty();
        try
        {
            _logger.LogInformation("Starting search query from {path}.",
                                   path);
            searchResults = await _searchService.SearchAsync(engine, query);
            _logger.LogInformation("Finishing search query from {path}.", path);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed query from {path}", path);
        }

        return await searchResults;
    }

请求完成后,不要使用 HttpContext

从此链返回的 Task 完成后,HttpContext 将被回收。

请勿执行此操作: 下面的示例使用 async void,这会在达到第一个 await 时完成 HTTP 请求:

  • 在 ASP.NET Core 应用程序中,这始终是一种不好的做法。
  • HTTP 请求完成后,访问 HttpResponse
  • 崩溃进程。
复制
 
public class AsyncBadVoidController : Controller
{
    [HttpGet("/async")]
    public async void Get()
    {
        await Task.Delay(1000);

        // The following line will crash the process because of writing after the 
        // response has completed on a background thread. Notice async void Get()

        await Response.WriteAsync("Hello World");
    }
}

执行以下操作: 下面的示例将 Task 返回到框架,以便在操作完成之前,不会完成 HTTP 请求。

复制
 
public class AsyncGoodTaskController : Controller
{
    [HttpGet("/async")]
    public async Task Get()
    {
        await Task.Delay(1000);

        await Response.WriteAsync("Hello World");
    }
}

不要捕获后台线程中的 HttpContext

这是一种不好的做法,因为工作项可以:

  • 在请求范围之外运行。
  • 尝试读取错误的 HttpContext
复制
 
[HttpGet("/fire-and-forget-1")]
public IActionResult BadFireAndForget()
{
    _ = Task.Run(async () =>
    {
        await Task.Delay(1000);

        var path = HttpContext.Request.Path;
        Log(path);
    });

    return Accepted();
}

执行以下操作: 下面的示例:

  • 在请求过程中复制后台任务所需的数据。
  • 不从控制器引用任何内容。
复制
 
[HttpGet("/fire-and-forget-3")]
public IActionResult GoodFireAndForget()
{
    string path = HttpContext.Request.Path;
    _ = Task.Run(async () =>
    {
        await Task.Delay(1000);

        Log(path);
    });

    return Accepted();
}

使用托管服务的后台任务。

不要捕获注入到后台线程控制器的服务

ContosoDbContext 的作用域限定为请求,导致 ObjectDisposedException

复制
 
[HttpGet("/fire-and-forget-1")]
public IActionResult FireAndForget1([FromServices]ContosoDbContext context)
{
    _ = Task.Run(async () =>
    {
        await Task.Delay(1000);

        context.Contoso.Add(new Contoso());
        await context.SaveChangesAsync();
    });

    return Accepted();
}

执行以下操作: 下面的示例:

  • IServiceScopeFactory 为单一实例。
  • 在后台线程中创建新的依赖项注入范围。
  • 不从控制器引用任何内容。
  • 不捕获传入请求中的 ContosoDbContext
复制
 
[HttpGet("/fire-and-forget-3")]
public IActionResult FireAndForget3([FromServices]IServiceScopeFactory 
                                    serviceScopeFactory)
{
    _ = Task.Run(async () =>
    {
        await Task.Delay(1000);

        using (var scope = serviceScopeFactory.CreateScope())
        {
            var context = scope.ServiceProvider.GetRequiredService<ContosoDbContext>();

            context.Contoso.Add(new Contoso());

            await context.SaveChangesAsync();                                        
        }
    });

    return Accepted();
}

以下突出显示的代码:

  • 在后台操作的生存期内创建一个范围,并从中解析服务。
  • 使用来自正确范围的 ContosoDbContext
复制
 
[HttpGet("/fire-and-forget-3")]
public IActionResult FireAndForget3([FromServices]IServiceScopeFactory 
                                    serviceScopeFactory)
{
    _ = Task.Run(async () =>
    {
        await Task.Delay(1000);

        using (var scope = serviceScopeFactory.CreateScope())
        {
            var context = scope.ServiceProvider.GetRequiredService<ContosoDbContext>();

            context.Contoso.Add(new Contoso());

            await context.SaveChangesAsync();                                        
        }
    });

    return Accepted();
}

请不要在响应正文开始后修改状态代码或标头

第一次写入响应时:

  • 标头将与主体块区一起发送到客户端。
  • 不能再更改响应标头。

请勿执行此操作: 以下代码在响应已启动之后尝试添加响应标头:

复制
 
app.Use(async (context, next) =>
{
    await next();

    context.Response.Headers["test"] = "test value";
});

 会引发异常。

执行以下操作: 下面的示例在修改标头之前检查 HTTP 响应是否已启动。

复制
 
app.Use(async (context, next) =>
{
    await next();

    if (!context.Response.HasStarted)
    {
        context.Response.Headers["test"] = "test value";
    }
});

执行以下操作: 下面的示例使用 HttpResponse.OnStarting 在将响应标头刷新到客户端之前设置标头。

检查响应是否尚未开始:

  • 提供了随时追加或重写标头的功能。
  • 不需要了解管道中的下一个中间件。
复制
 
app.Use(async (context, next) =>
{
    context.Response.OnStarting(() =>
    {
        context.Response.Headers["someheader"] = "somevalue";
        return Task.CompletedTask;
    });

    await next();
});

如果已开始写入响应正文,则不调用 next ()

仅当组件可以处理和操作响应时,才应调用组件。


鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

专题导读
热门推荐
热门话题
阅读排行榜

扫描微信二维码

查看手机版网站

随时了解更新最新资讯

139-2527-9053

在线客服(服务时间 9:00~18:00)

在线QQ客服
地址:深圳市南山区西丽大学城创智工业园
电邮:jeky_zhao#qq.com
移动电话:139-2527-9053

Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap