百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程网 > 正文

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

yuyutoo 2024-12-19 17:33 3 浏览 0 评论

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

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

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

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

底层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只是一个套壳。

看上面的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的存活周期。

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

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

public HttpClient CreateClient(string name)
{
ThrowHelper.ThrowIf(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.ThrowIf(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

相关推荐

史上最全的浏览器兼容性问题和解决方案

微信ID:WEB_wysj(点击关注)◎◎◎◎◎◎◎◎◎一┳═┻︻▄(页底留言开放,欢迎来吐槽)●●●...

平面设计基础知识_平面设计基础知识实验收获与总结
平面设计基础知识_平面设计基础知识实验收获与总结

CSS构造颜色,背景与图像1.使用span更好的控制文本中局部区域的文本:文本;2.使用display属性提供区块转变:display:inline(是内联的...

2025-02-21 16:01 yuyutoo

写作排版简单三步就行-工具篇_作文排版模板

和我们工作中日常word排版内部交流不同,这篇教程介绍的写作排版主要是用于“微信公众号、头条号”网络展示。写作展现的是我的思考,排版是让写作在网格上更好地展现。在写作上花费时间是有累积复利优势的,在排...

写一个2048的游戏_2048小游戏功能实现

1.创建HTML文件1.打开一个文本编辑器,例如Notepad++、SublimeText、VisualStudioCode等。2.将以下HTML代码复制并粘贴到文本编辑器中:html...

今天你穿“短袖”了吗?青岛最高23℃!接下来几天气温更刺激……

  最近的天气暖和得让很多小伙伴们喊“热”!!!  昨天的气温到底升得有多高呢?你家有没有榜上有名?...

CSS不规则卡片,纯CSS制作优惠券样式,CSS实现锯齿样式

之前也有写过CSS优惠券样式《CSS3径向渐变实现优惠券波浪造型》,这次再来温习一遍,并且将更为详细的讲解,从布局到具体样式说明,最后定义CSS变量,自定义主题颜色。布局...

柠檬科技肖勃飞:大数据风控助力信用社会建设

...

你的自我界限够强大吗?_你的自我界限够强大吗英文

我的结果:A、该设立新的界限...

行内元素与块级元素,以及区别_行内元素和块级元素有什么区别?

行内元素与块级元素首先,CSS规范规定,每个元素都有display属性,确定该元素的类型,每个元素都有默认的display值,分别为块级(block)、行内(inline)。块级元素:(以下列举比较常...

让“成都速度”跑得潇潇洒洒,地上地下共享轨交繁华
让“成都速度”跑得潇潇洒洒,地上地下共享轨交繁华

去年的两会期间,习近平总书记在参加人大会议四川代表团审议时,对治蜀兴川提出了明确要求,指明了前行方向,并带来了“祝四川人民的生活越来越安逸”的美好祝福。又是一年...

2025-02-21 16:00 yuyutoo

今年国家综合性消防救援队伍计划招录消防员15000名

记者24日从应急管理部获悉,国家综合性消防救援队伍2023年消防员招录工作已正式启动。今年共计划招录消防员15000名,其中高校应届毕业生5000名、退役士兵5000名、社会青年5000名。本次招录的...

一起盘点最新 Chrome v133 的5大主流特性 ?

1.CSS的高级attr()方法CSSattr()函数是CSSLevel5中用于检索DOM元素的属性值并将其用于CSS属性值,类似于var()函数替换自定义属性值的方式。...

竞走团体世锦赛5月太仓举行 世界冠军杨家玉担任形象大使

style="text-align:center;"data-mce-style="text-align:...

学物理能做什么?_学物理能做什么 卢昌海

作者:曹则贤中国科学院物理研究所原标题:《物理学:ASourceofPowerforMan》在2006年中央电视台《对话》栏目的某期节目中,主持人问过我一个的问题:“学物理的人,如果日后不...

你不知道的关于这只眯眼兔的6个小秘密
你不知道的关于这只眯眼兔的6个小秘密

在你们忙着给熊本君做表情包的时候,要知道,最先在网络上引起轰动的可是这只脸上只有两条缝的兔子——兔斯基。今年,它更是迎来了自己的10岁生日。①关于德艺双馨“老艺...

2025-02-21 16:00 yuyutoo

取消回复欢迎 发表评论: