ASP.NET Core对Azure应用服务的HTTP请求缓慢

时间:2019-07-26 19:08:44

标签: c# asp.net-core asp.net-core-webapi asp.net-core-2.2

我有一个ASP.NET Core中间件,该中间件调用另一个HTTP服务以检查用户是否有权进行请求。目前,它取决于提供的名为X-Parameter-Id的自定义标头。

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;

namespace ParameterAuthorization.Middleware
{
    public class ParameterAuthorizationMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly IParameterAuthorizationService _parameterAuthorizationService;

        public ParameterAuthorizationMiddleware(RequestDelegate next, IParameterAuthorizationService parameterAuthorizationService)
        {
            _next = next ?? throw new ArgumentNullException(nameof(next));
            _parameterAuthorizationService = parameterAuthorizationService ?? throw new ArgumentNullException(nameof(parameterAuthorizationService));
        }

        public async Task InvokeAsync(HttpContext httpContext, IConfiguration configuration)
        {
            if (httpContext is null)
            {
                throw new ArgumentNullException(nameof(httpContext));
            }

            if (parameterRequestContext is null)
            {
                throw new ArgumentNullException(nameof(parameterRequestContext));
            }

            if (!(httpContext.Request.Headers.ContainsKey("X-Parameter-Id") && httpContext.Request.Headers.ContainsKey("Authorization")))
            {
                await ForbiddenResponseAsync(httpContext);
            }

            var parameterIdHeader = httpContext.Request.Headers["X-Parameter-Id"].ToString();

            if (!int.TryParse(parameterIdHeader, out var parameterId) || parameterId < 1)
            {
                await ForbiddenResponseAsync(httpContext);
            }

            var authorizationHeader = httpContext.Request.Headers["Authorization"].ToString();

            var parameterResponse = await _parameterAuthorizationService.AuthorizeUserParameterAsync(parameterId, authorizationHeader);

            if (string.IsNullOrWhiteSpace(parameterResponse))
            {
                await ForbiddenResponseAsync(httpContext);
            }

            await _next.Invoke(httpContext);
        }

        private static async Task ForbiddenResponseAsync(HttpContext httpContext)
        {
            httpContext.Response.StatusCode = StatusCodes.Status403Forbidden;
            await httpContext.Response.WriteAsync("Forbidden");
            return;
        }
    }
}

这就是HTTP调用的实现:

using System;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
using Newtonsoft.Json;

namespace ParameterAuthorization.Middleware.Http
{
    public class ParameterAuthorizationService : IParameterAuthorizationService
    {
        private readonly HttpClient _httpClient;
        private readonly JsonSerializer _jsonSerializer;

        public ParameterAuthorizationService(HttpClient httpClient, JsonSerializer jsonSerializer)
        {
            _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
            _jsonSerializer = jsonSerializer ?? throw new ArgumentNullException(nameof(jsonSerializer));
        }

        public async Task<string> AuthorizeUserParameterAsync(int parameterId, string authorizationHeader)
        {
            var request = CreateRequest(parameterId, authorizationHeader);

            var result = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);

            if (!result.IsSuccessStatusCode)
            {
                return string.Empty;
            }

            using (var responseStream = await result.Content.ReadAsStreamAsync())
            using (var streamReader = new StreamReader(responseStream))
            using (var jsonTextReader = new JsonTextReader(streamReader))
            {
                return _jsonSerializer.Deserialize<ParameterResponse>(jsonTextReader).StringImInterestedIn;
            }
        }

        private static HttpRequestMessage CreateRequest(int parameterId, string authorizationHead1er)
        {
            var parameterUri = new Uri($"parameters/{parameterId}", UriKind.Relative);

            var message = new HttpRequestMessage(HttpMethod.Get, parameterUri);

            message.Headers.Add("Authorization", authorizationHead1er);

            return message;
        }
    }
}

这是DI的样板代码,名为HttpClient

sc.TryAddSingleton<JsonSerializer>();
sc.AddHttpClient<IParameterAuthorizationService, ParameterAuthorizationService>(client =>
{
    client.BaseAddress = authorizationServiceUri;
    client.DefaultRequestHeaders.Add("Accept", "application/json");
});

