0

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

目前我将它设置为 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() {}
}
4

3 回答 3

1

API 不会将任何值/状态返回给客户端。

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

对于这个问题的其余部分,我将假设您确实想要一个“即发即弃”的 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'ing。

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

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

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

PS - 有人告诉我建议使用 TaskCreationOptions.LongRunning,对吗?

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

于 2013-05-24T16:40:41.620 回答
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);
       }
   }

希望有帮助!

于 2013-05-24T11:49:03.453 回答
0

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

这意味着您只需要异步队列操作而不需要控制器本身。我没有使用 Azure 存储队列的经验,但如果我获得了正确的文档,看起来它使用的是传统的 .Net 异步编程模型(APM - 基于 Begin/End 方法)而不是更现代的基于事件异步模式(EAP - 基于以“异步”为后缀的方法)。

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

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 中使用异步方法的更多信息。

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

关于来自控制器的异步调用,最后要记住的一件事是它们可能会使您的 API 以一种误导的方式运行。您的 API 将能够每秒交付大量的大调用,但在幕后您可能会达到 Azure 存储队列的可伸缩性限制。对 BeginAddMessage 的调用是基于 ServicePointManager.DefaultConnectionLimit 在内部排队的,因此这将非常适合在短时间内进行大量调用,但如果您的 API 接收的每秒调用数一直高于存储队列可以传递,然后您的应用程序将在某个时候简单地中断,丢失大量接收到的消息(这些消息没有机会保留在队列中)。

于 2013-05-24T15:07:34.520 回答