9

我曾经在 .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 库允许使用点网套接字(Socket.BeginSend / Socket.BeginReceive / NetworkStream.BeginRead / NetworkStream.BeginWrite)进行异步 IO,其异步 IO 的未完成缓冲区数量(发送或接收)必须具有上限.

网络应用程序应该对其发布的未完成异步 IO的数量有一个上限 。

编辑:添加一些问号。

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

4

5 回答 5

11

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

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

于 2008-10-25T14:56:04.483 回答
3

这不仅限于 .Net。

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

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

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

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

于 2011-05-20T17:16:14.030 回答
3

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

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

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

于 2008-10-25T09:57:51.207 回答
0

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

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

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

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

于 2009-09-14T22:53:53.303 回答
0

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

于 2011-05-20T10:18:25.020 回答