Dynamics AX 4 и IMTS
От пользователей, работающих со старыми версиями Dynamics AX, достаточно часто приходится слышать жалобы на низкую производительность модуля логистики. Что нибудь типа “У нас стоит сервер БД на 4-х двухядерных Xeon (Opteron), 16 гигабайт оперативки и на небольшой 5 гигабайтной базе, создание строки заказа иногда занимает пару минут”. Если в такой ситуации запустить SQL Enterprise Manager или SQL Server Management Studio, то можно увидеть длинную очередь блокировок процессов, причем все блокированные процессы ожидают освобождения записей в таблице inventSum (Запасы в наличии). Грубо говоря – данная таблица содержит в себе информацию о складском остатке (ну и о количестве зарезервированного, скомплектованного, принятого и т.п. товара) в разрезе кодов номенклатур и кодов складской аналитики. Обновление этой таблицы (опять таки – в старых версиях DAX), организовано следующим образом: При любых модификациях таблицы складских проводок (inventTrans), система находит (или создает) соответствующую запись в таблице запасов в наличии и затем обновляет в ней количество. Естественно – при обновлении записи, до завершения транзакции, обновившей таковую, любой доступ к ней (и по чтению и по записи) из других соединений блокируется. (Случай Dirty Read не рассматриваем). Если у нас не используется учет по партиям или серийным номерам, то с некоторой долей приближения можно сказать, что если мы в транзакции изменили складскую проводку по некоторой номенклатуре и складу, то до конца этой транзакции, пользователи с других рабочих станций НЕ МОГУТ выполнять какие-то операции по данной номенклатуре на данном складе. Делается это по той простой причине, что до успешного завершения (или отмены) операции, остаток на складе представляет собой некоторую вероятностную величину. Давайте представим себе ситуацию, при которой подобных блокировок не происходит. Допустим - у нас на складе лежит 40 штук некого артикула. Кладовщик в данный момент проводит приходную отборочную накладную с еще 800 штуками. Два сейла резервируют по 20 и 40 штук соответственно. Другой кладовщик оформляет расходную отборочную накладную на 15 штук. Возникает вопрос - сколько у нас вообще на складе свободного товара и можно ли дать третьему сейлу зарезервировать под свой заказ еще 60 штук ? (Кстати – во всем дальнейшем изложении подразумевается , что режим отрицательного склада отключен.)
Таким образом – ситуация блокировок складских остатков не объясняется чисто технической проблемой скверной реализации контроля складского остатка. Эта проблема вытекает из самой сущности транзакционной модели работы с данными. Если в ходе транзакции у нас происходит модификация уровня некоторого критического ресурса (в нашем случае – складского остатка), то до конца транзакции, доступ к информации по уровню этого ресурса должен быть заблокирован для других соединений, чтобы избежать ситуации, грубо говоря, “продажи двух билетов на одно место”. Даже если бы у нас появилась какая-то сверхбыстрая СУБД, которая позволяла бы мгновенно считать остатки на основании самих складских проводок (без таблицы запасов в наличии), мы столкнулись бы с той же проблемой: Мы не смогли бы понять – нужно ли добавлять к сумме текущего остатка складские проводки, модифицированные в ходе незавершенных транзакций других сессий.
Для того чтобы оценить масштабы бедствия с блокировками, давайте рассмотрим другой пример:
Допустим кладовщик начали оформление приходной отборочной накладной на 120 позиций. Первые 40 позиций были благополучно получены, заблокировав информацию по остаткам по этой номенклатуре от всех остальных пользователей. Добравшись до 41 позиции, разноска отборочной накладной остановилась и стала ждать пока остаток по номенклатуре из 41 позиции будет освобожден первым сейлом, который в данный момент оформляет накладную из 8 позиций. В свою очередь этот первый сейл, который уже заблокировал 6 номенклатур, ждет освобождения 7ой, которая в этот момент заблокирована вторым сейлом. Второй сейл в свою очередь тоже ждет освобождения какой-то номенклатуры, заблокированной вторым кладовщиком, который оформляет отборочную накладную на списание. Второй кладовщик в свою очередь ждет освобождения артикула, заблокированного третьим сейлом и тд. В результате - все 4 участника процесса ждут окончания операции третьего сейла. После того как она будет закончена, цепочка будет медленно и печально разворачиваться в обратную сторону, и у кладовщика N 1 спустя какое-то время наконец-то будет получена очередная строка, конечно если всем повезет, и не выяснится что артикул из 8ой позиции , создаваемого первым сейлом оказался заблокирован сейлом N 4. При этом нужно помнить, что все участники забега уже заблокировали какие-то остатки, соответственно - есть шансы что их тоже кто-то ждет. Из за этого время выполнения логистической операции с парочкой номенклатур может колебаться от долей секунды, до нескольких минут. От реальной производительности сервера БД или сервера приложений это время мало зависит. Просто в зависимости от того как легла карта, очередь за ресурсом может состоять из одного человека, а может и из 10… Кстати - возможно многие видели, иногда появляющееся в DAX сообщение о том что сессия была закрыта из за “Тупиковой ситуации”. Время от времени возникают ситуации, при которых в списке ожидания образуются циклы, например, выясняется что сейл N4 которого ждет кладовщик N2, на самом деле сам ждет освобождения артикула заблокированного первым кладовщиком в начале процесса получения товара. В подобной ситуации система просто принудительно закрывает соединение последнего из участников очереди, отменяя начатые им операции и выдавая ему сообщение о тупиковой ситуации. Причем если в момент такого массового ожидания очереди на блокировку посмотреть на загрузку серверов, то выяснится что и сервер БД и AOS, скорее всего не очень-то и сильно загружены. Вот именно этот момент обычно и вызывает наибольшее недоумение системных администраторов. Как же так – сервера не загружены, а система так медленно работает ? Причем покупка значительно более мощного сервера БД, не решит проблему, поскольку производительность системы в данной ситуации определяется производительностью всей цепочки сервер БД-Сервер AOS-клиентский компьютер (причем с учетом задержек в сети). И если проапгрейдить сервера за разумные деньги еще можно, то апгрейд всех клиентских компьютеров и повышение производительности всех сетевых соединений может обойтись в совсем запредельную сумму.
Для того чтобы преодолеть эту проблему, в третьей версии Dynamics AX, был реализован механизм под названием Inventory MultiTransaction System – IMTS. (Строго говоря – он также был включен в версию 2.5Sp6, но она, помниться, вышла уже после выхода третьей версии).
В первом приближении – механизм этот работает следующим образом: При любом обновлении таблицы складских проводок (inventTrans) система в рамках ОТДЕЛЬНОГО соединения с БД НЕНАДОЛГО открывает транзакцию в ходе которой и обновляется таблица запасов в наличии (inventSum). Поскольку транзакция быстро завершается – продолжительность блокирования также мала. При любом обновлении таблицы складских проводок, также в ОТДЕЛЬНОМ соединении в таблицу inventSumLogTTS пишется протокол обновления складских проводок. Зачем он нужен ? Давайте подумаем – что система должна сделать, если основная транзакция (с обновлениями inventTrans,SalesLine и тп) будет отменена? По логике вещей – должны быть отменены и изменения записей с текущими остатками в таблице запасов в наличии. Однако – поскольку эти изменения были сделаны в отдельном соединении и в отдельной (уже подтвержденной) транзакции, сам MS SQL (или Oracle) этого явно не будет делать. В связи с этим, в DAX 3.0 был реализован механизм, который в момент отката основной транзакции, пробегает по связанным с этой транзакцией записям в inventSumLogTTS, и вычитает количества взятые из этой таблицы из соответствующих записей в inventSum. То есть – поскольку при включении механизма IMTS система ОБХОДИТ стандартный механизм транзакций СУБД, то приходится В РУЧНУЮ, на уровне прикладного кода (в классе InventUpdateTTSControl) откатывать изменения сделанные в обход штатного механизма транзакций. При этом таблица InventSumLogTTS фактически выполняет функции самодельного журнала транзакций, в котором протоколируются изменения записей в ходе основной транзакции.
Не смотря на то, что строго говоря, техническая проблема блокировок в этом механизме была преодолена, проблема НЕОПРЕДЕЛЕННОСТИ уровня запасов до завершения транзакции осталась. Ну например – если у нас на складе есть 5 штук изделия, еще 8 штук оприходовано в незавершенной транзакции – можем ли мы дать зарезервировать 10 штук ?
При настройке IMTS можно было указать два режима проверки количеств на складе при выполнении операции списания – оптимистический и пессимистический. В оптимистическом режиме, система при оценке уровня складских запасов считала, что все незавершенные транзакции будут завершены. Если включить этот режим, то система РАЗРЕШАЛА зарезервировать 10 штук в нашем примере. Правда – это порождало другую проблему. Если после выполнения резервирования, приходная транзакция была откачена, то мы получали ситуацию, при которой у нас поставлен резерв на отсутствующий в системе товар. Можно было также включить пессимистический режим проверки остатка. В этом режиме, для оценки уровня запасов система вычитала накопившееся в inventSumLogTTS количество по незавершенным транзакциям из текущего значения запасов в наличии из таблицы InventSum. Ситуацию с резервированием приходуемого товара этот режим разрешал. Правда – возникала проблема с выполнением параллельных операций списания товара. Например – один сейл резервирует товар в рамках незавершенной транзакции. Другой сейл также пытается зарезервировать этот товар. Поскольку система считает, что операция первого сейла не завершится (пессимистический режим), то второму сейлу разрешается поставить себе в резерв тот же самый товар. В результате – опять возникла ситуация при которой зарезервировано больше товара чем есть на складе.
Кроме этих идеологических проблем , IMTS также имел и несколько неприятных технических особенностей. Во первых – судя по всему, разработчики IMTS не были до конца уверены что ядро системы сможет во всех случаях выполнить при откате транзакции код, занимающийся ручным откатом изменений в таблице запасов в наличии (inventSum). В результате, был реализован хитрый механизм, который периодически (периодичность определялась настройками параметров IMTS) пробегает по всем записям в inventSumLogTTS, не помеченным как завершенные (isCommited). Далее – этот механизм пытается определить - активна ли еще та сессия, которая создала эту запись и, если она не активна, этот механизм откатывает соответствующие обновления в таблице inventSum. Причем, механизм этот периодически вызывался при завершении первой попавшейся складской транзакции, в результате чего, время от времени, какая-нибудь безобидная операция типа резервирования одной позиции, почему-то длилась пару минут, поскольку система решила проверить актуальность данных в inventSumLogTTS. Во вторых – настройка режима проверки количеств влияла только на проверку при выполнении списания, но не влияла на кучу отчетов и форм, которые прямолинейно брали количества из таблицы InventSum, работая, таким образом, в оптимистическом режиме. Получалось что в форме “Запасы в наличии” мы видим, что товар у нас на складе есть, а списать мы его не можем, потому что он еще приходуется и включен пессимистический режим оценки запасов.
На своих собственных проектах (на которых я был PM или системным архитектором) я так ни разу и не рискнул включить режим IMTS. Когда я расспрашивал знакомых, отзывы были самыми разнообразными. Кто-то говорил, что все хорошо работает, некоторые говорили что-то типа “иногда барахлит, но жить можно; лучше иногда запасы пересчитывать, чем блокировки терпеть”; кто-то говорил что-то типа “Включили. Попробовали. С ужасом выключили и на всякий случай скрыли форму включения/выключения”. Коротко говоря – в версии DAX 3, эффект от применения механизма IMTS был неоднозначным. К слову сказать – я видел внедрения в логистических и торговых компаниях на 200-250 пользователей с выключенным режимом IMTS. При этом пользователям, конечно действовало на нервы непредвиденное подтормаживание системы, но в проблемы для всего бизнеса это не превращалось, поскольку, существенного замедления бизнес-процессов в целом это не вызывало.
Поскольку при проектировании четвертой версии Dynamics AX, вопросы повышения производительности системы при работе большого количества пользователей были одними из самых первостепенных, разработчики предприняли еще одну попытку (и как мне кажется – на сей раз безоговорочно успешную) разрешения проблем блокировки складских остатков. Из предыдущего опыта понятно что:
1. Блокировать информацию об остатках надолго – нельзя – начинается разрастание дерева блокировок.
2. Обновлять информацию в отдельной транзакции – тоже нехорошо. Отказ обход стандартного транзакционного механизма может порождать больше проблем чем решать…
Посмотрев на проблему свежим взглядом, разработчики (кстати – уже в Microsoft, a не в Navision), сделали простой вывод: Раз мы не можем отказаться от блокировки остатков, надо просто перенести операции блокировки остатков, их проверки и обновления в самый конец транзакции, чтобы блокировка (которая длится до конца транзакции) не длились слишком долго. Сделано это было следующим образом: При обновлении таблицы складских проводок (inventTrans), обновления в inventSum НЕ ПИШУТСЯ. Вместо этого добавляется информация об обновлении в таблицу inventSumDelta и inventSumDeltaDim. При этом делается это в основном соединении и транзакции – дополнительных соединений не открывается в принципе. В самом конце транзакции, перед ее завершением, выполняются следующие действия:
- Одной операцией блокируются ВСЕ остатки в inventSum, подлежащие изменению в результате обновления складских проводок в данной транзакции. При этом блокировка выполняется как атомарное действие, то есть не может возникнуть ситуация при которой мы заблокировали 12 остатков из 20; Затем на 13ом остановились и стали ждать пока чужая сессия освободит этот 13ый остаток, продолжая при этом удерживать первые 12 остатков. Соответственно – не возникает ситуации разрастания дерева блокировок.
- Система пробегает по таблице InventSumDelta (и связанной с ней inventSumDeltaDim) и для каждого обновления проверяет – не случилась ли у нас ситуация отрицательного остатка. Если такая ситуация случилась – выдается сообщение об ошибке и транзакция откатывается.
- После проверки очередной записи в inventSumDelta, система обновляет соответствующую запись в inventSum, а запись в inventSumDelta и inventSumDeltaDim просто удаляет.
Как результат:
- Проблемы, вызванные использованием второго соединения и обходом штатного механизма транзакций СУБД – отсутствуют. В версии 4.0 все обновления inventSum делаются в одном соединении и транзакции.
- Проблемы вызванные разрастанием дерева блокировок (характерные для работы старых версий системы с отключенным IMTS) также отсутствуют, поскольку используется атомарная блокировка СРАЗУ ВСЕХ необходимых для выполнения транзакции ресурсов.
- Проблемы собственно блокировки остатков сведены к минимуму, поскольку блокировка выполняется перед самым завершением транзакции, а операции выполняемые системой после блокировки – легкие, короткие и не требуют для выполнения много времени и ресурсов.
Кстати сказать – для ускорения обновления таблицы inventSum, разработчики использовали direct SQL (То есть – использование запросов к СУБД в native диалекте, в обход стандартного механизма Dynamics AX). В случае MS SQL на сервер отправляется могучий запрос на целый экран с несколькими case и другими, отсутствующими в стандартном SQL-диалекте DAX расширениями. В случае Oracle, обновление inventSum вообще делается хранимой процедурой AxUpdateInventOnHand, создаваемой при первой синхронизации. Хотя такой подход, строго говоря, является нарушением требований best practice, но в данном случае его применение вполне оправдано.
Хочу также заметить, что проверка остатка при завершении транзакции не отменила проверку остатка при изначальном выполнении операции списания. Так что если мы пытаемся зарезервировать или продать товар, которого в принципе нет на складе, нам не придется ждать завершения транзакции чтобы получить информацию о невозможности такого действия.
Единственный возможный минус нового режима блокировки запасов в наличии - это чуть большее количество откатов транзакций, возникающих в тех случаях, когда при первоначальном выполнении списания товар еще был, а при окончательной проверке и коррекции остатка - выяснилось что по дороге этот товар кто-то уже списал (зарезервировал и тд). Большой дополнительной нагрузки на сервер БД эти откаты транзакций создавать не будут, поскольку в реальной жизни такие ситуации случаются не очень часто. Если между сотрудниками существует такая большая конкуренция за товар, то на практике он распределяется организационным образом, а не по принципу - “кто первый встал - того и сапоги”
Фактически - реализованный в DAX 4.0 механизм блокировки остатков является вариацией оптимистического режима блокирования СУБД. В классическом варианте этот режим работает следующим образом:
- При чтении данных внутри транзакции, таковые не блокируются.
- При обновлении данных внутри транзакции, система проверяет - не были ли они обновлены другой сессией после чтения в текущей транзакции. Если такая ситуация возникла - генерируется сообщение об ошибке и транзакция откатывается.
В случае обновления остатков, подобный механизм в чистом виде будет генерировать слишком много ошибок. Поэтому в DAX он реализован таким образом, чтобы в ошибка выдавалась только в том случае, если из за обновлений других сессий складского остатка не хватило для выполнения нашей операции. То есть - контролируется не факт изменения данных другой сессией, а факт захвата критически большого для нашей операции количества другой сессией.
Надо отметить, что оптимистический механизм блокировки информации о запасах не имеет ничего общего с оптимистическим механизмом оценки запасов (из IMTS 3ей версии). Это разные типы оптимизма
В первом случае - оптимистически оценивается вероятность завершения НАШЕЙ транзакции, то есть - если хватило запасов в момент создания складской проводки, то наверное, хватит их и в момент завершения транзации и контроля остатков. Во втором - оптимистически оценивается вероятность завершения ЧУЖИХ транзакций , то есть - если чужая незавершенная транзакция оприходовала товар, то скорее всего эта транзация не будет откачена.
Таблица InventSumLogTTS в 4ой версии DAX осталась, но обновляется она в рамках основной транзакции и используется только для сводного планирования. (Чтобы сводное могло понять, какие складские проводки менялись за последнее время и обновить связанные с этими проводками чистые потребности).
Надо сказать, что разработчикам все-таки пришлось в двух местах системы наступить на горло собственной песне и все-таки дописать кусочек кода, который для расчета остатка использует не только данные их inventSum, но и данные об обновлениях (inventSumDelta), выполненных в рамках ТЕКУЩЕЙ транзакции. Эти места – подстановка складской аналитики в зерезвировании и подбор номера палетты в расширенном складе. Представим себе ситуацию, при которой у нас в заказе есть две строки с одинаковой номенклатурой и мы в рамках одной транзакции резервируем эти строки, рассчитывая что система подберет нам правильные номера партий, присутствующих на складе. В старой версии, система просто подбирала номера партий, пробегаясь по запасам в наличии. Однако – поскольку в новой версии, обновление inventSum отложено до самого конца транзакции, то попытка резервировать по старому алгоритму, приведет к тому, что по второй строке будут зарезервированы те же самые партии что и по первой. Поэтому, разработчикам пришлось при резервировании, использовать данные не только из запасов в наличии, но и из таблицы inventSumDelta. С точки зрения нагрузки на систему и вероятности блокировки это не приводит к негативным последствиям, однако в целом несколько портит концептуальную целостность красивой идеи.
В заключение хочу заметить, что перенести этот механизм на версию 3.0 не представляется возможным, в связи с тем что в старых версиях Dynamics AX отсутствовал метод application.ttsNotifyPreCommit, вызывающийся ПЕРЕД завершением транзакции. Описанный в данной заметке механизм, основан на классе InventUpdateOnHand, вызывающемся из данного метода. В старой версии - были только методы application.ttsNotifyCommit, application.ttsNotifyAbort вызывающиеся ПОСЛЕ завершения транзакции.





