设计错误、缺陷及文档错误等导致正确使用.NET HttpClient变得出奇地困难。所以,即使是生产环境中看似运行正常的应用程序,在负荷不满的情况下,也遭受着性能问题和运行时故障。 来自ASP.NET Monsters的Simon Timms就通过一篇题为“你正在错误地使用HttpClient,它会破坏软件的稳定性”的文章揭示了这个事实。 人们对这篇文章的反应有所不同,但大多数都显示出了失望和沮丧:
-- Eirenarch C#开发人员所受到的培训 为了理解我们如何陷入了这种境地,我们首先需要看下另外一个面向连接的类SqlConnection。在第一次接受如何使用IDisposable和using语句的培训时,绝大多数开发人员看到的都是类似下面这样的例子: using (var con = new SqlConnection(connectionString)) { con.open(); //这里使用连接 } //这里关闭连接 虽然针对这个示例的说明并不完善,但这个模式是正确的,而且多年来很好地服务了开发人员。然而,如果你试图将这个模式应用到另一个IDisposable类HttpClient上,则会遇到一些始料未及的问题。 具体来说,它会打开许多套接字,比你实际的需求多许多,这极大地增加了服务器的负载。而且,这些套接字实际上不会被using语句关闭。相反,它们是在应用程序停止使用它们几分钟之后才会关闭。 连接池 回到SqlConnection的例子,多数面向连接的资源都会放入连接池。当你“打开”一个数据库连接时,它首先会检查连接池中是否存在未使用的连接。如果找到了,就重用它,而不是创建一个新的连接。 同样,当你“关闭”一个SqlConnection连接时,它只是简单地将连接放回连接池。最后,一个单独的进程可以关闭长期未使用的连接,但通常来说,你可以认为它会正确地执行操作,实现性能和服务器负载的平衡。 HttpClient的工作机制并非如此。当你销毁它时,它就启动一个进程,关闭在它控制之下的套接字。也就是说,你下次请求连接时,必须重复整个连接新建过程。如果网络延迟很高,或者连接是受保护的(需要新一轮的SSL/TLS协商),就会非常痛苦。 关闭一个套接字需要花费4分钟 如上所述,关闭套接字的过程并不快。当“关闭”套接字时,你真正做的是将其状态置为TIME_WAIT。在一个预先配置好的时间窗口内,Windows将保持该套接字的状态不变,默认情况下是4分钟。这是为了防止有任何剩余的数据包仍在传输。 这大大增加了可用套接字耗尽的可能,导致运行时错误,比如“无法连接到远程服务器。System.Net.Sockets.SocketException:每个套接字地址(协议/网络地址/端口)通常只允许使用一次”。Simon Timms写到:
.NET Core的性能影响 大多数仅仅使用.NET Framework完整版的开发人员不会注意到这些问题。不过,那些使用.NET Core的开发人员会有一个额外的问题,使得整个问题更加明显。 在.NET Core的RC1和RC2版本之间,引入了一个Bug,导致HttpClient.Dispose调用会产生一个介于1010毫秒和1030毫秒之间的延迟。在.NET Core 1.2之前,这个问题预计不会得到修复。 使用代理类作为解决方案 虽然HttpClient的文档没有提及,但微软模式&实践的GitHub站点介绍了一种模式。他们把HttpClient称为“代理类”,并作了如下描述:
Microsoft P&P建议创建一个HttpClient实例,把它存储在一个静态字段中,并在应用程序的生存期内共享该实例,而不是根据需求创建和销毁。 存在误导的文档 这将我们带回到了文档存在误导的问题。虽然是基本的样本文件,但官方文档v118(当前谷歌和必应搜索返回的结果)指出,HttpClient不支持跨线程共享。
差不多就是这样。当然,如果你看一下官方文档v110,就会发现下面这段有用的描述。
根据这份文档,以下方法是线程安全的。 这似乎是MSDN文档一直存在的问题。要了解任何类的演进过程,都必须检查每个版本的文档,才能了解到新增或删除的重要段落。 DNS Bug 如果我们遵循目前为止的建议,则会出现其他的问题。Ali Kheyrollahi写道:
这个问题并不是无法修复。理论上讲,HttpClient会遵循DNS TTL(生存期)值,默认为1小时。每次过期后,HttpClient会验证该DNS记录是否仍然有效,并在必要时新建一个连接指向更新后的IP地址。 但是,由于那种情况可能不会出现,所以Kheyrollahi为我们提供了一个更简单的变通方案。借助ServicePointManager,你可以告诉HttpClient自动回收连接。 var sp = ServicePointManager.FindServicePoint(new Uri("http://foo.bar")); sp.ConnectionLeaseTimeout = 60*1000; // 1分钟
(作者: ,译者: )查看英语原文:Bugs and Documentation Errors in .NET's HttpClient Frustrate Developers |