Кеширование в ASP.NET MVC
В прошлом посте я рассказывал о различных стратегиях кеширования. Там была голая теория, которая и так всем известна, а кому неизвестна, тому без примеров ничего не понятно.В этом посте я хочу показать пример кеширования в приложении ASP.NET MVC и какие архитектурные изменения придется внести, чтобы поддерживать кеширование.
Для примера я взял приложение MVC Music Store, которое используется в разделе обучение на сайте asp.net. Приложение представляет из себя интернет-магазин, с корзиной, каталогом товаров и небольшой админкой.
Исследуем проблему
Сразу создал нагрузочный тест на одну минуту, который открывает главную страницу. Получилось 60 страниц в секунду (все тесты запускал в дебаге). Это очень мало, полез разбираться в чем проблема.Код контроллера главной страницы:
public ActionResult Index() { // Get most popular albums var albums = GetTopSellingAlbums(5); return View(albums); } private List<Album> GetTopSellingAlbums(int count) { // Group the order details by album and return // the albums with the highest count return storeDB.Albums .OrderByDescending(a => a.OrderDetails.Count()) .Take(count) .ToList(); }
На главной странице выводится агрегирующий запрос, которому придется считать большую часть базы, чтобы вывести результат, при этом изменения на главной происходят нечасто.
При этом в каждой странице выводится персонализированная информация — количество элементов в корзине.
Код _layout.cshtml (Razor):
<div id="header"> <h1><a href="/">ASP.NET MVC MUSIC STORE</a></h1> <ul id="navlist"> <li class="first"><a href="@Url.Content("~")" id="current">Home</a></li> <li><a href="@Url.Content("~/Store/")">Store</a></li> <li>@{Html.RenderAction("CartSummary", "ShoppingCart");}</li> <li><a href="@Url.Content("~/StoreManager/")">Admin</a></li> </ul> </div>
Такой «паттерн» часто встречается в веб-приложениях. На главной странице, которая открывается чаще всего, выводится в одном месте статистическая информация, которая требует больших затрат на вычисление и меняется нечасто, а в другом месте — персонализированная информация, которая часто меняется. Из-за этого главная страница работает медленно, и средствами HTTP её кешировать нельзя.
Делаем приложение пригодным для кеширования
Чтобы такой ситуации, как описано выше, не происходило надо разделить запросы и собирать части страницы на клиенте. В ASP.NET MVC это сделать довольно просто.Код _layout.cshtml (Razor):
<div id="header"> <h1><a href="/">ASP.NET MVC MUSIC STORE</a></h1> <ul id="navlist"> <li class="first"><a href="@Url.Content("~")" id="current">Home</a></li> <li><a href="@Url.Content("~/Store/")">Store</a></li> <li><span id="shopping-cart"></span></li> <li><a href="@Url.Content("~/StoreManager/")">Admin</a></li> </ul> </div> <!-- skipped --> <script> $('#shopping-cart').load('@Url.Action("CartSummary", "ShoppingCart")'); </script>
В коде контроллера:
//[ChildActionOnly] //Убрал [HttpGet] //Добавил public ActionResult CartSummary() { var cart = ShoppingCart.GetCart(this.HttpContext); ViewData["CartCount"] = cart.GetCount(); this.Response.Cache.SetCacheability(System.Web.HttpCacheability.NoCache); // Добавил return PartialView("CartSummary"); }
Установка режима кеширования NoCache необходима, так как браузеры могут по умолчанию кешировать Ajax запросы.
Само по себе такое преобразование делает приложение только медленнее. По результатам теста — 52 страницы в секунду, с учетом ajax запроса для получения состояния корзины.
Разгоняем приложение
Теперь можно прикрутить lazy кеширование. Саму главную страницу можно кешировать везде и довольно долго (статистика терпит погрешности).
Для этого можно просто навесить атрибут OutputCache на метод контроллера:
[OutputCache(Location=System.Web.UI.OutputCacheLocation.Any, Duration=60)] public ActionResult Index() { // skipped }
Чтобы оно успешно работало при сжатии динамического контента необходимо в web.config добавить параметр:
<system.webServer> <urlCompression dynamicCompressionBeforeCache="false"/> </system.webServer>
Это необходимо чтобы сервер не отдавал заголовок Vary:*, который фактически отключает кеширование.
Нагрузочное тестирование показало результат 197 страниц в секунду. Фактически страница home\index всегда отдавалась из кеша пользователя или сервера, то есть настолько быстро, насколько возможно и тест померил быстродействие ajax запроса, получающего количество элементов в корзине.
Чтобы ускорить работу корзины надо сделать немного больше работы. Для начала результат cart.GetCount() можно сохранить в кеше asp.net, и сбрасывать кеш при изменении количества элементов в корзине. Получится в некотором роде write-through кеш.
В MVC Music Store сделать такое кеширование очень просто, как так всего 3 экшена изменяют состояние корзины. Но в сложном случае, скорее всего, потребуется реализации publish\subscribe механизма в приложении, чтобы централизованно управлять сбросом кеша.
Метод получения количества элементов:
[HttpGet] public ActionResult CartSummary() { var cart = ShoppingCart.GetCart(this.HttpContext); var cacheKey = "shooting-cart-" + cart.ShoppingCartId; this.HttpContext.Cache[cacheKey] = this.HttpContext.Cache[cacheKey] ?? cart.GetCount(); ViewData["CartCount"] = this.HttpContext.Cache[cacheKey]; return PartialView("CartSummary"); }
В методы, изменяющие корзину, надо добавить две строчки:
var cacheKey = "shooting-cart-" + cart.ShoppingCartId; this.HttpContext.Cache.Remove(cacheKey);
В итоге нагрузочный тест показывает 263 запроса в секунду. В 4 раза больше, чем первоначальный вариант.
Используем HTTP кеширование
Последний аккорд — прикручивание HTTP кеширование к запросу количества элементов в корзине. Для этого нужно:- Отдавать Last-Modified в заголовках ответа
- Обрабатывать If-Modified-Since в заголовках запроса (Conditional GET)
- Отдавать код 304 если значение не изменилось
Начнем с конца.
Код ActionResult для ответа Not Modified:
public class NotModifiedResult: ActionResult { public override void ExecuteResult(ControllerContext context) { var response = context.HttpContext.Response; response.StatusCode = 304; response.StatusDescription = "Not Modified"; response.SuppressContent = true; } }
Добавляем обработку Conditional GET и установку Last-Modified:
[HttpGet] public ActionResult CartSummary() { //Кеширование только на клиенте, обновление при каждом запросе this.Response.Cache.SetCacheability(System.Web.HttpCacheability.Private); this.Response.Cache.SetMaxAge(TimeSpan.Zero); var cart = ShoppingCart.GetCart(this.HttpContext); var cacheKey = "shooting-cart-" + cart.ShoppingCartId; var cachedPair = (Tuple<DateTime, int>)this.HttpContext.Cache[cacheKey]; if (cachedPair != null) //Если данные есть в кеше на сервере { //Устанавливаем Last-Modified this.Response.Cache.SetLastModified(cachedPair.Item1); var lastModified = DateTime.MinValue; //Обрабатываем Conditional Get if (DateTime.TryParse(this.Request.Headers["If-Modified-Since"], out lastModified) && lastModified >= cachedPair.Item1) { return new NotModifiedResult(); } ViewData["CartCount"] = cachedPair.Item2; } else //Если данных нет в кеше на сервере { //Текущее время, округленное до секунды var now = DateTime.Now; now = new DateTime(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second); //Устанавливаем Last-Modified this.Response.Cache.SetLastModified(now); var count = cart.GetCount(); this.HttpContext.Cache[cacheKey] = Tuple.Create(now, count); ViewData["CartCount"] = count; } return PartialView("CartSummary"); }
Конечно такой код в production писать нельзя, надо разбить на несколько функций и классов для удобства сопровождения и повторного использования.
Итоговый результат на минутном забеге — 321 страница в секунду, в 5,3 раза выше, чем в первоначальном варианте.
Заключение
В реальном проекте надо с самого начала проектировать веб-приложение с учетом кеширования, особенно HTTP-кеширования. Тогда можно будет выдерживать большие нагрузки на довольно скромном железе.Стратегия кеширования в приложении
Когда заходит разговор о кешировании складывается парадоксальная ситуация. С одной стороны все понимают важность и нужность кеширования в архитектуре приложений. С другой стороны мало кто может внятно объяснить что и как надо кешировать.
Обычно люди сходу начинают предлагать готовые реализации кеша, вроде memcached или HTTP-кеша, но это лишь ответ на вопрос где кешировать.
Кеширование – одна из многих тем, наряду с безопасностью и логированием, о которых знают и говорят все, но мало кто может это сделать правильно.
Зачем нужен кеш
Кеш приближает данные к месту их использования. В современном мире, состоящим на 98% из интернета, данные обычно лежат очень далеко от пользователя. На всем пути от хранилища к пользователю есть кеши, которые служат только одной цели – чтобы пользователь как можно быстрее получил свои данные.
Если рассмотреть внимательнее, то видно, что драгоценное время тратится на обработку данных в поставщике и передачу данных от поставщика клиенту, время обработки данных на клиенте тут не учитываем.
При наличии высоких нагрузок кеширование просто необходимо. Оно позволяет обслуживать больше клиентов, с теми же ресурсами, потому что поставщики данных больше отдыхают. Но даже при невысоких нагрузках кеширование положительно влияет на отзывчивость приложения.
Кеш нельзя просто включить
Одно из основных заблуждений насчет кеширования заключается в том, что многие думают что кеш можно просто включить.
На заре своей карьеры программиста я один раз просто так включил кеширование, буквально через час пришлось его выключить. Тогда я нарвался на основную проблему при кешировании – устаревание данных. Пользователь после изменения данных не видел результата 15 минут.
Очень важно понимать что и как вы собираетесь кешировать, чтобы не нарушать логику работы приложения. И первый вопрос, на который вам необходимо ответить – насколько устаревшие данные можно отдавать клиенту. Естественно можно сделать свой кеш для каждого клиента, это упростит решение вопроса об актуальности данных, но принесет много других проблем.
Типы кеширования
Есть три основных типа кеширования по механике работы:
- Lazy cache , он же ленивый кеш, он же тупой кеш – самый простой в реализации тип кеширования, зачастую встроен в фреймворки. Кеш просто сохраняет данные и отдает их пока не устареет.
- Synchronized cache, синхронизированный кеш – клиент вместе с данными получается метку последнего изменения и может спросить у поставщика не изменились ли данные, чтобы повторно из не запрашивать. Такой тип кеширования позволяет всегда иметь свежие данные, но очень сложен в реализации.
- Write-through cache, или кеш сквозной записи – любое изменение данных выполняется сразу в хранилище и в кеше. Этот тип кеша может никогда не устаревать, но возникают проблемы с так называемой “когерентностью”.
Наверное можно придумать и другие типы кешей, но я не встречал.
Устаревание и когерентность кеша
Объем кеша всегда ограничен. Зачастую он меньше объема данных, которые в этот кеш можно положить. Поэтому элементы, помещенные в кеш, рано или поздно будут вытеснены. Современные фреймворки для кеширования позволяют очень гибко управлять устареванием, учитывая приоритеты, время устаревания, объемы данных итд.
Если одни и те же данные попадают в разные кеши, то возникает проблема когерентности кеша. Например одни и те же данные используются для формирования разных страниц и кешируются страницы. Страницы сформированные позже будут содержать обновленные данные, а страницы, закешированные раньше, будут содержать устаревшие данные. Таким образом будет нарушена согласованность поведения.
Простой способ поддержания когерентности – принудительное устаревание (сброс) кеша при изменении данных. Поэтому увеличение памяти для кеша, чтобы он меньше устаревал, не всегда хорошая идея.
Эффективность кеша
Основной параметр, который характеризует систему кеширования – это процент попаданий запросов в кеш. Этот параметр довольно легко измерить, чтобы понять насколько ваша система кеширования эффективна.
Частые сбросы кеша, кеширование редко запрашиваемых данных, недостаточный объем кеша – все это ведет к пустой трате оперативной (обычно) памяти, не повышая эффективность работы.
Иногда данные меняются настолько часто и непредсказуемо, что кеширование не даст эффекта, процент попаданий будет близок к нулю. Но обычно данные считываются гораздо чаще, чем записываются, поэтому кеши эффективны.
Применение разных типов кеширования
Ленивый кеш
Это самый простой вид кеширования, но его нужно использовать осторожно, так как отдает устаревшие данные. Можно при каждой записи сбрасывать ленивый кеш, чтобы поддерживать актуальность данных, но тогда затраты на реализацию будут сравнимы с более сложными типами кеширования.
Такой тип кеширования можно использовать для данных, которые почти никогда не меняются. Другой вариант использования – делать ленивый кеш с небольшим временем устаревания для стабильной работы при всплесках нагрузки.
Такой тип кеширования позволит быстрее всех дать ответ.
Синхронизированный кеш
Это самый полезный тип кеширования, так как отдает свежие данные и позволяет реализовать многоуровневый кеш.
Такой тип кеширования встроен в протокол HTTP. Сервер отдает метку изменения, а клиент кеширует у тебя результат и в последующем запросе передает эту метку. Сервер может дать ответ, что состояние не изменилось и можно использовать кешированный на клиенте объект. Сервер в свою очередь, получив метку может переспросить у хранилища были ли изменения или нет.
Этот тип кеширования не избавляет от накладных расходов на общение между системами. Поэтому часто дополняется другими типами кеширования, чтобы ускорить работу.
Кеш сквозной записи
Если есть система распределенного кеширования (memcached, Windows Sever App Fabric, Azure Cache), то можно использовать кеш сквозной записи. Рукопашная реализация синхронизации кешей между узлами сама по себе отдельный большой проект, потому не стоит заниматься ей в рамках разработки приложения.
Не стоит пытаться кешировать все в синхронизированном кеше, иначе большая часть кода приложения будет заниматься перестройкой кеша.
Также не стоит забывать что системы распределенного кеширования также требуют общения между системами, что может сказываться на быстродействии.
Что еще нужно учитывать в стратегии кеширования
Выбирайте правильную гранулярность кешируемых данных. Например кеширование данных для каждого пользователя скорее всего будет неэффективно при большом количестве пользователей. Если кешировать данные всех пользователей, то возникнут проблемы с устареванием данных и когерентностью кеша.
Кешируйте данные как можно позже, непосредственно перед отдачей во внешнюю систему. Кешировать данные, полученные извне, необходимо только в случае проблем с производительностью на этом этапе. Внешние хранилища, такие как СУБД и файловые системы, сами реализуют кеширование, поэтому обычно нет смысла кешировать результаты запросов.
Не нужно городить свои велосипеды для кеширования в приложениях, обычно уже есть готовые инструменты и надо уметь ими пользоваться.
Заключение
Надеюсь статья была интересной и полезной для вас. Комментируйте, оценивайте, буду рад любым предложениям.
Как создавать надежные приложения в серверном коде для SharePoint
Несколько советов о том как сделать код более надежным и устойчивым:
- Получайте списки только с помощью такой конструкции
web.GetList(SPUrlUtility.CombineUrl(web.Url, listUrl)
Получение по Title списка ненадежно, так как Title может быть изменен пользователем без вашего ведома.
- При получении значений полей элементов списка используйте Id поля или его StaticName.
listItem[FieldId] //Если вы деплоите это поле или это встроенное поле listItem[FiledStaticName] //Если поле нестандартное и не вы его деплоите
Обращение по Title поля ненадежно, так как Title может меняться.
- Для стандартных полей используйте классы SPBuiltInFieldId и Microsoft.SharePoint.Publishing.FieldId
- Для обращения к стандартным типам контента используйте SPBuiltInContentTypeId и Microsoft.SharePoint.Publishing.ContentTypeId
- Используйте класс Convert для получения типизированного значения поля
Convert.ToDateTime(listItem["Created"])
Для разных типов полей SharePoint может возвращать разные типы данных. Например числа могут возвращаться как строки. Кроме того Convert корректно обрабатывает null. Даже если у вас Required поле, то все равно может вернуться null по многим причинам.
- Добавление элементов в список
list.AddItem() //только так //list.Items.Add() - ТАК ДЕЛАТЬ НЕЛЬЗЯ
- Количество элементов в списке
list.ItemCount //Только так //list.Items.Count – ТАК ДЕЛАТЬ НЕЛЬЗЯ
- AllowDeletion
Используйте AllowDeletion=”false” для списков и полей, которые вы деплоите в своем решении
- Sealed
Используйте Sealed=”true” для полей и типов контента, которые вы деплоите в своем решении
- Количество элементов в результатах
Обязательно указывайте RowLimit свойство при получении элементов с помощью SPQuery или KeywordQuery
- Проверка активации фич
В своем коде обязательно проверяйте, что необходимые для работы фичи активированы на узле\коллекции.
- Деплой веб-части вместе со списками
Если вы создаете веб-часть, которая обращается к списку, в том же решении, то обязательно поместите в одну фичу уровня Site. Внутри веб-части получайте список с RootWeb (см пункт 1).
- Не использовать RunWithElevatedPriveleges
Используйте конструктор SPSite с SPUserToken. Передавайте SPUserToken.SystemAccount.
Использование RunWithElevatedPriveleges оправдано только когда вы хотите обратиться к веб-сервису\БД от имени учетной записи пула приложений.
И не стоит забывать, что работает данный метод только в контексте веб-приложения.
- Не модифицировать SPPersistedObject в контексте веб-приложений
Это просто не работает. Можно обойти, но не стоит.
Все объекты, унаследованные от SPPersistedObject, должны создаваться\изменяться в фичах уровня Farm и WebApplication или в задачах таймера.
- Не обращаться к ресурсам компьютера
Их просто может не быть. Даже если вы при активации фичи создаете все что необходимо, то новый сервер может быть введен в ферму без переактивации фич.
Это касается фалов, вне тех что деплоятся в решении, ключей реестра, event source в windows event log и другого.
- Не использовать параметры в web.config
Код SharePoint может быть запущен не только в веб-приложении, но и в timerjob, powershell, процессе-домене службы поиска или в любом коде на сервере.
Естественно не везде будет работать стандартный класс ConfigurationManager(WebConfigurationManager).
Если есть еще советы – пишите, обязательно дополним список.
Будь мужиком, пиши правильный код, б***ть
Как вы знаете производительности хорошего программиста и плохого отличается в 28 раз. Чтобы стать хорошим программистом нужно как минимум следовать указанным ниже пунктам.
1. Читай книги, б***ть
Сколько книг по программированию вы прочитали за последний год? Одну? Три? Пять? Это слишком мало. Чтобы стать хорошим программистом надо много читать профессиональной литературы. По книге в месяц будет нормально, нельзя останавливаться, нельзя говорить себе “я уже все знаю, хватить читать”.
Если кончились книги – читайте статьи, кончатся статьи – смотрите видеокурсы. Никогда не останавливайте свое обучение.
При этом нельзя концентрироваться на одном языке или технологии, читайте про javascript, SQL, Web, UX, .NET, Java, функциональное программирование, архитектуру ПО, тестирование, алгоритмы, управление проектами, прикладную платформу, noSQL, снова про алгоритмы и архитектуру.
2. Читай код, б***ть
Важнейшее умение при работе в команде – чтение кода, вы должны по коду, без комментариев и документации, понимать что хотел сказать сказать автор кода. Это умение, как и все остальные требует постоянной тренировки.
Сейчас очень много кода есть в сети. Есть open-source проекты, есть репозитарии вроде github или codeplex, есть инструменты для реверсинга модулей. Уделяйте несколько часов в неделю чтению кода.
Читайте код платформы, с который работаете. Так вы лучше начнете понимать как (или чем) думали авторы и сможете обходить грабли.
3. Думай, б***ть, своей головой
Если вы будете много читать\смотреть видео, вам покажется что все уже украдено до вас, и достаточно будет быстро найти некоторое готовое решение и паттерн, решающий проблему. Так вы начнете создавать еще больше проблем.
Даже если вы находите готовое решение и паттерн – думайте подойдет ли оно для вашей ситуации. Оцените преимущества и недостатки, попробуйте найти подводные камни. Подумайте как бы вы сами написали этот код.
Вообще некоторые считают моветоном использовать код, который сами бы не смогли написать.
4. Пиши код, б***ть
Ну и самое важное в умении писать хороший код – непосредственно умение писать код. Да, его тоже надо тренировать. Даже если должность ваша уже не предполагает написание кода – заведите себе pet project и пишите там.
Будьте готовы в любой момент писать код. Вот прямо сейчас сесть и написать решение какой-нибудь задачи. Радуйтесь если на собеседовании вас попросят написать код.
Написание кода – как лакмусовая бумажка для всех ваших умений. Если вы все знаете и прочитали много книг, но не можете написать код, то грош-цена вашим умениям.
Но не стоит забывать про предыдущие три пункта. Если вы не будете знать какой код и как надо написать, если вы не будете понимать как ваш код взаимодействует с другим кодом, если вы не будете думать головой во время написания кода, то скорее всего ничего хорошего у вас не выйдет.
Даже если вы скопипастите решение, не включая голову, то потратите в 100500 раз больше времени на поиск решения проблем при поддержке или развитии кожа.
Заключение
Читай книги, читай код, думай головой, пиши код и будь настоящим мужиком б***ть!
Javascript-enabled SharePoint WCF services
К сожалению очень мало толковой информации о том, как создать WCF веб-сервис для SharePoint, который будет доступен как для WS-* клиентов, так и для Javascript.
Да-да, я знаю что уже вышел SharePoint 2013 и .NET 4.5, то что WCF уже не модно, и надо юзать WebAPI и REST. НО не факт что можно будет расширять API для SharePoint 2013, а WebAPI в SharePoint 2013 не работает (или по крайней мере это еще никто не сделал).
Так что пока используем WCF, благо его возможностей более чем достаточно для решения задачи.
Для начала надо поставить расширение студии CKS Dev (правильно говорить “секасдев”, @amarkeev гарантирует это), в нем есть шаблон для WCF сервиса
Этот пункт создает веб-сервис с BasicHttpBinding.
В SharePoint не обязательно параметры указывать в web.config, достаточно указать Factory в .svc файле, что и делает данный шаблон.
Какие есть Factory и зачем они нужны можно узнать по ссылке.
В принципе можно и этот сервис использовать в javascript коде, но придется долго и мучительно генерировать и парсить XML для SOAP. Чего делать крайне не хочется.
Для того, чтобы работать с теплым ламповым JSON достаточно сделать несколько изменений.
- Скопировать .svc файл и поставить ему те же параметры развертывания, что у исходного сервиса. Я даю сервисам имя <servicename>.json.svc
- В новом файле .svc зменить MultipleBaseAddressBasicHttpBindingServiceHostFactory на MultipleBaseAddressWebServiceHostFactory
- В интерфейсе сервиса добавить атрибуты методам, например
[ServiceContract] public interface ITestService { [OperationContract] [WebGet(BodyStyle = WebMessageBodyStyle.Bare, RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json)] //[WebInvoke(Method="POST", // BodyStyle = WebMessageBodyStyle.Bare, // RequestFormat = WebMessageFormat.Json, // ResponseFormat = WebMessageFormat.Json)] string HelloWorld(); }
И все, код сервиса править не надо. Атрибуты WebGet и WebInvoke можно найти в сборке System.SeviceModel.Web.
Код для вызова сервиса (javascript):
var webServerRelativeUrl = _spPageContextInfo.webServerRelativeUrl != "/" ? _spPageContextInfo.webServerRelativeUrl : ""; $.ajax({ type:"GET", url: webServerRelativeUrl + "/_vti_bin/testservice.json.svc/HelloWorld", dataType: 'json', success: function (data) { //success }, error: function (error) { //error } });
Если вам понадобится изменить параметры WCF сервиса, такие как максимальный объем сообщения, то это можно сделать с помощью класса SPWcfServiceSettings
public override void FeatureInstalled(SPFeatureReceiverProperties properties) { SPWebService contentService = SPWebService.ContentService; SPWcfServiceSettings wcfServiceSettings = new SPWcfServiceSettings(); wcfServiceSettings.ReaderQuotasMaxStringContentLength = Int32.MaxValue; wcfServiceSettings.ReaderQuotasMaxArrayLength = Int32.MaxValue; wcfServiceSettings.ReaderQuotasMaxBytesPerRead = Int32.MaxValue; wcfServiceSettings.MaxReceivedMessageSize = Int32.MaxValue; contentService.WcfServiceSettings["servicename.svc"] = wcfServiceSettings; contentService.Update(true); }
Если же вам понадобятся более глубокие возможности WCF, то можете создать свою ServiceHostFactory, отнаследовавшись от одной из тех, что предоставляет SharePoint.