Кэширование ресурсов

Кэширование ресурсов

Говоря о кэшировании ресурсов, мы обычно подразумеваем что-то, что было сериализировано в файловый формат и использовано в конечной точке. Это может быть что угодно, начиная с сериализированных объектов (например, XML, JSON) и заканчивая изображениями и видео. Для реализации такого кэширования можно попробовать использовать, например, http заголовки и мета-теги чтобы повлиять на механизм кэширование браузера. Но, необходимо понимать, что очень часто данные Вами указания не будут учтены. Из этого следует, что мы далеко не всегда можем успешно кэшировать медленно меняющийся веб контент на стороне клиента – во всяком случае, с гарантией производительности и устойчивости при нагрузке. Однако, вместо того чтобы переместить статические ресурсы назад на веб сервер, для большинства из них мы можем использовать сети доставки контента.

Давайте подумаем о том пути, который контент проходит от веб серверов до конечного клиента, и о том, сколько времени это занимает. Одной из возможностей увеличения скорости прохождения данного пути есть создание особых точек, в которых контент будет не только кэшироваться – но, что более, важно, он станет ближе к географически распределенным конечным пользователям. Сервера используемые для распространения контента теперь известны как сеть доставки контента. Использование такой сети является очень эффективным, особенно если нам нужно покрыть большую географическую область. В ранние дни интернета идея внедрения рассредоточенного кэширования ресурсов была достаточно нова и компании вроде Akami Technologies преуспели в продаже сервисов, направленных на расширение веб сайтов за счет сетей доставки контента. Двадцать лет спустя эта стратегия так же важна для мира, в котором Веб сводит вместе людей, которые физически находятся в разных точках планеты. В случае с Windows Azure, Майкрософт предоставляет свою сеть доставки контента - Windows Azure Content Delivery Network (CDN).

В большинстве случаев, во время развертывания веб сайта, кажется само собой разумеющимся, что все файлы должны располагаться на серверах сайта. В веб роли Windows Azure содержимое сайта развертывается пакетно – раз и все, готово. Стоп, а последние маркетинговые материалы не попали в пакет для развертывания, пора проводить повторное развертывание. Сейчас обновление контента означает повторное развертывание пакета. Конечно, можно развернуть его на отладочном окружении и потом сделать смену IP адресов, но этот процесс пройдет не без задержки или возможных неприятностей для пользователей.

Простой путь для создания обновляемого веб кэша – это хранение большей части контента в Windows Azure Storage и указывать всеми ссылками (URI) на контейнеры Windows Azure Storage. Однако, по разным причинам, может быть необходимо оставить контент с веб ролями. Одним из путей для реализации такого сценария есть размещение всего контента в службе Windows Azure Storage, а по необходимости, перемещать его в локальное хранилище ресурсов.

Кэширование в оперативную память

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

Сложный момент во внедрении стратегии кэширования на сайте приходится на то, как определить, что именно нам нужно кэшировать и как часто этот контент должен обновляться, с одной стороны, и что остается динамическим и генерируется при каждом запросе, с другой. Помимо стандартных возможностей, которые предоставляет Microsoft .NET Framework для кэширования потока вывода, Windows Azure предоставляет распределенный кэш под названием Windows Azure App­Fabric Cache.

Распределенный кэш

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

Одно из преимуществ AppFabric Cache – это то, что его можно использовать с состояниями сессий, изменив всего несколько настроек. Другим преимуществом является то что есть простой в использовании программный API.

Если рассматривать сценарий использования службы AppFabric Cache для кэширования состояния сеанса, то у многих может возникнуть вопрос – насколько это целесообразно? Многие разработчики привыкли, и не безосновательно, использовать для хранения состояния сеанса .net веб приложения MS SQL базу данных. Данное решение имеет ряд преимуществ, перечислять которые сейчас мы не будем. Оно довольно просто в настройке, хотя использование SQL Azure для хранения состояния сеанса пользователя требует дополнительной настройки. Но, проведя серию экспериментов с хранением состояния сеанса в MS SQL базе данных, с одной стороны, и хранением его в AppFabric Cache, с другой, я смог достичь увеличения производительности приложения до 30% просто за счет выбора другого механизма хранения состояния сеанса. Таким образом, потратив 5 минут рабочего времени, мы можем увеличить производительность приложения в среднем на 30%.

Давайте рассмотрим на примере каким образом мы можем использовать AppFabric Cache для хранения состояния сеанса нашего веб сайта.

Первым делом нужно зайти в панель управления своим аккаунтом Windows Azure и создать новый кэш, как это показано на рисунке.

Далее нам необходимо сделать некоторые изменения в файле web.config нашего сайта. Чтобы понять что именно нужно изменить, мы должны нажать на кнопку «view client configuration» в панели управления подпиской Windows Azure.

Как только мы это сделаем, мы увидим окно с подробной инструкцией по тому, какие модификации нам необходимо проделать.

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

<configSections> 
    <section name="dataCacheClients"
        type="Microsoft.ApplicationServer.Caching.DataCacheClientsSection,Microsoft.ApplicationServer.Caching.Core" 
        allowLocation="true" 
        allowDefinition="Everywhere"/> 
</configSections>

Теперь мы должны указать точку для подключения к нашему кэшу. Делается это путем добавления во все тот же файл web.config следующих конфигурационных элементов:

<dataCacheClients> 
  <dataCacheClient name="default"> 
    <hosts> 
      <host name="MyCacheNamespace.cache.windows.net" cachePort="22233" /> 
    </hosts> 
    <securityProperties mode="Message"> 
      <messageSecurity authorizationInfo="Your authorization token will be here."> 
      </messageSecurity> 
    </securityProperties> 
  </dataCacheClient> 
</dataCacheClients>

Тут стоит обратить внимание на одну вещь, платформа Windows Azure позволяет нам общаться с нашим кэшем как по открытому, так и по шифрованному каналу. Выбор каждого из них осуществляется путем смены порта подключения к службе AppFabric Cache. Если мы используем порт 22233, то наши данные будут передаваться по открытому каналу. Если мы используем порт 22243, то наши данные будут передаваться по шифрованному каналу.
Как только мы проделали эти шаги, мы можем менять конфигурацию провайдера состояния сеанса. Чтобы хранить состояние сеанса вашего сайта в кэше, необходимо установить следующую конфигурацию:

<sessionState mode="Custom" customProvider="AppFabricCacheSessionStoreProvider"> 
  <providers> 
    <add name="AppFabricCacheSessionStoreProvider"
    type="Microsoft.Web.DistributedCache.DistributedCacheSessionStateStoreProvider, Microsoft.Web.DistributedCache" 
        cacheName="default" 
        useBlobMode="true" 
        dataCacheClientName="default" /> 
  </providers> 
</sessionState>

Также вам необходимо включить в свой проект библиотеку Microsoft.Web.DistributedCache.dll, которая идет в поставке WindowsAzureSDK. Для этой библиотеки обязательно указать
Copy Local = True. Иначе вы не сможете использовать кэш для хранения состояния сеанса в веб роли.
На этом настройка AppFabric Cache для хранения состояния сеанса завершена. Теперь можно смело разворачивать веб сайт в облаке.
Для подключения к кэшу в будущем все что вам потребуется, это адрес кэша, порт и ключ аутентификации. Из этого выплывает очевидный факт, что для использования AppFabric Cache ваше приложение не обязано находится в облаке. Но, следует отметить, что хоть приложение и может использовать AppFabric Cache с любой точки планеты, где есть интернет, максимальная эффективность его использования достигается лишь тогда, когда ваше приложение и ваш кэш территориально расположены в рамках одного дата центра Windows Azure. Как, используя панель управления подпиской Windows Azure, найти необходимые параметры для подключения к кэшу показано на следующем рисунке.

Аналогично стоит отметить, что еще одной полезной возможностью AppFabric Cache есть возможность легко и просто использовать его не только для хранения состояния сеанса, но также и для кэширования потока вывода. Выполнить настройку такого кэширования представляет не больше сложностей, чем настройка хранения состояния сеанса в кэше.
В момент, когда вы нажимаете на кнопку «view client configuration» в панели управления подпиской Windows Azure, вы видите окно с набором разных конфигурационных секций файла web.config. Если немного покрутить полосу прокрутки, то можно найти конфигурационную секцию такого вида:

<caching>
  <outputCache defaultProvider="DistributedCache">
    <providers>
      <add name="DistributedCache"
    type="Microsoft.Web.DistributedCache.DistributedCacheOutputCacheProvider, Microsoft.Web.DistributedCache"
        cacheName="default"
        dataCacheClientName="default" />
    </providers> 
  </outputCache> 
</caching>

