вторник, 31 августа 2010 г.

Производительность SharePoint

Я столкнулся с проблемами производительности SharePoint при работе с MOSS 2007 для одного из наших крупных клиентов. Задержки при запросе страниц доходили аж до 15-30 секунд - и это на очень хороших серверах! Казалось бы, всё очевидно - медлительный BDC. Однако, сейчас, не затрагивая BDC, удалось производительность улучшить многократно (до значений 0.6-3.2 секунды) - исключительно за счет переписывания собственного кода + введения кэширования.

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

SPSite и SPWeb
1. Старайтесь вообще не создавать эти объекты без надобности. Это весьма долгий процесс (насколько я помню, аж целых 0.2 секунды). Самое смешное, когда пишут что-нибудь такое:
foreach (MyObject myObject in MyObjectCollection)
{
    using (SPSite site = new SPSite("http://localhost"))
    {
        // тут что-нибудь делаем
    }
}
Т.е. SPSite создается и затем освобождается в цикле. Естественно, цикл нужно внести внутрь блока using. Но ведь мы не привыкли задумываться о таких вещах в ASP.Net!
Да что там говорить! Даже очень опытные программисты, которые уже знают об этой тонкости, часто совершают эту ошибку. К примеру, у вас есть некая функция, которая осуществляет действия в контексте учетки пула (через SPSecurity.RunWithElevatedPrivileges). Эта функция, что совершенно логично, запрятана где-нибудь глубоко в классе, условно назовем, BusinessLogic... Естественно, чтобы использовать RunWithElevatedPrivileges, нужно создать сайт и веб. И вот, в один прекрасный момент вы начинаете вызывать вашу собственную функцию в цикле (скорее всего, уже даже не помня, что в ней вообще есть вызов RunWithEvaluatedPrivileges)...
foreach (SPListItem listItem in list.Items)
{
    // что-нибудь делаем, и в конце...
    BusinessLogic.ChangeItemPermissions(list.ID, listItem.UniqueId);
}

// ...
// где-то в классе BusinessLogic

public static void ChangeItemPermissions(Guid listID, Guid listItemID)
{
    SPSecurity.RunWithEvaluatedPrivileges(delegate() {
        using (SPSite site = new SPSite(siteUrl))
        {
           using (SPWeb web = site.OpenWeb())
           {
               SPList list = web.Lists[listID];
               SPListItem item = list.Items[listItemID];
               // ну и дальше выставляем привилегии...
           }
        }
    });
}
И вы еще потом удивляетесь: "Откуда же такие тормоза?" ?! :)

2. Не забывайте, что обращение к свойству экземпляра SPSite AllWebs равносильно созданию Web'а. Есть и другие подобные свойства. К примеру, совсем недавно я был очень удивлен, когда локализовал почти полусекундные тормоза в следующем фрагменте кода:
SPWeb rootWeb = contextParams.Web.Site.RootWeb;
Кто бы мог подумать, что даже если contextParams.Web уже является корневым узлом, то этот код всё равно открывает этот узел еще раз!

3. Всё, что сами создаете - нужно диспозить. Это очень тонкий момент, по нему написано множество руководств. Тонкий, потому что иногда диспозить нельзя. К примеру, нельзя диспозить RootWeb, он диспозится вместе с сайтом. Более банальный пример - нельзя диспозить общие объекты, такие как SPContext.Current.Site и SPContext.Current.Web, SharePoint делает это сам после Render'а.
Если вы что-то не высвободили - в случае циклов, это чаще всего огромные потери памяти. Т.е. после первого десятка-двух итераций цикл начинает выполняться всё меееедленнее и мееедленнееее....
В лог сыпятся ошибки "An SPRequest object was not disposed before the end of this thread. " и подобные. На самом деле довольно легко не заметить такие вещи, особенно благодаря наличию хитрых свойств, как упомянуто в п.2. Чтобы с этим бороться, можно, помимо аккуратного кодинга, использовать специальную утилиту от Microsoft - SPDisposeCheck, которая позволяет анализировать уже откомпилированные dll-ки, что весьма удобно.
Между прочим, в моей, уже весьма отутюженной сборке, эта штука с первого раза нашла 4 дырки (правда, одна впоследствии оказалась ложным срабатыванием):

