排队和处理后台作业时出现内存不足异常

Out of memory exception occurs when enqueuing and processing background jobs

在使用 Hangfire.

排队和处理后台作业时,我能够导致发生可重现的内存不足异常

这些作业很简单 Console.WriteLine 调用,所以我不希望堆内存增加它的方式。

我是不是配置不正确或者我应该考虑提交问题?

结果(VMMap

使用 Redis 作为作业的后备存储:

使用 SQL 存储时,限制要高得多 ~= 15,000 个作业。

设置

套餐

<?xml version="1.0" encoding="utf-8"?>
<packages>
  <package id="Hangfire.Core" version="1.6.6" targetFramework="net452" />
  <package id="Hangfire.Pro" version="1.4.7" targetFramework="net452" />
  <package id="Hangfire.Pro.PerformanceCounters" version="1.4.7" targetFramework="net452" />
  <package id="Hangfire.Pro.Redis" version="2.0.2" targetFramework="net452" />
  <package id="Hangfire.SqlServer" version="1.6.6" targetFramework="net452" />
  <package id="Microsoft.AspNet.WebApi.Client" version="5.2.3" targetFramework="net452" />
  <package id="Microsoft.AspNet.WebApi.Core" version="5.2.3" targetFramework="net452" />
  <package id="Microsoft.AspNet.WebApi.Owin" version="5.2.3" targetFramework="net452" />
  <package id="Microsoft.CodeDom.Providers.DotNetCompilerPlatform" version="1.0.0" targetFramework="net452" />
  <package id="Microsoft.Net.Compilers" version="1.0.0" targetFramework="net452" developmentDependency="true" />
  <package id="Microsoft.Owin" version="3.0.1" targetFramework="net452" />
  <package id="Microsoft.Owin.Host.SystemWeb" version="3.0.1" targetFramework="net452" />
  <package id="Newtonsoft.Json" version="9.0.1" targetFramework="net452" />
  <package id="Owin" version="1.0" targetFramework="net452" />
  <package id="StackExchange.Redis" version="1.1.606" targetFramework="net452" />
</packages>

控制器

public class DefaultController : ApiController
{
    static int _;

    [HttpPost]
    public void Post(int count = 1000)
    {
        for (var i = 0; i < count; ++i)
        {
            BackgroundJob.Enqueue(() => Console.WriteLine(_));

            ++_;
        }
    }
}

启动

static class AppSettings
{
    internal static bool   HangfireUseRedis => true;
    internal static int    RedisDatabase    => 0;
    internal static string RedisConnection  => "localhost:6379";

    internal static string SqlConnection    => "Data Source=(localdb)\v11.0;Initial Catalog=Hangfire";
}

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        var config = new HttpConfiguration();

        config.Routes.MapHttpRoute(
            name: "Default",
            routeTemplate: "{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );

        if (AppSettings.HangfireUseRedis)
        {
            var redisOptions = new RedisStorageOptions
            {
                Database = AppSettings.RedisDatabase,
                Prefix   = "Foobar:"
            };

            GlobalConfiguration.Configuration.UseRedisStorage(AppSettings.RedisConnection, redisOptions);
        }
        else
        {
            GlobalConfiguration.Configuration.UseSqlServerStorage(AppSettings.SqlConnection);
        }

        JobHelper.SetSerializerSettings(new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All });

        app.UseHangfireServer();
        app.UseHangfireDashboard();

        app.UseWebApi(config);
    }
}

收到您的小型转储文件 (1.2 GB) 后,我能够获得有关您进程堆的信息。它们中的大多数没有包含任何有趣的东西,并且它们的大小相对较小,这里是最重要的一次的摘录:

GC Heap Size:    Size: 0x9f7eb8 (10452664) bytes.
Jit code heap:   Size: 0x1000 (4096) bytes total, 0x905a4d00 (2421837056) bytes wasted.

正如我们所见,GC 堆大小约为 10 MB,因此 .NET 代码本身没有泄漏,因为它的大小相对较小。但是Jit代码堆看起来很奇怪,所以我决定看看进程使用了​​哪些模块,找到了Stackify Profiler的一个:

6b0d0000 6b23a000   StackifyProfiler_x86   (deferred)

PEB 显示环境变量 StackifyIsPrefix=1 告诉我们使用了 Stackify 前缀。探查器 可能 修改检测程序的 JIT 代码,因此我决定安装 Stackify Prefix 以尝试重现该问题。

我创建了一个简单的 MVC 应用程序,修改了 Home/Index 操作以将 10000 个后台作业排入队列,并启用了探查器。完成这一步后,我发现获取该页面的时间太长了 – 1.5 分钟,而且探查器没有显示任何数据。太长了。所以我决定比较关闭分析器的时间——只用了5秒。这是一个巨大的差异,但我无法重现内存问题。

我已经将作业数量减少到 100 个,打开分析器并意识到对 Redis 的每次调用都被计算在内,对 Redis 的调用有数百条记录。存储所有这些 可能 引入内存问题,但我不知道它们是如何存储在 Stackify 前缀中的。

我无法重现原来的内存问题。但是,Stackify Prefix 确实会通过增加其持续时间来显着影响执行。 您是否尝试禁用 Stackify Prefix 分析器并重新运行您的测试?更新版本也可能解决内存问题。

我同意 odinserj 的上述评论,因为我编写了前缀分析器。

我们进行了一些设计更改,以帮助解决 Hangfire 等库中 运行 的后台线程。问题是我们在每个线程的内存中保留影子堆栈——在普通的 Web 应用程序中,我们在请求结束时刷新此堆栈。但是 Hangfire 启动的线程将在应用程序域的生命周期内存在。

你会发现在最新版本中,影响应该小很多,因为我们已经考虑了一些特定的 hangfire 方法,然后我们释放了一些影子堆栈。