ASP.NET 对 Azure 应用服务的核心慢速 HTTP 请求
ASP.NET Core slow HTTP requests to Azure App Service
我有一个 ASP.NET 核心中间件,它调用另一个 HTTP 服务来检查用户是否有权继续请求。目前它取决于提供的自定义 header,称为 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;
}
}
}
这是名为 HttpClient
的 DI 的样板代码
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 的屏幕截图,显示了整个事件序列。
这两种服务都作为 Azure 应用服务部署在同一应用服务计划的同一订阅下。
代码工作正常,但我已经抓狂了,不知道是什么导致了这些性能异常。
我还检查了 Azure 应用服务中的 TCP 连接,它都是绿色的。
某些 HTTP 调用真的很慢的原因可能是什么?
更新
我的应用服务在 S1 应用服务计划上运行。 https://azure.microsoft.com/en-us/pricing/details/app-service/windows/
我相信我可能已经找到您的问题所在,摘自 Microsoft docs:
Although HttpClient
implements the IDisposable
interface, it's
designed for reuse. Closed HttpClient
instances leave sockets open in
the TIME_WAIT state for a short period of time. If a code path that
creates and disposes of HttpClient
objects is frequently used, the app
may exhaust available sockets. HttpClientFactory
was introduced in
ASP.NET Core 2.1 as a solution to this problem. It handles pooling
HTTP connections to optimize performance and reliability.
Recommendations:
- Do not create and dispose of HttpClient instances directly.
- Do use HttpClientFactory to retrieve HttpClient instances. For more information, see Use HttpClientFactory to implement resilient HTTP
requests.
我可以看到您在代码中使用了 HttpClient
,这可能会提示您应该将性能解决方案的重点放在哪里。
您在问题中标记了 asp.net 核心 2.2,因此我建议您按照建议在代码中使用 HttpClientFactory 而不是 HttpClient
。
如果写入 httpContent,则应将管道短路。见 Aspnet Core Middleware documentation :
Don't call next.Invoke after the response has been sent to the client.
使用这样的东西:
if (string.IsNullOrWhiteSpace(parameterResponse))
{
await ForbiddenResponseAsync(httpContext);
}
else
{
await _next.Invoke(httpContext);
}
为简单起见,还考虑使用从 aspnet 核心继承的中间件处理身份验证 AuthenticationHandler class to leverage all the aspnet core Authentification/Authorization facilities. Here is an implementation example of a 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
}
}
我有一个 ASP.NET 核心中间件,它调用另一个 HTTP 服务来检查用户是否有权继续请求。目前它取决于提供的自定义 header,称为 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;
}
}
}
这是名为 HttpClient
的 DI 的样板代码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 的屏幕截图,显示了整个事件序列。
这两种服务都作为 Azure 应用服务部署在同一应用服务计划的同一订阅下。
代码工作正常,但我已经抓狂了,不知道是什么导致了这些性能异常。
我还检查了 Azure 应用服务中的 TCP 连接,它都是绿色的。
某些 HTTP 调用真的很慢的原因可能是什么?
更新
我的应用服务在 S1 应用服务计划上运行。 https://azure.microsoft.com/en-us/pricing/details/app-service/windows/
我相信我可能已经找到您的问题所在,摘自 Microsoft docs:
Although
HttpClient
implements theIDisposable
interface, it's designed for reuse. ClosedHttpClient
instances leave sockets open in the TIME_WAIT state for a short period of time. If a code path that creates and disposes ofHttpClient
objects is frequently used, the app may exhaust available sockets.HttpClientFactory
was introduced in ASP.NET Core 2.1 as a solution to this problem. It handles pooling HTTP connections to optimize performance and reliability.Recommendations:
- Do not create and dispose of HttpClient instances directly.
- Do use HttpClientFactory to retrieve HttpClient instances. For more information, see Use HttpClientFactory to implement resilient HTTP
requests.
我可以看到您在代码中使用了 HttpClient
,这可能会提示您应该将性能解决方案的重点放在哪里。
您在问题中标记了 asp.net 核心 2.2,因此我建议您按照建议在代码中使用 HttpClientFactory 而不是 HttpClient
。
如果写入 httpContent,则应将管道短路。见 Aspnet Core Middleware documentation :
Don't call next.Invoke after the response has been sent to the client.
使用这样的东西:
if (string.IsNullOrWhiteSpace(parameterResponse))
{
await ForbiddenResponseAsync(httpContext);
}
else
{
await _next.Invoke(httpContext);
}
为简单起见,还考虑使用从 aspnet 核心继承的中间件处理身份验证 AuthenticationHandler class to leverage all the aspnet core Authentification/Authorization facilities. Here is an implementation example of a 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
}
}