三张大图剖析HttpClient和IHttpClientFactory在DNS解析问题上的殊途同归

88efc9b3245ac916f109ec97e07749ef.gif

在开发者便利度角度,我们很轻松地使用HttpClient对象发出HTTP请求,只需要关注应用层协议的BaseAddr、Url、ReqHeader、timeout。

实际在HttpClient请求在源码级别是 HttpMessageHandler在躬身前行。

92d2d204f9f037f33073b86b44965470.png

1. 早期.NET HttpClient遇到的Socket滥用/DNS解析问题

早期.NET的HttpClient使用HttpClientHandler[1], 该handler具备完整的async、proxy、dns、Connection pool 请求一条龙能力。

587c9a51083b0d10b27e4d05320ebe31.png

底层Handler又会构建tcp连接池 ,开发者不注意使用场景和底层原理容易造成Socket滥用、主机端口耗尽(参考资料是:tcp4次挥手,主动端开方不会立即释放端口,存在2min的time_wait状态)。

一般实践会采用单例模式,重用HttpClient对象(也即重用HttpClientHandler), 但此时又会遇到DNS解析问题的尴尬(HttpClient仅在创建时为连接解析DNS,不跟踪DNS服务器指令的TTL)。


意识到重用httpClient带上的dns解析副作用之后, .NET团队和.ASP.NETCore团队分别给出了技术路线来尝试解决这个问题,

前者在.NETCore 2.1 引入了具备对连接池中连接做生命周期管理能力的 SocketsHttpHandler;

后者基于ASP.NETCore框架随处可见的DI能力,实现了针对HttpClientHandler实例的缓存工厂。


2. .NET Core2.1+ HttpClient 改造HttpClientHandler证明自己

新版本的思路是哪里有问题, 我就改造哪里。

.NET Core 2.1改造了HttpClient原始的HttpClientHandler源码, 让其underlyingHandler=SocketsHttpHandler,也就是说在.NETCore2.1起HttpClient的核心Handler实质就是SocketsHttpHandler[2], HttpClientHandler只是一个套壳。

5ed2e83fc8addeb347b1b4f85388220f.png

看上面的UML图,被改造后的套壳HttpClientHandler内置了一个默认的SocketsHttpHandler来完成一条龙HTTP服务 (Dispose工作也全权交给了SocketsHttpHandler), 当然开发者也可以在构建HttpClient实例时指定handler。

SocketsHttpHandler中与连接生命周期相关的三个关键属性:

var handler = new SocketsHttpHandler
{
    PooledConnectionLifetime = TimeSpan.FromMinutes(15), // 限制连接的生命周期,默认无限  Recreate every 15 minutes, 这个配置可用于缓解DNS解析问题  
    PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2),   // 空闲连接在连接池中的存活时间, <=NET5默认2min, >NET6 1min  
    MaxConnectionsPerServer =  100,                  // 定义到每个目标服务节点能建立的最大连接数  未设置 = int.MaxValue

}; 
var sharedClient = new HttpClient(handler);

都聊到此了,在打算重用HttpClient实例时,插入SocketsHttpHandler并调整PooledConnectionLifetime,可缓解DNS解析问题。

3. ASP.NETCore IHttpClientFactory缓存工厂 曲线救国

IHttpClientFactory 充分体现了“计算机领域的任何问题都可以通过增加一个间接的中间层来解决” 这一方法论。

为解决重用HttpClient引起的DNS解析副作用,IHttpClientFactory对实际使用的核心HttpClienthandler开启了缓存工厂模式,在外侧尝试跟踪并控制Handler的存活周期。

344b8ecd430f3008751df7a7f3b4382b.png

① 通过IHttpClientFactory注入的命名的/类型化的HttpClient实例,底层核心的Handler来自缓存字典;

②  缓存字典中的缓存项默认2min,意味着2min时间内产生的命名HttpClient实例都是引用同一个核心HttpMessageHandler实例(LifeTimeTrackingHttpMessageHandler);

public HttpClient CreateClient(string name)
       {
           ThrowHelper.ThrowIfNull(name);

           HttpMessageHandler handler = CreateHandler(name);           
           var client = new HttpClient(handler, disposeHandler: false);  

           HttpClientFactoryOptions options = _optionsMonitor.Get(name);
           for (int i = 0; i < options.HttpClientActions.Count; i++)
           {
               options.HttpClientActions[i](client "i");
           }

           return client;
       }
 
     public HttpMessageHandler CreateHandler(string name)
     {
        ThrowHelper.ThrowIfNull(name);

        ActiveHandlerTrackingEntry entry = _activeHandlers.GetOrAdd(name, _entryFactory).Value; // 工厂模式,惰性取值

        StartHandlerEntryTimer(entry);      // 跟踪缓存项的过期时间

        return entry.Handler;

    }

缓存是用线程安全的字典ConcurrentDictionary以惰性生成的方式实现:

_activeHandlers = new ConcurrentDictionary<string, Lazy<ActiveHandlerTrackingEntry>>(StringComparer.Ordinal);

_entryFactory = (name) => {  
   return new Lazy<ActiveHandlerTrackingEntry>(() =>  
    {   
       return CreateHandlerEntry(name);  
    }, LazyThreadSafetyMode.ExecutionAndPublication);           
};

