1

编辑 我意识到我的问题表述得不够清楚,并对其进行了大量编辑。
这是一个开放式问题,所以提前道歉。

简而言之,我想在 Azure 辅助角色中实现 IIS 样式的异步请求处理

它可能非常简单,也可能非常困难——我正在寻找指向何处研究的指针。

虽然我的实现将使用 Azure Worker 和服务总线队列,但一般原则适用于工作进程正在侦听传入请求并为其提供服务的任何场景。

IIS 的作用

在 IIS 中有一个固定大小的线程池。如果您同步处理所有请求,那么您可以并行处理的最大请求数 == maxthreads。但是,如果您必须通过缓慢的外部 I/O 来处理请求,那么这是非常低效的,因为您最终可能会导致服务器处于空闲状态,而所有线程都处于等待外部 I/O 完成的状态。

来自MSDN

在 Web 服务器上,.NET Framework 维护一个线程池,用于为 ASP.NET 请求提供服务。当请求到达时,池中的一个线程被分派来处理该请求。如果请求是同步处理的,则处理请求的线程在处理请求时会被阻塞,并且该线程无法为另一个请求提供服务。

这可能不是问题,因为线程池可以足够大以容纳许多阻塞的线程。但是,线程池中的线程数是有限的。在处理多个同时长时间运行的请求的大型应用程序中,所有可用线程都可能被阻塞。这种情况称为线程饥饿。达到此条件时,Web 服务器将请求排队。如果请求队列已满,Web 服务器将拒绝 HTTP 503 状态(服务器太忙)的请求。

为了克服这个问题,IIS 有一些巧妙的逻辑允许您异步处理请求:

调用异步操作时,会发生以下步骤:

  1. Web 服务器从线程池中获取一个线程(工作线程)并调度它来处理传入的请求。此工作线程启动异步操作。

  2. 工作线程返回到线程池以服务另一个 Web 请求。

  3. 当异步操作完成时,它会通知 ASP.NET。

  4. Web 服务器从线程池(可能与启动异步操作的线程不同的线程)中获取一个工作线程来处理请求的其余部分,包括呈现响应。

这里的重点是当异步请求返回时,返回操作被安排在为初始传入请求提供服务的同一线程池之一上运行。这意味着系统限制了它同时执行的工作量,这就是我想要复制的内容。

我想做的事

我想创建一个 Worker 角色,它将在 Azure 服务总线队列上侦听传入的工作请求,也可能在 TCP 套接字上侦听。像 IIS 一样,我想拥有一个最大的线程池大小,并且我想限制工作人员并行执行的实际工作量;如果工作人员忙于服务现有请求 - 无论是新传入请求还是来自先前异步调用的回调 - 在某些线程被释放之前,我不想接收任何新传入请求。

限制我同时开始的工作数量不是问题——这很容易控制;它限制了我实际同时工作的数量。

让我们假设一个有 100 个线程的线程池。

  • 我收到 100 个发送电子邮件的请求,每封电子邮件需要 5 秒才能发送到 SMTP 服务器。如果我将我的服务器限制为同时只处理 100 个请求,那么我的服务器将在 5 秒内无法执行任何其他操作,而 CPU 则完全空闲。所以,我并不介意同时发送 1,000 或 10,000 封电子邮件,因为 99% 的“请求处理时间”将用于等待外部 I/O,而我的服务器仍然非常安静。因此,我可以通过继续无限制地接受传入请求来处理这种特定情况(或者只限制请求的开始,直到我触发异步调用;一旦调用 BeginSend,我就会返回并开始服务另一个请求)。

  • 现在,想象一下,我有一种类型的请求,它去数据库读取一些数据,对其进行一些繁重的计算,然后将其写回数据库。那里有两个数据库请求应该是异步的,但 90% 的请求处理时间将花在我的工作人员身上。因此,如果我遵循与上述相同的逻辑并保持启动异步调用并让返回做任何需要让线程继续运行的操作,那么我最终会得到一个非常过载的服务器。

不知何故,IIS 所做的是确保当异步调用返回时,它使用相同的固定大小的线程池。这意味着如果我触发大量异步调用,然后它们返回并开始使用我的线程,IIS 将不会接受新请求,直到这些返回完成。这是完美的,因为它确保了服务器上的合理负载,特别是当我有多个负载平衡的服务器和服务器从中挑选工作的队列系统时。

我有这种偷偷摸摸的怀疑,这可能很简单,我只是缺少一些基本的东西。或者,这可能非常困难。

4

6 回答 6

1

创建线程池应被视为独立于 Windows Azure。由于 Worker Role 实例实际上是 Windows 2008 Server R2(或 SP2),因此并没有什么不同。您只需要从您的OnStart()Run().

您想做的一件事是在扩展到更多/更少的工作实例时使用队列长度作为决定因素。请注意,服务总线队列不会公布队列长度,而 Windows Azure 队列(基于存储,而不是服务总线)会这样做。使用 Windows Azure 队列,您需要同步轮询消息(而服务总线队列具有长轮询操作)。查看服务总线队列和 Windows Azure 队列之间的差异可能是一个好主意,请点击此处

于 2012-05-30T00:14:43.243 回答
1