Если вы пройдете все шаги, описанные для добавления в ваш проект кэша для хранения состояния сеанса, но, вместо добавления конфигурационной секции хранения состояния сеанса добавите данную секцию, то ваш кэш будет использован для кэширования потока вывода. Еще одно важное замечание – мы используем распределенный кэш, т.е. доступ к нему одинаковый для всех экземпляров виртуальных машин, которые обслуживают нашу веб роль. Допустим, какой-то из пользователей сгенерировал запрос, он попадет на случайный экземпляр веб роли. Данный экземпляр сгенерирует пользователю ответ и закэширует его. До тех пор, пока не истекло время жизни этого ответа, любой запрос отправленный любым пользователем на любую веб роль будет обработан используя закэшированный вариант ответа. Этот момент очень важен, так как он избавляет нас от потребности генерировать ответы на аналогичные запросы каждым экземпляром веб роли.
Еще раз акцентирую внимание на том моменте, что подключение распределенного кэша как провайдера хранения состояния сеанса или как провайдера кэширования потока вывода не требует абсолютно никаких изменений в программном коде. Все модификации, для решения задач по этим двум сценариям, проводятся только в файле web.config.
К сожалению, если вы работаете с сайтом, который напрямую вызывает System.Web.Caching в коде, внедрение AppFabric Cache займет больше времени. Для этого есть две причины:

  1. Различия в API (Таблица 1)
  2. Стратегические решения о том, что кэшировать и где

Таблица 1 – Добавление контента через API кэша

AppFabric Cache System.Web.Caching
DataCacheFactory cacheFactory = 
new DataCacheFactory(configuration);

DataCache appFabCache = cacheFactory.GetDefaultCache(); 
string value = "This string is to be cached locally."; 
appFabCache.Put("SharedCacheString", value);
Cache LocalCache = 
new Cache(); 


string value = "This string is to be cached locally."; 
LocalCache.Insert("localCacheString", value);

Таблица 1 четко показывает, что даже глядя на базовые элементы API, можно увидеть разницу. Создание слоя перенаправления для отправки запросов поможет улучшить гибкость кода в приложении. Естественно, потребуется поработать над организацией легкого доступа к расширенным возможностям трех видов кэша, но выгоды перекроют все усилия для внедрения необходимого функционала.
Хотя распределенный кэш решает несколько обычно сложных проблем, он не должен использоваться как магическое зелье от всех болезней, иначе его эффективность будет примерно такой же, как у этого самого зелья. Во-первых, в зависимости от того как сбалансирована система вообще и от структуры данных, которые кэшируются, возможно, что потребуется больше не-машинных (ручных) выборок для добавления данных в локальный кэш клиент, что негативно повлияет на производительность. Также важным показателем есть стоимость внедрения распределенного кэша. На момент написания данной статьи, стоимость 4Гб распределенного кэша AppFabric составляет 325 долларов в месяц. Данная сума не является сама по себе очень большой и 4Гб выглядят как хороший объем места под кэш, но на сайте с высоким трафиком, особенно на сайте с большим количеством богатого контента можно легко заполнить несколько кэшей такого размера. Рассмотрите разные варианты покупки места под кэш или же обратитесь к провайдеру сервиса за специальным ценовым предложением.

Взгляд на кэш с другой стороны

Как и много других вещей в информационных технологиях (и, наверное, других сферах), готовое решение – это смесь идеальных технических решений, видоизмененных фискальными реалиями. Поэтому если вы всего лишь используете Windows Server 2008 R2 AppFabric Caching, все равно остаются причины использовать локальное кэширование, предоставленное в System.Web.Caching. В первом приближении я смог создать обертки вызовов (wrapped the calls) к каждой из библиотек кэширования и представил для каждой свою функцию, по типу AddtoLocalCache(ключ, объект) и AddtoSharedCache(ключ, объект). Однако это означает, что каждый раз, когда будет необходимо провести операцию кэширования, разработчик будет вынужден принимать довольно личное и размытое решение насчет того, где, а точнее в каком кэше, она должна состояться. Данный подход сложен в поддержке и быстро дает трещину при использовании его в больших командах, а также однозначно приводит к непредвиденным ошибкам, потому что разработчик мог решить добавить объект в неподходящий кэш или добавить объект в один кэш, а затем случайно считать его из другого.

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

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