Авторы пишут, что есть ложные срабатывания, да и не все дырки она находит. Но ведь всё равно, согласитесь, очень хорошая утилита для дополнительной проверки. Причем, благодаря командной строке, процесс проверки легко автоматизировать, используя соответствующий BuildAction.
Поскольку эта утилита - не панацея, то всем, кто еще не видел, будет ОЧЕНЬ полезно посмотреть на MSDN статейку с шаблонами для программирования многих распространенных ситуаций, и использовать эти шаблоны в дальнейшем. Пишите аккуратно, и потом, глядишь, переписывать не придется (как пришлось мне :(... )

SPList
Почти всё, что было сказано про SPWeb и SPSite, относится и к SPList. Его не нужно диспозить, но в остальном, этот объект и его внутренности обрабатываются весьма долго, и в циклах это может стать опасным. Чаще всего я стараюсь не использовать объекты SPList напрямую, скрывая их в DataLayer-е. Для этого нужно создать модель в виде обычного класса (не используйте для этого SPListItem, ведь это тоже великий и ужасный SharePoint-объект!), и все действия с моделью выполнять через контроллер, в который крайне желательно ввести функции кэширования.
Давайте возьмем для примера список продуктов, включающий три банальных колонки:

public class Product
{
    public string Name {get; set;}
    public double Price {get; set;}
    public int Quantity {get; set;}
}
Создадим для этого класса кэширующий контроллер-синглтон:
public class ProductController
{
    const string CacheKey = "ProductController";
    public static ProductController Instance
    {
        get
        {
            if (HttpRuntime.Cache[CacheKey] == null)
            {
               HttpRuntime.Cache.Insert(CacheKey, new ProductController(), DateTime.Now.AddSeconds(5), Cache.NoSlidingExpiration);
            }
            return (ProductController)HttpRuntime.Cache[CacheKey];
        }
    }

    // тут будет остальной код
}
Из фрагмента видно, что класс-контроллер будет кэшироваться каждые 5 секунд, т.е. изменения в списке будут в нем отображаться практически мгновенно. Давайте посмотрим, как же будут храниться и обрабатываться элементы списка. Вместо заглушки, вставляем следующий код:
    const ListName = "ProductsList";
    private List<Product> Products = null;

    public IEnumerable<Product> GetAllProducts()
    {
        if (Products == null)
        {
            Products = new List<Product>();
            SPWeb web = SharePointHelper.GetRootWeb();
            SPList list = web.Lists[ListName];
            foreach (SPListItem item in list.Items)
            {
               Product newProduct = new Product();
               newProduct.Name = item["Name"];
               newProduct.Price = item["Price"];
               newProduct.Quantity = item["Quantity"];
               Products.Add(newProduct);
            }
        }
        return Products;
    }
Если вам требуется помимо простого чтения, еще изменять продукты, или еще что-то с ними делать - добавьте соответствующие методы (например, void Save(Product product), и т.п.). В итоге, и с производительностью проблем не будет - за счет кэша, да и понятнее гораздо стало, по фэншую ака MVC :).

Что еще
Практически любые SharePoint-объекты следует использовать очень аккуратно. Первым номером, если говорить о MOSS, идет, бесспорно, BDC (Business Data Catalog). Кэшировать его! Хотя бы на пару секунд - чтобы успеть обработать постбэк. И не забывайте, что кэш должен быть зависимым от пользователя, особенно если вы используете какие-то собственные правила для ограничения доступа.

Другой пример из MOSS: в свое время мне требовалось сделать пользователе-зависимое хранение данных. Т.е., у каждого пользователя - своя настройка. Сделал через MOSS-овский UserProfileManager.
Совсем недавно обнаружил, что некий простейший цикл по добавлению колонок к SPGridView выполняется чудовищно медленно. Оказалось, в цикле фигурировало то самое свойство, которое так удачно было запрятано, что я и думать о нем забыл. Вывод: всё что шарепойнтовское прячете, кэшируйте как следует!

Удачей, и до новых встреч!

P.S. Код может не скомпилироваться, писал на ходу.

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

  1. "Из фрагмента видно, что класс-контроллер будет кэшироваться каждые 5 секунд"
    А мне показалось что из фрагмента следует, что через пять секунд кэш сбросится и при следующем обращении будет снова чтение информации. Никакого автоматического кэширования я не вижу. Ну или ткните где оно?

    ОтветитьУдалить
  2. имеется в виду, что информация будет обновляться с периодичностью не быстрее, чем раз в 5 секунд

    если же идет много-много запросов в течение нескольких секунд, как это бывает в случае пиковых нагрузок, то система будет отдавать пользователям кэш, что ускорит отклик на 1-2 порядка

    ОтветитьУдалить

Внимание! Реклама и прочий спам будут беспощадно удаляться.