HttpWebResponse不适用于并发的出站请求。

12

我有一个用C#编写的ASP.NET 3.5服务器应用程序。它使用HttpWebRequest和HttpWebResponse向REST API发出请求。

我设置了一个测试应用程序,在单独的线程上发送这些请求(模拟与服务器的并发)。

请注意,这更多是关于Mono/环境的问题,而不是代码问题;因此,请记住下面的代码不是一字不差的;只是功能位的剪切/粘贴。

以下是一些伪代码:

// threaded client piece
int numThreads = 1;
ManualResetEvent doneEvent;

using (doneEvent = new ManualResetEvent(false))
        {

            for (int i = 0; i < numThreads; i++)
            {

                ThreadPool.QueueUserWorkItem(new WaitCallback(Test), random_url_to_same_host);

            }
            doneEvent.WaitOne();
        }

void Test(object some_url)
{
    // setup service point here just to show what config settings Im using
    ServicePoint lgsp = ServicePointManager.FindServicePoint(new Uri(some_url.ToString()));

        // set these to optimal for MONO and .NET
        lgsp.Expect100Continue = false;
        lgsp.ConnectionLimit = 100;
        lgsp.UseNagleAlgorithm = true;
        lgsp.MaxIdleTime = 100000;        

    _request = (HttpWebRequest)WebRequest.Create(some_url);


    using (HttpWebResponse _response = (HttpWebResponse)_request.GetResponse())
    {
      // do stuff
    } // releases the response object

    // close out threading stuff

    if (Interlocked.Decrement(ref numThreads) == 0)
    {
        doneEvent.Set();
    }
}
如果在本地开发机器(Windows 7)上在Visual Studio Web服务器上运行应用程序,则可以增加numThreads并在1个“用户”或100个“用户”时获得相同的平均响应时间,并且变化很小。
将应用程序发布和部署到在Mono 2.10.2环境下的Apache2中,响应时间几乎呈线性增长。 (即1个线程= 300ms,5个线程= 1500ms,10个线程= 3000ms)。这会发生在不同的服务器端点上(不同的主机名,不同的网络等)。
使用IPTRAF(和其他网络工具),似乎应用程序仅打开1或2个端口来路由所有连接,并且剩余的响应必须等待。
我们构建了一个类似的PHP应用程序,并使用相同的请求在Mono中部署,而响应则适当地扩展。
我已经尝试了我能想到的每个Mono和Apache的配置设置,而两个环境之间(至少在代码中)唯一不同的设置是有时ServicePoint SupportsPipelining = false在Mono中,而从我的机器上是true。
看起来ConnectionLimit(默认为2)在Mono中由于某种原因没有被更改,但我在代码和web.config中为指定的主机设置了更高的值。
我和我的团队要么忽略了重要的事情,要么这是Mono中的某种错误。

1
你找到解决方案了吗? - Kugel
3个回答

9
我认为你遇到了HttpWebRequest的瓶颈问题。每个Web请求在.NET框架内使用通用的服务点基础设施。这似乎旨在允许重复利用同一主机的请求,但是根据我的经验,这会导致两个瓶颈。

首先,默认情况下,服务点仅允许对给定主机进行两个并发连接,以符合HTTP规范。可以通过将静态属性ServicePointManager.DefaultConnectionLimit设置为更高的值来覆盖此设置。有关详细信息,请参阅MSDN页面。看起来您已经在单个服务点本身上解决了这个问题,但由于服务点级别的并发锁定方案,这样做可能会导致瓶颈。

其次,ServicePoint类本身的锁粒度存在问题。如果你反编译并查看lock关键字的源代码,你会发现它使用实例自身进行同步,并且在许多地方都这样做。由于服务点实例在给定主机的Web请求之间共享,在我的经验中,随着更多的HttpWebRequests被打开,它倾向于成为瓶颈,并导致它的扩展性变差。这第二点主要是个人观察和对源代码的挖掘,所以拿它当权威来源需要谨慎对待。

不幸的是,在我处理这个问题时,我没有找到一个合理的替代品。现在已经发布了ASP.NET Web API,您可能需要看一下HttpClient。希望能帮到你。