Если вы используете кэширование, у вас должен быть четкий план. Очень часто оно просто бесцельно добавляется в конце проекта, но кэширование заслуживает такого же времени и важности при планировании и проектировании приложения, как и любой другой аспект. Это особенно важно, когда вы имеете дело с облаком, потому что плохо продуманные решения приведут к увеличению стоимости и неэффективному использованию ресурсов. Когда Вы определяете типы данных, которые должны быть кэшированы, Вы должны сперва определить их жизненный цикл в приложении и в пользовательской сессии. С этой точки зрения быстро понимаешь, что было бы хорошо, если бы сущность сама могла умно кэшироваться, базируясь на своем типе. К счастью, реализация этой задачи легко воплощается в жизнь при помощи пользовательских атрибутов (Custom Attribute).

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

Мы объявим перечисление, чтобы указать место для кэширования, и класс, который наследуется от класса Attribute, для реализации пользовательского атрибута, как показано в примере 1.

Пример 1 – Объявление перечисления и класса для реализации пользовательского атрибута:

public enum CacheLocationEnum
    {
        None = 0,
        Local = 1,
        Shared = 2
    }

    public class CacheLocation : Attribute
    {
        private CacheLocationEnum _location = CacheLocationEnum.None;
        public CacheLocation(CacheLocationEnum location)
        {
            _location = location;
        }
        public CacheLocationEnum Location { get { return _location; } }
    }

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

private static bool AddToLocalCache(string key, object newItem)
    {...}
    private static bool AddToSharedCache(string key, object newItem)
    {...}

В реальной реализации нам, скорее всего, понадобится больше информации (например, название кэша, частота обновления, зависимости, т.п.), но хватит и такого примера. Главная публичная функция для добавления контента в кэш – это шаблонный метод, что облегчает нам определение кэша по типу, как показано в примере 2.
Пример 2 – Добавление контента в кэш

public static bool AddToCache(string key, T newItem)
        {
            bool retval = false;
            Type curType = newItem.GetType();
            CacheLocation cacheLocationAttribute =
              (CacheLocation)System.Attribute.GetCustomAttribute(typeof(T),
              typeof(CacheLocation));
            switch (cacheLocationAttribute.Location)
            {
                case CacheLocationEnum.None:
                    break;
                case CacheLocationEnum.Local:
                    retval = AddToLocalCache(key, newItem);
                    break;
                case CacheLocationEnum.Shared:
                    retval = AddToSharedCache(key, newItem);
                    break;
            }
            return retval;
        }

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

[CacheLocation(CacheLocationEnum.Local)]
        public class WebSiteData
        {
            public int IntegerValue { get; set; }
            public string StringValue { get; set; }
        }
        [CacheLocation(CacheLocationEnum.Shared)]
        public class WebSiteSharedData
        {
            public int IntegerValue { get; set; }
            public string StringValue { get; set; }
        }

Вся инфраструктура приложения уже настроена, теперь ее можно использовать в коде приложения. Мы просто открываем файл default.aspx.cs, чтобы создать примеры запросов и добавить код для создания типов, установить несколько значений и добавить их в кэш:

WebSiteData data = new WebSiteData();
            data.IntegerValue = 10;
            data.StringValue = "ten";
            WebSiteSharedData sharedData = new WebSiteSharedData();
            sharedData.IntegerValue = 50;
            sharedData.StringValue = "fifty";
            CachingLibrary.CacheManager.AddToCache("localData", data);
            CachingLibrary.CacheManager.AddToCache("sharedData", data);

Наши названия типов однозначно указывают, где будут кэшироваться данные. Однако, можно было бы изменить названия типов и это было бы менее однозначно, при этом кэширование бы управлялось с помощью проверки пользовательского атрибута. Использование такого подхода спрячет от разработчика детальную информацию о том, куда кэшируются данные, а также другую информацию, связанную с настройками кэша. Таким образом, эти решения остаются на совести той части команды, которая создает словари данных и прописывает общий жизненный цикл данных. Обратите внимание, что тип передается в запросах к AddToCache(string, T). Реализация остальных методов для класса CacheManager (т.е. GetFromCache) потребует использования такого же шаблона, какой использован для метода AddToCache. 

Балансирование стоимости с производительностью и масштабируемостью

Windows Azure предоставляет всю необходимую инфраструктуру для того, чтобы помочь Вам в реализации механизма кэширования. Ключевой момент к созданию хорошего проекта и, как следствие, хорошей реализации, - это балансировка стоимости с производительностью и масштабируемостью. Если вы сейчас работаете над созданием нового приложения и собираетесь в нем использовать кэширование – потратьте время и постройте дополнительный слой передачи данных. Это дополнительная работа, но с появлением новых возможностей, таких как кэширование AppFabric, вам будет легче эффективно и правильно включать их в свое приложение.

Автор статьи:  Антон Бойко.