среда, 3 февраля 2010 г.

Первые впечатления от StreamInsight (aka Complex Event Processing от Microsoft)

В состав SQL Server 2008 R2 релиз которого намечен на май этого года, входит технология StreamInsight (официальная страница и блог). Несмотря на очевидную ассоциированность, к SQL Server-у эта технология не имеет практически никакого отношения – кроме того факта, что их “положили в одну коробку”.

StreamInsight – это попытка Microsoft выйти на новый для себя рынок Complex Event Processing (в конце поста есть несколько ссылок на информацию о других игроках). Общее описание идея очень сильно напомнило мне системы обработки сообщений: скорее всего из-за наличия потоков, очередей, обработки и прочих схожих концепций. Ключевым, на мой взгляд, является слово “Complex”: предполагается, что мы производим над входным потоком событий некоторые нетривиальные манипуляции, которые позволяют нам осуществлять анализ этого потока в реальном времени (точнее “с известной задержкой”, поскольку зачастую речь идет об операциях, проводимых на каком-то [скользящем] окне наблюдений). Технология доступна пока только для предварительно просмотра, скачать можно отсюда. Дистрибутив компактен (около 10 Мб) и содержит неплохую подборку примеров и обзорную документацию. Последняя, однако, не настолько актуальна, как MSDN.

В обзорной документации все описано простым и понятным языком:

  1. Определяем входной поток событий.
  2. Задаем механизм обработки - на самом деле Linq-запрос, в котором можно использовать некоторые специфичные для потоков событий функции (вроде обращения к “окну”).
  3. Задаем характеристики выходного потока - поток результатов выполнения запроса из п.2 на входном потоке событий.

Обещается масштабируемость “куда хочешь” и параллельная обработка “как хочешь”.

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

  • Для того, чтобы “воткнуть” поток в CEP Engine (который занимается вычислениями/обработкой) необходимо реализовать InputStreamAdapter (который выполняется в контексте того же процесса, который выполняет CEP Engine). Этот адаптер на самом деле является конечным автоматом (aka State Machine). Состояния вроде бы описаны, но машина далеко не так проста, как хотелось бы. Причем со всеми этими переходами приходится быть чрезвычайно аккуратным, ибо в примерах есть грозные предупреждения вида “если не сделать вот этот ‘финт ушами’, то в CEP Engine будет утечка памяти”. Таким образом, для реализации простейшего адаптера приходится писать довольно много не относящегося к делу кода. Аналогичная ситуация и с выходными потоками – тот же конечный автомат и те же проблемы: довольно много сервисного кода на ровном месте.
  • Не до конца понятен механизм типизации событий: она (типизация) вроде бы заложена в сам поток (который является generic-классом), но отсутствует в адаптере потока (который, собственно, и “производит” поток для CEP Engine).
  • Пока не совсем понятно, как все-таки эта штука масштабируется и какая у нее пропускная способность.
  • Также не очень ясно, насколько сложный анализ можно производить: в расчете простейших агрегаций особого (революционного) смысла нет, а вот относительно более сложных расчетов возникают вопросы: как их “скрестить” с Linq? какую величину окна сможет безболезненно (для производительности) сохранять CEP Engine? Ответов я пока не нашел.

Однако “попилим еще” и будет видно. Глядишь, к моменту выхода (или к версии 2 ;-) ) станет более понятно что к чему.

Где еще можно об этом почитать:

HTH,

AlexS

понедельник, 11 января 2010 г.

SqlDependency: размножение дикобразов

Как размножаются дикобразы?

Очень, очень осторожно либо безрезультатно – что-то дикобразов нигде не видно.

(С) “Афера Томаса Крауна”

В связи с переходом на новую работу, на мне “подвисла” одна задача, которую я не успел решить в старом проекте. Связана она была, как нетрудно догадаться из темы, с SqlDependency. Дело получается относительно давнее (вот уже больше двух месяцев как я в этом проекте не участвую), но поскольку недавно стало известно, что проблема решена, я  ставлю “зарубку на память”.

Вкратце предыстория такова: в какой-то момент времени было принято решение использовать SqlDependency для очистки кэша настроек приложения – сценарий достаточно стандартный. Как всегда нестандартными (на первый взгляд) были проблемы, возникшие позже. Настройки хранятся в специальной БД (ConfigDB), к которой обращаются приложения (ASP.NET App, несколько экземпляров потенциально на разных серверах) и [вспомогательные] “сервисы” в виде служб и консольных приложений, запускаемых по расписанию.

В какой-то момент с ConfigDB начинаются проблемы:

  • невозможно сохранить измененные настройки (timeout)
  • измененные настройки не подхватываются клиентами
  • SqlServer начинает ощутимо тормозить (хотя поначалу это не связывается с ConfigDB)
  • журнал ошибок SqlServer-а забивается сообщениями с “жалобами” на query norifications (“The query notifications dialog conversation handle ‘{xxxxxx….}’ closed due to the following error …”, with error code 8490 or 8470)

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

  • создание и использование новой ConfigDB помогает, правда ненадолго – через пару дней/неделю все возвращается на круги своя
  • восстановление ConfigDB из резервной копии занимает очень много времени – база объемом 500 Мб (из которых 400 – это журнал транзакций) при восстановлении на двухьядерной машине с 2 Гб оперативной памяти намертво подвешивает ее (процесс не завершился за 1.5 часа)
  • выясняется, что в ConfigDB очень много открытых подписок (query notification) и их количество растет

Проблему в итоге удалось устранить путем:

  1. Вдумчивого чтения и переработки кода, который работает с объектами SqlDependency.
  2. Закрытия всех открытых подписок (об этом ниже).

По ходу работы пару раз слышались голоса “SqlDependency – это зло”, “и зачем вы вообще это используете”, “да никогда и ни в жизни не пользовать ЭТО”, “лучше сделать все ‘руками’”. В итоге скептики были посрамлены, механизм работает как и ожидалось, проблему устранили. Но должен признать, что работать с SqlDependency необходимо очень осторожно и внимательно.

Итак, о чем нужно помнить при использовании этой технологии:

  • Строго следите за тем, чтобы на каждый вызов SqlDependency.Start приходился вызов SqlDependency.Stop – об этом явно написано в MSDN, но на моих глазах (в 2х случаях из 2х) об этом “забывали”, а потом долго искали решение возникших проблем. Хорошим решением будет обернуть всю работу с SqlDependency в некий класс (отвечающий за кеширование/инвалидацию), реализаций IDisposable, и помещение вызовов Start и Stop в конструктор и Dispose соответственно.
  • В обработчике события SqlDependency.OnChange не ленитесь анализировать причину вызова обработчика (SqlNotificationEventArgs.Info). Игнорирование этого простого совета может привести (и рано или поздно приводит) к возникновению эффекта, похожего на "положительную обратную связь”: когда на сервере что-то пошло не так, вызвался обработчик, мы тут же создали новую подписку, добавив тем самым проблем серверу, который тут же снова вызовет наш обработчик.
  • Не забывайте о протоколировании – наличие информации о том, когда и с какими параметрами вызывается обработчик события OnChange здорово упрощает диагностику.
  • Проводите мониторинг SqlServer-а. Отслеживание количества активных подписок (select count(*) from sys.dm_qn_subscriptions) позволит вам на самых ранних этапах обнаружить “утечку” и устранить ее пока это не переросло в БОЛЬШУЮ проблему. Обычно именно мониторингом все склонны пренебрегать.
  • Считайте количество подписок, которые будут регистрироваться на сервере в ходе нормального (штатного) функционирования вашего приложения. Десятки подписок – не проблема. При сотнях стоит уже быть ооочень осторожным. Если же расчетное количество подписок исчисляется тысячами, то следует подумать о изменении архитектуры с целью уменьшения этого числа.
  • Не самой лучше идеей будет [активно] использовать этот механизм в базе данных с высокой нагрузкой.
  • Если что-то пошло не так, помним о возможности “убить” подписку: KILL QUERY NOTIFICATION SUBSCRIPTION <subscription id/ALL>.

