вторник, 17 февраля 2009 г.

О бюрократии и .NET Remoting

Как и у любой технологии, у .NET Remoting есть свои недостатки, особенно при неправильном использовании.

Систему, которую я сопровождаю, использует всего лишь 50-70 пользователей. Сервер приложений один, failover или load balancing нет, клиентские приложения друг с другом не общаются, все работает в локальной сети... вообщем, мне не очень понятно, зачем вообще разработчики решили использовать Remoting – вроде обычного клиент-сервера бы хватило... Ну, решили, и ладно. Наверное, индусы хотели сделать по-пацански – отделить бизнес-логику от интерфейса. Правда, все равно ни фига не отделили на самом деле, но речь не о том.

Прикол в том, что некоторые серверные объекты кэшируются на клиентской стороне, и если я обновляю сервер приложений, то нужно обязательно обновить и клиенские программы (у всех пользователей)! Иначе они что-то ругаются насчет сериализации. У меня не было времени разобраться - наверное, эту проблему можно решить. Но просто смешно получается – часть бизнес-логики якобы отделена от клиента, но на самом деле клиенты жестко привязаны к серверу, и даже малейшее изменение в серверном коде приводит к ошибке.

А вторая потенциальная проблема с Remoting – это производительность... хотя тут можно поспорить, что вообще-то "все дело в ручках". Итак, вот моя грустная история...


Есть некая форма для конфигурирования системы, она позволяет задавать ключевые даты в производственном цикле (например, "Книга подписана в печать", "Отправлена на склад", "Протестирована на безопасность" и пр.). По идее, пользователи должны регулярно проверять, все ли там настроено правильно. Но то ли кто-то из менеджеров уволился, то ли еще что-то – вообщем, почти год никто ее не контролировал. В результате постепенно накапливалось все больше и больше ошибок в графиках производства соответствующих книг, их приходилось исправлять вручную.

Ну, вроде не беда, потому что форма-то умная. Когда ты исправляешь или вносишь какой-то параметр, то она задним числом пересчитывает все, что нужно. Да только за этот год бездействия уже накопились десятки тысяч несоответствий, и форма просто зависала, не справившись с таким backlog. Мне поставили задачу – улучшить производительность, чтобы пользователи смогли внести изменения за разумное время (не больше нескольких минут ожидания).

Я заглянул в исходники и ужаснулся. Нет, оно вроде как все круто – ООП, паттерны, но для каждой книги делаются десятки вызовов хранимых процедур. Т.е. очень много логики реализовано не на стороне базы данных. Это не проблема, когда мы работаем с одной книгой. Но когда тот же самый код используется для нескольких тысяч книг – вот тут-то начинаются проблемы. Индусы, конечно, об этом не подозревали - ведь когда они разрабатывали систему, база данных была маленькая.

Хотелось все нафиг переписать, но у меня было слишком мало времени. А главное, я не очень понимал, как оно все должно работать. Спецификация написана несколько лет назад, с тех пор систему меняли... Посоветовался со старшим товарищем, и он предложил мне просто кое-что немного оптимизировать, не переделывая по сути. Какая, собственно, разница – ведь мы говорим об одноразовой операции (пересчитать ключевые даты задним числом). А по плану через годик эту систему вообще должны заменить другой, так что нет смысла сильно напрягаться.

Так я и сделал - оптимизировал одну хранимую процедуру, и результат вроде был прекрасный. Производительность выросла в 10-20 раз, я был очень доволен собой. Протестировали, собрали все подписи, внедрили... Менеджер начинает вносить ключевые даты, вроде уже большую часть внес...

И тут у него начинает валить ошибка: Distributed transaction completed. Either enlist this session in a new transaction or the NULL transaction. Какая такая distributed transaction? Моя первая мысль - это что-то связанное с two-phase commit. Но у нас же всего лишь одна база данных... Быстрый поиск в Google подсказал идею: на самом деле сообщение об ошибке не несет в себе полезной информации, .NET Remoting ее скрывает; а истинная причина – таймаут.

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

Так я и сказал начальнице. Но она подняла панику: "Все пропало, ты все сломал, надо откатывать релиз! Я не верю, что это таймаут." Вообщем, я потратил пару дней на отработку других гипотез, но как часто бывает, первая мысль оказалась самой верной. Это действительно был таймаут. Причем он не имел отношения к распределенной транзакции между двумя базами данных – таймаут был прописан для транзакций COM+.

Главная проблема в том, что на тестовом сервере все работает, не удается воспроизвести ошибку. Я специально вставил задержку в свою процедуру... ага, теперь есть точно такая же ошибка, как у пользователя. Сначала я попросил DBA увеличить таймаут в настройках MSDTC. Хм, не помогает, блин – ровно через 5 минут после запуска валится ошибка. Оказалось, индусы использовали атрибут System.EnterpriseServices.TransactionAttrubute. Эти "5 минут" были тупо забиты в тексте программы, а не вынесены в конфигурационный файл.

Что же теперь делать? Увеличить таймаут просто так не могу, потому для этого надо перекомпилировать мой сервер приложений. А если я это сделаю, то перестанут работать все клиенты (смотри выше, клиенты жестко привязаны к определенной версии сервера). А переставить клиентов – это вообще кошмар: для этого нужно снова собирать разрешения со всяких менеджеров, а потом ждать недели три, пока Desktop Team соизволит выполнить мой заказ.

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

