Microsoft Azure
Облачный бизнес
Вам понадобится

Microsoft Azure

Попробуйте платформу Microsoft Azure совершенно бесплатно.

Visual Studio

Бесплатная версия Visual Studio, позволяющая создавать приложения для платформы Microsoft Azure.

SDKs и дополнительные
инструменты

Инструменты разработки приложений для платформы Microsoft Azure.

Кэширование в Windows Azure

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

MemoryCache


Вообще, этот класс не имеет отношение к Azure, но не упомянуть его в статье о кэшировании просто нельзя. 

Начиная с .Net 4 появилось новое пространство имен System.Runtime.Caching. Занимается MemoryCache, как следует из названия, созданием хранилища объектов в памяти. Для меня он важен тем, что работает гораздо быстрее, чем Cache из пространства System.Web.Caching. Еще приятной особенностью является то, что можно создать несколько кэшей с разными настройками.

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

<configuration>
  <system.runtime.caching>
    <memoryCache>
      <namedCaches>
          <add name="default" cacheMemoryLimitMegabytes="0"
 physicalMemoryPercentage="0" pollingInterval="00:02:00" />
      </namedCaches>
    </memoryCache>
  </system.runtime.caching>
</configuration>

In-Role Cache

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

Регулярно возникает желание, обеспечить единую «память» для всех экземпляров. Самым простым примером будет сессия. Для этого в Azure имеется механизм In-Role Cache, который позволяет выделить часть или всю память экземпляра под кэш, и он будет доступен из всех экземпляров приложения. 

Для его использования необходимо добавить в решение пакет  Windows Azure Caching и настроить роли для работы с ним.

Сначала нам нужно включить кэш на экземплярах или добавить выделенные под кэш экземпляры в решение.

Внимание: кэш не поддерживается на сверхмалых (extra small) экземплярах.


Картинки про включение кэша

Включение кэша на существующей роли


Создание роли, выделенной под кэш




Всегда будет присутствовать кэш с именем “default”. Мы можем добавить кэши со своими именами и различными настройками.

Настройки кэша


Высокая доступность (High Availability).
Требует, чтобы в решении было не менее двух экземпляров, на которых расположен кэш. Данные попадающие в кэш будут храниться минимум на двух экземплярах и выход из строя одного экземпляра не приведет к потере данных. Использовать эту опцию стоит осторожно, т.к. кэш начинает поглощать гораздо больше ресурсов.

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

Очистка (Eviction policy)
Как будет очищаться кэш в случае переполнения. Для включения пока доступна только одна опция – LRU (Least recent use). Т.е. к какому объекту меньше всего обращались – тот и будет удален.

Устаревание (Expiration Type) и время жизни (TTL, Time To Live)
Два взаимосвязанных параметра, указывают, как и за какой срок (в минутах) будут устаревать в кэше и исчезать из него. Т.е. если параметр очистки является больше аварийным (ситуация переполнения кэша ни к чему хорошему обычно не приводит), то устаревание позволяет нам описать как объекты должны исчезать из кэша при нормальной работе.
None. Объекты будут храниться в кэше вечно (до перезагрузки). Требует, чтобы время жизни было установлено в ноль.
Absolute. Объект хранится в кэше определенное время после того, как туда попал.
Sliding window. Моя любимая опция. Объект исчезнет из кэша через указанное время после последнего обращения. Т.е. объекты, к которым обращаются постоянно будут жить в кэше.

Настройка клиента кэша


В целом, всё довольно просто: в конфигурационном файле описываем, какие кэши у нас есть и где расположены. Вставляем в конфигурационный файл в секцию configuration следующие строки (их шаблон должен был уже создать NuGet при установки пакета кэширования).

<dataCacheClients>
  <dataCacheClient name="default">
    <autoDiscover isEnabled="true" identifier="CacheWorkerRole" />
    <localCache isEnabled="true" sync="TimeoutBased" objectCount="100000" ttlValue="300" />
  </dataCacheClient>
  <dataCacheClient name="MyNamedCache">
  …
</dataCacheClients>

В качестве идентификатора нужно указывать имя роли содержащей кэш в проекте. В нашем случае – CacheWorkerRole. А не имя точки доступа в Azure, типа mycoolapp.cloudapp.net.

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

Синхронизация по времени (TimeBased) указывает, что объекты будут храниться в локальном кэше указанное в ttlValue количество секунд. Синхронизации по оповещениям (NotificationBased) требует, чтобы механизм оповещений был включен в кэше. В этом случае параметр ttlValue указывает, с какой частотой локальный кэш будет проверять наличие изменений в кэше.

Базовые настройки завершены. Теперь, для примера, подключим сессии нашей веб-роли к кэшу. В IIS это делается очень просто, заменив стандартный InProc провайдер сессии на провайдер, использующий кэш.

<sessionState mode="Custom" customProvider="AFCacheSessionStateProvider">
      <providers>
        <add name="AFCacheSessionStateProvider" 
             type="Microsoft.Web.DistributedCache.DistributedCacheSessionStateStoreProvider, Microsoft.Web.DistributedCache" 
             cacheName="default" 
             dataCacheClientName="default" 
             applicationName="AFCacheSessionState"/>
      </providers>
</sessionState>

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

Перед тем, как перейти к примерам работы с кэшем из кода, остановимся на совсем новом виде кэша.

Cache Service


Пока кэш как сервис доступен в режиме предварительного просмотра (preview). Востребован он может быть в сценариях, где разные решения должны иметь доступ к одним и тем же данным. In-Role кэш доступен только в пределах того решения, к которому он привязан. Кэш-сервис такого недостатка лишен. 

Настройка кэш-сервиса один в один такая же, как и настройка In-Role кэша, но проводить её нужно не в студии, а на портале управления Azure. В конфигурацию роли добавится ключ доступа к кэшу. 


Еще плюсам кэш-сервиса можно отнести:
   — несколько меньшую цену;
   — отсутствие головной боли при обновлениях развертываний (они не будут затрагивать кэш);
   — поддержку протокола memcached, что позволяет подключить к нему не только PaaS решения, но и любой тип виртуалок.

Еще несколько слов по настройке


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

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

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

Для увеличения пропускной способности кэша можно увеличить кол-во подключений к нему параметром maxConnectionsToServer. По умолчанию создается только одно подключение к кэшу.

Работа с кэшем из кода


Всё необходимое для работы с кэшем живет в Microsoft.ApplicationServer.Caching. 

Для начала создать объект кэша и командами Add, Put, Get и Remove начать работать с данными.

DataCache dc = new DataCache("default");

dc.Add("test", DateTime.Now);           //добавить объект в кэш
dc.Put("test", DateTime.Now);           //добавить или заменить 
DateTime dt=(DateTime)dc.Get("test");   //получить
dc.Remove("test");                      //удалить

Гонка

Для предотвращения гонки, следует использовать GetAndLock, PutAndUnlock и Unlock. Оператор GetAndLock не блокирует обычный Get и не мешает «грязному» чтению.

try
{
     DataCacheLockHandle lockHndl;
     object value = dc.GetAndLock("test", new TimeSpan(0, 0, 5), out lockHndl);
     //модифицируем объект
     dc.PutAndUnlock("test", value, lockHndl);
     //или dc.Unlock("test", lockHndl) если ничего не меняли
}
catch (DataCacheException de)
{
      if (de.ErrorCode == DataCacheErrorCode.KeyDoesNotExist)
      {
         //объекта нет
      }
}

Чтение обновлений


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

object val = DateTime.Now;
DataCacheItemVersion version = dc.Put("test", val);
while (true)
{
     val = dc.GetIfNewer("test", ref version);
     if (val != null)
     {
          //объект изменился
     }
     Thread.Sleep(1000);
}

Если объект появился в кэше, где-то в другом месте, можно получить его версию из объекта DataCacheItem.

DataCacheItem dci = dc.GetCacheItem("test");
DataCacheItemVersion version = dci.Version;
object val = dci.Value;

Регионы

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

if (dc.CreateRegion("region")) 
{ //региона не было, он создан} dc.Put("test", DateTime.Now, "region"); 
foreach (KeyValuePair<string, object> kvp in dc.GetObjectsInRegion("region"))
 { //обрабатываем объекты}

У регионов есть пара особенностей, которые стоит учитывать при работе с ними. 

Не стоит перебирать объекты в регионе напрямую как в коде выше. Если другой поток сложит или уберет в нем объект, нарвемся на исключение «коллекция изменилась». 

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

О чём стоит помнить при работе с кэшем


   — Размер объекта в кэше ограничен 8 мегабайтами
   — Если используются регионы, они должны наполняться равномерно
   — Кэшируйте объекты локально всегда, когда это возможно.
   — Включайте высокую доступность только там, где это нужно.
   — Используйте блокировки (GetAndLock) только там, где это необходимо
   — Не читайте объекты, если они не обновлены (используйте GetIfNewer)

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

Автор статьи: Александр Кузнецов.

 

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