UPDATE: как было совершенно справедливо замечено Airex-ом, при использовании SqlDependency в контексте ASP.NET, наилучшим местом для вызова SqlDependency.Stop является Application_End (в Global.asax). И не стоит надеяться на деструктор того класса, в который вы “запрячете” всю работу с SqlDependency – в этой связи немного обновил (уточнил) первый пункт выше.

HTH,

AlexS

вторник, 5 января 2010 г.

Enum.Parse vs string comparison

В ходе code review столкнулся с подобным кодом:

if ((SomeEnum)Enum.Parse(typeof(SomeEnum), value) == SomeEnum.SomeValue)
{
// делается что-то полезное
}

И так много раз подряд. Выглядит громоздко и не слишком элегантно. К тому же меня стало терзать сомнение что Enum.Parse – не самый быстрый метод. Первой мыслью было заменить на что-нибудь вроде такого:

if (value == SomeEnum.SomeValue.ToString())
{
// делается что-то полезное
}

Но потом решил проверить. Так вот, первый вариант работает в 3-4 раза быстрее второго. Использование Reflector-а позволяет узнать, что внутри Enum.Parse кэшируются значения перечислений и в результате со второго вызова вся эта операция сводится к поиску значения в хэш-таблице. Преобразование элемента перечисления к строке (второй вариант) оказывается гораздо сложнее и использует рефлексию.
Ситуация, однако, кардинально меняется, если value содержит значение, которому нет соответствия SomeEnum. При этом выбрасывается исключение (которое в оригинальном коде никак не обрабатывается) и выполнение кода существенно замедляется – в 30 раз по сравнению с вариантом №2.
Резюме: если вы уверены в том, что в подавляющем большинстве случаев к вам придет допустимое значение, то используйте Enum.Parse - думаю 3-4х кратный прирост производительности компенсирует некоторое ухудшение читаемости кода.

HTH,
AlexS

суббота, 31 октября 2009 г.

Dev magic и прочий ooops

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

DevMagic

Как может заключить "простой пользователь" взглянув на это окно, магии сделано уже 4655 из потенциальных 31820. Типа "побольше магии хорошей и разной". Что за магия и к чему она применяется - непонятно. Впрочем, может оно и к лучшему - меньше поводов волноваться.

Конечно же это не более чем баловство (от которого именно в этом конкретном случае можно, и даже нужно, было бы отказаться), но оно поднимает более важный вопрос об отношении к тому, что ты делаешь и как. На эту тему интересно высказался Jeff Atwood в своем посте о сообщениях об ошибках в Google Chrome, и я его поддерживаю - серьезное и вдумчивое отношение к продукту своей работы совершенно не обязательно должно выражаться в стандартизированной скуке всего и вся.

 

HTH,

AlexS

воскресенье, 18 октября 2009 г.

Объединение истории сообщений Skype

В последние полтора года я стал достаточно активно использовать Skype в качестве IM-клиента. Причем корпоративная политика отразилась и на персональных предпочтениях, ведь привычка - вторая натура. Тем более, что начиная с версии 4 Skype стал достаточно удобным в повседневном использовании (до этого я очень плохо с ним уживался).

В связи со сменой работы возник вопрос синхронизации истории сообщений между профилем, который я забрал с работы и личным профилем, который хранился дома. Ну или экспорт/импорт (в крайнем случае). Поясню: я использую одину и ту же учетную запись дома и на работе. Частенько получается, что начинаешь разговор с кем-то с работы, а заканчиваешь его дома. В результате история сообщений разорвана между двумя (а то и больше) компьютерами. Гугл мало чем смог помочь: есть плагин к Skype, который автоматом сохраняет все звонки и исообщения в гуглопочте - неплохо, конечно, но во-первых избыточно, во-вторых за деньги ($25 может и немного, но нет, спасибо). Еще на форуме Skype можно найти пару ссылок на программы, которые могут вытащить историю и выгрузить ее в txt или html. Тоже не совсем то. Пришлось разбираться самому.

