站点图标 江湖人士

ASP.NET Core 性能优化最佳做法

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

主动缓存

此文档的几个部分讨论了缓存。 有关详细信息,请参阅 ASP.NET Core 中的响应缓存

了解热代码路径

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

ASP.NET Core 性能优化最佳做法

避免阻止调用

应将 ASP.NET Core 应用程序设计为同时处理许多请求。 异步 Api 允许一小部分线程通过不等待阻止调用来处理上千个并发请求。 线程可以处理另一请求,而不是等待长时间运行的同步任务完成。

ASP.NET Core 应用中的常见性能问题是阻止可能是异步的调用。 很多同步阻塞调用会导致 线程池 不足并降低响应时间。

ASP.NET Core 性能优化最佳做法请勿

Do

探查器(如 PerfView)可用于查找频繁添加到 线程池中的线程。 Microsoft-Windows-DotNETRuntime/ThreadPoolWorkerThread/Start事件指示添加到线程池的线程。

ASP.NET Core 性能优化最佳做法

返回 IEnumerable <T> 或 IAsyncEnumerable<T>

IEnumerable<T>从操作返回会导致序列化程序同步集合迭代。 因此会阻止调用,并且可能会导致线程池资源不足。 若要避免同步枚举,请 ToListAsync 在返回可枚举的前使用。

从 ASP.NET Core 3.0 开始, IAsyncEnumerable<T> 可将其用作 IEnumerable<T> 异步枚举的替代方法。 有关详细信息,请参阅 控制器操作返回类型

最小化大型对象分配

.Net Core 垃圾回收器在 ASP.NET Core 应用中自动管理内存的分配和释放。 自动垃圾回收通常意味着开发人员无需担心如何或何时释放内存。 但是,清理未引用的对象会占用 CPU 时间,因此开发人员应最大限度地减少 热代码路径中的对象分配。 垃圾回收在大型对象上特别昂贵 ( # A0 85 K 字节) 。 大型对象存储在 大型对象堆 上,需要完整的 (第2代) 垃圾回收。 与第0代和第1代回收不同,第2代回收需要临时暂停应用执行。 频繁分配和取消分配大型对象会导致性能不一致。

ASP.NET Core 性能优化最佳做法建议:

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

有关详细信息,请参阅 垃圾回收和性能

ASP.NET Core 性能优化最佳做法

优化数据访问和 i/o

与数据存储和其他远程服务的交互通常是 ASP.NET Core 应用程序的最慢部分。 有效读取和写入数据对于良好的性能至关重要。

ASP.NET Core 性能优化最佳做法建议:

请参阅 EF 高性能 ,ASP.NET Core 性能优化最佳做法了解可提高大规模应用程序性能的方法:

建议在提交基本代码之前测量前面的高性能方法的影响。 已编译查询的额外复杂性可能不会提高性能。ASP.NET Core 性能优化最佳做法。

通过查看 Application Insights 或分析工具访问数据所用的时间,可以检测到查询问题。 大多数数据库还提供有关频繁执行的查询的统计信息。

与 HttpClientFactory 建立池 HTTP 连接

尽管 HttpClient 实现了 IDisposable 接口,但它是为重复使用而设计的。 关闭 HttpClient 的实例使套接字在 TIME_WAIT 一小段时间内处于打开状态。 如果经常使用创建和处置对象的代码路径 HttpClient ,应用可能会耗尽可用的套接字。 ASP.NET Core 2.1 中引入了HttpClientFactory作为此问题的解决方案。 它处理池 HTTP 连接以优化性能和可靠性。

ASP.NET Core 性能优化最佳做法建议:

快速保持通用代码路径

您希望所有代码的速度都很快。 经常称为 “代码路径” 是最重要的。 这些方法包括:

ASP.NET Core 性能优化最佳做法建议:

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

大多数对 ASP.NET Core 应用程序的请求都可以通过控制器或页面模型进行处理,该模型调用必要的服务并返回 HTTP 响应。 对于涉及长时间运行的任务的某些请求,最好将整个请求响应过程设为异步处理。

ASP.NET Core 性能优化最佳做法建议:

缩小客户端资产

具有复杂前端的 ASP.NET Core 应用通常会提供许多 JavaScript、CSS 或图像文件。 可以通过以下方式改善初始负载请求的性能:

ASP.NET Core 性能优化最佳做法建议:

压缩响应

减小响应大小通常会显著提高应用程序的响应能力。 减少负载大小的一种方法是压缩应用的响应。 有关详细信息,请参阅 响应压缩

使用最新 ASP.NET Core 版本

ASP.NET Core 的每个新版本都包括性能改进。 .NET Core 和 ASP.NET Core 中的优化意味着较新版本通常优于较旧的版本。 例如,.NET Core 2.1 添加了对范围 <T> 内已编译的正则表达式和获益的支持。 ASP.NET Core 2.2 添加了对 HTTP/2 的支持。 ASP.NET Core 3.0 添加了许多改进 ,减少了内存使用量并提高了吞吐量。 如果性能是优先考虑的,请考虑升级到 ASP.NET Core 的当前版本。

最小化异常

异常应极少。 相对于其他代码流模式,引发和捕获异常的速度很慢。 因此,不应使用异常来控制正常的程序流。

ASP.NET Core 性能优化最佳做法建议:

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

性能和可靠性

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

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

ASP.NET Core 中的所有 i/o 都是异步的。 服务器实现 Stream 了接口,该接口同时具有同步和异步重载。 应首选异步文件以避免阻塞线程池线程。 阻塞线程可能会导致线程池不足。

请勿执行此操作: 下面的示例使用 ReadToEnd 。 此方法阻止当前线程等待结果。 这是一个 通过异步同步的示例。

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

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

在上面的代码中, Get 将整个 HTTP 请求正文以同步方式读入内存中。 如果客户端缓慢上传,则应用通过异步执行同步。 应用通过异步同步,因为 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 请求正文读入内存中。

 警告

如果请求很大,则将整个 HTTP 请求正文读取到内存中可能会导致内存不足 (OOM) 情况。 OOM 可能会导致拒绝服务。 有关详细信息,请参阅本文档中的 避免将大型请求正文或响应正文读入内存 中。

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

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

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

首选 ReadFormAsync over 请求。窗体

请使用 HttpContext.Request.ReadFormAsync,而不是 HttpContext.Request.Form HttpContext.Request.Form 只能在以下条件下安全地读取:

请勿执行此操作: 下面的示例使用 HttpContext.Request.Form 。 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();
    }

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

