发布 HTTP 请求而不等待结果

Muh*_*een 2 c# async-await

我有以下端点:

[HttpPost("Submit")]
public String post()
{
    _ = _service.SubmitMetric("test", MetricType.Count, 60, 1);
    return "done";
}
Run Code Online (Sandbox Code Playgroud)

以及服务实现:

public Task<HttpResponseMessage> SubmitMetric(<params>)
{
    // build payload
    using (var httpClient = new HttpClient())
    {
        return httpClient.PostAsync(<params>);
    }
}
Run Code Online (Sandbox Code Playgroud)

当我运行代码并调用端点时,不会触发 HTTP POST。但是,如果我将代码更改为:

public async Task<HttpResponseMessage> SubmitMetric(<params>)
{
    // build payload
    using (var httpClient = new HttpClient())
    {
        return await httpClient.PostAsync(<params>);
    }
}
Run Code Online (Sandbox Code Playgroud)

POST 按预期提交。为什么会发生这种情况?如果我并不真正关心 HTTP 响应,我该怎么办?我只想提交它并继续我的流程。我不应该能够在不await获取结果的情况下使用它吗?例如:

public void SubmitMetric(<params>)
{
    // build payload
    using (var httpClient = new HttpClient())
    {
        httpClient.PostAsync(<params>);
    }
}
Run Code Online (Sandbox Code Playgroud)

Pan*_*vos 6

这段代码有两个问题。如果其中一个被修复,就不会有问题:

  1. HttpClient 使用不正确。HttpClient 对象是线程安全的,旨在重用,而不是释放。像这样处理它会泄漏套接字,并可能导致应用程序崩溃或更糟糕的不稳定。HttpClient 将 URL 的主机解析为套接字并缓存该套接字。操作系统还缓存打开的套接字,因为打开它们的成本很高。即使应用程序关闭它们,它们也会保持活动一段时间,因为某些数据包可能仍在传输中
  2. 通过不等待PostAsync执行,退出using块,并且HttpClient在请求有机会启动之前实例就被处置。

无论如何,创建 POST 不会花费很长时间,因此无需使该方法“即发即忘”。此外,很少有应用程序可以接受丢失指标,尤其是当出现问题时。这就是指标最有用的时候。这就是为什么 ASP.NET Core 6 添加了对 OpenTelemetry 跟踪和指标的内置支持。最后将详细介绍这一点,但支持包也可以在 ASP.NET Framework 中使用。您也许可以用内置服务替换当前的服务。

使用await - 还不够

解决此问题的一种方法是使用await,但这并不能解决 HttpClient 使用问题。

public async Task<HttpResponseMessage> SubmitMetric(<params>)
{
    // build payload
    using (var httpClient = new HttpClient())
    {
        return await httpClient.PostAsync(<params>);
    }
}
Run Code Online (Sandbox Code Playgroud)

至少 HttpClient 应该存储在一个字段中。一旦完成,就不再有任何理由await,只要服务本身仍然存在:

HttpClient httpClient = new HttpClient();

public Task<HttpResponseMessage> SubmitMetric(<params>)
{
    return httpClient.PostAsync(<params>);
}
Run Code Online (Sandbox Code Playgroud)

长期服务

这使我们能够保持服务。在 ASP.NET 和 ASP.NET Core 中,每个请求都由 Controller 类的新实例中的单独线程提供服务。请求本身用作 GC 范围,因此请求期间创建的任何内容都会在请求结束后被处理,包括 HttpClient 实例。

为了保留 Metrics 服务,我们需要将其注册为SingletonASP.NET Core 的 DI,将其设为后台服务,或者确保它是 ASP.NET Framework 中的单例。我们可以使该字段静态,但这会导致下一个问题。

正确的 HttpClient 使用

如果用作单例,HttpClient 仍然会导致问题。HttpClient 将套接字缓存到特定的机器。如果该机器消失,HttpClient 仍会尝试与其通信,从而导致错误。当远程服务使用负载平衡器或故障转移到新服务器时,很容易发生这种情况。为了解决这个问题,HttpClient 实例或者更确切地说是套接字需要定期回收。

这就是 HttpClientFactory 的工作。此类缓存并回收 SocketClientHandler 实例,这些类在 HttpClient 中执行实际工作。这些会定期回收,例如每 10 分钟回收一次。当请求新的 HttpClient 实例时,它会创建一个包装已有可用处理程序之一的新实例。

当您services.AddHttpClient在 ASP.NET Core 中使用时,您实际上是在配置 HttpClientFactory。当您在控制器中添加 HttpClient 依赖项时,该实例将由配置的 HttpClientFactory 创建。

这意味着以下操作可以正常工作:

HttpClient _client;

public MyController(HttpClient client)
{
    _client=client;
}

[HttpPost("Submit")]
public String post()
{
    await _client.PostAsync(<params>);
    return "done";
}
Run Code Online (Sandbox Code Playgroud)

具有依赖关系的作用域服务HttpClient也可以工作:

MyService _service;

public MyController(MyService service)
{
    _service=service;
}

HttpPost("Submit")]
public String post()
{
    await _service.SubmitMetric("test", MetricType.Count, 60, 1);
    return "done";
}
Run Code Online (Sandbox Code Playgroud)

哪里MyService

class MyService
{
    HttpClient _client;
    public MyService(HttpClient client)
    {
        _client=client;
    }

    public Task<HttpResponseMessage> SubmitMetric(<params>)
    {
        // build payload
  
        return httpClient.PostAsync(<params>);
    }
}
Run Code Online (Sandbox Code Playgroud)

在这种情况下,没有真正需要在内部等待SubmitMetric,这由操作来处理。

使用内置的 OpenTelemetry 跟踪和指标

即将推出的长期支持版本 ASP.NET Core 6 添加了对 OpenTelemetry 标准的本机支持,用于日志记录、跟踪和指标。这允许使用标准 API 将指标推送到许多不同的可观察性应用程序,例如 Prometheus、Jaeger、Zipking、Elastic 和 Splunk。

与其滚动自己的指标基础设施,不如使用标准 API。OpenTelemetry for .NET在 ASP.NET Framework 4.6 及更高版本中支持此功能。ASP.NET Core 5 及更高版本可通过内置命名空间和类向 OpenTelemetry 提供程序发布指标和跟踪。System.DiagnosticsActivity

事实上,控制器已经进行了检测,因此您可以摆脱指标服务,将任何标签和行李添加到请求的当前活动中:

[HttpPost("Submit")]
public String post()
{
    Activity.Current?.AddTag("test");
    ...
    
    return "done";
}
Run Code Online (Sandbox Code Playgroud)

ASP.NET Core 6 Preview 5 中添加了指标

Meter meter = new Meter("my.library.meter.name", "v1.0");

Counter<int> _counter;

public MyController(...)
{
    _counter = meter.CreateCounter<int>("Requests");
}

[HttpPost("Submit")]
public String post()
{
    counter.Add(60, KeyValuePair.Create<string, object>("request", "test"));

    return "done";
}
Run Code Online (Sandbox Code Playgroud)