65.9K
CodeProject 正在变化。 阅读更多。
Home

调整 WCF 以构建高度可伸缩的异步 REST API

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (34投票s)

2011年7月31日

CPOL

25分钟阅读

viewsIcon

122397

downloadIcon

1162

您可以使用 WCF 构建异步 REST API,但由于 WCF 实现中的一些错误,它无法像您期望的那样扩展。这是我与微软 WCF 团队一起探索问题并找到正确修复方法的历程。

image004.jpg

下载 WcfAsyncRestApi.zip - 30.18 KB  

介绍 

早上 9 点,在您业务流量高峰期,您接到紧急电话,称您构建的网站已不可用。它不响应任何请求。有些人等待很长时间后可以看到一些页面,但大多数人看不到。所以,您认为这一定是某个慢查询或数据库可能需要一些调整。您进行常规检查,例如查看数据库服务器上的 CPU 和磁盘。您发现那里没有任何问题。然后您怀疑一定是网络服务器运行缓慢。所以,您检查网络服务器上的 CPU 和磁盘。您也没有发现任何问题。网络服务器和数据库服务器的 CPU 和磁盘使用率都非常低。然后您怀疑一定是网络。所以,您尝试从网络服务器到数据库服务器之间以及反向复制一个大文件。不,文件复制得很好,网络没有问题。您还快速检查了所有服务器上的 RAM 使用情况,但发现 RAM 使用情况非常好。作为最后一道防线,您对负载均衡器、防火墙和交换机进行了一些诊断,但发现一切都处于良好状态。但您的网站已宕机。查看网络服务器上的性能计数器,您看到大量请求排队,并且请求执行时间非常高,请求等待时间也很高。

image001.jpg

所以,您进行 IIS 重启。您的网站在线了几分钟,然后又宕机了。经过多次重启后,您意识到这不是基础设施问题。您的代码中存在一些可伸缩性问题。您阅读的关于可伸缩性的所有好东西,并认为它们是童话故事,永远不会发生在您身上,现在正发生在你眼前。您意识到您应该使您的服务异步化。

这里有什么问题?

请求执行时间很高。这意味着请求完成的时间非常长。可能是您正在使用的外部服务太慢或已宕机。结果,线程没有以与传入请求相同的速率释放。所以,请求正在排队(因此应用程序队列中的请求 > 0),并且直到线程池中的某个线程被释放后才会被执行。

在 N 层应用程序中,您可以拥有多层服务。您可以拥有从外部服务消费数据的服务。这在从各种互联网服务收集数据的 Web 2.0 应用程序以及在企业应用程序中很常见,在企业应用程序中,您是为自己的应用程序提供一些服务的组织的一部分,但这些服务需要消费组织其他部分公开的其他服务。例如,您有一个使用 WCF 构建的服务层,与一个可能或可能不是 WCF 服务层的外部服务层通信。

ntier_arch.png

在这种架构中,如果您有大量请求,例如每台服务器每秒大约 100 个请求,那么您需要考虑使用异步服务以获得更好的性能。如果外部服务位于 WAN 或互联网上或执行时间很长,那么使用异步服务不再是实现可伸缩性的可选方案,因为长时间运行的服务将耗尽 .NET 线程池中的线程。从异步 WCF 服务调用另一个异步 WCF 服务很容易,您可以轻松地将它们链接起来。但是,如果外部服务不是 WCF 并且只支持 HTTP(例如 REST),那么它就会变得异常复杂。让我向您展示如何将多个异步 WCF 服务链接起来,以及如何将普通的 HTTP 服务与异步 WCF 服务链接起来。

什么是异步服务?

异步服务在执行 IO 操作时不会占用线程。您可以启动一个外部异步 IO 操作,例如调用外部 Web 服务,或访问某个 HTTP URL,或异步读取某个文件。在 IO 操作进行时,执行 WCF 请求的线程被释放,以便它可以服务其他请求。因此,即使外部操作需要很长时间才能返回结果,您也不会占用 .NET 线程池中的线程。这可以防止线程拥塞并提高应用程序的可伸缩性。您可以从这篇 MSDN 文章中阅读有关如何使服务异步的详细信息。简而言之,同步服务是我们通常做的,我们只是调用一个服务并等待它返回结果。

image003.jpg

在这里,您的客户端应用程序正在调用您的服务,该服务又调用外部服务以获取数据。在外部服务响应之前,服务器一直占用一个线程。当来自 StockService 的结果到达时,服务器将结果返回给客户端并释放线程。

但在异步服务模型中,服务器不等待外部服务返回结果。它在向 StockService 发送请求后立即释放线程。

image004.jpg

正如您从图中看到的,一旦服务器在 StockService 上调用 BeginGetData,它就会释放服务器生命线上由垂直框表示的线程。但客户端尚未收到任何响应。客户端仍在等待。WCF 将保留请求并且尚未返回任何响应。当 StockService 完成执行时,服务器从 ThreadPool 中选择一个空闲线程,然后执行服务器中的其余代码并将响应返回给客户端。

这就是异步服务的工作方式。

给我看服务

让我们看一个典型的异步 WCF 服务。假设您有这个服务

public interface IService
{
[OperationContractAttribute] string GetStock(string code);
} 

如果你想让它异步,你创建一个 Begin 和 End 对。

public interface IService
{
    [OperationContractAttribute(AsyncPattern = true)]
    IAsyncResult BeginGetStock(string code, AsyncCallback callback, object asyncState);
    //Note: There is no OperationContractAttribute for the end method.
    string EndGetStock(IAsyncResult result);
}  

现在,如果您在 .NET 2.0 中使用旧的 ASMX 服务做过异步服务,您就会知道可以用几行代码完成。

