.net core 于 10月17日发布了 ASP.NET Core 2.2.0 -preview3,在这个版本中,我看到了一个很让我惊喜的新特性:HTTP Client Performance Improvements ,并且在Linux上性能提高了60% !html
以前就一直苦于 HttpClient 的糟糕特性,你们耳熟能详的 You are using HttpClient wrong。
由于 HttpClient 实现了 IDisposable
若是用完就释放,Tcp 链接也会被断开,而且一个HttpClient 一般会创建不少个 Tcp 链接 。 Tcp 链接断开的过程是有一个 Time_Wait 状态的,由于要保证 Tcp 链接可以断开,以及防止断开过程当中还有数据包在传送。这自己没有毛病,可是若是你在使用 HttpClient 后就将其注销,而且同时处于高并发的状况下,那么你的 Time_Wait 状态的 Tcp 链接就会爆炸的增加,
他们占用端口和资源并且还迟迟不消失,就像是在 嘲讽 你。因此临时解决方式是使用静态的 HttpClient 对象,No Dispose No Time_Waitweb
后来在 .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 的通道的末尾了。这是 摸出来的 链条 :this
SocketsHttpHandle
----> HttpConnectionHandler/HttpAuthenticatedConnectionHandler
----> HttpConnectionPoolManager
.net
---> 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 性能的提高而解决,如今也不能肯定。还须要进一步检测验证。