您是否考虑过使用专用的 WCF 实例(不是托管 WAS 或 IIS)来缓冲长时间运行的请求?它将有自己的专用应用程序池,具有来自 IIS 的单独的最大值设置,不会与您的 ASP.NET HTTP 请求竞争。(HTTP 请求由

然后使用 IIS 异步方法通过受约束的应用程序池调用 WCF。

于 2012-05-30T21:27:03.377 回答
1

我过去曾将SmartThreadPool项目用作每个实例的池,如果我没看错的话,它应该具有您需要的所有回调和工人限制功能。实际上,我的公司目前在 Azure 上运行它,用于您描述的异步读取消息总线请求的确切目的。

于 2012-05-30T23:34:21.477 回答
1

我一直在这方面进行挖掘,发现它确实相对容易。 http://www.albahari.com/threading/得到了一些很好的信息,实际上我最终购买了该网站本质上正在推广的书。

我发现的是;

  • 默认情况下,您的应用程序有一个可用的 ThreadPool
  • 您可以限制 ThreadPool 中可用的线程数
  • 当您使用QueueUserWorkItemTask.Factory.StartNew开始在 ThreadPool 中的线程上运行作业时
  • 当您在框架中使用其中一个异步 IO 调用(Begin...方法WebcClient.DownloadStringAsync等)时,回调也将在来自 ThreadPool 的线程上运行(IO 请求本身发生的事情超出了本讨论的范围)。

到现在为止还挺好。问题是我可以随心所欲地继续调用Task.Factory.StartNew,而 ThreadPool 将简单地将工作排队,直到有空闲线程为它们提供服务。因此,对于 Azure Worker,即使我的 Worker 正忙于服务现有请求(以及来自现有请求的回调),我也可以轻松清空队列。这是我问题的核心。我想要的是在我真正有一些空闲线程来服务请求之前不要从队列中取出任何东西。

这是一个非常简单的例子,说明了如何实现这一点。本质上,我使用 anAutoResetEvent来确保在前一个任务实际开始之前我不会从队列中启动另一个任务。诚然,我确实在有空闲线程之前从队列中取出了东西,但总的来说,这应该避免工作人员的疯狂过载,并允许我启动更多工作人员来分担负载。

ThreadPool.SetMaxThreads(5, 1000); // Limit to 5 concurrent threads
ThreadPool.SetMinThreads(5, 10); // Ensure we spin up all threads

var jobStart = new AutoResetEvent(true);

// The "listen" loop
while (true) 
{   
    var job = this.jobQueue.Dequeue();
    jobStart.WaitOne(); // Wait until the previous job has actually been started
    Task.Factory.StartNew(
        () =>
            {
                jobStart.Set(); // Will happen when the threadpool allocates this job to a thread
                this.Download(job);
            });

}

这可以 - 并且可能应该 - 变得更加复杂,包括超时,如果在合理的时间内无法分配线程,则将工作项放回队列中等等。另一种方法是ThreadPool.GetAvailableThreads在开始侦听队列之前检查是否有空闲线程,但这感觉更容易出错。

于 2012-06-04T14:21:33.687 回答
0

据我了解,您希望限制用于同时处理某种类型消息的线程数。

一种方法是简单地包装消息处理器,在新线程上调用类似

try
{
   Interlocked.Increment(ref count)

   Process(message);
}
finally 
{
    Interlocked.Decrement(ref count)
}

在调用包装器之前,只需检查“计数”是否小于您的阈值计数;并停止轮询/处理更多消息,直到计数足够低。

编辑根据评论添加了更多信息

Frans,不知道为什么你会看到基础设施和业务代码是耦合的。一旦您将要作为任务服务的业务流程放在新线程上以异步运行,您就不必担心异步执行额外的 IO 绑定调用。这是一个更简单的编程模型。

这就是我的想法。

// semi - pseudo-code

// Infrastructure – reads messages from the queue 
//    (independent thread, could be a triggered by a timer)
while(count < maxCount && (message = Queue.GetMessage()) != null)
{
    Interlocked.Increment(ref count);

  // process message asynchronously on a new thread
  Task.Factory.StartNew(() => ProcessWrapper(message));     
}

// glue / semi-infrastructure - deals with message deletion and exceptions 
void ProcessWrapper(Message message)
{
   try
   {
      Process(message);
      Queue.DeleteMessage(message);
   }
   catch(Exception ex)
   {
      // Handle exception here.
      // Log, write to poison message queue etc ...
   }
   finally 
   {
      Interlocked.Decrement(ref count)
   }
}

// business process
void Process(Message message)
{
  // actual work done here
  ;
}
于 2012-05-30T00:41:25.093 回答
0

不知何故,IIS 所做的是确保当异步调用返回时,它使用相同的固定大小的线程池。

这不是真的:当您的代码响应 HTTP 请求而运行时,决定继续函数执行哪些线程。通常,这是线程池。并且线程池是一个应用域范围的资源,在所有请求之间共享。

我认为 IIS 没有你想象的那么“神奇”。它所做的只是限制并行 HTTP 请求的数量和积压的大小。一旦获得 ASP.NET 的控制权,您就可以决定发生什么。

如果您的代码没有防止服务器超载,即使在 IIS 上,您也会使服务器超载

于 2012-05-30T17:46:51.523 回答