HttpClient参观记:.net core 2.2 对HttpClient到底做了什么?
.net core 于 10月17日发布了 ASP.NET Core 2.2.0 -preview3,在这个版本中,我看到了一个很让我惊喜的新特性:HTTP Client Performance Improvements ,而且在Linux上性能提升了60% !
之前就一直苦于 HttpClient 的糟糕特性,大家耳熟能详的 You are using HttpClient wrong。
因为 HttpClient 实现了 IDisposable
如果用完就释放,Tcp 连接也会被断开,并且一个HttpClient 通常会建立很多个 Tcp 连接 。 Tcp 连接断开的过程是有一个 Time_Wait 状态的,因为要保证 Tcp 连接能够断开,以及防止断开过程中还有数据包在传送。这本身没有毛病,但是如果你在使用 HttpClient 后就将其注销,并且同时处于高并发的情况下,那么你的 Time_Wait 状态的 Tcp 连接就会爆炸的增长,
他们占用端口和资源而且还迟迟不消失,就像是在 嘲讽 你。所以临时解决方式是使用静态的 HttpClient 对象,No Dispose No Time_Wait
后来在 .net core2.1 中,引入了 HttpClientFactory
来解决这一问题。 HttpClientFactory 直接负责给 HttpClient 输入 全新的 HttpMessageHandle
对象,并且管理 HttpMessageHandle 的生杀大权,这样断开 Tcp 连接的操作都由 HttpClientFactory 来用一种良好的机制去解决。
上面说了一堆,其实和主题关系不大。 因为我在实际生产环境中,无论使用静态的 HttpClient 还是使用 HttpClientFactory ,在高并发下的情况下 Tcp 连接都陡然上升。直到我将 .net core 2.1 升级到 .net core 2.2 preview 问题似乎奇迹般的解决了。在介绍 .net core 2.2 如何提升 HttpClient 性能的时候,需要先简单介绍下 HttpClient :
上面说到了 HttpMessageHandle ( 顾名思义:Http消息处理器 ) 它是一个抽象类,用来干嘛的呢? 处理请求,又是顾名思义。 HttpClient 的发送请求函数 :SendAsync()
public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption,
CancellationToken cancellationToken)
{
....
}
最后调用的就是 HttpMessageHandle 的 SendAsync 抽象函数。
事实上通过阅读源码发现,几乎所有继承 HttpMessageHandle 的子类都有一个 HttpMessageHandle 类型的属性 : _handle
,而每个子类的 SendAsync 函数都调用 _handle 的 SendAsync()。我们知道在初始化一个 HttpClient 的时候或者使用 HttpClientFactory 创建一个HttpClient 的时候都需要新建 或者传入一个 HttpMessageHandle 我把它叫做起始消息处理器。 很容易想像,HttpClient 的 SendAsync 函数是 一个 HttpMessageHandle 调用 下一个 HttpMessageHanlde 的SendAsync,而下一个 HttpMessageHandle 的SendAsync 是调用下下一个HttpMessageHandle 的 SendAsync 函数。每一个HttpMessageHandle 都有其自己的职责。
层层嵌套,环环相扣,循环往复,生生不息,额不对,这样下去会死循环。 直到它到达终点,也就是Tcp 连接建立,抛弃回收,发送请求的地方。 所以 HttpClient 的核心 就是由这些 HttpMessageHandle 扣起来,打造成一个 消息通道。 每个请求都无一例外的 通过这个通道,找到它们的最终归宿。
这其中的顺序到底是啥,我并不关心,我只关心其中一个 环:SocketsHttpHandle 因为.net core 2.2 就是从这个环开始动了手术刀,怎么动的,按照上面的说法,我们从 SocketHttpHandle 开始顺藤摸瓜。其实顾名思义 SocketsHttpHandle 已经很接近 HttpClient 的通道的末尾了。这是 摸出来的 链条 :
SocketsHttpHandle
----> HttpConnectionHandler/HttpAuthenticatedConnectionHandler
----> HttpConnectionPoolManager
---> HttpConnectionPool
最后一个加粗是有原因的,因为我们摸到尾巴了,HttpConnectionPool
( 顾名思义 Http 连接 池) 已经不继承 HttpMessageHandle 了 ,它就是我们要找的终极,也是请求最终获取连接的地方,也是.net core 2.2 在这条链中的 操刀的地方。
接下来就要隆重介绍 手术过程。手术的位置在哪里? 就是获取 Tcp 连接的函数。我们看手术前的样子,也就是System.Net.Http 4.3.3 版本的样子。
List<CachedConnection> list = _idleConnections;
lock (SyncObj)
{
while (list.Count > 0)
{
CachedConnection cachedConnection = list[list.Count - 1];
HttpConnection conn = cachedConnection._connection;
list.RemoveAt(list.Count - 1);
if (cachedConnection.IsUsable(now, pooledConnectionLifetime, pooledConnectionIdleTimeout) &&
!conn.EnsureReadAheadAndPollRead())
{
if (NetEventSource.IsEnabled) conn.Trace("Found usable connection in pool.");
return new ValueTask<(HttpConnection, HttpResponseMessage)>((conn, null));
}
if (NetEventSource.IsEnabled) conn.Trace("Found invalid connection in pool.");
conn.Dispose();
}
if (_associatedConnectionCount < _maxConnections)
{
if (NetEventSource.IsEnabled) Trace("Creating new connection for pool.");
IncrementConnectionCountNoLock();
return WaitForCreatedConnectionAsync(CreateConnectionAsync(request, cancellationToken));
}
else
{
if (NetEventSource.IsEnabled) Trace("Limit reached. Waiting to create new connection.");
var waiter = new ConnectionWaiter(this, request, cancellationToken);
EnqueueWaiter(waiter);
if (cancellationToken.CanBeCanceled)
{
waiter._cancellationTokenRegistration = cancellationToken.Register(s =>
{
var innerWaiter = (ConnectionWaiter)s;
lock (innerWaiter._pool.SyncObj)
{
if (innerWaiter._pool.RemoveWaiterForCancellation(innerWaiter))
{
bool canceled = innerWaiter.TrySetCanceled(innerWaiter._cancellationToken);
Debug.Assert(canceled);
}
}
}, waiter);
}
return new ValueTask<(HttpConnection, HttpResponseMessage)>(waiter.Task);
}
整个过程一目了然,list
是存放 闲置的Tcp连接 的链表,当一个 请求
千辛万苦到了这里,它要开始在链表的末尾开始 查找有没有可以用的 小跑车
(Tcp连接),先把从小跑车 从 车库
(list)里搬出来,然后检查下动力系统,轮子啥的,如果发现坏了( 当前连接不可用 ,已经被服务端关闭的,或者有异常数据的 等等 ), 你需要用把这个坏的车给砸了( 销毁Tcp连接 ),再去搬下一个小跑车。
如果可以用,那么很幸运,这个请求可以立刻开着小跑车去飙车(发送数据)。如果这个车库的车全是坏的或者一个车都没有,那么这个请求就要自己造一个小跑车 ( 建立新的TCP 连接 )。 这里还有一个点,小跑车数量是有限制的。假如轮到你了,你发现车库里没有车,你要造新车,但是系统显示车子数量已经达到最大限制了,所以你就要等 小伙伴 ( 别的请求 ) 把 小跑车用完后开回来,或者等车库里的坏车 被别的小伙伴砸了。
整个过程看起来好像也挺高效的,但是请注意 lock (SyncObj)
上述所有操作的都被上锁了,这些操作同时只能有一个小伙伴操作,这样做的原因当然是为了安全,防止两个请求同时用了同一个Tcp连接,这样的话车子会被挤坏掉的。 于是小伙伴们都一个一个的排着队。 试想,当我们的请求很多很多的时候,队伍很长很长,那每个请求执行的时间久会变长。
那有没有什么方法可以加快速度呢? 其实是有的,事实上危险的操作 只是从 list 中去取车,和造新车。防止抢车和两个小伙伴造了同一个车。于是手术后的样子是这样的:
while (true)
{
CachedConnection cachedConnection;
lock (SyncObj)
{
if (list.Count > 0)
{
cachedConnection = list[list.Count - 1];
list.RemoveAt(list.Count - 1);
}
else
{
if (_associatedConnectionCount < _maxConnections)
{
.
IncrementConnectionCountNoLock();
return new ValueTask<HttpConnection>((HttpConnection)null);
}
else
{
waiter = EnqueueWaiter();
break;
}
}
}
HttpConnection conn = cachedConnection._connection;
if (cachedConnection.IsUsable(now, pooledConnectionLifetime, pooledConnectionIdleTimeout) &&
!conn.EnsureReadAheadAndPollRead())
{
if (NetEventSource.IsEnabled) conn.Trace("Found usable connection in pool.");
return new ValueTask<HttpConnection>(conn);
}
if (NetEventSource.IsEnabled) conn.Trace("Found invalid connection in pool.");
conn.Dispose();
}
可以看出,它把加锁执行的内容减少了,将检查车子的工作放到锁外。此外 将 lock...while 变成了while...lock 这样有什么影响呢:可以减少线程之间的竞争,如评论所说,lock...while 是霸道的,一线程阻塞,万线程等待竞争,而 while...lock 所有线程展开公平的竞争,大家持有锁几乎是相同的几率。
没想到这样一个操作,在Linux中提升了60% 的性能。减少了小伙伴之间的等待时间。
那么 静态的HttpClient 和 HttpClientFactory 的二者使用,哪个性能更好呢? 我认为是前者,在高并发的实验过程中也确实如此。因为 静态HttpClient 只有一个消息通道,从头用到尾,这样无疑是最高效的。而HttpClientFactory 需要销毁 HttpMessageHandle 销毁 HttpMessageHanlde 的过程是链条中的节点一个一个被摧毁的过程,直到最后的Tcp 连接池也被销毁。但是 静态HttpClient 有个DNS 解析无法更新的硬伤,所以还是应该 使用HttpClientFactory 。 在使用Service.AddHttpClient 时需要设置生存周期,这就是HttpMessageHandle 的生存时长,我认为应该将其设置的长一些,这样HttpMessageHandle 或者叫做消息通道 就可以多多的被重复利用,因为HttpClientFactory 可以给不同HttpClient实例注入相同的HttpMessageHandle
看完这篇文章 还可以看下这篇文章的姊妹篇:工厂参观记:.NET Core 中 HttpClientFactory 如何解决 HttpClient 臭名昭著的问题
当然我遇到的问题 是否真的是因为 HttpClient 性能的提升而解决,现在也不能确定。还需要进一步检测验证。
HttpClient参观记:.net core 2.2 对HttpClient到底做了什么?的更多相关文章
- .NET Core 中正确使用 HttpClient 的姿势
为了更方便在服务端调用 HTTP 请求,微软在 .NET Framework 4.x 的时候引入了 HttpClient.但 HttpClient 有很多严重问题,一直饱受诟病,比如 InfoQ 的这 ...
- 理解ASP.NET Core - 发送Http请求(HttpClient)
注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 前言 在.NET中,我们有很多发送Http请求的手段,如HttpWebRequest.WebC ...
- Android网络(4):HttpClient必经之路----使用线程安全的单例模式HttpClient,及HttpClient和Application的融合
上文简介了HttpClient和Tomcatserver的交互,主角是HttpClient,然后它跟server交互有两种方式即get和post.所以这个HttpClient就相似于电脑上用的浏览器. ...
- ndroid网络(4):HttpClient必经之路----使用线程安全的单例模式HttpClient,及HttpClient和Application的融合
上文简 单介绍了HttpClient和Tomcat服务器的交互,主角是HttpClient,然后它跟服务器交互有两种方式即get和post.所以这个 HttpClient就类似于电脑上用的浏览器.当我 ...
- Httpclient远程调用WebService示例(Eclipse+httpclient)
package cn.com.taiji.pos.httpserver; import java.io.BufferedInputStream;import java.io.ByteArrayOutp ...
- 项目开发中的一些注意事项以及技巧总结 基于Repository模式设计项目架构—你可以参考的项目架构设计 Asp.Net Core中使用RSA加密 EF Core中的多对多映射如何实现? asp.net core下的如何给网站做安全设置 获取服务端https证书 Js异常捕获
项目开发中的一些注意事项以及技巧总结 1.jquery采用ajax向后端请求时,MVC框架并不能返回View的数据,也就是一般我们使用View().PartialView()等,只能返回json以 ...
- HttpClient参观记:.net core 2.2 对HttpClient到底做了神马
.net core 于 10月17日发布了 ASP.NET Core 2.2.0 -preview3,在这个版本中,我看到了一个很让我惊喜的新特性:HTTP Client Performance Im ...
- HttpClient在.NET Core中的正确打开方式
问题来源 长期以来,.NET开发者都通过下面的方式发送http请求: using (var httpClient = new HttpClient()) { var response = await ...
- ASP.NET Core 2.1 : 十三.httpClient.GetAsync 报SSL错误的问题
不知什么时候 ,出现了这样的一个奇怪问题,简单的httpClient.GetAsync("xxxx")居然报错了.(ASP.NET Core 系列目录) 一.问题描述 把原来的程序 ...
随机推荐
- windows服务器基本管理及服务搭建
windows服务器基本管理及服务搭建 ****windows服务器系统版本:2000 2003 2008 2012 1.用户与组管理 用户:账户=账号/用户名+密码 每个账户有自己唯一的SID 账户 ...
- JAVA进阶8
间歇性混吃等死,持续性踌躇满志系列-------------第8天 1.遍历输出HashSet中的全部元素 import java.util.HashSet; import java.util.Ite ...
- Thread和Runnable的区别
Runnable源码 Thread源码 结论 Thread实现了Runnable接口的类,使得run支持多线程. 因类的单一继承原则,推荐使用Runnable接口实现多线程
- phpstudy 2016 切换Nginx+php7.0版本所需运行库 vc14 + 安装redis拓展
去微软官方下载vc14的运行库 链接:https://www.microsoft.com/en-us/download/details.aspx?id=48145 32位运行库 安装成功 切换版本成功 ...
- [Kubernetes]谈谈Kubernetes的本质
当下k8s算是比较火的一个内容,那么它到底是什么呢,它为什么会这么火呢,它解决的是什么问题呢.这篇文章就尝试着来讲讲,Kubernetes的本质. 当我们谈Kubernetes的时候,总是会想起来Do ...
- vim配置(使用Vundle)
1.前言 Vim的配置文件位于~/.vimrc,文件使用VimScript语法来编写. 2. vim插件管理 Vundle是一个全自动的插件管理器,让我们通过维护插件列表的方式管理插件.它为安装.更新 ...
- .Net Core---- 通过EPPlus批量导出
前台代码: 前台代码是在.net core bootstrap集成框架上的(这是效果浏览地址:http://core.jucheap.com[效果地址来自:http://blog.csdn.net/a ...
- 洛谷P5280 [ZJOI2019]线段树 [线段树,DP]
传送门 无限Orz \(\color{black}S\color{red}{ooke}\)-- 思路 显然我们不能按照题意来每次复制一遍,而多半是在一棵线段树上瞎搞. 然后我们可以从\(modify\ ...
- day18正则及re模块
在线测试工具 http://tool.chinaz.com/regex/ 正则表达式本身和python语言没什么联系,只是匹配字符串内容的一种规则:详见:http://www.cnblogs.com/ ...
- 一JavaScript获取当前月份的前12个月,获取最近的12个月二js实现获取当前月份前的12个月份,格式化后放在一个数组里
一 ,var dataArr = []; var data = new Date(); var year = data.getFullYear(); data.setMonth(data.getMon ...