public class Service : IService
{
 public IAsyncResult BeginGetStock(string code, AsyncCallback callback, object asyncState)
 {
  ExternalStockServiceClient client = new ExternalStockServiceClient();
  var myCustomState = new MyCustomState { Client = client, WcfState = asyncState };
                return client.BeginGetStockResult(code, callback, myCustomState);
 }
 public string EndGetStock(IAsyncResult result)
 {
  MyCustomState state = result.AsyncState as MyCustomState;
  using (state.Client)
   return state.Client.EndGetStockResult(result);
 }
} 

但令人惊讶的是,这在 WCF 中不起作用。当您调用 EndGetStock 时,客户端将收到此异常

An error occurred while receiving the HTTP response to https://:8080/Service.svc. This could be due to the service endpoint binding not using the HTTP protocol. This could
also be due to an HTTP request context being aborted by the server (possibly due to the service shutting down). See server logs for more details. 

InnerException 也完全没用。

The underlying connection was closed: An unexpected error occurred on a receive. 

您会发现,尽管您调用了它,但服务上的 EndGetStock 方法从未被调用。

它在旧的 ASMX 服务中完美运行。我在创建异步 Web 服务时多次这样做。例如,我使用 ASMX 构建了一个 AJAX 代理服务,以克服浏览器中 JavaScript 的跨域 HTTP 调用限制。您可以从这里阅读并获取代码。在 WCF 中有效的解决方案非常难以记住,而且令人难以置信地费解。

WCF 异步服务异常复杂

在 WCF 中,传统的异步模式不起作用,因为如果您仔细研究代码,您会发现代码完全忽略了传递给 BeginGetStock 方法的 asyncState

public IAsyncResult BeginGetStock(string code, AsyncCallback callback, object asyncState)
{
        ExternalStockServiceClient client = new ExternalStockServiceClient();
        var myCustomState = new MyCustomState { Client = client, WcfState = asyncState };
        return client.BeginGetStockResult(code, callback, myCustomState);
} 

旧 ASMX 和新 WCF 的区别在于 ASMX 中的 asyncState 为 null,但在 WCF 中不是。asyncState 中有一个非常有用的对象

image005.jpg

正如您所看到的,MessageRpc 存在,它负责管理运行请求的线程。因此,将它与其他异步调用一起携带绝对重要,以便最终负责运行请求的线程的 MessageRpc 有机会调用 EndGetStock

以下是 WCF 异步模式中可行的代码

public IAsyncResult BeginGetStock(string code, AsyncCallback wcfCallback, object wcfState)
{
        ExternalStockServiceClient client = new ExternalStockServiceClient();
        var myCustomState = new MyCustomState
        {
                Client = client,
                WcfState = wcfState,
                WcfCallback = wcfCallback
        };
        var externalServiceCallback = new AsyncCallback(CallbackFromExternalStockClient);
        var externalServiceResult = client.BeginGetStockResult(code, externalServiceCallback, myCustomState);
        return new MyAsyncResult(externalServiceResult, myCustomState);
}
private void CallbackFromExternalStockClient(IAsyncResult externalServiceResult)
{
        var myState = externalServiceResult.AsyncState as MyCustomState;
        myState.WcfCallback(new MyAsyncResult(externalServiceResult, myState));
}
public string EndGetStock(IAsyncResult result)
{
        var myAsyncResult = result as MyAsyncResult;
        var myState = myAsyncResult.CustomState;
        using (myState.Client)
                return myState.Client.EndGetStockResult(myAsyncResult.ExternalAsyncResult);            
}

从代码中很难理解谁在调用谁以及回调如何执行。这是一个序列图,展示了它的工作原理

image006.jpg

这是正在发生的事情的演练

  • 客户端(可以是 WCF 代理)调用 WCF 服务。
  • WCF 运行时(可能由 IIS 托管)接收调用,然后从线程池中获取一个线程并调用其上的 BeginGetStock,传递回调和 WCF 状态。
  • 服务中的 BeginGetStock 方法(我们自己的代码)对 StockService(另一个 WCF 服务)进行异步调用。但是,在这里它不传递 WCF 回调和状态。在这里它需要传递一个新的回调,该回调将在服务上触发,以及一个包含服务完成其 EndGetStock 调用所需内容的新的状态。我们需要的强制内容是 StockService 的实例,因为在服务中的 EndGetStock 内部,我们需要调用 StockService.EndGetStockResult
  • 一旦在 StockService 上调用了 BeginGetStockResult,它就开始了一些异步操作,并立即返回一个 IAsyncResult
  • 服务上的 BeginGetStock 接收 IAsyncResult,将其包装在 MyAsycResult 实例中并将其返回给 WCF 运行时。
  • 一段时间后,当 StockService 开始的异步操作完成时,它直接在服务上触发回调。
  • 然后,回调触发 WCF 回调,WCF 回调又触发服务上的 EndGetStock
  • 然后 EndGetStock 从 MyAsyncResult 获取 StockService 的实例,然后调用其上的 EndGetStockResult 以获取响应。然后它将响应返回给 WCF 运行时。
  • WCF 运行时然后将响应返回给客户端。

这很难理解,而且很难正确地为数百个方法完成它。如果您有一个复杂的服务层,它调用许多其他服务并对它们进行数百次操作,那么请想象一下为每个操作编写如此多的代码。

我提出了一种使用委托和某种“代码反转”模式来减轻痛苦的方法。当您遵循这种模式时,它看起来是这样的

