Проблема кэширования встает перед любым высоконагруженным приложением. В 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)
Надеюсь, эта статья поможет другим пройти по меньшему количеству граблей, чем собрал я. Успехов в разработке, пусть ваши приложения будут быстрыми.
Автор: unconnected