.NET 没有可靠的异步套接字通信?

Posted

技术标签:

【中文标题】.NET 没有可靠的异步套接字通信?【英文标题】:.NET Does NOT Have Reliable Asynchronouos Socket Communication? 【发布时间】:2010-09-19 04:01:20 【问题描述】:

我曾经在 .NET 中编写了一个 Crawler。为了提高它的可扩展性,我尝试利用 .NET 的异步 API。

System.Net.HttpWebRequest 具有异步 API BeginGetResponse/EndGetResponse。但是,这对 API 只是为了获取一个 HTTP 响应头和一个 Stream 实例,我们可以从中提取 HTTP 响应内容。所以,我的策略是使用 BeginGetResponse/EndGetResponse 异步获取响应 Stream,然后使用 BeginRead/EndRead 从响应 Stream 实例中异步获取字节。

在 Crawler 进行压力测试之前,一切似乎都很完美。在压力测试下,Crawler 内存使用率很高。我用 WinDbg+SoS 检查了内存,发现很多字节数组都被 System.Threading.OverlappedData 实例固定了。在网上搜索了一番后,我从微软找到了这个 KB http://support.microsoft.com/kb/947862。

根据 KB,异步 I/O 的数量应该有一个“上限”,但它并没有告诉一个“建议的”界限值。所以,在我看来,这个知识库没有任何帮助。这显然是一个 .NET 错误。最后,我不得不放弃从响应流中异步提取字节的想法,而是以同步的方式进行。

.NET 库允许 带点网套接字的异步 IO (Socket.BeginSend / Socket.BeginReceive / NetworkStream.BeginRead / NetworkStream.BeginWrite) 必须有一个 缓冲区数量的上限 未完成(发送或接收) 使用他们的异步 IO。

网络应用程序应该有一个 数量的上限 优秀的它发布的异步 IO。

编辑:添加一些问号。

有人有在 Socket 和 NetworkStream 上进行异步 I/O 的经验吗? 一般来说,生产中的爬虫是同步还是异步与互联网进行I/O?

【问题讨论】:

不是一个单独的问号,除了主题......一个不好的迹象。 【参考方案1】:

嗯,这不是 .NET 框架的问题。链接的知识库文章可能更明确一点:“你使用的是一把上膛的枪,当你将它瞄准你的脚时会发生这种情况”。那把枪里的子弹是 .NET,使您能够启动尽可能多的异步 I/O 请求。它会做你要求它做的事情,直到你达到某种资源限制。在这种情况下,第 0 代堆中的固定接收缓冲区可能过多。

资源管理仍然是我们的主要工作,而不是 .NET。这与无限制地分配内存没有什么不同。解决这个特定问题需要您限制未完成的 BeginGetResponse() 请求的数量。拥有数百个没有什么意义,每个人都必须一次挤过一个 Intertube。添加另一个请求只会导致它需要更长的时间才能完成。或者让你的程序崩溃。

【讨论】:

但是,我如何知道我的程序中的“上限”?事实是,即使应用程序在超时后中止 BeginXXX 操作,.NET 也不会释放固定字节数组。我仍然相信这是一个 .net 错误。 看不出这是一个有用的答案吗?! 调用 EndXxxx 释放资源是一个要求。不要跳过那个。显然,当您实施超时方案时,很容易意外跳过。【参考方案2】:

您显然希望限制并发请求的数量,无论您的爬虫是同步/异步。这个限制不是固定的,它取决于你的硬件、网络……

我不太确定您的问题是什么,因为 HTTP/Sockets 的 .NET 实现是“好的”。有一些漏洞(请参阅my post,了解如何正确控制超时),但它可以完成工作(我们有一个生产爬虫,每秒可抓取数百页)。

顺便说一句,我们使用同步 IO,只是为了方便。每个任务都有一个线程,我们限制并发线程的数量。对于线程管理,我们使用了Microsoft CCR。

【讨论】:

我毫不怀疑 Socket 上的同步 I/O 在 DotNet 中运行良好。我只是不相信它的异步 I/O API。 问题是中止/取消操作,它在 .NET 中永远无法正常工作。您应该始终更喜欢同步 API(带有超时),这样您就不需要自己取消操作。 我还建议将同步 WebRequest 包装在任务中。此外,不要使用线程,而是使用 Tasks - 这将通过使用线程池来保护您免受广泛的线程生成。如果您额外使用 TaskCancelationSource,您可以轻松取消正在运行的任务【参考方案3】:

这不仅限于 .Net。

这是一个简单的事实,每个异步请求(文件、网络等)都使用内存和(在某些时候,至少用于网络请求)非分页池(请参阅here 了解在非托管中可能遇到的问题的详细信息代码)。因此,未完成请求的数量受内存量的限制。在 Vista 之前,有一些非常低的非分页池限制会在内存不足之前给您带来问题,但在后 vista 环境中,对于非分页池的使用情况要好得多(请参阅here)。

托管代码稍微复杂一些,因为除了在非托管世界中遇到的问题外,您还必须处理用于异步请求的内存缓冲区在这些请求完成之前被固定的事实。听起来您在读取时遇到了这些问题,但对于写入而言,即使不是更糟,也同样糟糕(一旦 TCP 流控制在连接上启动,这些发送完成将开始花费更长的时间发生,因此这些缓冲区被固定的时间越来越长 - 请参阅 here 和 here)。

问题不在于 .Net 异步的东西被破坏了,而只是抽象使得它看起来比实际上要容易得多。例如,为避免固定问题,请在程序启动时将所有缓冲区分配在一个大的连续块中,而不是按需分配...

就我个人而言,我会在非托管代码中编写这样的爬虫,但这只是我 ;) 您仍将面临许多问题,但您对它们有更多的控制权。

【讨论】:

【参考方案4】:

没有知识库文章可以给你一个上限。上限可能因可用硬件而异 - 2G 内存机器的上限对于具有 16g 内存的机器会有所不同。它还取决于 GC 堆的大小、碎片程度等。

您应该做的是使用信封计算得出您自己的指标。弄清楚每分钟要下载多少页。这应该确定您想要处理多少异步请求 (N)。

一旦知道 N,就可以创建一段代码(如生产者-消费者管道的消费者端),它可以创建 N 个未完成的异步下载请求。一旦请求完成(由于超时或由于成功),通过从队列中拉出一个工作项来启动另一个异步请求。

您还需要确保队列不会超出界限,例如,如果由于某种原因导致下载变慢。

【讨论】:

【参考方案5】:

当您使用套接字的异步发送 (BeginSend) 方法时会发生这种情况。如果您使用自己的自定义线程池,并使用同步发送方法通过线程发送数据主要是解决这个问题。经过测试和证明。

【讨论】:

以上是关于.NET 没有可靠的异步套接字通信?的主要内容,如果未能解决你的问题,请参考以下文章

核心编程练习

服务之间异步通信的最佳实践

套接字编程

Python异步通信模块asyncore

QT下UDP套接字通信——QUdpSocket 简单使用

.Net 异步套接字操作限制?