public IAsyncResult BeginGetStock(string code, AsyncCallback wcfCallback, object wcfState)
{
   return WcfAsyncHelper.BeginAsync<ExternalStockServiceClient>(
                new ExternalStockServiceClient(), 
                wcfCallback, wcfState,
                (service, callback, state) => service.BeginGetStockResult(code, callback, state));
}
public string EndGetStock(IAsyncResult result)
{
        return WcfAsyncHelper.EndAsync<ExternalStockServiceClient, string>(result,
                (service, serviceResult) => service.EndGetStockResult(serviceResult),
                (exception, service) => { throw exception; },
                (service) => (service as IDisposable).Dispose());
} 

在这种方法中,创建 CustomAsyncResultCustomState 等的所有管道工作,创建回调函数以从外部服务接收回调,然后触发 EndXXX,以及在 EndXXX 中再次处理 AsyncResults 和状态,都被完全移入通用助手。我相信,现在您必须为要进行的每个操作编写的代码量是相当合理的。

让我们看看 WcfAsyncHelper 类是什么样的

public static class WcfAsyncHelper 
{
        public static bool IsSync<TState>(IAsyncResult result)
        {
                return result is CompletedAsyncResult<TState>;
        }
        public static CustomAsyncResult<TState>BeginAsync<TState>(
                TState state,
                AsyncCallback wcfCallback, object wcfState,
                Func<TState, AsyncCallback, object, IAsyncResult> beginCall)
        {
                var customState = new CustomState<TState>(wcfCallback, wcfState, state);
                var externalServiceCallback = new AsyncCallback(CallbackFromExternalService<TState>);
                var externalServiceResult = beginCall(state, externalServiceCallback, customState);
                return new CustomAsyncResult<TState>(externalServiceResult, customState);
        }

        public static CompletedAsyncResult<TState> BeginSync<TState>(
                TState state,
                AsyncCallback wcfCallback, object wcfState)            
        {
                var completedResult = new CompletedAsyncResult<TState>(state, wcfState);
                wcfCallback(completedResult);
                return completedResult;
        }
        private static void CallbackFromExternalService<TState>(IAsyncResult serviceResult)
        {
                var serviceState = serviceResult.AsyncState as CustomState<TState>;
                serviceState.WcfCallback( new CustomAsyncResult<TState>(serviceResult,
serviceState));
        }

        public static TResult EndAsync<TState,
TResult>(IAsyncResult result,
                Func<TState, IAsyncResult, TResult> endCall,
                Action<Exception, TState> onException,
                Action<TState> dispose)
        {
                var myAsyncResult = result as CustomAsyncResult<TState>;
                var myState = myAsyncResult.CustomState;
                try
                {
                        return endCall(myState.State, myAsyncResult.ExternalAsyncResult);
                }
                catch (Exception x)
                {
                        onException(x, myState.State);
                        return default(TResult);
                }
                finally
                {
                        try
                        {
                                dispose(myState.State);
                        }
                        finally
                        {
                        }
                }
        }
        public static TResult EndSync<TState,
TResult>(IAsyncResult result,
                Func<TState, IAsyncResult, TResult> endCall,
                Action<Exception, TState> onException,
                Action<TState> dispose)
        {
                var myAsyncResult = result as
CompletedAsyncResult<TState>;
                var myState = myAsyncResult.Data;
                try
                {
                        return endCall(myState, myAsyncResult);
                }
                catch (Exception x)
                {
                        onException(x, myState);
                        return default(TResult);
                }
                finally
                {
                        try
                        {
                                dispose(myState);
                        }
                        finally
                        {
                        }
                }
        }
}

如您所见,BeginAsync 和 EndAsync 接管了处理 CustomStateCustomAsyncResult 的管道工作。我制作了一个类似的 BeginSyncEndSync,如果您的 BeginXXX 调用需要立即返回响应而无需进行异步调用,则可以使用它们。例如,您可能已将数据缓存,并且不想再次进行外部服务调用来获取数据,而是立即返回它。在这种情况下,BeginSyncEndSync 对会有所帮助。

使用异步模式在 WCF 中制作的异步 AJAX 代理

如果您想直接从 JavaScript 获取外部域的数据,则需要一个 AJAX 代理。JavaScript 调用 AJAX 代理,传递最终需要访问的 URL,然后代理从请求的 URL 传输数据。这样您就可以克服浏览器中的跨浏览器 XmlHttp 调用限制。这在大多数 Web 2.0 启动页面中都需要,例如 My Yahoo、iGoogle、我之前的创业公司 — Pageflakes 等等。您可以从我的开源 Web 2.0 启动页面项目 Dropthings 中看到 AJAX 代理的使用。

proxy.png

这是一个 WCF AJAX 代理的代码,它可以从外部 URL 获取数据并返回。它还支持缓存数据。因此,一旦它从外部域获取数据,它就会缓存它。代理是完全流式的。这意味着代理不会首先下载整个响应,然后缓存它,然后返回它。这样,如果可以直接从浏览器访问外部 URL,延迟将几乎是直接访问外部 URL 的两倍。我在这里的代码将直接从外部 URL 流式传输响应,并且它还将即时缓存响应。

首先是 BeginGetUrl 函数,它接收调用并在请求的 URL 上打开 HttpWebRequest。它调用 HttpWebRequest 上的 BeginGetResponse 并返回。

[ServiceBehavior(InstanceContextMode=InstanceContextMode.PerCall, ConcurrencyMode=ConcurrencyMode.Multiple), AspNetCompatibilityRequirements(RequirementsMode= AspNetCompatibilityRequirementsMode.Allowed), ServiceContract]
public partial class AsyncService
{
  [OperationContract(AsyncPattern=true)]
  //[WebGet(BodyStyle=WebMessageBodyStyle.Bare, UriTemplate="/Url?uri={url}&cacheDuration={cacheDuration}")] 
  [WebGet(BodyStyle=WebMessageBodyStyle.Bare)]
  public IAsyncResult BeginGetUrl(string url, int cacheDuration, AsyncCallback wcfCallback, object wcfState)
  {
      /// If the url already exists in cache then there's no need to fetch it from the source.
      /// We can just return the response immediately from cache
      if (IsInCache(url))
      {
        return WcfAsyncHelper.BeginSync<WebRequestState>(new WebRequestState
            {
                Url = url,
                CacheDuration = cacheDuration,
                ContentType = WebOperationContext.Current.IncomingRequest.ContentType,
            },
            wcfCallback, wcfState);
      }
      else
      {
        /// The content does not exist in cache and we need to get it from the
        /// original source                 
        HttpWebRequest request = WebRequest.Create(url) as HttpWebRequest;
        request.Method = "GET";
        //request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
        return WcfAsyncHelper.BeginAsync<WebRequestState>(
          new WebRequestState
          {
              Request = request,
              ContentType = WebOperationContext.Current.IncomingRequest.ContentType,
              Url = url,
              CacheDuration = cacheDuration,
          },
          wcfCallback, wcfState,
          (myState, externalServiceCallback, customState) => 
              myState.Request.BeginGetResponse(externalServiceCallback, customState));
      }
  } 

首先,它检查请求的 URL 是否已在缓存中。如果是,则它将从缓存中同步返回响应。这就是 BeginSyncEndSync 派上用场的地方。如果数据不在缓存中,它将向请求的 URL 发出 HTTP 请求。

当响应到达时,EndGetUrl 被触发。

public Stream EndGetUrl(IAsyncResult asyncResult)
{
  if (WcfAsyncHelper.IsSync<WebRequestState>(asyncResult)) 
  {
    return WcfAsyncHelper.EndSync<WebRequestState, Stream>(
      asyncResult, (myState, completedResult) =>
      {
          CacheEntry cacheEntry = GetFromCache(myState.Url);
          var outResponse = WebOperationContext.Current.OutgoingResponse;
          SetResponseHeaders(cacheEntry.ContentLength, cacheEntry.ContentType, 
              cacheEntry.ContentEncoding, 
              myState, outResponse);               
          return new MemoryStream(cacheEntry.Content);
      },
      (exception, myState) => { throw new ProtocolException(exception.Message, exception); },
      (myState) => { /*Nothing to dispose*/ });
  }
  else
  {
    return WcfAsyncHelper.EndAsync<WebRequestState, Stream>(asyncResult,
        (myState, serviceResult) => 
      {
        var httpResponse = myState.Request.EndGetResponse(serviceResult) as HttpWebResponse;
        var outResponse = WebOperationContext.Current.OutgoingResponse;
        SetResponseHeaders(httpResponse.ContentLength,
            httpResponse.ContentType, httpResponse.ContentEncoding,
            myState, outResponse);
        // If response needs to be cached, then wrap the stream so that
        // when the stream is being read, the content is stored in a memory
        // stream and when the read is complete, the memory stream is stored
        // in cache.
        if (myState.CacheDuration > 0)
          return new StreamWrapper(httpResponse.GetResponseStream(),
            (int)(outResponse.ContentLength > 0 ? outResponse.ContentLength : 8 * 1024),
            buffer =>
            {
              StoreInCache(myState.Url, myState.CacheDuration, new CacheEntry
              {
                Content = buffer,
                ContentLength = buffer.Length,
                ContentEncoding = httpResponse.ContentEncoding,
                ContentType = httpResponse.ContentType
              });
            });
        else
          return httpResponse.GetResponseStream();
      },
      (exception, myState) => { throw new ProtocolException(exception.Message, exception); },
      (myState) => { /*Nothing to dispose*/ });
  }
}  

在这里,它检查请求是否同步完成,因为 URL 已经缓存。如果同步完成,它将直接从缓存返回响应。否则,它会读取 HttpWebResponse 的数据并将数据作为流返回。

我创建了一个 StreamWrapper,它不仅从原始流返回数据,还将缓冲区存储在 MemoryStream 中,以便在从原始流读取数据时可以缓存数据。因此,它不会在首先下载整个数据然后缓存它,然后才将其发送回客户端时增加任何延迟。返回响应和缓存响应都在一次传递中发生。

public class StreamWrapper : Stream, IDisposable
{
	private Stream WrappedStream;
	private Action<byte[]> OnCompleteRead;
	private MemoryStream InternalBuffer;
	public StreamWrapper(Stream stream, int internalBufferCapacity, Action<byte[]> onCompleteRead)
	{
		this.WrappedStream = stream;
		this.OnCompleteRead = onCompleteRead;
		this.InternalBuffer = new MemoryStream(internalBufferCapacity);
	}
.
.
.
	public override int Read(byte[] buffer, int offset, int count)
	{
		int bytesRead = this.WrappedStream.Read(buffer, offset, count);
		if (bytesRead > 0)
			this.InternalBuffer.Write(buffer, offset, bytesRead);
		else
			this.OnCompleteRead(this.InternalBuffer.ToArray());
		return bytesRead;
	}
	public new void Dispose()
	{
		this.WrappedStream.Dispose();
	}
}

是时候进行负载测试并证明它确实具有可伸缩性了。

它根本不扩展! 

我进行了一项负载测试,以比较异步实现相对于同步实现的改进。同步实现很简单,它只是使用 WebClient 从给定 URL 获取数据。我制作了一个控制台应用程序,它将启动 50 个线程,并使用 25 个线程访问服务,使用另外 25 个线程访问一些 ASPX 页面,以确保在服务调用进行时 ASP.NET 网站正常运行。该服务托管在另一台服务器上,并且故意使其响应非常慢,需要 6 秒才能完成请求。负载测试是使用三台 HP ProLiant BL460c G1 Blade 服务器(64 位硬件,64 位 Windows 2008 Enterprise Edition)进行的。

load_test_server_config.png

客户端代码启动 50 个并行线程,然后首先访问服务,以便在运行长时间运行的服务时线程耗尽。在服务请求执行期间,它启动另一组线程来访问一些 ASPX 页面。如果页面及时响应,那么我们已经克服了线程问题,并且 WCF 服务确实是异步的。如果不是,那么我们还没有解决问题。

static TimeSpan HitService(string url, 
  string responsivenessUrl, 
  int threadCount, 
  TimeSpan[] serviceResponseTimes, 
  TimeSpan[] aspnetResponseTimes, 
  out int slowASPNETResponseCount,
  string logPrefix)
{            
  Thread[] threads = new Thread[threadCount];
  var serviceResponseTimesCount = 0;
  var aspnetResponseTimesCount = 0;
  var slowCount = 0;
  var startTime = DateTime.Now;
  var serviceThreadOrderNo = 0;
  var aspnetThreadOrderNo = 0;
  for (int i = 0; i < threadCount/2; i++)
  {
    Thread aThread = new Thread(new ThreadStart(() => {
            using (WebClient client = new WebClient())
            {
                try
                {
                    var start = DateTime.Now;
                    Console.WriteLine("{0}\t{1}\t{2} Service call Start", logPrefix, serviceThreadOrderNo,
                        Thread.CurrentThread.ManagedThreadId);
                    var content = client.DownloadString(url);
                    var duration = DateTime.Now - start;
                    lock (serviceResponseTimes)
                    {
                        serviceResponseTimes[serviceResponseTimesCount++] = duration;
                        Console.WriteLine("{0}\t{1}\t{2} End Service call. Duration: {2}", 
                            logPrefix, serviceThreadOrderNo,
                            Thread.CurrentThread.ManagedThreadId, duration.TotalSeconds);
                        serviceThreadOrderNo++;
                    }
                }
                catch (Exception x)
                {
                    Console.WriteLine(x);
                }
            }
        }));
    aThread.Start();
    threads[i] = aThread;
  }
  // Give chance to start the calls
  Thread.Sleep(500);
  for (int i = threadCount / 2; i < threadCount; i ++)
  {
    Thread aThread = new Thread(new ThreadStart(() => {
            using (WebClient client = new WebClient())
            {
                try
                {
                    var start = DateTime.Now; 
                    Console.WriteLine("{0}\t{1}\t{2} ASP.NET Page Start", logPrefix, aspnetThreadOrderNo,
                        Thread.CurrentThread.ManagedThreadId);
                    var content = client.DownloadString(responsivenessUrl);
                    var duration = DateTime.Now - start;
                    lock (aspnetResponseTimes)
                    {
                        aspnetResponseTimes[aspnetResponseTimesCount++] = duration;
                        Console.WriteLine("{0}\t{1}\t{2} End of ASP.NET Call. Duration: {3}", 
                            logPrefix, aspnetThreadOrderNo,
                            Thread.CurrentThread.ManagedThreadId, duration.TotalSeconds);
                        aspnetThreadOrderNo++;
                    }
                    if (serviceResponseTimesCount > 0)
                    {
                        Console.WriteLine("{0} WARNING! ASP.NET requests running slower than service.", logPrefix);
                        slowCount++;
                    }
                }
                catch (Exception x)
                {
                    Console.WriteLine(x);
                }
            }
        }));
    aThread.Start();
    threads[i] = aThread;
  }
  // wait for all threads to finish execution
  foreach (Thread thread in threads)
      thread.Join();
  var endTime = DateTime.Now;
  var totalDuration = endTime - startTime;
  Console.WriteLine(totalDuration.TotalSeconds);
  slowASPNETResponseCount = slowCount;
  return totalDuration;
} 

当我为同步服务执行此操作时,它演示了预期的行为,ASP.NET 页面的执行时间比服务调用长,因为 ASP.NET 请求没有机会运行。

[SYNC]  15     116 End of ASP.NET Call. Duration: 10.5145348 
[SYNC] WARNING! ASP.NET requests running slower than service.
[SYNC]  16     115 End of ASP.NET Call. Duration: 10.530135
[SYNC] WARNING! ASP.NET requests running slower than service.
[SYNC]  17     114 End of ASP.NET Call. Duration: 10.5457352
[SYNC] WARNING! ASP.NET requests running slower than service.
[SYNC]  12     142 End Service call. Duration: 142
[SYNC]  18     112 End of ASP.NET Call. Duration: 10.608136
[SYNC] WARNING! ASP.NET requests running slower than service.
[SYNC]  19     113 End of ASP.NET Call. Duration: 10.608136
[SYNC] WARNING! ASP.NET requests running slower than service.
[SYNC]  20     111 End of ASP.NET Call. Duration: 11.0293414
[SYNC] WARNING! ASP.NET requests running slower than service.
[SYNC]  21     109 End of ASP.NET Call. Duration: 11.0605418

当我运行异步服务的测试时,令人惊讶的是,结果是一样的

[ASYNC] 13      134 End of ASP.NET Call. Duration: 12.0745548
[ASYNC] WARNING! ASP.NET requests running slower than service.
[ASYNC] 14      135 End of ASP.NET Call. Duration: 12.090155
[ASYNC] WARNING! ASP.NET requests running slower than service.
[ASYNC] 15      136 End of ASP.NET Call. Duration: 12.1057552
[ASYNC] WARNING! ASP.NET requests running slower than service.
[ASYNC] 14      111 End Service call. Duration: 111 
[ASYNC] 15      110 End Service call. Duration: 110
[ASYNC] 16      137 End of ASP.NET Call. Duration: 12.5737612
[ASYNC] WARNING! ASP.NET requests running slower than service.
[ASYNC] 17      138 End of ASP.NET Call. Duration: 12.5893614
[ASYNC] WARNING! ASP.NET requests running slower than service.
[ASYNC] 18      139 End of ASP.NET Call. Duration: 12.6049616

统计数据显示,我们有相同数量的慢速 ASP.NET 页面执行,并且同步和异步版本之间的性能和可伸缩性确实没有明显差异。

<a name="OLE_LINK1">Regular service slow responses: 25</a> 
Async service slow responses: 25
Regular service average response time: 19.39416864
Async service average response time: 18.5408377
Async service is 4.39993564993564% faster.
Regular ASP.NET average response time: 10.363836868
Async ASP.NET average response time: 10.4503867776
Async ASP.NET is 0.828198146555863% slower.
Async 95%ile Service Response Time: 14.5705868
Async 95%ile ASP.NET Response Time: 12.90994551
Regular 95%ile Service Response Time: 15.54793933
Regular 95%ile ASP.NET Response Time: 13.06594751
95%ile ASP.NET Response time is better for Async by 1.19395856963763%
95%ile Service Response time is better for Async by 6.28605829528921%

真的没有显著差异。我们看到的微小差异很可能是由于网络故障或仅仅是一些线程延迟造成的。它并不能真正向我们展示有显著的改进。

然而,当我为异步 ASMX 服务(即旧的 ASP.NET 2.0 ASMX 服务)执行此操作时,结果符合预期。无论有多少服务调用正在进行,ASP.NET 请求执行都没有延迟。可伸缩性改进显著。服务响应时间快 33%,最重要的是,ASP.NET 网页的响应速度提高 73% 以上。

Regular service slow responses: 25 
Async service slow responses: 0
Regular service average response time: 23.25053808
Async service average response time: 13.90445826
Async service is 40.1972624798712% faster.
Regular ASP.NET average response time: 11.2884919224
Async ASP.NET average response time: 2.211796356
Async ASP.NET is 80.4066267557752% faster.
Async 95%ile Service Response Time: 14.1493814
Async 95%ile ASP.NET Response Time: 3.7596482
Regular 95%ile Service Response Time: 21.2318722
Regular 95%ile ASP.NET Response Time: 14.06436031
95%ile ASP.NET Response time is better for Async by 73.2682602185126%
95%ile Service Response time is better for Async by 33.3578251285819%

那么,这是否意味着我们读到的所有关于 WCF 支持异步、使您的服务对于长时间运行的调用更具可伸缩性的文章都是谎言?

我以为我一定是把客户端写错了。所以我使用了 HP Performance Center 来运行一些负载测试,看看同步和异步服务是如何比较的。我使用了 50 个虚拟用户来访问服务,另外 50 个虚拟用户同时访问 ASPX 页面。运行测试 15 分钟后,结果相同,WCF 服务的同步和异步实现都显示相同的结果。这意味着异步实现没有任何改进。

image009.jpg

图:异步服务负载测试结果

image010.jpg

图:同步服务负载测试结果

如您所见,两者表现或多或少相同。这意味着 WCF 本身不支持异步服务。

阅读一些 MSDN 博客文章,我得到了证实。默认情况下,WCF 有一个同步 HTTP 处理程序,它会一直持有 ASP.NET 线程直到 WCF 请求完成。事实上,当 WCF 托管在 IIS 上时,它甚至比旧的 ASMX 更糟糕,因为它每个请求使用两个线程。博客是这样说的

在 .NET 3.0 和 3.5 中,您会观察到 IIS 托管的 WCF 服务有一个特殊的行为。每当请求进来时,系统都会使用两个线程来处理请求

一个线程是 CLR ThreadPool 线程,它是来自 ASP.NET 的工作线程。

另一个线程是由 WCF IOThreadScheduler 管理的 I/O 线程(实际上是由 ThreadPool.UnsafeQueueNativeOverlapped 创建的)。

博客文章为您提供了一个黑客手段,您可以实现自己的自定义 HttpModule,它将处理 WCF 请求的默认同步 HttpHandler 替换为异步 HttpHandler。根据博客,它应该支持 WCF 请求的异步处理,而无需占用 ASP.NET 线程。在实现黑客手段并运行负载测试后,结果相同,黑客手段也无效。

image011.jpg

图:实现自定义 HttpModule 后的负载测试报告

我通过在 web.config 中配置模块成功安装了 HttpModule

<system.webServer> 
    <validation validateIntegratedModeConfiguration="false"/>
    <httpErrors errorMode="Detailed">
    </httpErrors>
    <modules>
      <remove name="ServiceModel"/>
      <add name="MyAsyncWCFHttpModule" type="AsyncServiceLibrary.MyHttpModule" preCondition="managedHandler" />
    </modules>
  </system.webServer> 

但仍然没有改进。 

路到尽头了吗?不,.NET 3.5 SP1 修复了问题。 

真正有效的最终解决方案 

最后,我找到了MSDN 帖子,它证实了在 .NET 3.5 SP1 中,Microsoft 发布了 HTTP 处理程序的异步实现。使用一个工具,您可以在 IIS 上安装异步处理程序作为默认设置,只有这样 WCF 异步服务才能真正成为异步服务。

除了现有的同步 WCF HTTP 模块/处理程序类型之外,WCF 还引入了异步版本。现在总共实现了四种类型的 Http 模块

Synchronous Module: System.ServiceModel.Activation.HttpModule, System.ServiceModel, Version=3.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
Asynchronous Module (new): System.ServiceModel.Activation.ServiceHttpModule,
System.ServiceModel, Version=3.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089

Synchronous Handler: System.ServiceModel.Activation.HttpHandler, System.ServiceModel, Version=3.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 
Asynchronous Handler (new): System.ServiceModel.Activation.ServiceHttpHandlerFactory, System.ServiceModel, Version=3.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 

以下命令将安装异步 http 模块,并将节流设置为 200。这意味着不会有超过 200 个请求排队。这将确保异步请求不会无限期排队,从而导致服务器出现内存不足的问题。

WcfAsyncWebUtil.exe /ia /t 200 

工具安装并运行后,我运行客户端测试改进,可以看到响应时间显著改善。

工具运行后

工具运行前

常规服务慢响应:0

常规服务慢响应:25

异步服务慢响应:0

异步服务慢响应:25

常规服务平均响应时间:16.46133104

常规服务平均响应时间:19.39416864

异步服务平均响应时间:12.45831972

异步服务平均响应时间:18.5408377

常规 ASP.NET 平均响应时间:2.1384130152

常规 ASP.NET 平均响应时间:10.363836868

异步 ASP.NET 平均响应时间:2.214292388

异步 ASP.NET 平均响应时间:10.4503867776

异步 95% 服务响应时间:7.1448916

异步 95% 服务响应时间:14.5705868

异步 95% ASP.NET 响应时间:3.62782651

异步 95% ASP.NET 响应时间:12.90994551

常规 95% 服务响应时间:15.32017641

常规 95% 服务响应时间:15.54793933

常规 95% ASP.NET 响应时间:3.30022231

常规 95% ASP.NET 响应时间:13.06594751

运行该工具后,WCF 服务的响应速度提高了 2 倍,ASP.NET 页面的响应速度提高了 4 倍。

负载测试报告也证实了显著的改进

image012.jpg

图:运行 WCF 工具后,每秒事务处理量提高了 2 倍

image013.jpg

图:运行 WCF 工具后,服务和 ASP.NET 页面的平均响应时间几乎减半。

因此,这证明 WCF 支持异步服务,并且 .NET 3.5 SP1 在支持真正的异步服务方面取得了显著的进步。

等等,它对同步和异步都显示出相同的改进!

你说得对,它不仅使异步更好,而且使同步也更好。两者都显示出相同的改进。原因是该工具只是增加了可用于服务请求的线程数。它并没有使异步服务更好。如果您发现服务器并未真正发挥其最佳性能,您绝对应该使用该工具来增强服务器的容量。但它并不能真正解决异步服务未能提供比同步服务明显更好的结果的问题。

所以,我与微软 WCF 团队的 Dustin 进行了电子邮件交流。他是这样解释的

嗨,奥马尔,

我想这可能有助于说明您的测试中发生的情况。 

image014.jpg

首先,在 Wenlong 的博客文章 WCF 请求节流和服务器可伸缩性中,他讨论了为什么每个请求有两个线程:一个工作线程和一个 IO 线程。您的测试很好地说明了这一点,并且在上面的配置文件中可见。

我特别展示了一个高亮显示的线程(4364)。这是一个工作线程池线程。它正在等待 HostedHttpRequestAsyncResult.ExecuteSynchronous,如堆栈跟踪中突出显示的那样。您还可以通过该线程可用后立即发生的三次 1 秒睡眠来判断这是一个工作线程池线程。您还可以看到,这个工作线程被它正下方的线程(3852)解除阻塞。

线程 3852 有趣之处在于它并没有与 4364 同时启动。这是因为您在 WCF 中使用了异步模式。然而,一旦后端服务(Echo)开始返回数据,就会从 IO 线程池中获取一个 IO 线程用于读取返回的数据。由于您的后端是流式的,并且在返回数据时有很多暂停,因此获取整个数据需要很长时间。因此,一个 IO 线程被阻塞。

如果您使用 WCF 的同步模式,您会注意到 IO 线程在工作线程被阻塞的整个时间内都会被阻塞。但由于在同步或异步情况下,IO 线程无论如何都会被阻塞很长时间,所以结果并没有太大区别。

使用异步模式时,工作线程池将成为瓶颈。如果您只是增加 processModel 上的 minWorkerThreads,所有 ASP.NET 请求将会在 WCF 异步请求之前通过(在工作线程上),这正是您试图通过测试代码实现的目标。

现在,假设您切换到 .Net 4.0。现在您不再有每个请求使用两个线程的相同问题。这是一个仅在中间层使用 .Net 4.0 的配置文件: 

image015.jpg

请注意,7 个 ASP.NET 请求同时发生(显示为蓝色 1 秒睡眠)。还有三个 1 秒睡眠稍后发生,这是因为没有足够的工作线程可用。您还会注意到 IO 线程以独特的紫色表示,这表示等待 IO。由于后端的 Echo 服务是流式传输的并且有暂停,因此 WCF 请求在接收数据时必须阻塞一个线程。我正在使用默认设置和只有 2 个核心,因此默认的最小 IO 线程数是 2(其中一个被计时器线程占用)。其余的服务调用必须等待新的 IO 线程被创建。同样,这显示的是 WCF 异步情况。如果您切换到同步情况,它不会有太大区别,因为它会阻塞一个 IO 线程,但只是时间更短。

我希望这有助于解释您在测试中看到的情况。

谢谢

Dustin 

但让我困惑的是,如果 WCF 3.5 支持异步服务,并且安装了异步 http 模块,为什么我们仍然存在相同的可伸缩性问题?是什么让 WCF 4.0 的性能比 WCF 3.5 好那么多?

因此,我尝试了 minWorkerThreads、maxWorkerThreads、minIOThreads、maxIOThreads、minLocalRequestFreeThreads 设置的各种组合,以查看 WCF 3.5 SP1 中可用的 Async http 模块是否真的有任何区别。以下是我的发现

线程

maxWorkerThreads

maxIOThreads

minWorkerThreads

minIOThreads

minFreeThreads

minLocalRequestFreeThreads

ASP.NET 更好

服务更好

100

200

200

40

30

20

20

-1.49%

66%

100

100

100

40

30

352

304

0%

66%

100

auto

auto

auto

auto

auto

auto

0%

66%

100

100

100

50

50

352

304

-7%

66%

300

auto

auto

auto

auto

auto

auto

1.26%

37%

300

200

200

40

30

20

20

0%

38%

300

200

200

40

30

352

304

0%

39%

300

100

100

40

30

352

304

0%

39%

300

100

100

40

30

20

20

这没有区别。

所以回到达斯汀

嗨,奥马尔,

所以我更彻底地查看了您的示例,我相信我已经弄清楚了是什么导致了您的问题。以下是我对您的示例进行一些修改后运行测试的示例输出

Regular service slow responses: 0 
Async service slow responses: 0
Regular service average response time:
11.6486576
Async service average response time: 2.7771104
Async service is 76.1593953967709% faster.
Regular ASP.NET average response time: 1.018747336
Async ASP.NET average response time: 1.016686512
Async ASP.NET is 0.202290001374735% faster.
Async 95%ile Service Response Time: 2.55532172
Async 95%ile ASP.NET Response Time: 1.03106226
Regular 95%ile Service Response Time: 11.41521426
Regular 95%ile ASP.NET Response Time: 1.03451364
95%ile ASP.NET Response time is better for Async by 0.333623440673065%
95%ile Service Response time is better for Async by 77.6147721646006%

这是使用 100 个线程运行的。完整日志已附上。我注意到一些阻止您看到异步和同步之间差异的事情

