Net Core - 为什么异常中间件如此缓慢?

3
首先,抱歉我的英语不好。我正在开发一个 .Net Core Rest Api(之前我已经在 asp.net mvc 中开发过,但在 net core 中是第一次),最近遇到了一个奇怪的问题。
应用程序分为多个层:WebApi -> Core <- Infrastructure。
问题在于,在核心层的服务中,我决定针对某些业务规则抛出一些异常。例如,有一些可翻译的实体,当您想要创建新实体时,必须为应用程序中定义的每种文化提供翻译。如果缺少翻译,我会向上抛出一个异常。
下面是一个方法的示例:
public async Task<CompanyResource> Add(CompanyResource companyResource)
    {
        var cultures = await _cultureRepository.GetAllAsync();
        if (cultures.Any(culture => companyResource.Translations.All(t => t.CultureCode != culture.Code)))
            throw new MissingTranslationsException();

        await _companyResourceRepository.AddAsync(companyResource);

        return companyResource;
    }

我在想如何处理控制器中的异常,以便根据每种异常类型发送适当的httpStatusCode和可读的消息给客户端。我发现了三种主要方法: - 在控制器方法中使用Try/Catch - 自定义异常过滤器 - 自定义异常中间件
我决定选择中间件,因为它似乎是最好的方法,在整个应用程序中拥有一个集中处理异常并向客户端发送消息的地方,所以我实现了这个功能:
public class ErrorHandlingMiddleware
{
    private readonly RequestDelegate _next;

    public ErrorHandlingMiddleware(RequestDelegate next)
    {
        this._next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            await HandleExceptionAsync(context, ex);
        }
    }

    private static async Task HandleExceptionAsync(HttpContext context, Exception exception)
    {

        var code = HttpStatusCode.InternalServerError;

        if (exception is InvalidPermissionsException) code = HttpStatusCode.Forbidden;
        else if (exception is DuplicateEntityException) code = HttpStatusCode.Conflict;
        else if (exception is MissingTranslationsException) code = HttpStatusCode.BadRequest;
        else if (exception is ApplicationException) code = HttpStatusCode.BadRequest;

        var result = JsonConvert.SerializeObject(new []{ exception.Message });
        context.Response.ContentType = "application/json";
        context.Response.StatusCode = (int)code;
        await context.Response.WriteAsync(result);

    }

}

然后我将中间件连接到了管道中:

最初的回答

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {

        //...other code

        app.UseMiddleware<ErrorHandlingMiddleware>();

        app.UseHttpsRedirection();

        app.UseMvc();

    }

在这个阶段,一切都很完美,但是我刚刚注意到当中间件处理异常时,响应时间远远不够理想。这是使用中间件的响应时间:enter image description here测试了多次,平均响应时间为460ms + -。我认为一定有一种更快的方法,于是我尝试了另外两种方法。使用异常过滤器属性:
public class HandleExceptionAttribute : ExceptionFilterAttribute
{
    public override void OnException(ExceptionContext context)
    {
        var code = HttpStatusCode.InternalServerError;
        var exception = context.Exception;

        if (exception is InvalidPermissionsException) code = HttpStatusCode.Forbidden;
        else if (exception is DuplicateEntityException) code = HttpStatusCode.Conflict;
        else if (exception is MissingTranslationsException) code = HttpStatusCode.BadRequest;
        else if (exception is ApplicationException) code = HttpStatusCode.BadRequest;

        context.HttpContext.Response.StatusCode = (int)code;
        context.Result = new JsonResult(new[] { exception.Message });

    }
}

使用过滤器时,响应时间平均降至250毫秒左右。但我仍觉得这太慢了。因此,我选择了最后一个选项:在操作方法内部使用try catch:

Original Answer翻译成"最初的回答"

[HttpPost]     
    public async Task<IActionResult> Post([FromBody] CompanyResourceDto companyResourceDto)
    {

        if (!ModelState.IsValid) return BadRequest(ModelState);
        var companyresource = _mapper.Map<CompanyResource>(companyResourceDto);

        try
        {
            companyresource = await _resourcesService.Add(companyresource);
        }
        catch (Exception e)
        {
            return BadRequest(new[] {e.Message});
        }     

        return CreatedAtRoute("Getresource", new { id = companyresource.Id }, _mapper.Map<CompanyResourceDto>(companyresource));
    }

同时,在出现异常的情况下,平均响应时间下降到了85毫秒+-(这是我从一开始就期望的,通过中间件实现)。

我认为,为了易于维护和关注点分离,使用中间件是最好的选择,因为我不希望我的控制器意识到除了正常流程之外的任何事情。

所以,我的问题是:

我在中间件方面做错了什么吗? 如果没有,为什么会如此缓慢,考虑到响应时间对该应用程序至关重要,我应该采取什么方法? 如果有,我应该改变什么?

谢谢和问候。

最初的回答:


1
可能与您的性能问题无关,但在异常处理程序中添加像异步 I/O 这样复杂的东西是一个不好的想法。如果异常处理程序内部的代码失败,原始异常将被“覆盖”,这会让故障排除变得非常困难。最好使用简单的代码,这样就不会出错;最简单的方法是在处理程序中设置一个变量,并在 try / catch 之后检查它。 - Jeroen Mostert
1
编写响应存在许多(外部)失败条件,包括但不限于网络故障。在这种情况下,您可能不需要调查由连接消失导致的堆栈跟踪,但如果需要,则可能会完全掩盖原始问题。为自己避免潜在的头痛。 - Jeroen Mostert
2
这很可能与调用顺序有关:过滤器顺序文档。3)控制器返回OK:没有人关心,结束请求,2)控制器抛出异常,由过滤器处理,过滤器获取并返回,1)控制器抛出异常,经过过滤器和中间件直到被异常中间件捕获。 - Camilo Terevinto
5
你是否处于调试模式?如果是,堆栈跟踪收集很可能会影响性能。 - Robert Perry
@RobertPerry 感谢您指出这一点。实际上,在我的情况下,Visual Studio在Debug和Release模式下都收集了堆栈跟踪。当单独启动应用程序(例如运行 .exe 文件)时,它可以正常工作。我正在使用ProblemDetailsMiddleware NuGet包来收集异常跟踪。当请求抛出异常时,持续时间为 1600 毫秒(在 Visual Studio 中)与 3 毫秒(单独进程)。 - user3625699
显示剩余10条评论
1个回答

3

经过一些测试,我还没有找到演示的方法,但似乎问题是关于本地机器的调试模式和堆栈跟踪性能过热,正如Robert Perry在评论中建议的那样。

在一个全新的服务器上设置Api后,中间件捕获所有异常的平均时间为15毫秒+-。

如果我发现为什么在本地机器上生产模式下的响应时间不是最好的(也许与环境变量有关,但我不确定),我会在这里发布更多信息。

无论如何,谢谢大家。

问候


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接