Кеширование в 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 сделать таrое кеширование очень просто, как так всего 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 кеширование к запросу количества элементов в корзине. Для этого нужно:

  1. Отдавать Last-Modified в заголовках ответа
  2. Обрабатывать If-Modified-Since в заголовках запроса (Conditional GET)
  3. Отдавать код 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-кеширования. Тогда можно будет выдерживать большие нагрузки на довольно скромном железе.

ссылка на оригинал статьи http://habrahabr.ru/post/168869/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *