9

我有 WCF 服务。在服务工作期间,它需要调用两个 Web 服务。所以有类似这样的代码:

var task1 = Task.Factory.StartNew(() => _service1.Run(query));
var task2 = Task.Factory.StartNew(() => _service2.Run(query));
Task.WaitAll(new[] { task1 , task2 });

大多数情况下这工作正常,但偶尔我会看到执行时间出现峰值,第一个任务甚至需要几秒钟才能开始。看着 perfmon,我意识到这正是 GC 发生的时候。显然,GC 的优先级高于运行我的任务。这是不可接受的,因为延迟对我来说非常重要,而且我希望 GC 在请求之间而不是在请求中间发生。

我试图以不同的方式解决这个问题,我没有使用自己的任务,而是使用了WebClient.DownloadStringTask

return webClient.DownloadStringTask(urlWithParmeters).ContinueWith(t => ProcessResponse(clientQuery, t.Result),
                                                                           TaskContinuationOptions.ExecuteSynchronously);

这没有帮助;GC 现在在任务开始之后运行,但在继续之前。同样,我猜它认为系统现在处于空闲状态,所以现在是开始 GC 的好时机。只是,我无法承受延迟。

使用 TaskCreationOptions.LongRunning 会导致调度程序使用非线程池线程,这似乎可以解决这个问题,但我不想创建这么多新线程 - 这段代码会运行很多(每个请求多次)。

克服这个问题的最佳方法是什么?

4

5 回答 5

3

让我先清理一下在这个页面上看到的一些误解:

  • 空闲时不会发生 GC。它在由于分配失败(新)、GC.Collect 或操作系统内存压力而触发时发生
  • GC 可以停止应用程序线程。它不会同时运行(至少在一定时间内)
  • “% time in GC”是一个不会在 GC 之间更改的计数器,这意味着您可能会看到一个陈旧的值
  • 异步代码对 GC 问题没有帮助。事实上,它会产生更多的垃圾(任务、IAsyncResult 和可能的其他东西)
  • 在专用线程上运行代码不会阻止它们被停止

如何解决这个问题?

  1. 产生更少的垃圾。附加一个内存分析器(JetBrains 易于使用)并查看产生垃圾的内容以及堆上的内容
  2. 减少堆大小以减少暂停时间(3GB 堆可能是由于一些缓存?也许缩小缓存?)
  3. 使用同一个应用程序启动多个 ASP.NET 站点,连接 GC 通知以感知 GC 的到来,并在它们具有 GC 时使一些 IIS 站点脱离负载平衡轮换(http://blogs.msdn.com/b /jclauzel/archive/2009/12/10/gc-notifications-asp-net-server-workloads.aspx?Redirected=true )

您会注意到没有简单的解决方法。我不知道,但如果问题是由 GC 引起的,上述之一将解决问题。

于 2012-07-23T09:00:17.963 回答
1

我知道你的问题是关于 GC 的,但我想先谈谈异步实现,然后看看你是否还会遇到同样的问题。

离开初始实现的示例代码,您现在将浪费三个 CPU 线程等待 I/O:

  • 浪费的第一个线程是执行调用的原始 WCF I/O 线程。当子任务仍然未完成时,它将被 Task.WaitAll 阻止。
  • 被浪费的另外两个线程是您用来执行对 Service1 和 Service2 的调用的线程池线程

一直以来,虽然 Service1 和 Service2 的 I/O 非常出色,但您浪费的三个 CPU 线程无法用于执行其他工作,并且 GC 必须在它们周围小心翼翼。

因此,我最初的建议是将您的 WCF 方法本身更改为使用 WCF 运行时支持的异步编程模型 (APM) 模式。这通过允许调用您的服务实现的原始 WCF I/O 线程立即返回其池以便能够为其他传入请求提供服务,从而解决了第一个浪费线程的问题。完成此操作后,您接下来要从客户端的角度对 Service1 和 Service2 进行异步调用。这将涉及以下两件事之一:

  1. 生成其合约接口的异步版本,同样使用 WCF 在客户端模型中支持的 APM BeginXXX/EndXXX。
  2. 如果这些是您正在与之交谈的简单 REST 服务,那么您还有以下其他异步选择:
    • WebClient::DownloadStringAsync实现(WebClient不是我个人最喜欢的 API)
    • HttpWebRequest::BeginGetResponse+ HttpWebResponse::BeginGetResponseStream+HttpWebRequest::BeginRead
    • 使用新的 Web API 走在前沿HttpClient

将所有这些放在一起,当您在服务中等待来自 Service1 和 Service2 的响应时,不会有浪费的线程。假设您采用 WCF 客户端路由,代码将如下所示:

// Represents a common contract that you talk to your remote instances through
[ServiceContract]
public interface IRemoteService
{
   [OperationContract(AsyncPattern=true)]
   public IAsyncResult BeginRunQuery(string query, AsyncCallback asyncCallback, object asyncState);
   public string EndRunQuery(IAsyncResult asyncResult);

}

// Represents your service's contract to others
[ServiceContract]
public interface IMyService
{
   [OperationContract(AsyncPattern=true)]
   public IAsyncResult BeginMyMethod(string someParam, AsyncCallback asyncCallback, object asyncState);
   public string EndMyMethod(IAsyncResult asyncResult);
}

// This would be your service implementation
public MyService : IMyService
{
    public IAsyncResult BeginMyMethod(string someParam, AsyncCallback asyncCallback, object asyncState)
    {
        // ... get your service instances from somewhere ...
        IRemoteService service1 = ...;
        IRemoteService service2 = ...;

        // ... build up your query ...
        string query = ...;

        Task<string> service1RunQueryTask = Task<string>.Factory.FromAsync(
            service1.BeginRunQuery,
            service1.EndRunQuery,
            query,
            null);

        // NOTE: obviously if you are really doing exactly this kind of thing I would refactor this code to not be redundant
        Task<string> service2RunQueryTask = Task<string>.Factory.FromAsync(
            service2.BeginRunQuery,
            service2.EndRunQuery,
            query,
            null);

        // Need to use a TCS here to retain the async state when working with the APM pattern
        // and using a continuation based workflow in TPL as ContinueWith 
        // doesn't allow propagation of async state
        TaskCompletionSource<object> taskCompletionSource = new TaskCompletionSource<object>(asyncState);

        // Now we need to wait for both calls to complete before we process the results
        Task aggregateResultsTask = Task.ContinueWhenAll(
             new [] { service1RunQueryTask, service2RunQueryTask })
             runQueryAntecedents =>
             {
                 // ... handle exceptions, combine results, yadda yadda ...
                 try
                 {
                     string finalResult = ...;

                     // Propagate the result to the TCS
                     taskCompletionSoruce.SetResult(finalResult);
                 }
                 catch(Exception exception)
                 {
                     // Propagate the exception to the TCS 
                     // NOTE: there are many ways to handle exceptions in antecedent tasks that may be better than this, just keeping it simple for sample purposes
                     taskCompletionSource.SetException(exception);
                 }
             });

         // Need to play nice with the APM pattern of WCF and tell it when we're done
         if(asyncCallback != null)
         {
             taskCompletionSource.Task.ContinueWith(t => asyncCallback(t));
         }

         // Return the task continuation source task to WCF runtime as the IAsyncResult it will work with and ultimately pass back to use in our EndMyMethod
         return taskCompletionSource.Task;
    }

    public string EndMyMethod(IAsyncResult asyncResult)
    {
        // Cast back to our Task<string> and propagate the result or any exceptions that might have occurred
        return ((Task<string>)asyncResult).Result;
    }
}

一旦一切就绪,从技术上讲,当 Service1 和 Service2 的 I/O 非常出色时,您将不会执行任何 CPU 线程。在这样做的过程中,GC 没有线程,甚至大部分时间都不必担心中断。现在唯一发生实际 CPU 工作的时间是最初的工作调度,然后继续在 ContinueWhenAll 上处理任何异常并处理结果。

于 2012-07-19T23:51:25.693 回答
0

我建议您重新考虑德鲁的回答。完全异步的系统将是理想的。

但是如果你想改变更少的代码,你可以使用FromAsync代替StartNew(这需要异步代理Service1Service2):

var task1 = Task.Factory.FromAsync(_service1.BeginRun, _service1.EndRun, query, null);
var task2 = Task.Factory.FromAsync(_service2.BeginRun, _service2.EndRun, query, null);
Task.WaitAll(task1, task2);

这将每次使用的线程池线程数WaitAll从 3 减少到 1。您仍然不是理想的 (0),但您应该会看到改进。

于 2012-07-22T21:37:59.983 回答
0

当您执行许多 Web 请求时,您会将大量临时对象加载到托管堆中。当堆确实增长时,GC 会在分配新的 GC 段之前尝试释放一些内存。这是您在工作时看到 GC 发生的主要原因。

现在到了有趣的部分:您的 GC 堆已经是 3 GB,并且您在 GC 堆上还有一些带有短期对象的 Web 请求。Full GC 将花费大量时间来遍历您肯定很复杂的对象图(全部 3 GB)以查找死对象。在如此高吞吐量的情况下,您将通过网络为每个请求获取大量临时数据,您将强制执行大量 GC。

此时您已被 GC 绑定:应用程序性能不再受您的控制。您可以通过仔细设计数据结构和访问模式来正常解决此问题,但 GC 时间将在很大程度上(我猜 > 95%)支配您的应用程序性能。

没有简单的方法可以解决这个问题。如果它是一个大型复杂系统,通过检查整体内存消耗来使 GC 段更小可能很难。另一种方法可能是产生一个额外的进程(不是新的 AppDomain,因为 GC 根本不知道 AppDomains)并在你的 web 请求中创建你的短期对象。然后,如果您可以在您的小进程中计算出有意义的响应,然后由您的大服务器进程使用,那么您就可以摆脱这种混乱。如果您的流程确实创建了与您的原始 Web 请求相同数量的临时数据,那么您又回到了原点,您什么也得不到。

它可能有助于重用来自先前 Web 请求的对象并保持对象池准备好减少分配的数量。

如果您的进程堆中有很多相同的字符串,那么如果它们永远不会被释放,那么实习它们可能会有所帮助。这有助于简化您的对象图。

于 2012-07-26T12:32:27.343 回答
0

您可能想尝试一下,但它可能会稍微解决问题:

try
{
    GCSettings.LatencyMode = GCLatencyMode.LowLatency;

    // Generation 2 garbage collection is now
    // deferred, except in extremely low-memory situations

    var task1 = Task.Factory.StartNew(() => _service1.Run(query));
    var task2 = Task.Factory.StartNew(() => _service2.Run(query));
    Task.WaitAll(new[] { task1 , task2 });
}
finally
{
    // ALWAYS set the latency mode back
    GCSettings.LatencyMode = oldMode;
}

应归功于:https ://stackoverflow.com/users/153498/mgbowen

于 2012-07-26T04:52:19.510 回答