Исключение из-за нехватки памяти возникает при постановке в очередь и обработке фоновых заданий

Я могу вызвать исключение воспроизводимой нехватки памяти при постановке в очередь и обработке фоновых заданий с помощью Hangfire.

Работа проста Console.WriteLine вызовы, поэтому я не ожидал бы, что память кучи увеличит то, как это происходит.

Я неправильно настроил или мне нужно подумать о регистрации проблемы?

Результаты ( VMMap)

Использование Redis в качестве резервного хранилища для заданий:

  • В начале общая куча = 29,088K;
  • после 5000 рабочих мест - 938 672 тыс.
  • 6000 рабочих мест, 1 056 004 000;
  • 7000 рабочих мест, 1 219 296 000;
  • 8000 заданий, кучи значения нет;
  • в течение 100 рабочих мест, iisexpress.exe Экземпляр падает.

С хранилищем SQL предел намного выше ~ 15 000 заданий.

Настроить

  • пустой проект ASP.NET;
  • установить пакеты Owin для хостинга IIS и Hangfire;
  • класс запуска и контроллер.

пакеты

<?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);
    }
}

2 ответа

Решение

После получения файла мини-дампа (1,2 ГБ) я смог получить информацию о кучах вашего процесса. Большинство из них не содержат ничего интересного, и их размер относительно невелик, вот выдержка из наиболее важных понятий:

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

Как мы видим, размер кучи GC составляет около 10 МБ, поэтому в самом коде.NET нет утечек, поскольку его размер относительно невелик. Но куча кода Jit выглядит очень странно, поэтому я решил посмотреть, какие модули используются процессом, и нашел один из Stackify Profiler:

6b0d0000 6b23a000   StackifyProfiler_x86   (deferred)

PEB показывает переменную среды StackifyIsPrefix=1 это говорит нам, что используется префикс Stackify. Профилировщики могут изменить код JIT для инструментальных куколок, поэтому я решил установить префикс Stackify, чтобы попытаться воспроизвести проблему.

Я создал простое приложение MVC, изменил Home/Index действие для постановки в очередь 10000 фоновых заданий и включения профилировщика. Сделав этот шаг, я обнаружил, что получение этой страницы занимает слишком много времени - 1,5 минуты, и профилировщик не показывает никаких данных. Это было слишком долго. Поэтому я решил сравнить синхронизацию с выключенным профилировщиком - это заняло всего 5 секунд. Это огромная разница, но я не смог воспроизвести проблемы с памятью.

Я уменьшил количество заданий до 100, включил профилировщик и понял, что каждый вызов в Redis считается, есть сотни записей для вызовов в Redis. Хранение их всех может привести к проблемам с памятью, но я не знаю точно, как они хранятся в префиксе Stackify.

Скриншот Stackify Prefix

Я не смог воспроизвести исходную проблему с памятью. Однако префикс Stackify существенно влияет на выполнение, увеличивая его продолжительность. Вы пытались отключить профилировщик префиксов Stackify и повторно запустить свои тесты? Обновленная версия также может исправить проблему с памятью.

Я могу согласиться с приведенным выше комментарием от odinserj, потому что я написал профилировщик префиксов.

Мы внесли некоторые изменения в дизайн, чтобы учесть фоновые потоки, которые работают в таких библиотеках, как Hangfire. Проблема в том, что мы сохраняем теневые стеки в памяти для каждого потока - в обычном веб-приложении мы очищаем этот стек, когда запрос заканчивается. Но потоки, которые раскручивает Hangfire, будут существовать на протяжении всего жизненного цикла домена приложения.

Вы обнаружите, что в последней версии влияние должно быть намного меньше, так как мы учли некоторые конкретные методы зависания, и затем мы выпустим часть этого стека теней.

Другие вопросы по тегам