缓存的是LifeTimeTrackingHttpMessageHandler[3]对象,这是一个托管资源。

③ 每个活跃的核心handler上外挂了存活时间, 一旦到期便从活跃字典中移出, 并移动到过期handler队列[4]

internal sealed class ExpiredHandlerTrackingEntry
    {
        private readonly WeakReference _livenessTracker;

        // IMPORTANT: don't cache a reference to `other` or `other.Handler` here.
        // We need to allow it to be GC'ed.
        public ExpiredHandlerTrackingEntry(ActiveHandlerTrackingEntry other)
        {
            Name = other.Name;
            Scope = other.Scope;

            _livenessTracker = new WeakReference(other.Handler);    // 跟踪LifeTimeTrackingHttpMessageHandler 托管资源
            InnerHandler = other.Handler.InnerHandler!;        // InnerHandler 是托管资源底层引用的非托管资源
        }

        public bool CanDispose => !_livenessTracker.IsAlive;

        public HttpMessageHandler InnerHandler { get; }

        public string Name { get; }

        public IServiceScope? Scope { get; }
    }

托管资源LifeTimeTrackingHttpMessageHandler 不接受dispose(httpclient)的指引,而是由gc跟踪再无HttpClient引用而被清理。

Q:此时就出现了一个问题, 托管资源已经被gc清理, 那依赖的底层非托管资源什么时候清理的?这个不清理可是有大问题。

A :这里使用了一个C#高级的用法:弱引用WeakReference[5]:能够在不影响gc的情况下,获得对象的“弱引用”, 并据此知道该实例是不是已经被gc清理了;本文是弱引用_livenessTracker跟踪了托管资源LifeTimeTrackingHttpMessageHandler, 该托管资源被gc清理后_livenessTracker会得到感知。

btw,关于弱引用,我会开一新篇章来讲述。

④ 最后由程序内置的定时清理程序来清理底层非托管资源。

if (entry.CanDispose)       //跟踪到托管对象已经被gc
 {
       try
       {
              entry.InnerHandler.Dispose(); 
              entry.Scope?.Dispose();
              disposedCount++;
       }
       catch (Exception ex)
      {
             Log.CleanupItemFailed(_logger, entry.Name, ex);
      }
  }
//注意:InnerHandler并不是托管对象LifeTimeTrackingHttpMessageHandler

具体是通过弱引用entry.CanDispose得知引用被gc之后,再去清理底层的非托管资源:InnerHandler.Dispose()

在使用层面, IHttpClientFactory并非直接管控连接池连接,而是在外层对Handler做存活缓存,故工厂对外只提供了SetHandlerLifetime(TimeSpan.FromMinutes(5))  这一个配置函数。

IHttpCLientFactory 工厂除了具备 “通过管理HttpClientHandler实例的缓存生存期,避免手动管理 HttpClient 生存期时出现的DNS问题”, 还具有

  • HttpClient实例的产生更符合.NET 框架的调性:DI、 以委托方式配置HttpClient中间件的惯例

  • 中心化配置、 命名或者类型化客户端

  • 提供基于 Polly 的中间件的扩展方法,以利用 HttpClient 中的委托处理程序。

  • (通过 ILogger)添加可配置的记录体验,以处理工厂创建的客户端发送的所有请求。

总结

本文从早期的HttpClient带来的尴尬(重用HttpClient带来的DNS解析问题), 扩展到.NET团队尝试解决该问题的两个思路。

.NET Core 2.1的思路是增强HttpClient库底层的连接池能力,提供了SocketsHttpHandler来控制连接的生命周期,

IHttpClientFactory的思路是绕过HttpClient本身的问题,在上层用存活缓存的思路来使用HttpClientHandler实例, 充分贯彻了“计算机领域的任何问题都可以通过增加一个间接的中间层来解决”的思想。

本篇文字和图片均为原创,读者可结合图片探索源码, 欢迎反馈 ~。。~。

参考资料

[1]

早期.NET的HttpClient使用HttpClientHandler: https://github.com/microsoft/referencesource/blob/51cf7850defa8a17d815b4700b67116e3fa283c2/System/net/System/Net/Http/HttpClientHandler.cs#L917

[2]

SocketsHttpHandler: https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocketsHttpHandler.cs

[3]

LifeTimeTrackingHttpMessageHandler: https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Http/src/LifetimeTrackingHttpMessageHandler.cs

[4]

过期handler队列: https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Http/src/ExpiredHandlerTrackingEntry.cs

[5]

弱引用WeakReference: https://learn.microsoft.com/en-us/dotnet/api/system.weakreference?view=net-9.0

356f85f9d8a4876a4c8614f1819b9f3f.gif

自古以来,同步/异步都是八股文第一章

async/await 贴脸输出,这次你总该明白了

一次sql请求,返回分页数据和总条数

在浏览器输入网址,Enter之后发生了什么?

两将军问题和TCP三次握手

"家长进校园"之《计算机和人工智能》

unix进程间通信信号的有效实践

fd56845db695c3db3bc4ea1cc953296f.png

点“11f24b0a37ac4b043e2e71db4cbd0fec.gif戳“在看a128037ac03d15e13636d26b41afcbc1.gif

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

有态度的马甲

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值