Оптимизация процессинга в Windows Azure. Часть 1.

Для тех кто не в курсе: Windows Azure – “облачная” платформа Microsoft. Создавая приложения, работающие “в облаке”, у вас есть возможность разделять систему на “роли”. Бывают веб-роли, которые представляют из себя обычные веб-приложения,  бывают также worker-роли (далее воркеры), предназначенные для вычислений.

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

Типовой код для воркера Windows Azure на C# такой:

while (true)
{
    var msg = queue.GetMessage();
    if (msg != null)
    {
        //do some work
        queue.DeleteMessage(msg);
    }
    else
    {
        Thread.Sleep(10000);
    }

    Trace.WriteLine("Working", "Information");
}

Как вы думаете сколько стоит этот воркер. В смысле реальных денег потребляемых таким приложением, развернутым на Windows Azure.

Для этого надо посмотреть цены: https://www.windowsazure.com/en-us/pricing/details/.

Если задеплоить такую роль в одном small экземпляре, то получится $2,88 в день/$86,4 в месяц/~2600 рублей в месяц. Так? А вот и нет…

Есть еще “скрытая” стоимость такой архитектуры, заключается она в том что транзакции к хранилищу тоже оплачиваются https://www.windowsazure.com/en-us/pricing/details/#storage. Всего  $0.01 за 10,000 транзакций. Каждая транзакция – это один запрос к azure storage.

Код выше выполняет один запрос каждые 10 секунд даже если нету никаких сообщений в очереди.
Стоимость такого кода получается 60*60*24*30/(10 * 1000) = $25,92 в месяц. вместе со стоимостью compute hours это выходит $112,32 в месяц. И это даже если код не выполняет никакой работы!

Кроме того SLA гарантирует работоспособность роли 99,95% только при наличии минимум двух инстансов, так что для устойчивости надо еще умножить цену на 2. Итого $250 в месяц.

Вывод

Архитектура, которую предлагает Microsoft для масштабирования довольно дорого стоит. Используйте код из примеров очень осторожно, он может увести ваш проект в большой минус.

Что делать?

Вариант первый – использовать service bus, в нем тоже есть очереди, но API позволяет в одной транзакции ожидать сообщения, а не сразу null возвращать при его отсутствии.

Вариант второй – использовать адаптивную подстройку интервала опроса очереди и выключать опрос в случае отсутствия сообщений.

Второй вариант кажется хорошей идеей так как позволяет масштабировать подход как “вниз”, так и “вверх”. Но тут возникает вопрос, а если мы прекратим прием сообщений, то как его потом возобновить? Видимо надо передать сообщение… Приходим снова к той же проблеме.

Но сигнал к “пробуждению” читателя сообщений можно передавать по более дешевому каналу, например через wcf internal endpoint.

Реализация

Чтобы абстрагироваться от всех деталей с сообщениями, таймаутами и каналами удобно использовать библиотеку Rx. Я использую Experimental версию так как в ней собрано много нужных комбинаторов.

Для начала надо вписать код в концепцию Rx. Длительные операции, вроде вызовов методов Cloud Storage и тайматуов сделать в виде IObservable.

public static IObservable<CloudQueueMessage> ObserveMessages(this CloudQueue queue)
{
    return Observable.Create<CloudQueueMessage>(obs => Iterator(obs, queue));
}

private static IEnumerable<IObservable<object>> Iterator(
                                                    IObserver<CloudQueueMessage> result, 
                                                    CloudQueue queue)
{
    //Observable queue.GetMessage
    var getMessage = Observable.FromAsyncPattern<CloudQueueMessage>(
                                        queue.BeginGetMessage,
                                        queue.EndGetMessage);
    //Observable queue.DeleteMessage
    var deleteMessage = Observable.FromAsyncPattern<CloudQueueMessage>(
                                        queue.BeginDeleteMessage,
                                        queue.EndDeleteMessage);

    while (true)
    {
        //var msg = queue.GetMessage();               
        var msgObs = getMessage().ToListObservable();
        yield return msgObs;
        var msg = msgObs[0];

        if (msg != null)
        {
            //do some work
            result.OnNext(msg);

            //queue.DeleteMessage(msg);                     
            yield return deleteMessage(msg).ToListObservable();
        }
        else
        {
            //Thread.Sleep(10000);
            //Same pattern as above
            yield return Observable.Timer(TimeSpan.FromSeconds(10))
                                   .ToListObservable();
        }

        Trace.WriteLine("Working", "Information");
    }
}