authorizationServiceUri是我通过自定义扩展方法提供的。

问题在于,我对此服务的呼叫将随机花费7、10,甚至20秒才能使用此服务,然后它很快就会变慢。我称它为Postman的确切ParameterAuthorizationService,持续时间不到50毫秒。

我要附加Application Insights的屏幕快照,显示事件的整个序列。

Application Insights performance log

这两个服务均作为Azure App Services部署在同一App Service计划内的同一订阅下。

代码工作得很好,但是我已经不知道可能导致这些性能异常的原因了。

我还检查了Azure App服务中的TCP连接,它都是绿色的。

TCP Connections

某些HTTP调用会真的很慢的原因可能是什么?

2 个答案:

答案 0 :(得分:1)

如果您写入httpContent,则应使管道短路。参见Aspnet Core Middleware documentation

  

不要再呼叫。将响应发送到客户端后调用。

使用类似这样的内容:

if (string.IsNullOrWhiteSpace(parameterResponse))
{
    await ForbiddenResponseAsync(httpContext);
}
else
{
    await _next.Invoke(httpContext);
}

还考虑使用从aspnet核心AuthenticationHandler类继承的中间件来处理身份验证,以利用所有aspnet核心的Authentification / Authorization功能。为简单起见,这是BasicAuthentification handler的实现示例。

您的ParameterAuthorizationService看起来不错。我认为这不是您请求速度慢的原因。可以肯定的是,您可以track the entire service call通过测量花费的时间并将其发布到appinsights中:

public class ParameterAuthorizationService : IParameterAuthorizationService
{
    //...

    private readonly TelemetryClient _telemetryClient;

    public ParameterAuthorizationService(HttpClient httpClient, JsonSerializer jsonSerializer)
    {
        //...

        _telemetryClient = new TelemetryClient();
    }

    public async Task<string> AuthorizeUserParameterAsync(int parameterId, string authorizationHeader)
    {
        var startTime = DateTime.UtcNow;
        var timer = Stopwatch.StartNew();
        var isSuccess = true;

        try
        {
            return await AuthorizeUserParameterImpl(parameterId, authorizationHeader);
        }
        catch (Exception ex)
        {
            timer.Stop();
            isSuccess = false;

            _telemetryClient.TrackException(ex);
            _telemetryClient.TrackDependency("Http", nameof(ParameterAuthorizationService), nameof(AuthorizeUserParameterAsync),
                startTime, timer.Elapsed, isSuccess);
            throw;
        }
        finally
        {
            if (timer.IsRunning)
                timer.Stop();
            if (isSuccess)
                _telemetryClient.TrackDependency(
                "Http", nameof(ParameterAuthorizationService), nameof(AuthorizeUserParameterAsync),
                startTime, timer.Elapsed, isSuccess);
        }                
    }

    private async Task<string> AuthorizeUserParameterImpl(int parameterId, string authorizationHeader)
    {
        //Your original code
    }
}

答案 1 :(得分:0)

我相信我可能已经找到了您的麻烦所在,摘自Microsoft docs

  

尽管HttpClient实现了IDisposable接口,但是   为重复使用而设计。关闭的HttpClient实例将套接字保持打开状态   短时间的TIME_WAIT状态。如果一个代码路径   创建并处理HttpClient个对象,该应用程序   可能会耗尽可用的插槽。 HttpClientFactory的引入   ASP.NET Core 2.1作为此问题的解决方案。处理池   HTTP连接以优化性能和可靠性。

     

建议:

     
      
  • 请勿直接创建和处理HttpClient实例。
  •   
  • 请使用HttpClientFactory检索HttpClient实例。有关更多信息,请参见使用HttpClientFactory实现弹性HTTP
      请求。
  •   

我可以看到您在代码中使用HttpClient,这可能暗示您应该将性能解决方案放在哪里。

您在问题中标记了asp.net core 2.2,因此,我建议您按照建议在代码中使用HttpClientFactory而不是HttpClient