На проверку все оказалось не так плохо (хотя и несколько запутано):

  • для хранения профиля (в том числе и истории звонков и сообщений) Skype использует базу данных SQLite - что хорошо (ибо эта СУБД открытая и для нее имеется .NET Provider)
  • схема не то чтобы сложная (16 таблиц), но о связях приходится догадываться (ибо внешние ключи не определены) и таблицы довольно "широкие" (до 40 полей)

За выходные в неспешном режиме написал программулину, которая делает то, что нужно (ну или почти): копирует сообщения из одного профиля в другой (если их там еще нет). Выглядит сиё чудо инженерной мысли следующим образом:

image

Скачать его можно отсюда (выложил на SkyDrive на случае если кому еще пригодится).

Что следует иметь в виду, в ходе синхронизации:

  • профили должны быть от одной версии Skype (строго, ибо даже между v.4.0 и v.4.1 схема имеет некоторые различия)
  • профиль хранится в каталоге %user dir%\AppData\Roaming\Skype\<your skype name>\main.db
  • перед внесением изменений в целевой профиль, делается его резервная копия (так что в случае чего его можно вернуть)
  • свойства чата не обновляются - так что в списке "последних разговоров" можно не увидеть действительно последние разговоры, которые были добавлены в ходе синхронизации (но в базе они будут)
  • иногда происходит дублирование сообщений (не всегда) - я так и не смог надежно определить условия, при которых это происходит
  • в некоторых случаях в истории появляется сообщение "это сообщение было удалено из чата" - тоже не смог понять из-за чего (подозреваю что где-то не хватате связанной записи ... но вот где?)

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

HTH,

AlexS

понедельник, 28 сентября 2009 г.

Неочевидный redirect

На днях коллега ((C) Серега К.) натолкнулся на такую любопытную особенность HttpResponse.Redirect. Предположим, что у нас где-то в коде страницы или компонента ASP.NET есть такой-вот код, вполне на первый взгляд логичный, и не вызывающий бурю протеста:

try

{

     // что-то делаем

     Response.Redirect("url1");

}

catch (Exception ex)

{

     // Обрабатываем все на свете

     Response.Redirect("url2");

}

А теперь вопрос: куда будет перенаправлен запрос в результате нормального (т.е. без исключений) выполнеия кода? url1? А вот и нет - на самом деле url2. Все дело в том, что внутри HttpResponse.Redirect для прекращения обработки запроса вызывается Thread.CurrentThread.Abort. Таким образом наш обработчик "всего на свете", отловит это исключение и перенаправим запрос на url2.

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

Redirect(string url, bool endResponse)

второй аргумент следует установить в false.

 

HTH

AlexS

суббота, 8 августа 2009 г.

За что я не люблю визуальные дизайнеры и case-средства для разработки БД

  1. Не дают полного контроля над тем, что происходит с БД.
    Особенно это неприятно/опасно в случае, когда с помощью визуального дизайнера редактируется таблица, в которой есть данные - во многих случаях изменения потребуют пересоздания таблицы. Да, SQL Server Management Studio (в частности) во-первых будет сохранять данные, а во-вторых по-умолчанию запрещает вносить подобные изменения, но все-равно ... я предпочитаю вносить подобные изменения сознательно и сохранить соответствующий скрипт "для будущих поколений" (ибо он наверняка понадобится).
  2. Задают (слишком) много параметров "по-умолчанию".
    Чем больше таких параметров - тем больше вероятность забыть о них, что чревато проблемами. А чем позднее мы обнаруживаем эти проблемы - тем дороже стоит их решение.
  3. Не слишком-то экономят время.
    Нечасто приходится разрабатывать схему большой БД "с нуля". И тем более, это никогда не пироисходит "все и сразу" - схема появляется постепенно. А коли так, то я быстрее напишу SQL код "вручную", чем "дизайнер + генерация + доработка напильником". Если мне нужна картинка, то reverse engineering по уже созданной (из скриптов) базе никто не отменял.
  4. Не способствуют изучению SQL.
    Это, конечно, так себе аргумент, но тем не менее - знание SQL еще никому не мешало, а использование дизайнеров развращает.

HTH,

AlexS