是单例 HttpClient X 分钟后接收新的 HttpMessageHandler

Posted

技术标签:

【中文标题】是单例 HttpClient X 分钟后接收新的 HttpMessageHandler【英文标题】:Is a Singleton HttpClient receiving a new HttpMessageHandler After X Minutes 【发布时间】:2021-08-17 15:22:22 【问题描述】:

我在工厂注册了我的 HttpClient:

services.AddHttpClient<ICatalogService, CatalogService>()
    .SetHandlerLifetime(TimeSpan.FromMinutes(2));

这个 IDatalogService 是通过 Singleton Service 中的构造函数注入的。

我会在 2 分钟后在内部收到一个新的 HttpMessageHandler,还是仅在 2 分钟后将 IcatalogService 注入到非单例服务中?

基本上,当包装 HttpHandler 时,内部 HttpMessageHandler 是否也会过期 用作单例?

【问题讨论】:

【参考方案1】:

基本上,当包装HttpHandler用作Singleton时,内部HttpMessageHandler是否也会过期?

警告:通过单例保持HttpClient 实例处于活动状态是安全的。

配置的HttpMessageHandler 实例将在两分钟后过期。在到期日期之后创建的任何HttpClient 都将获得一个包含新到期日期的新HttpMessageHandler。但这对保持活动状态的 HttpClient 实例没有帮助。

重要提示: HttpClient 将一直使用相同的 HttpMessageHandler,只要它存在并且将会,因此, not respect DNS changes。只有新的 HttpMessageHandler 实例才能看到 DNS 更新。

这意味着只要您保持HttpClient 处于活动状态,HttpClient 就会错过 DNS 更改,这就是为什么 HttpClient 实例应该只存活很短的时间。只要您重用底层的HttpMessageHandler 实例(这就是基础架构为您所做的),创建和清理HttpClient 实例就非常便宜。

不幸的是,您的HttpClient 被注入到CatalogService 中,而CatalogService 又被注入到Singleton 消费者中。 Singleton 使 CatalogService 保持活动状态,因此,在应用程序期间间接保持 HttpClient 活动 - 您的 HttpClient 现在是 Captive Dependency。

您遇到的是新的 .NET Core IHttpClientFactory 基础架构中的一个不幸的设计缺陷。我认为这是一个缺陷,因为 IMO 基础设施应该阻止您持有 HttpClient 实例俘虏,例如通过将客户端(您的CatalogService)注册为Scoped。我早在 2019 年 1 月就reported this issue。微软已经承认了这个问题,但在撰写本文时,还没有可用的解决方案。

由于目前还没有解决方案,因此您必须非常小心,不要让HttpClient 作为 Captive Dependency 保持活动状态。这意味着您不能将其注入Singleton 消费者(甚至是间接消费者)。

防止这种情况的一个好方法是将客户端(您的CatalogService)注册为Scoped。这允许框架的配置系统验证它是否被注入到Singleton 中。但由于没有对此的直接支持,您必须手动进行此注册,例如使用以下扩展方法:

public static IHttpClientBuilder AddTypedClientScoped<TClient>(
  this IHttpClientBuilder builder)
  where TClient : class

    ...
    builder.Services.AddScoped<TClient>(s =>
    
        var httpClientFactory = s.GetRequiredService<IHttpClientFactory>();
        var httpClient = httpClientFactory.CreateClient(builder.Name);
        var factory = s.GetRequiredService<ITypedHttpClientFactory<TClient>>();
        return factory.CreateClient(httpClient);
    );

    return builder;

在启动 ASP.NET Core 网站 while debugging 时,框架将为您验证范围,这会在客户端注入单例时导致异常。

但请注意,使用此扩展方法不会检测到所有捕获的 HttpClient 实例。这是因为在 ASP.NET Core 中有一些地方注册为 Transient 的组件在应用程序期间仍然保持活动状态。一种这样的情况是hosted services。 AddHostedService 扩展方法就是这样的例子。托管服务注册为Transient,同时保持活动为Singleton。在我看来,另一个设计缺陷。但这意味着在将HttpClient 实例直接或间接注入托管服务时应该小心。

【讨论】:

【参考方案2】:

我会在 2 分钟后在内部收到一个新的 HttpMessageHandler,还是仅在 2 分钟后将 IcatalogService 注入到非单例服务中?

每 2 分钟后,旧的 HttpMessageHandler 将被丢弃,而新的 HttpMessageHandler 在此之后不会立即重新生成,而是在您的 HttpClient 发出请求的那一刻。

基本上,当包装HttpHandler用作Singleton时,内部HttpMessageHandler是否也会过期?

我不确定你将如何包装处理程序,因为它必须从 DelegatingHandler 继承? 如果它会像services.AddSingleton&lt;SomeDelegatingHandler&gt;(); 那么干脆不要这样做

但实际上,您的 2 行代码在您的用例中非常尴尬……这就是原因

services.AddHttpClient&lt;ICatalogService, CatalogService&gt;() 将 HttpClient 直接注入您的CatalogService,并将您的CatalogService 注册为Transient。因此,为什么要在单例实例中注入临时服务?这让我很困惑。

【讨论】:

嗨,Gordon,虽然您在技术上正确地说旧的HttpMessageHandler 将在两分钟后被丢弃,但这只会导致新创建的HttpClient 实例获得新的HttpMessageHandler。不幸的是,任何现有的HttpClient 都将保留其对原始HttpMessageHandler 的引用。这意味着长期存在的HttpClient 实例将不会遵守这些到期时间。在问题中,CatalogServiceHttpClient 由 Singleton 保持活动状态,从长远来看可能会导致问题。有关详细信息,请参阅我的答案。 我已经阅读了您的解释,实际上我错过了参考指向部分。感谢您启发我@Steven

以上是关于是单例 HttpClient X 分钟后接收新的 HttpMessageHandler的主要内容,如果未能解决你的问题,请参考以下文章

单例多例

HttpClient获取请求取消,以防操作时间超过x分钟

web 单例 多例

「源码分析」— 为什么枚举是单例模式的最佳方法

「源码分析」— 为什么枚举是单例模式的最佳方法

分分钟带你理解单例