Начальница категорически говорит "нет": данные принадлежат пользователям, программисты не имеют право их менять самостоятельно, потому что это противоречит Sarbanes-Oxley и т.д. Я все это понимаю, но в данном случае ситуация доведена до абсурда. Фактически нужно ввести одну дату и нажать кнопку "Сохранить", остальное делается автоматически. Какая разница, кто это сделает? Мне не нужно напрягать мозги, что-то там считать... весь вопрос только в том, кто нажимает кнопку "Сохранить".

Ну ладно, нет так нет. Вторая идея: я увеличиваю таймаут, но устанавливаю новую версию программы только на компьютере пользователя (и то временно). Он нажимает кнопку "Сохранить", и я ему ставлю опять старую версию. Этот вариант начальнице тоже не нравится.

Выход у меня остается один – думать, как оптимизировать какую-нибудь хранимую процедуру или создать новый индекс, чтобы уложиться в эти злосчастные 5 минут. С помощью SQL Profiler выяснил, что одного индекса действительно не хватает.

Создал – производительность улучшилось, укладываюсь в 2 минуты. Но достаточно ли этого? Я ведь не знаю, насколько быстро будет на рабочем сервере; может, там будет 10 минут. Кстати, странно – почему на тестовом сервере 2 минуты, а если и база, и сервер приложений на моем компьютере, то всего 30 секунд? У меня что, компьютер быстрее сервера?

Всего запускалось 11 тысяч запросов. Пытаюсь оптимизировать дальше. И вдруг понимаю удивительную вещь: все эти 11 тысяч запросов выполняются всего лишь за несчастных 8 секунд! Остальное время (почти две минуты) уходит на общение между клиентом и сервером приложений через .NET Remoting! Вот это оказалось узким местом. Вот почему на моем компьютере работает в 4 раза быстрее – не нужно слать всякую ерунду по сети.

Т.е. оптимизировать что-то дальше бесполезно. Даже если все 11 тысяч запросов будут выполнятся мгновенно, это мне почти не поможет. Мне остается только создать индекс и надеятся, что этого будет достаточно.

Но не все так просто! Нельзя просто так взять и создать индекс – "пользователи сначала должны его протестировать". Говорю начальнице – как протестировать? Ввести ту самую злосчастную дату на тестовом сервере, нажать "Сохранить" и убедиться, что заработало быстрее? Но ведь я только что сделал то же самое!

Она краснеет, ей тоже понятно, что это идиотизм, и риск при создании нового индекс близок к нулю, и это вопрос скорее к DBA, а не к пользователям... но правила есть правила. Пытаюсь отловить пользователя, что он "протестировал" и дал добро. А он то занят, то снег в Лондоне пошел, и неделю на работу никто не ходил... за эту неделю накопились еще новые несоответствия в базе, и процедура работает еще медленее. К счастью, после долгих мучений мне повезло. Мы уложились в 4 минуты.

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

Кроме описанных здесь проблем с .NET Remoting, был еще один интересный момент: за те полтора месяца, которые прошли между тестированием и внедрением системы, появились тысячи новых несоответствий в базе данных. Просто потому, что тестировали мы перед Рождеством, в тихий период. А к концу января пользователи уже навводили кучу новой информации, и это тоже сильно повлияло на произодительность. Так что я извлек для себя важный урок: бывают случаи, когда ценность проведенных тестов резко уменьшается с течением времени.

4 комментария:

eter-de-panji комментирует...

У меня был случай когда мы ради такой задачи клиентскую машину втыкали напрямую без к серверу приложений для проведения разовой операции.

Z комментирует...

хорошая история!
первая мысль котроая пришла - создание индекса. хотя бы временно.
Да, иногда поговорка "семь раз отмерь один раз отрежь " не верна. Разум должен торжествовать над бюрократией, иначе бизнес загнётся.

Анонимный комментирует...

почти детективная история :) с системой бороться всегда тяжело

Z комментирует...

поделюсь ещё одной историей. Без названий - в одной компании где я работал, за основу был принят принцип централизации всех систем. Счета выставлялись 6 раз в месяц и на высталение счетов (с распечаткой) уходило больше суток. Т.е. реально принтер печатал больше суток, расходуя при этом сотни тысяч листов. И так по всей России. И одна мелкая ошибка или проблемы с сетью приводили к тому, что очередь надо было стартовать заново... а наши конкуренты, тем временем, печатали счета каждый в своём филиале... и не дули в ус..

А вот и ещё одна история про MSSQL и узкие места в нём. Уже в той компании, где я сейчас работаю. Если есть коннект хотя бы у одного юзера к таблице (а это может быть боевая система отчётности или любой из аналитиков), то не пройдёт служебный процесс maintain DB, который не пропустит ночной ETL, который по одной завалит все системы отчётности, job`ы, загубит почту алертами и утром, когда придёшь на работу... ты тысячу раз пожалеешь, что просто открыл вчера список таблиц, который и создал эту долбанную сессию :)

Ratings by outbrain