MVC3 / Azure - 处理50k + /秒快速API调用的最具可扩展性的方法是什么?

时间:2013-05-22 22:46:05

标签: .net asp.net-mvc-3 api asynchronous azure-web-roles

我在Azure上运行基于MVC3的Web角色,可能需要每秒处理~50k API调用。这些调用很快,他们所做的就是向Azure存储队列中添加一些数据(我知道那里有配额,我的队列已经分区了)。 API不会将任何值/状态返回给客户端。目前每次通话大约需要100-200ms。

目前我已将其设置为AsyncController,我的代码如下。

我向大家提出的问题 - 这是处理此类负载的最具扩展性,最有效和最快捷的方法吗?

PS - 我被告知推荐TaskCreationOptions.LongRunning,是吗?

非常感谢任何帮助!

public class ApiController : AsyncController {
    public void CallAsync(string data) {
        AsyncManager.OutstandingOperations.Increment(1);

        Task.Factory.StartNew(() => {
           // add to my queue here
           AsyncManager.OutstandingOperations.Decrement();
        }, TaskCreationOptions.LongRunning);
    }
    public void CallCompleted() {}
}

3 个答案:

答案 0 :(得分:1)

  

API没有将任何值/状态返回给客户端。

这几乎总是一个错误。如果由于某种原因未保存值,您真的不希望通知客户吗?比方说,expired Azure Storage certificate?您是否确定您希望您的错误处理行为“只是删除数据并假装从未发生过呼叫”?

对于这个问题的其余部分,我将假设您实际上想要一个“即发即弃”的API调用,即使失败也会始终返回成功,因为这是您在问题中指定的内容。

  

调用很快,他们所做的就是向Azure存储队列添加一些数据。

有两种不同的方法可以优化:平均情况和最坏情况。

如果您想针对普通情况进行优化,请考虑使用阻止调用。 (哇!)

正确管理的Azure存储队列的平均延迟是微不足道的10毫秒,事务时间低至0.5毫秒。速度快,你最好只是阻止ASP.NET请求线程,而不是切换到线程池线程(或启动新线程)。此外,您的代码变得非常容易阅读:

public class ApiController : Controller {
  public void CallAsync(string data) {
    // add to my queue here
  }
}

阻塞方法的缺点是,如果队列延迟突然增加,那么ASP.NET服务器将非常快速填满被阻塞的线程,您将开始503'。

如果你想针对最坏的情况进行优化,那么你应该将排队折腾到后台线程然后(同步)返回。您可以使用BackgroundTaskManager from my blog向ASP.NET注册后台任务:

public class ApiController : Controller {
  public void CallAsync(string data) {
    BackgroundTaskManager.Run(async () => {
      // add to my queue here
    });
  }
}

这样做的好处是,您的服务器可以在内存中“缓冲”更多作为后台任务而不是线程。因此,如果队列延迟激增然后恢复,您的Web服务器将能够处理它。这种方法的缺点是请求可能都比同步版本稍慢。

  

PS - 我被告知建议使用TaskCreationOptions.LongRunning,是吗?

绝对不是你使用它的方式。您当前的代码实际上是为每个传入请求创建一个新线程 - 非常低效。 LongRunning只应在创建长期运行的任务时使用。

答案 1 :(得分:0)

继续对该问题的最后评论。不需要从AsyncController派生或使用Async / Completed约定。如果我利用async / await重写代码,它将如下所示:

    public class ApiController : Controller
    {
       public async Task Call(string data)
       {
           await SomeUpdateMethod(data).ContinueWith(returnedData =>
            {
                if (returnedData.Result)
                {
                    //Log success or return confirmation to client
                }
                else
                {
                    //Log failure or return failure message to client
                }
            });
       }

       private Task<bool> SomeUpdateMethod(string data)
       {
           var result = false;
           //Push onto queue and if succesfull set result to true
           return Task.FromResult(result);
       }
   }

希望有所帮助!

答案 2 :(得分:0)

如果我正确理解您的问题,您需要控制器启动入队操作,然后立即返回响应。如果队列操作成功或失败,则不会考虑响应。

这意味着您只需要队列操作是异步的而不是控制器本身。我没有使用Azure存储队列的经验,但是如果我得到了正确的documentation,它看起来就像是使用传统的.Net异步编程模型(APM - 基于开始/结束方法)而不是更多现代的基于事件的异步模式(EAP - 基于带有'Async'的方法)。

所以你的代码看起来像这样:

public void Call(string data)
{ 
    // Some synchronous code here

    cloudQueue.BeginAddMessage(message // your message object
        , iar => { } // callback method executed after the async operation is finished. The controller does not wait for this
        , state // an object to be transmited to callback
    );
}

BeginAddMessage使用异步I / O,这意味着它在执行时不消耗任何线程池线程。作为第二个参数接收的回调方法将在异步操作完成后立即在线程池线程上执行。如果您不需要回调,请将参数传递为null。

在上面的代码中,控制器在启动BeginAddMessage后立即返回响应。这是因为控制器不是异步的,它不会等待其中的异步操作完成。

如果你需要让控制器等待BeginAddMessage完成(如果API响应需要反映入队操作的成功或失败,则需要这样做),那么你必须通过添加使控制器异步它的async关键字并在调用BeginAddMessage时等待(你需要TaskFactory.FromAsync)。您可以在ASP .Net MVC here中阅读有关使用异步方法的更多信息。

在幕后,BeginAddMessage对Azure REST API进行HTTP调用。 .Net使用ServicePointManager.DefaultConnectionLimit限制对HTTP主机的并发调用次数,默认情况下将其设置为2.您可能希望增加该值以增加应用程序的吞吐量。但请记住,增加到非常高的值也会对性能和可靠性产生负面影响(我最近发布了一个触及这方面的article)。可能一个好的值大概在100左右,但这总是取决于你的应用程序/托管环境。

关于来自控制器的异步调用,最后要记住的是,它们可能会使您的API以误导的方式运行。您的API将能够提供非常多的大型调用/秒,但在幕后您可能会达到Azure存储队列的可伸缩性限制。对BeginAddMessage的调用在内部基于ServicePointManager.DefaultConnectionLimit进行排队,因此这将在很短的时间内很好地适应大量的调用,但是如果您的API接收的调用/秒数始终高于存储队列可以提供,然后您的应用程序将在某个时刻中断,丢失大量收到的消息(没有机会被持久存储在队列中)。