Теперь надо немного изменить код, сделав таймаут адаптивным.

Функция вычисления таймаута:

private static TimeSpan CalulateDelay(int idleCount, int minimumIdleIntervalMs,  int maximumIdleIntervalMs, int deltaBackoffMs)
{
    // Calculate a new sleep interval value that will follow a random exponential back-off curve.
    int delta = (int)((Math.Pow(2.0, (double)idleCount) - 1.0) * (new Random()).Next((int)(deltaBackoffMs * 0.8), (int)(deltaBackoffMs * 1.2)));
    int interval = Math.Min(minimumIdleIntervalMs + delta, maximumIdleIntervalMs);

    // Pass the calculated interval to the dequeue task to enable it to enter into a sleep state for the specified duration.
    return TimeSpan.FromMilliseconds((double)interval);            
}

Честно украдена отсюда.

Сам код воркера:

var idleCount = 0;
while (true)
{
    var msgObs = getMessage().ToListObservable();
    yield return msgObs;
    var msg = msgObs[0];

    if (msg != null)
    {
        idleCount = 0;

        //do some work
        result.OnNext(msg);

        yield return deleteMessage(msg).ToListObservable();
    }
    else
    {
        var delay = 
                CalulateDelay(idleCount++, 
                              MinimumIdleIntervalMs, 
                              MaximumIdleIntervalMs, 
                              100);
        if (delay.TotalMilliseconds >= MaximumIdleIntervalMs)
        {
            yield break;
        }

        yield return Observable.Timer(delay).ToListObservable();
    }
}

Выключать цикл опроса сообщений мы научились, теперь попробуем научиться его включать. Будем считать что “внешний раздражитель”, который будет будить цикл выборки сообщений, выглядит как IObservable<T>.

public static IObservable<CloudQueueMessage> ObserveMessages<T>(
                                                this CloudQueue queue, 
                                                IObservable<T> haveMoreMessages)
{
    var iterator = Observable.Create<CloudQueueMessage>(
                                  obs => Iterator(obs, queue));
    IDisposable subscription = null;

    return Observable.Create<CloudQueueMessage>(
        obs => haveMoreMessages.Subscribe(
            _ =>
            {
                if (subscription == null)
                {
                    subscription = iterator.Subscribe(
                                                obs.OnNext, 
                                                obs.OnError, 
                                                () => subscription = null);
                }
            }, 
            () => subscription.Dispose() ));
}

Код получился запутанный, но при некоторой сноровке читается очень хорошо.

На сегодня все. В следующей части я расскажу как сделать  пробуждение воркеров по сигналу и какими еще способами можно оптимизировать стоимость решения для Windows Azure.



Применимость DDD

До сих пор не утихают холивары на тему DDD\rich vs anemic. С одной стороны апологеты DDD (domain driven design, дизайн на основе предметной области) твердят о том как это круто, с другой стороны говоря что оно не везде подходит. На вопрос где же оно подходит обычно затрудняются ответить или отвечают “for compex domain”, причем примеров применения такого встретить непросто.

 

Попробуем разобраться. Если отбросить всю философскую шелуху DDD, то придем к очень простой концепции жирной (rich, насыщенной) модели, описанной Фаулером. С одной стороны Фаулер предлагает поместить логику в классы “сущностей”, соответствующие данным предметной области. С другой стороны он прекрасно понимает что логика будет сложна и надо каким-то образом декомпозировать её. Кроме того есть логика, которая оперирует более чем одной сущностью и поместить её в один из классов сущностей не выгодно. Таким образом создаются классы сервисов, стратегий итп. Их всех объединяет свойство, что они не содержат данные предметной области и для работы обращаются к классам сущностей. По сути вся сложная логика располагается в этих самых сервисах.

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