  • 最重要的是后端服务 Echo.ashx。即使 ASP.NET 允许所有请求通过,但由于它执行了多次 Thread.Sleep(),它会阻塞工作线程。工作线程以每秒 2 个的速度增加,所以如果您一次抛出 50 个请求,您将不得不等待。您是否在中间层使用异步都不会产生影响。
  • 您在异步和同步之间设置了 10 秒的睡眠时间。这实际上使结果偏向于同步。为处理异步工作而创建的线程不会因不活动而死亡,至少需要 15 秒。我将此睡眠时间增加到 30 秒,以示公平。
  • 您的 Echo.ashx 在 1 秒后开始返回数据,然后又需要 1.5 秒才能完成数据返回。这会阻塞中间层中的 IO 线程,从而导致两个问题: 
    1. 如果 minIoThread 计数不够高,那么您仍然需要等待每秒 2 个的门限,才能获得更多线程来处理同时进行的工作。 
    2. 由于此处提到的错误,minIoThread 设置可能不会随着时间推移而生效。 

为了使您的测试正常工作,我做了一些更改

  • 增加了后端 Echo.ashx 服务上的 minWorkerThreads,以便它可以同时处理所有流量。
  • 增加了中间层服务上的 minWorkerThreads,以便它可以同时处理所有 ASP.NET 请求。
  • 使用了上述博客文章中的解决方法,即使用 SynchronizationContext 将中间层 AsyncService 中传入的工作从 IO 线程池转移到工作线程池。如果一切按计划进行,所有 ASP.NET 请求应该在 Echo.ashx 开始返回数据之前完成,因此应该有足够的工作线程可用。 

我没有更改 minFreeThreads 或 minLocalRequestFreeThreads。

如果有什么不清楚的地方,请告诉我。

谢谢

Dustin

因此,WCF 的 IO 线程池管理中存在未记录的行为。问题在于,当线程启动时,启动时间很慢。即使您突然将 100 个工作排入线程池,线程也会逐渐添加。您可以从性能计数器中看到这一点。如果您在 w3wp 进程中添加线程,您会看到启动缓慢

image016.gif

WCF 使用 .Net CLR I/O 完成端口线程池来执行您的 WCF 服务代码。当 .Net CLR I/O 完成端口线程池进入无法足够快地创建线程以立即处理突发请求的状态时,就会遇到问题。随着新线程以每 500 毫秒 1 个的速度创建,响应时间会意外增加。

最明显的做法是增加 IO 线程的最小数量。您可以通过两种方式实现:使用 ThreadPool.SetMinThreads 或在 machine.config 中使用 <processModel> 标签。以下是后者的实现方式

<system.web> 
  <!--<processModel
autoConfig="true"/>-->
  <processModel 
    autoConfig="false" 
    minIoThreads="101" 
    minWorkerThreads="2" 
    maxIoThreads="200" 
    maxWorkerThreads="40" 
    />

务必关闭 autoConfig 设置,否则其他选项将被忽略。如果再次运行此测试,我们将获得更好的结果。将之前的 permon 快照与此快照进行比较

image017.gif

但是,如果您让负载测试运行一段时间,您会发现它又回到了旧的行为。

image018.gif

所以,确实无法通过调整配置设置来解决这个问题。这证实了我之前对各种线程池设置组合的研究,并没有发现任何区别。

因此,达斯汀推荐了 Juval Lowy 的自定义 SynchronizationContext 实现。使用它,您可以将请求从 WCF IO 线程池移动到 CLR Worker 线程池。CLR Worker 线程池没有这个问题。使用 CLR Worker 线程池执行 WCF 请求的解决方案记录在此处:http://support.microsoft.com/kb/2538826

结论 

WCF 本身到处都支持异步模式,并且可以用于构建比 ASMX 更快、更具可伸缩性的 Web 服务。只是 WCF 的默认安装没有正确配置以支持异步模式。因此,您需要通过运行该工具来调整 IIS 配置,并使用 IO 线程池错误修复,才能充分利用 WCF 的优势。

Plea for Charity 

© . All rights reserved.