C# LazyCache параллельный словарь сборщик мусора - PullRequest
1 голос
/ 04 февраля 2020

Возникли проблемы с веб-приложением. Net (C#). Я использую библиотеку LazyCache для кэширования частых JSON ответов (некоторые в пределах и около 80+ КБ) для пользователей, принадлежащих к одной и той же компании в течение сеансов пользователей.

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

Мы выбираем библиотеку LazyCache , поскольку мы хотели сделать это в памяти без необходимости использования внешнего кэша. источник, такой как Redis et c, поскольку у нас нет интенсивного использования.

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

Для этого у нас есть глобальный кэш, к которому имеют доступ все веб-контроллеры.

private readonly IAppCache _cache = new CachingService();

protected IAppCache GetCache()
{
    return _cache;
}

Упрощенный пример (простите за опечатки!) Наших контроллеров, использующих этот кэш, будет выглядеть примерно так:

[HttpGet]
[Route("{customerId}/accounts/users")]
public async Task<Users> GetUsers([Required]string customerId)
{
    var usersBusinessLogic = await _provider.GetUsersBusinessLogic(customerId)

    var newCacheKey= "GetUsers." + customerId;

    CacheUtil.StoreCacheKey(customerId,newCacheKey)

    return await GetCache().GetOrAddAsync(newCacheKey, () => usersBusinessLogic.GetUsers(), DateTimeOffset.Now.AddMinutes(10));
}

Мы используем класс util с методами stati c и stati c параллельный словарь для хранения ключей кэша - каждая компания (GUID) может иметь много ключей кэша.

private static readonly ConcurrentDictionary<Guid, ConcurrentHashSet<string>> cacheKeys = new ConcurrentDictionary<Guid, ConcurrentHashSet<string>>();

public static void StoreCacheKey(Guid customerId, string newCacheKey)
{
    cacheKeys.AddOrUpdate(customerId, new ConcurrentHashSet<string>() { newCacheKey }, (key, existingCacheKeys) =>
    {
        existingCacheKeys.Add(newCacheKey);
        return existingCacheKeys;
    });
}

В том же самом классе утилит, когда нам нужно удалить все ключи кеша для конкретной компании, у нас есть метод, аналогичный приведенному ниже (который вызывается при внесении мутантных изменений в другие контроллеры)

public static void ClearCustomerCache(IAppCache cache, Guid customerId)
{
    var customerCacheKeys = new ConcurrentHashSet<string>();

    if (!cacheKeys.TryGetValue(customerId,out customerCacheKeys))
    {
        return new ConcurrentHashSet<string>();
    }


    foreach (var cacheKey in customerCacheKeys)
    {
        cache.Remove(cacheKey);
    }

    cacheKeys.TryRemove(customerId, out _);
}

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

Глядя на метрики сбора мусора, мы, кажется, замечаем большой размер кучи Gen 2 и большой размер объекта, который, кажется, идет вверх - мы не видим, что память была восстановлена.

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

Также мы используем режим сбора мусора на рабочей станции, представьте, что переключение в режим сервера G C поможет нам (наш IIS-сервер имеет 8 процессоров + 16 ГБ ОЗУ), но не уверен, что переключение решит все проблемы.

Ответы [ 2 ]

0 голосов
/ 05 февраля 2020

Возможно, вы захотите воспользоваться свойством ExpirationTokens класса MemoryCacheEntryOptions. Вы также можете использовать его из аргумента ICacheEntry, переданного в делегате метода LazyCache.Providers.MemoryCacheProvider.GetOrCreateAsync. Например:

Task<T> GetOrAddAsync<T>(string key, Func<Task<T>> factory,
    int durationMilliseconds = Timeout.Infinite, string customerId = null)
{
    return GetMemoryCacheProvider().GetOrCreateAsync<T>(key, (options) =>
    {
        if (durationMilliseconds != Timeout.Infinite)
        {
            options.SetSlidingExpiration(TimeSpan.FromMilliseconds(durationMilliseconds));
        }
        if (customerId != null)
        {
            options.ExpirationTokens.Add(GetCustomerExpirationToken(customerId));
        }
        return factory();
    });
}

Теперь GetCustomerExpirationToken должен возвращать объект, реализующий интерфейс IChangeToken. Все становится немного сложнее, но потерпите меня на минуту. Платформа. NET не предоставляет встроенную реализацию IChangeToken, подходящую для этого случая, поскольку она в основном ориентирована на наблюдателей файловой системы. Однако реализовать его не сложно:

class ChangeToken : IChangeToken, IDisposable
{
    private volatile bool _hasChanged;
    private readonly ConcurrentQueue<(Action<object>, object)>
        registeredCallbacks = new ConcurrentQueue<(Action<object>, object)>();

    public void SignalChanged()
    {
        _hasChanged = true;
        while (registeredCallbacks.TryDequeue(out var entry))
        {
            var (callback, state) = entry;
            callback?.Invoke(state);
        }
    }

    bool IChangeToken.HasChanged => _hasChanged;

    bool IChangeToken.ActiveChangeCallbacks => true;

    IDisposable IChangeToken.RegisterChangeCallback(Action<object> callback,
        object state)
    {
        registeredCallbacks.Enqueue((callback, state));
        return this; // return null doesn't work
    }

    void IDisposable.Dispose() { } // It is called by the framework after each callback
}

Это общая реализация интерфейса IChangeToken, который активируется вручную с помощью метода SignalChanged. Сигнал будет распространен на базовый объект MemoryCache, который впоследствии сделает недействительными все записи, связанные с этим токеном.

Теперь осталось связать эти токены с клиентом. и хранить их где-нибудь. Я думаю, что ConcurrentDictionary должно быть вполне адекватным:

private static readonly ConcurrentDictionary<string, ChangeToken>
    CustomerChangeTokens = new ConcurrentDictionary<string, ChangeToken>();

private static ChangeToken GetCustomerExpirationToken(string customerId)
{
    return CustomerChangeTokens.GetOrAdd(customerId, _ => new ChangeToken());
}

Наконец, метод, который необходим, чтобы сигнализировать, что все записи определенного c клиента должны быть признаны недействительными:

public static void SignalCustomerChanged(string customerId)
{
    if (CustomerChangeTokens.TryRemove(customerId, out var changeToken))
    {
        changeToken.SignalChanged();
    }
}
0 голосов
/ 04 февраля 2020

Большие объекты (> 85 тыс.) Принадлежат к куче больших объектов второго поколения (LOH) и закреплены в памяти.

  1. G C сканирует LOH и отмечает мертвые объекты
  2. Смежные мертвые объекты объединяются в свободную память
  3. LOH сжат не
  4. При дальнейшем распределении попытайтесь заполнить только дыры , оставленные мертвыми объектами.

gc LOH

Без сжатия, но только перераспределение может привести к фрагментации памяти. Это может быть выполнено долго выполняющимися серверными процессами - это не редкость. Вы, вероятно, видите, что со временем происходит фрагментация.

Сервер G C просто многопоточный - я не ожидал, что это решит фрагментацию.

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

Вы можете попробовать установить LargeObjectHeapCompaction после очистки кэша - при условии, что это нечасто.

GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();

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

...