Jesse,谢谢你提供的信息。设置ServicePointManager DefaultConnectionLimit仍然只是针对每个ServicePoint/endpoint进行设置,所以我不知道它是否会绕过你在SP级别描述的锁定问题。我肯定希望ServicePoint在线程/请求增长时成为瓶颈,但不会如此剧烈。而且似乎请求可以顺利发送,但响应需要等待。我将不得不考虑使用HttpClient作为替代方案。 - erasend
关于DefaultConnectionLimit - 我不会声称它会有奇迹般的效果,但它确实绕过了在ServicePoint本身的ConnectionLimit的setter中调用lock(this)的步骤。至于扩展性,我的印象也是如此。我仍然不能声称真正理解为什么瓶颈会如此严重。在获取响应时有一些同步构造,再次获取流时也是如此。在您的情况下,我会推测从本地到非本地的降级是由于与外部服务器的额外延迟...但是,这只是纯粹的猜测。 - Jesse Squire
DefaultConnectionLimit 没有改变任何东西(并且从 SP 对象中删除 ConnectionLimit 会将其恢复为默认值 2)。从本地到非本地的差异肯定与 Mono/环境相关,因为 PHP 脚本没有问题-相同的请求。这就是我试图找出根本原因的地方,在假设它是 Mono 如何处理这些响应的情况下。 - erasend
我认为这个人的回答没有考虑到问题是关于Mono的,所以进行了投反对票。 - knocte
@knocte - 当然,您有权发表自己的观点,但我不同意。讨论集中在使用一个类的公共文档接口,该类对Mono和CLR都是通用的,以解决问题。如果我“考虑到问题是关于Mono的”,我不确定您希望看到什么样子。在我看来,在这种情况下,否决似乎是不合适的。 - Jesse Squire
显示剩余4条评论

8

我知道这很旧,但我把它放在这里,以防它可能帮助那些遇到此问题的人。 我们遇到了同样的问题,即并行出站HTTPS请求。 几个问题需要解决。

首先,据我所知,ServicePointManager.DefaultConnectionLimit没有改变连接限制。将其设置为50,创建一个新连接,然后检查新连接服务点的连接限制为2。将其设置为50一次似乎有效,并且对于最终通过该服务点的所有连接都保持不变。

我们遇到的第二个问题是线程。目前的mono线程池实现似乎每秒最多创建2个新线程。如果您正在进行许多同时启动的并行请求,则这将是一段时间。为了抵消这种情况,我们尝试将ThreadPool.SetMinThreads设置为更高的数字。无论当前线程数与所需数量之间的差距如何,它似乎只会创建1个新线程。我们通过循环调用SetMinThreads直到线程池有所需数量的空闲线程来解决了这个问题。

我提出了一个关于后一个问题的错误,因为我最有信心它没有按预期工作:https://bugzilla.xamarin.com/show_bug.cgi?id=7055


可以确认在Mono中ServicePointManager.DefaultConnectionLimit没有效果,限制仍然是2。 - KCD
将其设置在 app.configweb.config 中可以正常工作... 但是在 Mono 中使用除 2 以外的任何值都会极大地降低吞吐量(?!)。此外,ServicePoint.Connections 始终报告为 0,而 Windows 报告的连接限制则略高于 ConnectionLimit... - KCD

0
如果@jake-moshenko关于在Mono中更改ServicePointManager.DefaultConnectionLimit没有任何效果的说法是正确的,请将其作为http://bugzilla.xamarin.com/中的错误进行报告。
但是,在完全放弃这个问题是Mono问题之前,我会尝试一些事情:
  1. 尝试使用 SGen 垃圾回收器,而不是旧的 Boehm 回收器,通过将 --gc=sgen 作为标志传递给 Mono。
  2. 如果上述方法无效,请升级到 Mono 3.2(默认也是 SGEN GC),因为自您提出问题以来已经进行了许多修复。
  3. 如果上述方法无效,请构建您自己的 Mono(主分支),因为最近已合并关于线程的 此重要拉取请求
  4. 如果上述方法无效,请构建您自己的 Mono,并添加 此拉取请求。如果它解决了您的问题,请在拉取请求中添加“+1”。这可能是 bug 7055 的修复。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接