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



我有一个运行在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() {}
}

API不向客户端返回任何值/状态。

这几乎总是一个错误。如果由于某种原因没有保存值,您真的不希望通知您的客户端吗?比如,过期的Azure存储证书?您是否确定希望您的错误处理行为是"只是删除数据并假装调用从未发生过"?

对于这个问题的其余部分,我将假设您确实想要一个即使失败也会始终返回成功的"触发并忘记"API调用,因为这是您在问题中指定的。

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

有两种不同的优化方法:针对一般情况和最坏情况。

如果你想针对平均情况进行优化,那么考虑使用阻塞调用。(喘气)

正确管理的Azure存储队列的平均延迟为极小的10ms,事务时间低至0.5ms。有了这么快的速度,你最好屏蔽ASP。NET请求线程,而不是切换到线程池线程(或启动一个新线程)。此外,您的代码变得非常容易阅读:

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

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

如果你想针对最坏的情况进行优化,那么你应该把队列丢给后台线程,然后(同步)返回。您可以使用我的博客中的BackgroundTaskManager来注册ASP的后台任务。净:

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

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

PS -我被告知TaskCreationOptions。推荐LongRunning,对吗?

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

关于这个问题的最后一个评论。不需要从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);
       }
   }

希望有帮助!

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

这意味着您只需要异步队列操作,而不需要控制器本身。我没有Azure存储队列的经验,但如果我得到正确的文档,它看起来像是使用传统的。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)。从同步)。你可以在这里阅读更多关于在asp.net MVC中使用异步方法的内容。

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

关于来自控制器的异步调用,要记住的最后一件事是,它们可能会使您的API以误导的方式运行。你的API将能够提供非常多的大调用/秒,但在幕后,你可能会达到Azure存储队列的可伸缩性限制。调用BeginAddMessage是基于ServicePointManager的内部排队。DefaultConnectionLimit,所以这将在短时间内很好地保持大量调用,但是如果API接收的调用数/秒不断高于存储队列可以交付的调用数,那么您的应用程序将在某个点上崩溃,丢失大量接收到的消息(这些消息没有机会在队列中持久化)。

相关内容

最新更新