Стратегии масштабирования для приложений ASP.NET - Кэширование

ОГЛАВЛЕНИЕ

Кэширование

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

Кэширование страниц намного проще остальных форм кэширования. Для его использования добавьте директиву @OutputCache к странице ASP.NET и включите правило, чтобы указать срок ее истечения. Например, можно указать, что страницу следует кэшировать в течение 60 секунд. При наличии такой директивы первый запрос страницы будет произведен как обычно и получит доступ к базе данных и прочим ресурсам, которые могут быть нужны для создания страницы. После этого страница удерживается в памяти веб-сервера 60 секунд, и все запросы к ней обслуживаются напрямую из памяти.

Увы, хотя этот пример и прост, он игнорирует фундаментальную истину кэширования страниц: практически не существует страниц ASP.NET, которые были бы достаточно статичны для кэширования их целиком на сколь-либо заметный промежуток времени. Здесь-то и помогает частичное кэширование страниц. С его помощью можно пометить части страницы ASP.NET как кэшируемые, чтобы вычислялись только регулярно изменяющиеся части. Этот метод более сложен, но эффективен.

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

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

На загруженном сервере ASP.NET память становится существенной проблемой по ряду причин. При каждом вычислении страницы ASP.NET используется некоторое количество памяти. И Microsoft® .NET Framework настроена на очень быстрое выделение памяти, но сравнительно медленное ее высвобождение через сбор мусора. Рассказа о сборе мусора и выделении памяти .NET хватит на отдельную статью (написанную уже не раз). Достаточно сказать, что на загруженном веб-сервере 2 ГБ пространства памяти, доступных приложению ASP.NET, пользуются большим спросом. В идеале большая часть этого использования памяти является временным, поскольку память выделяется переменным и структурам, используемым при вычислении веб-страницы.

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

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

Когда общий процент используемой памяти приближается к пределу памяти кэша ASP.NET по умолчанию (90%), вызывается событие сборки мусора. Событие сборки мусора проходит по пространству памяти, перемещая вниз задержавшиеся в ней объекты (вроде объектов кэша и объектов сеанса) и освобождая более не используемую память (которая была задействована для вычисления веб-страниц). Освобождение неиспользуемой памяти происходит быстро – в отличие от перемещения задержавшихся объектов. Так что чем их больше, тем сложнее сборщику мусора выполнить его работу. Этот тип проблемы можно опознать в perform.exe по большому числу коллекций gen-2.

И помните, что пока идет сборка мусора, этот сервер ASP.NET не может обслуживать страницы; все остальное ждет в очереди, ожидая завершении процесса сборки мусора. А IIS наблюдает за этим. Если покажется, что процесс занимает слишком долго и, возможно, завис, то рабочий поток будет перезапущен. И хотя это очень быстро высвободит массу памяти, выбросив все эти задержавшиеся в памяти объекты, некоторые клиенты явно будут недовольны.

Сейчас существует исправления для ASP.NET, автоматически удаляющее объекты из программного кэша при недостатке памяти, что, на первый взгляд, может показаться хорошей идеей. Это лучше, чем полный отказ. Просто помните, что при каждом удалении чего-либо из кэша код со временем поместит этот объект обратно.

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

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

Возможно проблема состоит в схеме истечения срока кэширования: истечение срока по времени неудовлетворительно. Можно кэшировать число в каталоге до момента, когда кто-нибудь купит штуковину, после чего завершить пребывания объекта в кэше. Это более логично, но что произойдет при наличии более чем одного сервера ASP.NET? В зависимости от сервера, в каталоге будет указываться разное число штуковин. Если учесть, что получение нового каталога (добавляющего к цифрам) даже не пройдет через веб-приложение, то получается целый новый способ сделать все неправильно.

Синхронизация истечений срока кэширования между серверами ASP.NET возможна, но требует осторожности. Объем общения между веб-серверами возрастает в геометрической прогрессии по мере роста числа объектов кэша на них.

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

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

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

А теперь пройдемся по этому случаю снова: приходит первый запрос, обнаруживает, что элемент кэша не заполнен, применяет блокировку к коду и выполняет запрос на заполнение объекта кэша. Второй запрос прибывает через секунду, пока первый еще работает, обнаруживает, что объект кэша не заполнен, но существует блокировка и блокируется. Как и следующие 28 запросов. Затем первый из них завершает свой процесс, снимает блокировку и продолжает. Что происходит с остальными 29 запросами? Они больше не блокированы, так что они также продолжают исполняться. Но они уже проверили объект кэша на заполненность (в момент, когда он не был заполнен). Так что они попытаются ухватить блокировку, одному это удастся, и он выполнит запрос снова.

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

Рис. 5 Проверка, блокировка и повторная проверка объекта кэша

// check for cached results
object cachedResults = ctx.Cache["PersonList"];
ArrayList results = new ArrayList();

if (cachedResults == null)
{
  // lock this section of the code
  // while we populate the list
  lock(lockObject)
  {
  // only populate if list was not populated by
  // another thread while this thread was waiting
  if (cachedResults == null)
  {
  ...
  }
  }
}

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