июня 11, 2007 at 11:26 дп
Очень интересная статья!!! Добавила мне концептуального понимания логики системы при работе с остатками.
июня 11, 2007 at 11:58 дп
Крутая статья!
Автору высказываю признание и уважение!
июня 18, 2007 at 3:04 пп
А перенести этот механизм в 3 можно? Или сидьно большой геморой?
августа 1, 2007 at 10:07 дп
Благодарю автора за статью! Это важная информация, учитывая, что на проектах часто приходиться модиицировать механизм резервирования. Перенос блокировок в InventSum в конец транзакции решение разумное. Таким же образом решается проблема ошибок ввода данных в DAX - поскольку перепровести документ нельзя, а сторнирование вызывает еще больше вопросов, то с помощью процессирования отодвигается момент его разноски. Тем не менее хотелось бы отметить, что в случае разделения моментов обработки строки в InventTrans и обновления строки в InventSum может иметь место расхождение в данных! При использовании WTS задержка может быть существенной. Получается, что в отчетах и запросах использовать InventSum не целесообразно, т.е. остатки на дату следует считать по оборотам от рождества Христова (начала использования DAX)? В том то и заключался смысл блокировки InventSum, чтобы обеспечить достоверность между остаками товара на складе и оборотами его образующими.
октября 11, 2007 at 8:19 пп
Может кто подскажет в чем может быть проблема?
При разноске складских движений получаю ошибку - База данных SQL обнаружила ошибку”, которая выскакивает на коде
new Connection().createStatement().executeUpdate(statement);
Подробнее тут:
http://axforum.info/forums/showthread.php?p=150662#post150662