Тут стоит отступить назад и посмотреть на общую картину. Как выглядит код приложения, использующий жирную модель (на основе модели в статье Фаулера):

class ContractsController
{
    public Result CalculateRecognitionsAction(int contractId)
    {
        var contract = repository.GetById(contractId);
        contract.CalculateRecognitions();
        repository.Save();
    }
}

Contract.CalculateRecognitions свою очередь выглядит как-то так:

public void CalculateRecognitions()
{
    this.Product.CalculateRecognitions(this);
}

А Product.CalculateRecognitions выглядит так:

public void CalculateRecognitions(Contract c)
{
    var recognitions = recognitionService.CalculateRecognitions(this);
    c.SetRecognitions(recognitions);
}

Теперь попробуем выполнить простое преобразование: на верхнем уровне будем вызывать сервис, а не методы сущностей.

class ContractsController
{
    public Result CalculateRecognitionsAction(int contractId)
    {
        var p = repository.GetProductByContractId(contractId);
        var recognitions = recognitionsService.CalculateRecognitions(p);
        repository.SaveRecognitionsForContract(contractId, recognitions);
    }
}

Такой код меньше по объему, имеет меньшую связность межу классами, от этого он более гибок и лучше подается оптимизации.

Сдвиг предмета моделирования

Если посмотреть какие концептуальные изменения произошли в коде в примере выше, то становится понятно что центральным объектом у нас стал RecognitionsService, а не Contract. То есть вместо модели исходных данных (предметной области) задачи мы начали использовать модель решения этой задачи. Модели исходных данных никуда не делась, просто её роль в решении стала гораздо меньше. Такая модель называется стройной (anemic).

Очевидно что модель решения задачи для решения задачи подходит лучше чем модель исходных данных задачи. Ведь моделировать экскаватор правильнее чем писать земля.Копайся() (спасибо за метафору Sinclair). Причем чем сложнее задача, тем выгоднее строить модель решения, а не исходных данных (предметной области).

Таким образом DDD не подходит для сложных задач.

Лирическое отступление

Если вы думаете что вынося всю логику из класса Product в ProductService\ProductHelper\ProductManager вы получаете модель решения, то вы жестоко ошибаетесь. Фактически это будет DDD, только в худшем его проявлении.

Сложные предметные области

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

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

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

И снова такое решение гораздо боле гибкое,чем жирная модель. Эти же валидаторы можно приспособить для валидации ввода пользователя, для возвращения ему осмысленных ошибок, а не исключений. Кроме того можно предусмотреть сохранение “черновиков” во внешнем хранилище, которые не проходят всех правил.

Так где же область применимости DDD?

Как не удивительно, но DDD хорошо работает на простых задачах, там где нет необходимости разделять систему на слои, уменьшать связность путем выноса логики в отдельные стратегии итд. В простых задачах модель исходных данных (предметной области) и модель решения почти совпадают.

Например взять блог. Обычный блог как например этот. Моделью предметной области являются: блог, посты, комменты, страницы, виджеты в интерфейсе. Решение включает в себя функции: создать пост, изменить пост, удалить пост, получить список, поменять настройки блога. Все эти функции вполне можно поместить в класс Блога, а умный ORM разберется как потом все изменения положить в БД.

Но почему DDD столь популярен?

Именно потому что DDD хорошо работает на простых примерах. Ведь в книгах и презентациях невозможно привести пример на 20kloc. А на примерах в 100 строк DDD выглядит очевидно и очень привлекательно.

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

Заключение

Не стоит слепо следовать DDD. У догматичного DDD очень узкая область применения. Старайтесь моделировать решение задачи, не зацикливайтесь на моделировании исходных данных. Всегда оценивайте как то или иное решение повлияет на качество кода безотносительно DDD, ООП или других баззвордов.



