Есть ли способ использовать сессионное хранилище в .NET core без накладных расходов двоичных сериализаторов?

#c# #asp.net-core

#c# #asp.net-core

Вопрос:

Порядок работы сеанса в приложениях .net core требует, чтобы вы сначала преобразовали его в массив байтов, прежде чем извлекать его. По сравнению с хранилищем .net Framework, накладные расходы на эти операции сериализации и десериализации сопряжены со значительными накладными расходами. Операции, которые занимают 5 мс в нашем .Приложения Net Framework занимают более 850 мс.Сетевое ядро. Мне нужна возможность хранить и извлекать довольно большие объемы данных из кэша сервера высокопроизводительным способом, аналогично тому, как мы можем использовать сессионное хранилище в .Net framework.

Наши приложения потребляют много несколько больших ADO.NET таблицы данных. Нередко они содержат тысячи строк и десятки столбцов. В прошлом мы использовали .Net Framework с сессионным хранилищем для быстрого извлечения ADO.NET объекты с возможностью передачи данных в сеанс и из сеанса.

 DataTable dt = new DataTable();
HttpContext.Current.Session["data"] = dt; // store in session
dt = (DataTable)HttpContext.Current.Session["data"]; // retrieve from session
  

У нас также есть наши собственные пользовательские классы, которые содержат таблицы данных в качестве членов. Эти классы по-разному манипулируют данными. Мы также храним и извлекаем их из сеанса.

 [Serializable]
 MyClass {
    public DataTable dt;
 }
  

На нашем .Приложения NET Framework обычно запрашивают данные для фильтрации и подкачки в оба конца примерно за 5 мс. Доступ к сеансу осуществляется очень быстро, а снижение производительности для каждой операции получения и установки незначительно.

Мы пытались перейти на .NET Core, который управляет сеансом немного по-другому. Вместо того, чтобы сохранять и извлекать любой объект в сеансе, я сначала должен преобразовать его в массив байтов. Я использую двоичный форматировщик с небольшим количеством логики для обработки операций get и set. Приведенный ниже код не так эффективен, как мог бы быть, но, безусловно, самым большим узким местом является операция десериализации в методе retrieveData.

 Public class SessionManager : ISessionManager
    {
        private readonly IHttpContextAccessor _contextAccessor;
        public SessionManager(IHttpContextAccessor contextAccessor) {
            _contextAccessor = contextAccessor;
         }
        public T get<T>(string key)
        {
            object data = retrieveData(key);
            return data == null ? default(T) : (T)data;
        }

        public void set(string key, object data)
        {
            serializeBinary(key, data); 
        }

        public void remove(string key) {
            _contextAccessor.HttpContext.Session.Remove(key);
        }

        private void serializeBinary(string key, object data) {
            BinaryFormatter bf = new BinaryFormatter();
            using (var ms = new MemoryStream())
            {
                bf.Serialize(ms, data);
                var bytes = ms.ToArray();
                _contextAccessor.HttpContext.Session.Set(key, bytes);
            }
        }

        private object retrieveData(string key) {
            byte[] data = null;
            _contextAccessor.HttpContext.Session.TryGetValue(key, out data);
            if (data == null) return null;
            using (MemoryStream ms = new MemoryStream(data))
            {
                IFormatter br = new BinaryFormatter();
                return br.Deserialize(ms);
            }
        }
    }
}
  

Использование:

 MyClass c;
c.dt = _myRepo.getLotsOfData();
_SessionManager.set("data", c);
c = _SessionManager.get<MyClass>("data");
  

Те же операции подкачки и фильтрации, которые мы выполняем над таблицами данных, используя те же классы, занимают от 850 до 950 мс по сравнению с нашими приложениями .Net Framework, которые занимают 5 мс. При профилировании производительности в Visual Studio операция десериализации заняла львиную долю этого времени, 600 мс только для этой операции.

Я знаю о других библиотеках (таких как protobuf), которые намного быстрее, чем двоичный форматировщик, и, вероятно, к этому я перейду следующим. Однако, если я уменьшу время десерилизации до 300 мс или даже 200 мс, я все равно значительно потеряю производительность по сравнению to.Net Фреймворк.

