NHibernate и WCF: производительность (повторное использование сеанса) и параллелизм (одновременные запросы)

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

В основном мы используем NHibernate для подключения к базам данных. Одно приложение — это служба WCF, которая должна вставлять входящие данные во вложенные таблицы, которые действительно широки (десятки столбцов). Очевидно, что производительность является проблемой, поэтому я стремился быть максимально экономичным с транзакциями NHibernate. В то же время параллелизм оказался проблемой. В продакшне мы начали получать некоторые ошибки зомбированных транзакций (взаимоблокировки).

Я пытался балансировать между этими двумя проблемами, но на самом деле не устранил проблемы параллелизма.

Поведение службы настроено на обработку одного запроса за раз, например:

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall, ConcurrencyMode=ConcurrencyMode.Single]
public class LegacyGateService : ILegacyGateService

Ранее, после некоторого «вдохновения» (читай: копирования/вставки) из Интернета, я закончил тем, что добавил набор классов, названных чем-то вроде XxxNHibernateUtil, для вспомогательной базы данных и устаревших баз данных, соответственно. Эти классы управляют сеансами NHibernate и генерируют или повторно используют сеансы из предварительно инициализированных SessionFactories.

Для вспомогательной базы это выглядит так:

public static class LegacyGateNHibernateUtil
{
    private static readonly ISessionFactory sessionFactory = BuildSessionFactory();

    private static ISessionFactory BuildSessionFactory()
    {
        try
        {
            Configuration Cfg = new Configuration();
            Cfg.Configure();
            Cfg.AddAssembly("LegacyGate.Persistence");
            return Cfg.BuildSessionFactory();
        }
        catch (Exception ex)
        {
            throw ex;
        }
    }

    public static ISessionFactory GetSessionFactory()
    {
        return sessionFactory;
    }

    public static ISession GetCurrentSession()
    {
        if (!CurrentSessionContext.HasBind(GetSessionFactory()))
            CurrentSessionContext.Bind(GetSessionFactory().OpenSession());

        return GetSessionFactory().GetCurrentSession();
    }

    public static void DisposeCurrentSession()
    {
        ISession currentSession = CurrentSessionContext.Unbind(GetSessionFactory());

        if (currentSession != null)
        {
            if (currentSession.IsOpen)
                currentSession.Close();
            currentSession.Dispose();
        }
    }
}

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

EDIT: Контекст сеанса, конечно же, задается в файле hibernate.cfg.xml следующим образом:

  <property name="current_session_context_class">call</property>

Для устаревших баз данных NHibernateUtil адаптирован для работы с различными возможными базами данных. Для этого каждое соединение получает свою собственную SessionFactory, которую нужно искать в коллекции Dictionary. В остальном принципы те же.

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

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


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

  1. «Произошла проблема при преобразовании IDataReader в NDataReader» / «Неверная попытка вызвать метаданные, когда программа чтения закрыта».
  2. "незаконный доступ к загрузке коллекции"
  3. «Начать не удалось с исключением SQL» / «Время ожидания истекло. Время ожидания истекло до завершения операции или сервер не отвечает».
  4. «не удалось выполнить запрос» / «ExecuteReader требует открытого и доступного соединения. Текущее состояние соединения закрыто».
  5. «не удалось инициализировать коллекцию:» ​​/ «Время ожидания истекло. Время ожидания истекло до завершения операции или сервер не отвечает».
  6. "Транзакция не была успешно начата"
  7. "Транзакция либо не связана с текущим соединением, либо уже завершена".
  8. "не удалось инициализировать коллекцию:" / "Неверная попытка вызвать Read, когда программа чтения закрыта".

Исключение 1, по крайней мере, указывает на то, что к одному и тому же сеансу обращаются несколько потоков (вероятно, вызовы). Также остальные указывают на то, что текущий сеанс прерван другими процессами. Но как это может быть, когда я пытался изолировать звонки и ставить их в очередь?

Для другого метода обслуживания эти проблемы не появляются со вспомогательной базой данных, но через некоторое время я начинаю получать исключения ZombiedTransaction (взаимоблокировки) с транзакциями к устаревшим базам данных. Еще... Что дает?