在 .NET 中,大于 85 KB 的每个对象分配将在大型对象堆 (LOH) 结束。 大型对象的开销很大:

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

分配大型对象时,会将其标记为第2代对象。 对于小对象,不是0代。 后果是,如果在 LOH 中用尽内存,GC 将清除整个托管堆,而不仅是 LOH。 因此,它会清除第0代第1代和第2代,包括 LOH。 这称为完整垃圾回收,是最耗费时间的垃圾回收。 许多应用程序都可以接受。 但一定不能用于高性能的 web 服务器,在这种情况下,需要少量的大内存缓冲区来处理平均 web 请求 (从套接字读取、解压缩、解码 JSON & 更) 。

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

使用同步数据处理 API

使用仅支持同步读和写的序列化程序/反序列化程序时 (例如, JSON.NET) :

警告

如果请求很大,则可能会导致内存不足 (OOM) 情况。 OOM 可能会导致拒绝服务。 有关详细信息,请参阅本文档中的 避免将大型请求正文或响应正文读入内存 中。

System.Text.Json默认情况下,ASP.NET Core 3.0 使用 JSON 序列化。 System.Text.Json:

不要在字段中存储 IHttpContextAccessor

IHttpContextAccessor.HttpContext HttpContext 从请求线程访问时,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");
        }
    }
}

前面的代码 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是线程安全的。 HttpContext并行从多个线程进行访问可能会导致未定义的行为,如挂起、崩溃和数据损坏。

请勿执行此操作: 下面的示例执行三个并行请求,并在传出 HTTP 请求之前和之后记录传入的请求路径。 可以从多个线程访问请求路径,可能会并行进行。

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

HttpContext 只有在 ASP.NET Core 管道中存在活动 HTTP 请求时,它才有效。 整个 ASP.NET Core 管道是一系列执行每个请求的委托。 当从此 Task 链返回的完成时, HttpContext 会回收。

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

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 从 Controller 属性捕获。 这是一种不好的做法,因为工作项可以:

[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();
}

应将后台任务作为托管服务实现。 有关详细信息,请参阅使用托管服务的后台任务

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

请勿执行此操作: 下面的示例演示关闭 DbContext Controller 操作从操作参数捕获。 这是一种不好的做法。 工作项可以在请求范围之外运行。 的 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();
}

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

[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();
}

以下突出显示的代码:

[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();
}

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

ASP.NET Core 不会缓冲 HTTP 响应正文。 第一次写入响应时:

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

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

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

在前面的代码中, context.Response.Headers["test"] = "test value"; 如果 next() 已写入响应,将引发异常。

执行以下操作: 下面的示例在修改标头之前检查 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();
});

如果已开始写入响应正文,请不要调用下一个 ( # A1

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

使用 IIS 中的进程内托管

使用进程内托管,ASP.NET Core 在与其 IIS 工作进程相同的进程中运行。 进程内托管提供了对进程外托管的性能改进,因为请求未通过环回适配器进行代理。 环回适配器是一种将传出的网络流量返回到相同计算机的网络接口。 IIS 使用 Windows 进程激活服务 (WAS) 处理进程管理。

项目默认为 ASP.NET Core 3.0 及更高版本中的进程内承载模型。

本文转载自:https://docs.microsoft.com/zh-cn/aspnet/core/performance/performance-best-practices?view=aspnetcore-3.1

退出移动版