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

时间:2008-10-25 09:49:08

标签: c# .net sockets stream web-crawler

我曾经在.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的数量应该具有“上限”,但它不会告诉“建议的”绑定值。所以,在我看来,这个KB没有任何帮助。这显然是一个.NET错误。最后,我不得不放弃从响应Stream中异步提取字节的想法,而只是以同步方式进行。

  

允许的.NET库   带点网套接字的异步IO   (Socket.BeginSend /   Socket.BeginReceive /   NetworkStream.BeginRead /   NetworkStream.BeginWrite)必须有   缓冲量的上限   优秀(发送或接收)   与他们的异步IO。

     

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

编辑:添加一些问号。

任何人都有在Socket& amp;上进行异步I / O的经验。的NetworkStream? 一般来说,生产中的爬虫是否通过同步或异步的互联网进行I / O?

5 个答案:

答案 0 :(得分:11)

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

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

答案 1 :(得分:3)

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

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

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

答案 2 :(得分:3)

这不仅限于.Net。

一个简单的事实是,每个异步请求(文件,网络等)都使用内存和(在某些时候,至少是网络请求)非页面缓冲池(有关您可以获得的问题的详细信息,请参阅here在非托管代码中)。因此,未完成请求的数量受内存量的限制。在Vista之前有一些非常低的非页面缓冲池限制会导致你在内存不足之前出现问题,但在后vista环境中,非页面缓冲池使用情况要好得多(参见here)。

在托管代码中有点复杂,因为除了在非托管世界中遇到的问题之外,还必须处理这样一个事实,即用于异步请求的内存缓冲区会被固定,直到这些请求完成为止。听起来你在读取时遇到了这些问题,但是对于写入来说,如果不是更坏的话,就会一样糟糕(一旦TCP流量控制启动连接,那么发送完成将开始花费更长时间,因此这些缓冲区固定的时间越长越长 - 请参阅herehere)。

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

就我个人而言,我会在非托管代码中编写这样的抓取工具,但这只是我;)您仍然会面临许多问题,但您可以对它们进行更多控制。

答案 3 :(得分:0)

没有KB文章可以给你一个上限。上限可以根据可用的硬件而变化 - 对于具有16g内存的机器,2G内存机器的上限将是不同的。它还取决于GC堆的大小,碎片的碎片等等。

您应该做的是使用信封计算后面的公制。计算出每分钟要下载的页数。这应该决定你想要多少异步请求(N)。

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

您还需要确保队列不会超出边界,例如,无论出于何种原因,下载都会变慢。

答案 4 :(得分:0)

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