HTTP Client Performance Improvements

https://blogs.msdn.microsoft.com/webdev/2018/10/17/asp-net-core-2-2-0-preview3-now-available/

https://www.cnblogs.com/dacc123/p/9892274.html

.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 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 ----> HttpConnectionPoolManager

---> HttpConnectionPool

最后一个加粗是有原因的,因为我们摸到尾巴了,HttpConnectionPool( 顾名思义 Http 连接 池) 已经不继承 HttpMessageHandle 了 ,它就是我们要找的终极,也是请求最终获取连接的地方,也是.net core 2.2 在这条链中的 操刀的地方。

接下来就要隆重介绍 手术过程。手术的位置在哪里? 就是获取 Tcp 连接的函数。我们看手术前的样子,也就是System.Net.Http 4.3.3 版本的样子。

List 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 性能的提升而解决,现在也不能确定。还需要进一步检测验证。

HTTP Client Performance Improvements的更多相关文章

  1. HttpClient参观记:.net core 2.2 对HttpClient到底做了什么?

    .net core 于 10月17日发布了 ASP.NET Core 2.2.0 -preview3,在这个版本中,我看到了一个很让我惊喜的新特性:HTTP Client Performance Im ...

  2. HttpClient参观记:.net core 2.2 对HttpClient到底做了神马

    .net core 于 10月17日发布了 ASP.NET Core 2.2.0 -preview3,在这个版本中,我看到了一个很让我惊喜的新特性:HTTP Client Performance Im ...

  3. PatentTips - Optimizing Write Combining Performance

    BACKGROUND OF THE INVENTION The use of a cache memory with a processor facilitates the reduction of ...

  4. 论文阅读:Andromeda: Performance, Isolation, and Velocity at Scale in Cloud Network Virtualization (全文翻译用于资料整理和做PPT版本,之后会修改删除)

    Abstract: This paper presents our design and experience with Andromeda,  Google Cloud Platform’s net ...

  5. SQLite源程序分析之sqlite3.c

    /****************************************************************************** ** This file is an a ...

  6. EntityFramework 7 更名为EntityFramework Core(预发布状态)

    前言 最近很少去学习和探索新的东西,尤其是之前一直比较关注的EF领域,本身不太懒,但是苦于环境比较影响自身的心情,所以迟迟没有下笔,但是不去学习感觉在精神层面缺少点什么,同时也有园友说EF又更新了,要 ...

  7. EF 5 最佳实践白皮书

    Performance Considerations for Entity Framework 5 By David Obando, Eric Dettinger and others Publish ...

  8. MAGIC XPA最新版本Magic xpa 2.4c Release Notes

    New Features, Feature Enhancements and Behavior ChangesSubforms – Behavior Change for Unsupported Ta ...

  9. Flume官方文档翻译——Flume 1.7.0 User Guide (unreleased version)(一)

    Flume 1.7.0 User Guide Introduction(简介) Overview(综述) System Requirements(系统需求) Architecture(架构) Data ...

随机推荐

  1. 【三小时学会Kubernetes!(零) 】系统结构及相关示例微服务介绍

    写在前面 牢牢占据容器技术统治地位的 Kubernetes,其重要性想必不言而喻,我保证本文是最详尽的 Kubernetes 技术文档,从我在后台排版了这么漫长的时间就能看出来.废话不多说 — — 以 ...

  2. head first python选读(5)

    python web 开发 犯了低级错误,这本书看了一半了才知道书名应为<head first python>,不是hand first.. 现在开始一个web应用. 总算是熟悉的内容了. ...

  3. Kotlin------函数和代码注释

    定义函数 Kotlin定义一个函数的风格大致如下 访问控制符 fun 方法名(参数,参数,参数) : 返回值类型{ ... ... } 访问控制符:与Java有点差异,Kotlin的访问范围从大到小分 ...

  4. 搞懂分布式技术21:浅谈分布式消息技术 Kafka

    搞懂分布式技术21:浅谈分布式消息技术 Kafka 浅谈分布式消息技术 Kafka 本文主要介绍了这几部分内容: 1基本介绍和架构概览 2kafka事务传输的特点 3kafka的消息存储格式:topi ...

  5. 3.java内存模型以及happens-before规则

    1. JMM的介绍 在上一篇文章中总结了线程的状态转换和一些基本操作,对多线程已经有一点基本的认识了,如果多线程编程只有这么简单,那我们就不必费劲周折的去学习它了.在多线程中稍微不注意就会出现线程安全 ...

  6. ffmpeg播放RTSP的一点优化

    简单记录一下最近使用ffmpeg播放RTSP做的一点参数优化. 先做如下定义: AVDictionary* options = NULL; 1.画质优化 原生的ffmpeg参数在对1920x1080的 ...

  7. maven-surefire-plugin的forkMode分析

    Maven运行测试用例时,是通过调用maven的surefire插件并fork一个子进程来执行用例的.forkmode属性中指明是要为每个测试创建一个进程,还是所有测试在同一个进程中完成. <p ...

  8. 这可能是最详细的 iOS 学习入门指南(含书目/文档/学习资料)

    1 零基础小白如何进行 iOS 系统学习 首先,学习目标要明确: 其次,有了目标,要培养兴趣,经常给自己一些正面的反馈,比如对自己的进步进行鼓励,在前期小步快走: 再次,学技术最重要的一点就是多动手. ...

  9. Python中的map和reduce函数简介

    ①从参数方面来讲: map()函数: map()包含两个参数,第一个是参数是一个函数,第二个是序列(列表或元组).其中,函数(即map的第一个参数位置的函数)可以接收一个或多个参数. reduce() ...

  10. Java复习6异常处理

    Java复习6.异常处理 20131005 前言: Java中的异常处理机制是非常强大的,相比C++ 来说,更加系统.但是我们开发人员没有很好的使用这一点.一些小的程序是没有什么问题的,但是对于大型项 ...