В поисках неподвижной точки

Наверное все знают что в C# 3.0 лямбда выражения, которые позволяют записывать анонимные функции (то есть функции без имени).
Например:

seq.Select(x => x * x);

Выражение x => x*x является функцией одного аргумента и возвращает значение квадрата числа.

А теперь попробуем  записать функцию факториала:

Func<int, int> f = x => x > 1 ? x * f(x - 1) : 1;

Компилятор C# такое выражение не компилирует. Ругается на неинициализированную переменную f в правой части. Кстати roslyn такое прожевывает нормально. Тем не менее код выше не является выражением, его нельзя передать параметром в функцию.

Попробуем превратить его в выражение.

fact = f => x => x > 1 ? x * f(x - 1) : 1;

Тип выражения справа получится Func<Func<int,int>, Func<int,int>>, параметром передается рекурсивный вызов, чтобы его сформировать надо снова подставить рекурсивный вызов в функцию.  Получится что-то вроде бесконечного вызова
fact(fact(fact(fact(…. но реально число вызовов конечно.

Немного теории

Для функций f которая принимает аргумент и возвращают значения из одного и того же множества (на C# это записывается как Func<T,T>, а в математике T –> T) может существовать “неподвижная точка” x, для которой f(x) = x. Чтобы находить неподвижные точки можно построить комбинатор g:(T->T)->T, такой что g(f) = x и f(x) = x. Функция g называется комбинатором неподвижной точки.

В лямбда исчислении есть теоремы доказывающие существование неподвижных точек у некоторых функций и формулы  комбинаторов. Не все формулы можно перенести в типизированные языки.

Краткая формула для рекурсивного комбинатора неподвижной точки Y выглядит как Y(g) = g(Y(g)). Если выполнить подстановку то получится g(g(Y(g))), выполняя подстановку бесконечное число раз получим как раз то что нам нужно для факториала.

Вернемся к практике

Попробуем написать на C#

static T Y<T>(Func<T, T> f)
{
    return f(Y(f));
}

Но язык C# использует энергичные вычисления и Y-комбинатор сразу попытается посчитать бесконечную рекурсию. Что приведет к StackOverflowException.

Ленивость вычислений как всегда вводится через лямбды.

static Func<T, T> Y<T>(Func<Func<T, T>, Func<T, T>> f)
{
    return x => f(Y(f))(x);
}

После этого вполне можно написать следующий код:

var fact = Y<int>(f => x => x > 1 ? x * f(x - 1) : 1);

Или например

seq.Select(Y<int>(f => x => x > 1 ? x * f(x - 1) : 1));

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

Заключение

Знание фундаментальной теории очень помогает писать программы и зачастую дает возможность улучшить их крайне неожиданными способами. Изучение таких тем никогда не будет лишним багажом.



Октябрьская встреча Russian SharePoint User Group

Ссылка на анонс на сайте RUSUG: http://rusug.net/News/Lists/Posts/Post.aspx?ID=64

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

  1. Настройка поиска SharePoint Server
  2. Out-of-box возможности поиска
  3. Кастомизация out-of-box функциональности
  4. Использование серверного API поиска
  5. Использование клиентского API поиска в sandboxed решениях
  6. Архитектура поиска SharePoint Server

Все темы в пределах одного доклада не смогу рассказать, но 3-4 из них вполне могу пройти достаточно подробно.

Пишите пожелания в комментах. Приходите на встречу. Но даже если вы не сможете прийти на встречу, то будет видеозапись докладов и ваши пожелания будут учтены.



Почему вам нужен SharePoint

Последнее время на различных конференциях я слышу один и тот же вопрос:

А зачем мне нужен SharePoint?

Вопрос банальный, частота его задавания связана с тем что очень много крупных и не очень компаний получают SharePoint вместе с различными пакетами программ Microsoft. Но вразумительных ответов на этот вопрос я пока не слышал.

Ниже “краткий” ответ, может быть вы найдете что-нибудь для себя.

  • Если вы руководитель или ИТ-директор компании более 5 человек и
    • Используете  MS Office
    • Отправляете документы по электронной почте
    • Использует расшаренные папки для хранения документов в электронном виде
    • У вас есть процессы согласования и утверждения документов
    • Храните данные в Excel или Access
    • Хотите создать базу знаний
    • Хотите развернуть helpdesk
    • Пользуетесь средствами средствами Microsoft BI
    • Хотите отображать данные из разных источников в одном месте
    • Хотите развернуть корпоративный портал для сотрудников

    если одно из вышеперечисленного верно, то вам однозначно нужен SharePoint. Он поможет вам создать единое хранилище документов и табличных данных с богатыми возможностями отображения и поиска. SharePoint позволяет создавать решения без среды разработки и написания кода. Продвинутые пользователи самостоятельно смогут создавать и улучшать решения в SharePoint.

  • Если вы менеджер проектов, то вам должны быть знакомы продукты MS Project и Project Server. Последний является надстройкой над SharePoint.
    Но даже без Projet Server вы можете:
    • Создавать отдельные сайты для проектов несколькими кликами мыши, где можно будет размещать и согласовывать документы
    • Хранить и отображать на портале списки задач из Microsoft Project
    • Отслеживать риски и проблемы
    • При необходимости вывести создать на портале SharePoint интерфейс к другим системам управления проектами
    • Получать сводку по вашим проектами
    • Получать отчеты и KPI на портале

  • Если вы архитектор или ведущий разработчик и разрабатываете корпоративный софт, то вам нужен SharePoint потому что:
    • он включает в себя возможности управления документами
    • он имеет надежную систему разграничения доступа
    • он интегрируется с MS Office
    • он позволяет искать по всему содержимому
    • он поддерживает длительные рабочие процессы, которые могут продолжаться больше чем время непрерывной работы серверов
    • он имеет модульный пользовательский интерфейс
    • он позволяет интегрироваться с другими системами
    • он поддерживает масштабируемость всех своих компонент
    • он почти весь функционал SharePoint поддается расширению и кастомизации
    • содержит систему установки и удаления приложений, гораздо проще, чем написание инсталляторов вручную

  • Если вы IT-специалист, то вам обязательно нужен SharePoint. Он вам позволит:
    • Собирать в одном месте данные из различных систем
    • Отображать таблицы, графики, отчеты, KPI на портале
    • Автоматизировать процессы процессы организации с помощью простых инструментов
    • Размещать веб-контент не имея навыков веб-разработки
    • Управлять множеством сервисов со сложной топологией с помощью простого графического интерфейса
    • Заскриптовать любые действия с помощью PowerShell

  • Если вы рядовой .NET разработчик, то вы сможете в SharePoint:
    • Применить уже имеющиеся навыки
      • для разработки интерфейса
      • для создания решений по интеграции с другими системами
      • для создания рабочих процессов
    • Изучив платформу более детально вы сможете создавать любые решения и превратитесь из рядового разработчика в высокооплачиваемого специалиста :)

  • Если вы веб-разработчик, то ваши навыки будут очень востребованы в среде SharePoint
    • для брендинга портала, это сейчас очень востребованная тема
    • для разработки макетов веб-страниц для размещения контента
      (html + css + js)
    • для создания представлений данных и результатов поиска
      (xslt + html + css + js)
    • для приложений на javascript или silverlight, большая часть функциональности SharePoint доступна на клиентской стороне

 

Несмотря на богатые возможности примеров успешных внедрений не так много, как хотелось бы. Это связано с тем что платформа SharePoint сложна, а специалистов не хватает. По большей части не хватает именно разработчиков, которые хорошо владеют функционалом и могут собрать из него решение.

Если у вас будут возникать вопросы по SharePoint , то присоединяйтесь с сообществу http://area51.stackexchange.com/proposals/35899/sharepoint-in-russian, поддержите его развитие и вы сможете получать много полезной информации.