Мне кажется, что требуется стратегия кэширования другого типа. Есть ли способ хранить и извлекать данные в .Приложения Net Core, которые не требуют дополнительных затрат на сериализацию и десериализацию данных в первую очередь?

Комментарии:

1. Вместо Session того, что вы можете попробовать IMemoryCache , вы можете вставить в это любой объект.

2. Спасибо, Йохан, я попробую. Похоже, мне нужно создать собственное управление сеансами, построенное на кеше памяти, поскольку это системный кеш, а не кеш сеанса. Это рекомендуемый подход?

3. Да, вам придется заставить ключи вашего кэша использовать некоторый уникальный идентификатор пользователя, userid или что-то еще, что идентифицирует пользователя в данный момент. В противном случае используйте подход Марка Гравелла

Ответ №1:

Наши приложения потребляют много несколько больших ADO.NET таблицы данных. Нередко они содержат тысячи строк и десятки столбцов.

Я думаю, мы нашли проблему 🙂

Предложения:

  1. не пытайтесь сериализовать огромные наборы данных в таком состоянии; они просто слишком дороги
  2. включите dataSet.RemotingFormat = System.Data.SerializationFormat.Binary; перед сериализацией DataSet в BinaryFormatter
  3. прекратите использовать DataSet и прекратите использовать BinaryFormatter ; Я не говорю это свободно — они довольно дороги, поэтому, если данные действительно довольно фиксированы, рассмотрите возможность использования типа POCO с другим сериализатором — protobuf был бы намного эффективнее как с точки зрения процессора, так и с точки зрения пропускной способности (объем данных)

Комментарии:

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

Ответ №2:

В итоге я использовал IMemoryCache, как рекомендовано JohanP. IMemoryCache — это системное хранилище, которое не требует от вас сериализации ваших объектов для их хранения и извлечения.

Я сохранил свое приложение настроенным для сеанса, так что.NET продолжит предоставлять cookie для идентификатора сеанса. Затем я добавляю идентификатор сеанса к предоставленному пользователем ключу перед сохранением или извлечением элемента, что более или менее соответствует способу управления сеансом в .NET framework.

 public class SessionManager : ISessionManager
    {
        private readonly IHttpContextAccessor _contextAccessor;
        private readonly IMemoryCache _cache;
        public SessionManager(IHttpContextAccessor contextAccessor
                            , IMemoryCache cache
        ) {
            _contextAccessor = contextAccessor;
            _cache = cache;
         }
        public T get<T>(string key)
        {
           object data;
           _cache.TryGetValue(buildSessionKey(key), out data);
           return data == null ? default(T) : (T)data;
        }

        public void set(string key, object data, TimeSpan maxLifeTime = default(TimeSpan))
        {
            TimeSpan adjustLifeTime = maxLifeTime.TotalMinutes < 5 ? TimeSpan.FromMinutes(20) : maxLifeTime;
            if (adjustLifeTime.TotalMinutes > 90) adjustLifeTime = TimeSpan.FromMinutes(90);
            MemoryCacheEntryOptions cacheEntryOptions = new MemoryCacheEntryOptions()
            .SetSlidingExpiration(TimeSpan.FromMinutes(20))
            .SetAbsoluteExpiration(adjustLifeTime);
            _cache.Set(buildSessionKey(key), data);
        }

        public void remove(string key) {
            _cache.Remove(buildSessionKey(key));
        }

        private string buildSessionKey(string partialKey) {
            string sessionID = _contextAccessor.HttpContext.Session.Id;
            return sessionID   partialKey;
        } 
    }
  

Startup.cs

         services.AddMemoryCache(options => 
        {
            // options.SizeLimit = 4096;
            options.ExpirationScanFrequency = TimeSpan.FromMinutes(20);
        });
        services.AddSession(options =>
        {
            options.IdleTimeout = TimeSpan.FromMinutes(20);
            options.Cookie.HttpOnly = true;
            options.Cookie.IsEssential = true;
        });
  

При использовании этого подхода производительность аналогична нашей .Приложения NET framework.