Введение
Несмотря на возрастание скоростей работы процессоров и сетей,
производительность остается основной заботой разработчиков приложений. А
значит, независимо от того, пишете ли вы Web-сервис на основе XML (XML
Web Service), загружаете битовые карты изображений в память видеоплаты
или даже проектируете какой-нибудь потрясающий чип нового поколения, вам
безусловно стоит подумать об универсальном механизме повышения
производительности: о кэшировании.
В этом выпуске рубрики At Your Service будет рассмотрено, как вы,
разработчик и потребитель Web-сервисов, можете задействовать
преимущества кэширования. Мы обсудим кэширование на уровне приложения в
ASP.NET, а также кэширование в HTTP и его применение к Web-сервисам XML.
Наконец, мы подумаем, как использовать пример сервиса Discovery
вымышленной компании MSDN Pencil Company, и реализовать для него
стратегию кэширования, имеющую смысл при ежедневном обновлении каталога
продукции этой компании.
На что обращать внимание при выборе вариантов кэширования
И создатель, и потребитель Web-сервиса может реализовать самые
разнообразные варианты кэширования. Однако не все механизмы реализации
кэша эффективно увеличивают производительность или хотя бы создают
ощущение, будто она повысилась. Вам следует проанализировать, что имеет
смысл в вашем конкретном случае. Вот несколько вопросов, на которые вы
должны сами себе ответить, выбирая стратегию кэширования Web-сервиса.
Много ли у меня динамических данных?
Времена, когда считалось, что кэширование - это всегда хорошо,
прошли. Так, если Web-сервис постоянно возвращает разные данные,
кэширование вряд ли вам поможет. Но только из того, что данные
динамические, вовсе не следует, будто о кэшировании надо забыть. Если
хотя бы часть ответа сервиса относительно статична, кэширование способно
повысить производительность вашего Web-сервера. Возьмем случай, когда
информация меняется, но не для каждого запроса. Например, если вы
ежесекундно получаете сотни запросов к сервису, сообщающему сведения о
погоде, то, наверное, захотите в ответ на большинство запросов посылать
данные из кэша, а его обновление выполнять, скажем, раз в 5 минут.
Не конфиденциальные ли это данные?
Часто Web-сервисы XML имеют дело с данными, специфичными для
конкретного пользователя. Это уменьшает пользу от кэширования, но не
отказывайтесь от него лишь потому, что вы работаете с такими данными.
Скажем, если число пользователей Web-сервиса невелико, может оказаться
разумным кэшировать информацию для каждого пользователя индивидуально -
особенно если пользователи неоднократно запрашивают одну и ту же
информацию. И даже если это не так, может существовать общий экземпляр
класса, к которому происходит обращение при каждом запросе от какого-то
пользователя. Однако будьте осторожны при кэшировании конфиденциальной
информации, поскольку ошибки в коде такого рода могут привести к ее
раскрытию. С точки зрения безопасности следует ввести ограничения
доступа.
Использует ли Web-сервис ресурсы, общие для нескольких запросов?
Кэширование применимо не только к ответам, но и к любым данным или
ресурсам приложения, что нередко позволяет значительно увеличить
производительность. Например, может оказаться разумным сохранять один и
тот же набор данных (dataset) для обслуживания нескольких запросов.
Данные в ответе варьируются в зависимости от конкретных запросов к
набору данных, но сам набор может оставаться одним и тем же для
множества запросов.
Можно ли предсказать будущую потребность в ресурсах?
Подумайте о сценариях применения вашего Web-сервиса. Можете ли вы
предсказать характер его использования? Допустим, Web-сервис позволяет
потребителям находить некую статью, а затем скачивать ее. В этом случае
справедливо предположить, что вслед за успешным поиском статьи последует
запрос на ее скачивание. Web-сервис может заблаговременно начать
потенциально длительный процесс загрузки статьи в память (из файла или
базы данных), и тогда к моменту поступления запроса на скачивание статьи
все будет готово.
Где кэшировать данные Web-сервиса?
Зачастую правильный ответ на этот вопрос: где угодно. Но каковы
варианты их кэширования? Чтобы ответить на этот вопрос, рассмотрим
потенциально возможный сценарий работы Web-сервиса, приведенный на рис.
1.
|
Браузер конечного пользователя
HTTP-прокси
Web-сервер
Web-сервис
Внутренний Web-сервис
SQL-сервер |
Рис. 1. Возможные варианты кэширования для одного из сценариев работы
Web-сервиса XML
Как видите, конечный пользователь обращается к сайту (он показан в
желтом прямоугольнике), не зная, что этот сайт находится за HTTP-прокси.
Затем Web-сервер посылает SOAP-запрос к Web-сервису в другой организации
(в зеленом прямоугольнике). SOAP-запрос тоже проходит через HTTP-прокси.
После этого первый Web-сервис переправляет запрос другому, внутреннему
Web-сервису, который запрашивает требуемые данные у Microsoft® SQL
Server и наконец возвращает ответ. SQL-данные используются при
подготовке ответа от внутреннего Web-сервиса, а на основе его ответа
создается ответ от первого Web-сервиса. На сайте из ответа Web-сервиса
формируется HTML-страница, которая и возвращается браузеру конечного
пользователя.
Запрос и ответ проходят через множество прокси-серверов и
маршрутизаторов. Так где же кэшировать данные ответа? В этом сценарии -
на каждом этапе. SQL-сервер может кэшировать результаты запроса к
сервису, внутренний Web-сервис - результаты SQL-запроса, первый
Web-сервис - результаты внутреннего Web-сервиса (равно как и HTTP-прокси
в "зеленой" организации), Web-сервер - ответ Web-сервиса, прокси-сервер
в "желтой" организации - ответ Web-сервера, а браузер конечного
пользователя - HTML-страницу.
Когда заканчивается срок хранения данных?
При разработке стратегии кэширования необходимо ответить на один из
важнейших вопросов: когда удалять данные из кэша. Иногда это сделать
очень просто, так как процесс, обновляющий данные, запускается через
регулярные промежутки времени. Однако в других ситуациях данные
обновляются через произвольные интервалы. Тогда надо выяснить
оптимальный срок, по истечении которого следует обновлять кэш, соблюдая
баланс между риском возврата из кэша устаревших данных и ростом
производительности, достигаемым за счет применения кэша. Определив такой
интервал, включите эту информацию в данные, чтобы кэширующие системы
могли соответственно обновлять содержимое кэшей.
Как уведомлять потребителей Web-сервиса о том, что данные
устарели?
Ответ на этот вопрос зависит от способа кэширования. Клиентским
приложениям часто имеет смысл кэшировать данные. В этом случае
клиентские приложения должны быть своевременно информированы об
устаревании данных. Для этого, вероятно, потребуется включать в
возвращаемые данные сведения о сроке их жизни. Для Web-сервиса это может
означать добавление к XML-ответу специального поля, где указывается
"срок жизни".
Встроенные механизмы кэширования обычно включают в себя средства для
задания сроков устаревания. В случае с HTTP-кэшированием эти сведения
помещаются в HTTP-заголовки, доступные прокси-серверам и клиентским
системам. В ASP.NET есть класс кэша, в который можно записывать данные.
При этом у вас есть возможность указывать, когда данные следует удалять
из кэша.
Можно ли полагаться на то, что данные будут в кэше?
Если коротко - нет. Почти в любой механизм кэширования встроен
алгоритм удаления устаревший информации из кэша. Данные обычно удаляются
из-за устаревания, но иногда и из-за того, что к ним давно не
обращались; при этом в кэш загружаются другие данные, ведь его емкость
ограниченна. Так что большинство механизмов кэширования не гарантирует
наличия данных в кэше. Это тем более верно в отношении механизмов
кэширования в сетях (shared caching mechanisms) вроде кэшей HTTP-прокси
или даже ASP.NET.
Что будет, если потребители Web-сервиса не воспользуются
кэшированными данными?
Существует несколько причин, по которым нужные данные могут не
попасть в кэш. Например, данные с более высоким приоритетом способны
просто вытеснить ваши данные из кэша. В таком случае разработчик,
писавший код для доступа к вашему Web-сервису, по-видимому, не
позаботился о повторном использовании уже полученных данных. Проектируя
Web-сервис, помните о возможности увеличить производительность за счет
кэширования, но предусматривайте ситуации, в которых данные не попадут в
кэш. Вы должны уметь справляться с такими ситуациями, при которых
кэширование работает неоптимально.
Сценарии кэширования
Теперь, когда мы рассмотрели некоторые вопросы, требующие ответа для
оценки возможностей кэширования, обсудим, каковы эти возможности для
разработчиков Web-сервисов XML. Сначала мы поговорим о двух подходах к
кэшированию: на прикладном уровне и на уровне протокола. Затем проверим,
что предлагает ASP.NET для реализации кэширования на обоих уровнях.
Кэширование на прикладном уровне
Словом "приложение" (application) в наше время слишком
злоупотребляют. В этой статье под термином "приложение" я понимаю
"прикладной уровень" (application level), и он относится как к
Web-сервису, так и клиенту этого сервиса, т. е. к тем областям, где
разработчик пишет код, быстродействие которого чувствительно к
кэшированию.
Следовательно, "кэширование на прикладном уровне" относится и к коду
Web-сервиса, и к коду клиента, который тоже так или иначе кэширует
данные. Кэширование в Web-сервисе может заключаться в сохранении в
памяти компьютера экземпляров повторно используемых классов или не
меняющихся данных.
На уровне клиентского приложения кэширование - это сохранение ответа
Web-сервиса для того, чтобы клиенту не приходилось посылать еще один
запрос для получения тех же данных.
Поддержка кэширования обычно связана с предоставлением сведений о
сроке жизни данных. Если этот срок всегда постоянный, его можно
документировать и "зашить" в код клиента. Тогда указывать его в ответе
Web-сервиса не потребуется. Однако во многих случаях срок жизни данных
изменчив, и соответствующие сведения нужно хранить вместе с кэшируемыми
данными. При кэшировании на прикладном уровне в данные можно включить
новое поле, содержащее параметр - срок жизни. Так как обычно срок жизни
- это фактически метаинформация, описывающая данные, подходящее место
для ее хранения этих сведений - элемент SOAP-заголовков. Хранить
метаинформацию о SOAP-сообщении следует именно там.
HTTP-кэширование
HTTP предоставляет эффективные механизмы кэширования. В его
спецификации описывается, по каким правилам системные сервисы должны
кэшировать данные. В основном HTTP-прокси и клиентские компьютеры
обеспечивают кэширование без каких-либо усилий со стороны разработчика
приложений, использующих HTTP. Но область применения HTTP-кэширования
для Web-сервисов ограниченна.
В настоящее время для обращения к Web-сервисам используются
SOAP-сообщения, помещаемые в тело HTTP-запроса POST. В отличие от
HTTP-запросов GET, тело POST-запросов выходит за область действия HTTP.
Поэтому реализации протокола HTTP в прокси-серверах и клиентах не
способны определять, как кэшировать ответы на HTTP-запросы POST. ASP.NET
поддерживает вызов Web-методов через GET-запросы, но этот механизм в
основном предназначен для отладки и не поддерживается большинством
других инструментальных средств SOAP.
Возможности кэширования в ASP.NET
Одна из приятных особенностей разработки Web-сервисов XML в среде
ASP.NET - преимущества обширной функциональности, которой уже пользуются
разработчики приложений Web Forms. Встроенная функциональность ASP.NET
облегчает кэширование Web-сервисов XML. Как раз сейчас Роб Ховард (Rob
Howard) в своей колонке
Nothing But ASP.NET начал цикл статьей, посвященных кэшированию в
этой среде. Чтобы лучше понять специфику ASP.NET с точки зрения
кэширования, ознакомьтесь с этими статьями. Я же сосредоточусь на
механизмах, помогающих в написании Web-сервисов.
По сути, в ASP.NET существуют три различных подхода к кэшированию:
кэширование выходных данных ASP.NET, HTTP-ответов и приложений ASP.NET.
При кэширование выходных данных разработчик сообщает ASP.NET, что
результат запроса к конкретной страницы можно возвращать в ответ на все
дальнейшие запросы к этой странице. Для последующих запросов
ASPX-сценарий не исполняется - вместо этого незамедлительно возвращается
результат предыдущего запроса. В кэш выходных данных можно поместить как
всю страницу целиком, так и отдельные элементы управления ASP.NET.
Существуют механизмы для указания срока жизни кэшированных данных, равно
как и механизмы для кэширования различных представлений страницы в
зависимости от входных данных Web-форм.
Чтобы настроить кэширование выходных данных ASP.NET для Web-сервисов,
добавьте параметр CacheDuration к атрибуту WebMethod в определении
Web-метода. Этот параметр определяет время (в секундах), в течении
которого ответ хранится в кэше выходных данных. Следующий фрагмент кода
демонстрирует, как поместить данные в кэш на 60 секунд.
<WebMethod(CacheDuration:=60)> _
Public Function HelloWorld() As String
Return "Hello World"
End Function
В отличие от кэширования выходных данных ASP.NET кэширование
HTTP-ответов - просто способ, которым ASP.NET позволяет настроить
HTTP-заголовки так, чтобы клиент и прокси-серверы знали, как кэшировать
возвращаемые им HTTP-ответы. Для этого применяется класс
HttpCachePolicy. В коде Web-сервиса он доступен из
Context.Response.Cache, но, как уже говорилось, возможности его
применения к SOAP-запросам в теле POST-запросов ограниченны.
Третья форма кэширования в ASP.NET реализована в виде весьма
интересного класса Cache. Не путайте классы HttpCachePolicy и Cache -
даже несмотря на то, что их родительские классы ссылаются на оба этих
свойства как на "cache". Класс Cache доступен прямо из класса
HttpContext Web-сервиса. Он предоставляет базовую поддержку кэширования
для ASP.NET-приложения. В кэше можно хранить любые данные из его набора
(collection). Во многих отношениях этот класс похож на класс
HttpApplicationState, хранящий глобальные данные приложения в своем
наборе.
Однако в отличие от последнего для класса Cache разрешается задавать
критерий устаревания его данных. К примеру, можно указать, что объект,
сохраненный в классе, устареет в определенное время. Допускается
устанавливать и такие ограничения, при которых объект удаляется из кэша,
если к нему давно не было никаких обращений. Можно даже установить связь
между кэшированными элементами и файлами - тогда при изменении файла
соответствующий элемент удаляется из кэша. При следующем обращении к
кэшу этого элемента в нем не будет, и вам придется обновить данные -
скорее всего на основе новой информации в связанном файле. И конечно,
как и любой другой "законный" механизм кэширования, класс Cache
реализует средства для удаления неактивных элементов при нехватке
ресурсов.
Dim Foo as New MyFooClass()
Context.Cache.Insert("foo", _
Foo, _
Nothing, _
DateAdd(DateInterval.Minute, 30, Now()), _
System.Web.Caching.Cache.NoSlidingExpiration)
Функция Insert поддерживает несколько вариантов добавления данных в кэш.
Первый аргумент, "foo", - это ключ для ссылки на наш объект в наборе.
Второй параметр представляет сам элемент, помещаемый в кэш. Третий -
устанавливает зависимости вроде файловой, о которой мы уже упоминали. В
нашем случае никаких зависимостей нет, так что этот параметр равен
"Nothing". Следующий параметр явно задает срок устаревания элемента в
кэше. Вызовом функции DateAdd мы указали, что объект устареет через 30
минут. Наконец, последний параметр задает "скользящее" (sliding)
устаревание. Скользящее устаревание означает, что кэшированный элемент
удаляется, если к нему не обращаются в течении указанного промежутка
времени. Так как мы явно установили срок жизни данных (30 минут), этому
параметру присваивается значение NoSlidingExpiration.
Кэширование каталога MSDN Pencil Company
Теперь рассмотрим конкретный пример и определим стратегию кэширования
в этом сценарии. В последней статье рубрики At Your Service Скотт
определил ряд изменений в интерфейсе PencilDiscovery MSDN Pencil
Company, так что теперь можно запрашивать весь каталог, не заставляя
пользователей по нескольку раз выполнять поиск. Цель этого решения -
позволить "умным" клиентским приложениям кэшировать весь каталог и
обеспечить возможность запросов к данным. Это снизит нагрузку на наш
сервис за счет уменьшения запросов и, кроме того, предоставит
дополнительную информацию клиентам, пользующимся нашим сервисом. Мы
решили, что в нашей реализации мы скорее всего будем обновлять данные
раз в день, добавляя в каталог новые карандаши или удаляя те, которых
больше нет на складе.
С точки зрения выбора стратегий кэширования, решение этой задачи
упрощается благодаря нескольким моментам. Во-первых, данные
общедоступны, т. е. нам не нужно беспокоиться о том, чтобы у разных
пользователей были разные представление данных. Во-вторых, нам известно
точное время, когда данные обновляются, а значит, можно четко указать
срок их жизни.
Теперь выберем параметры кэширования прикладного уровня. С клиентской
стороны с этим особых проблем нет. Клиентское приложение запрашивает
данные раз в день и выполняет к ним любые запросы, пока не настанет
следующий день. Но одна проблема все же есть: уведомление клиентского
приложения о сроке жизни данных.
Один из вариантов решения этой проблемы - просто документировать, что
клиентское приложение должно обновлять данные каждые 24 часа, но тогда
появляются временные окна (максимальная продолжительность которых - 24
часа), в течение которых данные клиента могут отличаться от данных
Web-сервиса. Кроме того, мы определяем интерфейс, который могут
реализовать различные компании, и не исключено, что в какой-то компании
решат обновлять данные еженедельно, а не ежедневно.
Выход прост: включать информацию о сроке жизни данных в ответ. В
определение интерфейса Скотт
включил элемент ValidUntil (в раздел объявления типов каталога
карандашей). Мы воспользуемся этим полем для указания срока устаревания
данных каталога. Кроме того, включив информацию о времени в
SOAP-сообщение, мы получим дополнительное преимущество - поддержку
кэширования данных, даже если они передаются не по HTTP, а по другому
протоколу. Например, возможна ситуация, когда кто-то запрашивает каталог
от Web-сервиса по HTTP, а кто-то - по SMTP. Так как срок жизни хранится
не только в HTTP-заголовках, оно не потеряется при отправке сообщения по
SMTP.
Следующий клиентский код на Microsoft® Visual Basic® .NET
иллюстрирует использование клиентом свойства ValidUntil для того, чтобы
определить, надо ли обновить содержимое кэша каталога до выполнения
запроса пользователя.
Dim PencilResults() As org.pencilsellers.Pencil
If PencilCatalog.ValidUntil < Now() Then
Dim Discovery As New org.pencilsellers.DiscoveryBinding()
PencilCatalog = Discovery.GetCatalog()
End If
PencilResults = QueryCachedCatalog(PencilCatalog, QueryCriterion)
На серверной стороне мы должны не только предоставить клиенту сведения о
том, когда истекает срок хранения данных (с помощью элемента
ValidUntil), но и подумать о том, как избежать создания каталога "с
чистого листа" при получении каждого запроса. Один из способов добиться
этого - добавить параметр CacheDuration к атрибуту WebMethodAttribute. В
нашем случае CacheDuration имеет один недостаток: время хранения данных
фиксируется на этапе разработки. Установив CacheDuration равным 24
часам, мы можем столкнуться со следующей проблемой.
Допустим, мы создаем каталог в 6 утра 1-го апреля и устанавливаем
элемент ValidUntil, соответствующим 6 утра 2-го апреля. Эти данные
попадут в первый же ответ, и он окажется в кэше выходных данных ASP.NET.
Теперь предположим, что примерно в 10 вечера 1-го апреля поступает
огромное количество запросов к другим ASP.NET-страницам. Так как к
каталогу запросы не поступают, система, вероятно, удалит его из кэша,
чтобы освободить ресурсы кэша для более важных данных. Затем в 10:30
вечера 1-го апреля поступает еще один запрос к каталогу карандашей. Так
как в кэше выходных данных ответа нет, Web-метод запускается еще раз,
при этом срок жизни данных устанавливается равным 10:30 вечера 2-го
апреля. Тут-то проблема и проявляется: каталог карандашей обновится в 6
утра 2-го апреля, а из кэша будут по-прежнему поступать данные за
вчерашний день. Так что нам нужна система кэширования, которая позволяет
явно указать срок хранения данных в период выполнения.
Кэш приложений ASP.NET предлагает простой способ решения этой
проблемы. Запускаем утилиту WSDL.EXE из .NET Framework SDK с параметром
командной строки /Server и создаем различные классы на базе
WSDL-определения интерфейса Pencil Discovery, в том числе класс Catalog,
основанный на определенном в интерфейсе типе. Мы просто создаем каталог
из результатов SQL-запроса и добавляем его к кэшу приложений ASP.NET
посредством метода Insert. Срок жизни элемента в кэше устанавливается
равным значению ValidUntil каталога. Код для Web-метода GetCatalog
приведен ниже.
Заметьте, что я все равно применяю параметр CacheDuration для
добавления ответа к кэшу выходных данных ASP.NET, но теперь срок жизни
относительно мал - 10 минут. Тем самым я минимизирую время, в течение
которого возможен возврат устаревших данных, но все равно увеличиваю
производительность за счет кэширования, что весьма полезно, когда к
каталогу поступает множество запросов. Мы полагаем, что большинство
запросов к каталогу поступает в пределах 10 минут до окончания срока
хранения данных.
<System.Web.Services.WebMethodAttribute( _
CacheDuration:=600), _
System.Web.Services.Protocols.SoapDocumentMethodAttribute( _
"http://pencilsellers.org/2002/04/pencil/GetCatalog", _
RequestNamespace:= _
"http://pencilsellers.org/2002/04/pencil/discovery", _
ResponseNamespace:= _
"http://pencilsellers.org/2002/04/pencil/discovery", _
Use:=System.Web.Services.Description.SoapBindingUse.Literal, _
ParameterStyle:= _
System.Web.Services.Protocols.SoapParameterStyle.Wrapped, _
Binding:="DiscoveryBinding")> _
Public Overrides Function GetCatalog() As Catalog
Dim PencilCatalog As Catalog
If Context.Cache("PencilCatalog") Is Nothing Then
PencilCatalog = CreateCatalog()
Context.Cache.Insert("PencilCatalog", _
PencilCatalog, _
Nothing, _
PencilCatalog.ValidUntil, _
System.Web.Caching.Cache.NoSlidingExpiration)
Else
PencilCatalog = Context.Cache("PencilCatalog")
If PencilCatalog.ValidUntil < Now() Then
Context.Cache.Remove("PencilCatalog")
PencilCatalog = CreateCatalog()
Context.Cache.Insert("PencilCatalog", _
PencilCatalog, _
Nothing, _
PencilCatalog.ValidUntil, _
System.Web.Caching.Cache.NoSlidingExpiration)
End If
End If
Return PencilCatalog
End Function
Заключение
Весьма вероятно, что при разработке Web-сервисов XML вам захочется
реализовать какой-нибудь механизм кэширования данных. Это можно сделать
разными способами: воспользоваться ограниченными функциями кэширования
HTTP, выполнять кэширование прикладного уровня на сервере, кэшировать
ответы на клиенте или просто разработать Web-сервис так, чтобы
"интеллектуальные" клиенты снимали часть нагрузки с сервера. В следующей
статье Скотт собирается рассмотреть проблему, с которой часто
сталкиваются разработчики и потребители Web-сервисов: слияние XML-данных
(XML merging). Скотт объединит каталог, возвращаемый Web-методом
GetCatalog, с каталогом другой компании, и Web-сайт сможет предоставлять
своим пользователям общий каталог карандашей.
Мэт Пауэлл (Matt Powell) - член группы MSDN Architectural Samples.
Участвовал в разработке прорывного SOAP Toolkit 1.0. Среди других
достижений Мэта - книга "Running Microsoft Internet Information Server"
(Microsoft Press), написанная в соавторстве, а также многочисленные
статьи в журналах. А дома его всегда ждет замечательная семья.