person Fedor Alexander Steeman    schedule 05.01.2012    source источник
comment
Похоже, вы сталкиваетесь с проблемами параллелизма. Какую версию NH вы используете? Если вы используете 3.2, я бы предложил использовать wcf_operation в качестве контекста вашего сеанса. Если вы не используете 3.2, вы можете сделать это простым способом.   -  person Cole W    schedule 05.01.2012
comment
Кроме того, заявление throw ex; выше — это большое нет-нет. Вы уничтожаете трассировку стека, когда делаете это. Вы должны использовать throw; Поскольку вы ничего не делаете, за исключением того, что не должно быть даже блока try catch.   -  person Cole W    schedule 05.01.2012
comment
Прости. Я проглядел ваши комментарии. Я использую версию 3.1, но я могу быстро обновиться, если это поможет! :-) И хорошо, я исправлю свои исключения и учту это на будущее! Спасибо!   -  person Fedor Alexander Steeman    schedule 05.01.2012
comment
Выполнено обновление до 3.2, но загрузка не включала сборку Castle ByteCode. Я использовал версию 3.1, но она не распознается/не обнаруживается при запуске приложения. Я получаю сообщение о невозможности загрузки типа «NHibernate.ByteCode.Castle.ProxyFactoryFactory, NHibernate.ByteCode.Castle». ???   -  person Fedor Alexander Steeman    schedule 06.01.2012
comment
Похоже, мне это больше не нужно, поэтому я удалю все упоминания о замке.   -  person Fedor Alexander Steeman    schedule 06.01.2012


Ответы (1)


Простой ответ: сеансы NHibernate повторно не используются.

Это не тяжеловесные объекты, и они предназначены для создания, управления и удаления в соответствии с шаблоном Unit of Work. Попытка «расшарить» их по нескольким запросам противоречит их предполагаемому использованию.

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

Помните, что фабрика сеансов NHibernate — это тяжеловесный объект. Он потокобезопасен, поэтому вы можете и должны использовать один и тот же экземпляр для всех запросов из коробки.

Ваш код должен выглядеть концептуально так:

public class Service : IService
{
    static Service()
    {
        Configuration Cfg = new Configuration();
        Cfg.Configure();
        Cfg.AddAssembly("LegacyGate.Persistence");
        Service.SessionFactory = Cfg.BuildSessionFactory();
    }

    protected static ISessionFactory SessionFactory { get; private set; }

    public void ServiceMethod(...)
    {
        using(var session = Service.SessionFactory.CreateSession())
        {
            // Do database stuff
            ...
        }
    }
}

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

person Paul Turner    schedule 05.01.2012
comment
Также стоит отметить, что сеанс NHibernate используется для отслеживания сущностей, что может вызвать поведение, похожее на утечку памяти, если вы используете один и тот же сеанс в течение длительного времени (т. е. все сущности в базе данных могут в конечном итоге отслеживаться сеансом). . - person Liedman; 05.01.2012
comment
Хм... Хорошая мысль! Поэтому в основном измените метод GetCurrentSession на одну строку: return GetSessionFactory().OpenSession(); ? - person Fedor Alexander Steeman; 05.01.2012
comment
Но устранит ли это и взаимоблокировки? - person Fedor Alexander Steeman; 05.01.2012
comment
Я думаю, что это плохая идея @FedorSteeman, если вы не реализуете блоки using вокруг каждого вызова GetCurrentSession. Если бы это был я, я бы исследовал, действительно ли совместное использование сеансов происходит между потоками, как это звучит, и выяснил, как это исправить, прежде чем вы выберете легкий путь. Вы пробовали мое предложение выше? - person Cole W; 05.01.2012
comment
@ColeW: Хорошо. Однако, возможно, я мог бы сделать это только для вспомогательной базы данных, а не для устаревших баз данных, где возникают взаимоблокировки. Я в любом случае обновлюсь до 3.2 как можно скорее и отчитаюсь. - person Fedor Alexander Steeman; 05.01.2012
comment
Я не могу проголосовать за это достаточно. Не используйте сеансы повторно — поскольку сеанс с отслеживанием состояния концептуально является единицей работы, вы в основном будете цепляться за каждый объект, который вы когда-либо загружали через него, не говоря уже о куче другого дерьма (включая, но не ограничиваясь копии значений в каждом поле для отслеживания изменений). Вы не повышаете производительность за счет повторного использования сеансов. - person Rytmis; 06.01.2012
comment
Хорошо. Теперь я больше не использую Sessions повторно, и это устранило почти все упомянутые проблемы параллелизма, кроме одной: зомбированные транзакции продолжают происходить всякий раз, когда я достаточно сильно нагружаю нагрузку. Я считаю, что транзакции базы данных все еще занимают слишком много времени, вызывая взаимоблокировки. В любом случае, отмечу это как ответ, так как это вернуло меня на правильный путь. - person Fedor Alexander Steeman; 09.01.2012
comment
@ColeW Выполняя рефакторинг, я обнаружил оставшийся код, который на самом деле вызвал множество ошибок параллелизма, о которых я упоминал. Так что вы были правы, что не выбрали легкий путь и сначала выяснили, что пошло не так! Что-то скрывалось за всем этим... - person Fedor Alexander Steeman; 12.01.2012