Оптимизация сети

62

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

Включение HTTP-заголовков кеширования

Один из способов сэкономить трафик - обеспечить кеширование на стороне браузера всего содержимого, которое остается неизменным в течение некоторого времени. Отличными кандидатами для кеширования в браузере являются статические файлы изображений, сценариев JavaScript и CSS. Однако динамическое содержимое, такое как файлы .aspx и .ashx также часто могут кешироваться, если фактическое их содержимое изменяется не слишком часто.

Настройка заголовков кеширования для статического содержимого

Статические файлы обычно отправляются клиентам с двумя заголовками кеширования:

ETag

В этот HTTP-заголовок сервер IIS записывает хеш, вычисленный на основе даты последнего изменения содержимого. Для статического содержимого, такого как файлы изображений и CSS, в заголовке ETag сервер IIS возвращает дату последнего изменения файла. В последующем, когда на сервер будут поступать запросы с ранее вычисленным значением ETag, IIS вычислит ETag для запрошенного файла и, если оно не совпадет с клиентским значением ETag, обратно будет отправлен запрошенный файл, а если совпадет, клиенту будет отправлен ответ HTTP 304 (Not Modified). Значение ETag, сохраненное клиентом, передается серверу в HTTP-заголовке If-None-Match запроса.

Last-Modified

В этот HTTP-заголовок сервер IIS записывает дату последнего изменения запрошенного файла. Это - дополнительный заголовок кеширования, который может использоваться как запасной вариант, когда поддержка заголовка ETag отключена, когда на сервер будут поступать запросы, содержащие дату последнего изменения, IIS сравнит ее с датой последнего изменения файла и решит, отправить ли содержимое файла (если время последнего изменения файла изменилось) или послать ответ HTTP 304. Дата последнего изменения файла, сохраненная клиентом, передается серверу в HTTP-заголовке If-Modified-Since Запроса.

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

Этого можно добиться с помощью HTTP-заголовка Cache-Control с атрибутом max-age или HTTP-заголовка Expires. Разница между атрибутом max-age и заголовком Expires в том, что max-age определяет срок хранения, а заголовок Expires позволяет указать фиксированную дату и время, когда истекает срок хранения содержимого.

Например, если установить в атрибуте max-age значение 3600, браузер будет хранить содержимое в кеше один час (3600 секунд = 60 минут = 1 час) и автоматически использовать его, не посылая запросы серверу. Как только срок хранения содержимого истечет (независимо от использовавшегося заголовка кеширования), оно будет помечено как устаревшее. Когда в следующий раз браузер обнаружит, что содержимое устарело, он отправит на сервер запрос на получение более нового содержимого.

Убедиться в отсутствии запросов на получение кешированного содержимого можно с помощью инструментов мониторинга HTTP-трафика, таких как Fiddler, и узнать, какие запросы, посылаются серверу. Если обнаружится запрос на содержимое, которое по вашему мнению должно кешироваться, проверьте наличие заголовков max-age/Expires в ответе на этот запрос.

Использование max-age/Expires совместно с ETag/Last-Modified гарантирует, что на запрос, отправленный после того, как истечет срок хранения содержимого, будет возвращен ответ HTTP 304, если запрошенное содержимое на сервере в действительности не изменилось. Ответ в этом случае будет содержать новый HTTP-заголовок max-age/Expires.

В большинстве браузеров щелчок на кнопке Refresh (Обновить) (или нажатие клавиши F5) заставит браузер обновить кеш, проигнорировав заголовок max-age/Expires, и отправить запрос на получение кешированного содержимого, даже если срок его хранения еще не истек. Запросы все еще будут содержать заголовки If-Modified-Since/If-None-Match, если они применимы, чтобы сервер мог вернуть ответ HTTP 304, если содержимое не изменилось.

Чтобы включить поддержку заголовка max-age, добавьте следующие настройки в файл web.config:

<configuration>
  <system.webServer>
    <staticContent>
      <clientCache cacheControlMode="UseMaxAge" 
	               cacheControlMaxAge="0:10:00" />
    </staticContent>
  </system.webServer>
</configuration>

Фрагмент с настройками выше гарантирует, что в ответ на все запросы, отправленные для получения статического содержимого, будут возвращаться ответы с HTTP-заголовком cache-control, содержащим атрибут max-age со значением 600 секунд.

Чтобы задействовать заголовок Expires, измените элемент clientCache, как показано ниже:

<configuration>
  <system.webServer>
    <staticContent>
      <clientCache cacheControlMode="UseMaxAge" 
	               httpExpires = "Fri, 11 Jul 2015 6:00:00 GMT" />
    </staticContent>
  </system.webServer>
</configuration>

Фрагмент с настройками выше сообщает, что срок хранения статического содержимого выше истечет 11 июля 2015 года в 6 часов утра.

Если потребуется указать разные сроки хранения для разного содержимого, например, для файлов JavaScript - фиксированную дату, а для изображений 100-дневный срок хранения, можно добавить разделитель location и указать разные настройки для разных компонентов приложения, как показано ниже:

<configuration>
  <location path="Scripts">
    <system.webServer>
      <staticContent>
        <clientCache cacheControlMode="UseExpires" 
		             httpExpires="Fri, 11 Jul 2015 6:00:00 GMT" />
      </staticContent>
    </system.webServer>
  </location>
  <location path="Images">
    <system.webServer>
      <staticContent>
        <clientCache cacheControlMode="UseMaxAge" 
		             cacheControlMaxAge="100.0:00:0" />
      </staticContent>
    </system.webServer>
  </location>
</configuration>

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

Настройка заголовков кеширования для динамического содержимого

Статические файлы имеют дату последнего изменения, которую можно использовать для проверки факта изменения содержимого после сохранения в кеше. Однако динамическое содержимое не имеет такой характеристики, потому что динамическое содержимое воссоздается заново в ответ на каждый запрос и датой его последнего изменения фактически является текущая дата, поэтому такие заголовки, как ETag и Last-Modified не подходят для кеширования динамического содержимого.

С другой стороны, если изучить содержимое динамической страницы, можно найти способ выразить дату последнего ее изменения или даже рассчитать значение ETag для нее. Например, если в ответ на запрос информация о продукте извлекается из базы данных, таблица со списком продуктов могла бы хранить поле с датой последнего изменения, которую в свою очередь можно было бы использовать для установки заголовка Last-Modified. В случае отсутствия такого поля в таблице, можно попробовать вычислить контрольную сумму MD5 полей и отправлять результат в заголовке ETag. При получении последующих запросов сервер мог бы вычислять контрольную сумму MD5 заново и при совпадении ее со значением ETag возвращать ответ HTTP 304.

Например, следующий код устанавливает заголовок Last-Modified для динамической страницы с описанием продукта:

Response.Cache.SetLastModified(product.LastUpdateDate);

В отсутствие даты последнего изменения, можно возвращать в заголовке ETag контрольную сумму MD5, как показано ниже:

Response.Cache.SetCacheability(
    HttpCacheability.ServerAndPrivate);

// Вычислить контрольную сумму MD5
System.Security.Cryptography.MD5 md5 = 
    System.Security.Cryptography.MD5.Create();

string contentForEtag = entity.PropertyA + entity.NumericProperty.ToString();
byte[] checksum = md5.ComputeHash(
    System.Text.Encoding.UTF8.GetBytes(contentForEtag));

// Создать строку ETag на основе контрольной суммы. 
// Строки ETag должны заключаться в двойные кавычки, как того
// требует стандарт
string etag = "\"" + Convert.ToBase64String(checksum, 0, checksum.Length)
    + "\"";
	
Response.Cache.SetETag(etag);

По умолчанию поддержка заголовка ETag в ASP.NET выключена. Чтобы включить ее, необходимо изменить режим кеширования в ServerAndPrivate, разрешив кеширование содержимого на стороне сервера и на стороне клиента, но не на промежуточных компьютерах, таких как прокси-серверы.

Получив запрос с заголовком ETag, вы можете сравнить вычисленное значение ETag с полученным от браузера и, если они совпадают, послать ответ HTTP 304, как показано ниже:

if (Request.Headers["If-None-Match"] == calculatedETag)
{
    Response.Clear();
    Response.StatusCode = (int)System.Net.HttpStatusCode.NotModified;
    Response.End();
}

При наличии каких-либо предположений о сроке жизни динамического содержимого, можно устанавливать заголовки max-age или Expires. Например, если предполагается, что описание снятого с производства продукта никогда не изменится, можно установить срок хранения описания этого продукта равным одному году, как в следующем фрагменте:

if (productIsDiscontinued)
    Response.Cache.SetExpires(DateTime.Now.AddYears(1));

Для той же цели можно использовать атрибут max-age в заголовке Cache-Control:

if (productIsDiscontinued)
    Response.Cache.SetMaxAge(TimeSpan.FromDays(365));

Вместо установки сроков кеширования в коде, их можно устанавливать в файлах .aspx, в директивах кеширования. Например, если информация о продукте, отображаемая на странице, может храниться в кеше в течение 10 минут (600 секунд) на стороне клиента, в страницу можно добавить следующую директиву:

<%@ Page ... %>
<%@ OutputCache Duration="600" Location="Client"%>

При использовании директивы OutputCache указанная продолжительность хранения содержимого в кеше передается одновременно в заголовках max-age и Expires (значение для заголовка Expires вычисляется добавление продолжительности к текущей дате).

Включение сжатия в IIS

Кроме мультимедийных файлов (аудио- и видеороликов, изображений) и двоичных файлов, таких как компоненты Silverlight и Flash, большая часть содержимого (страницы HTML, таблицы стилей CSS, сценарии JavaScript, данные в формате XML и JSON) передается веб-сервером в текстовом виде. Используя поддержку сжатия, имеющуюся в IIS, эти текстовые данные можно сжимать в размерах, что позволит уменьшить объем передаваемых данных и время его передачи. Поддержка сжатия в IIS позволяет уменьшать размеры ответов до 50-60 процентов от их оригинального размера, а иногда и больше.

Сервер IIS поддерживает два типа сжатия, статическое и динамическое. Прежде чем использовать сжатие, убедитесь сначала в наличии компонентов статического и динамического сжатия.

Статическое сжатие

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

Статическое сжатие может пригодиться для файлов, которые обычно не изменяются (то есть, являются «статическими»), таких как файлы CSS и JavaScript. Но даже если оригинальный файл изменится, IIS обнаружит это и повторно сожмет его.

Имейте в виду, что наибольшего эффекта можно получить при сжатии текстовых файлов (*.htm, *.txt, *.css) и некоторых двоичных, таких как файлы документов Microsoft Office (*.doc, *.xsl). Но при применении к уже сжатым файлам, таким как файлы изображений (*.jpg, *.png) и сжатым файлам документов Microsoft Office (.docx, *.xslx) может получиться даже обратный эффект.

Динамическое сжатие

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

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

Настройка сжатия

Первое, что следует сделать для использования сжатия, - включить динамическое или статическое сжатие, или оба сразу. Чтобы включить сжатие в IIS, откройте приложение IIS Manager, выберите свой компьютер, щелкните на пункте Compression (Сжатие) и выберите параметры сжатия, как показано на рисунке ниже:

Включение динамического и статического сжатия в приложении IIS Manager

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

После выбора типа сжатия можно пойти еще дальше и определить, какие типы MIME должны сжиматься статически, а какие динамически. К сожалению, IIS не поддерживает возможность выполнения этих настроек из приложения IIS Manager, поэтому вам придется изменить их вручную, в конфигурационном файле IIS - applicationHost.config, находящийся в папке %windir%\System32\inetsrv\config folder.

Откройте файл и найдите раздел <httpCompression>. В нем уже должно быть указано несколько типов MIME для статического и динамического сжатия. В дополнение к уже указанным типам вы можете добавить свои типы MIME содержимого, используемого вашими веб-приложениями. Например, при использовании функций AJAX, возвращающих данные в формате JSON, можно добавить динамическое сжатие для этого формата. Следующий фрагмент демонстрирует, как включить поддержку динамического сжатия для формата JSON:

<httpCompression>
	<dynamicTypes>
		<add mimeType="application/json; charset=utf-8" enabled="true" />
	</dynamicTypes>
</httpCompression>

После добавления в список новых типов MIME проверьте, действительно ли сжатие выполняется, с помощью инструментов перехвата HTTP-трафика, таких как Fiddler. Сжатые ответы должны иметь заголовок Content-Encoding со значением gzip или deflate.

Сжатие и клиентские приложения

Чтобы сервер IIS мог сжимать исходящие ответы, он должен быть уверен, что клиентское приложение способно распаковывать сжатое содержимое. Поэтому, когда клиентское приложение посылает запрос серверу, оно должно добавить HTTP-заголовок Accept-Encoding со значением gzip или deflate.

Большинство известных браузеров автоматически добавляют этот заголовок, поэтому при работе с веб-приложением или с приложением Silverlight посредством браузера, IIS будет возвращать сжатое содержимое. Однако, если вы посылаете HTTP-запросы из своего приложения для .NET, используя для этого тип HttpWebRequest, заголовок Accept-Encoding не добавляется автоматически, поэтому вам необходимо добавить его вручную. Кроме того, HttpWebRequest не распаковывает сжатые ответы, если не настроить его специально.

Например, при использовании объекта HttpWebRequest добавьте следующий код, чтобы включить возможность приема и распаковывания сжатого содержимого:

var request = (HttpWebRequest)HttpWebRequest.Create(uri);
request.Headers.Add(HttpRequestHeader.AcceptEncoding, 
    "gzip,deflate");
request.AutomaticDecompression = 
    DecompressionMethods.GZip | DecompressionMethods.Deflate;

Другие объекты, обладающие возможностью обмена по протоколу HTTP, такие как прокси-объект веб-службы ASMX или объект WebClient, также поддерживают сжатие, но требуют ручной настройки для отправки заголовка и распаковывания сжатого содержимого. Службы WCF на основе протокола HTTP, до версии WCF 4, не поддерживали сжатие в клиентах .NET, использующих ссылки на службы или фабрику каналов. Начиная с версии WCF 4, сжатие поддерживается автоматически (отправка заголовков и распаковывание сжатого содержимого).

Минификация и объединение

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

  1. Количество запросов, которые браузер должен отправить и на которые должен получить ответы. Чем больше запросов приходится отправлять, тем больше времени будет тратиться на отправку всех запросов, потому что браузеры могут одновременно открывать лишь ограниченное количество соединений к единственному серверу (например, IE 9 может одновременно послать одному и тому же серверу не более 6 запросов).

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

Чтобы решить эти проблемы, необходимо средство, позволяющее уменьшать размеры ответов и сокращать количество запросов (и ответов, соответственно). В ASP.NET MVC 4 и ASP.NET 4.5 это средство встроено в фреймворк и называется «Bundling and minification» (Объединение и минификация).

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

Применение приема минификации совместно со сжатием может значительно уменьшить размеры ответов. Например, оригинальный файла библиотеки jQuery 1.6.2 имеет размер 240 Кбайт. После сжатия он уменьшается примерно до 68 Кбайт. Минифицированная версия оригинального файла имеет размер 93 Кбайт, чуть больше сжатой версии, а после сжатия минифицированной версии, размер файла уменьшается до 33 Кбайт, что составляет примерно 14 процентов от первоначального размера.

Подробно процесс минификации файлов с помощью пакета Microsoft.AspNet.Web.Optimization обсуждается в статье "Сжатие кода JavaScript и CSS".

Использование сетей доставки содержимого (CDN)

Одной из проблем производительности, с которой часто сталкиваются веб-приложения, - это задержки, возникающие при доступе к ресурсам в сети. Когда веб-сервер и конечный пользователь находятся в одной локальной сети, время отклика обычно остается достаточно маленьким, но когда веб-приложение переносится в глобальную сеть и к нему начинают обращаться пользователи из разных уголков Земли через Интернет, удаленные пользователи, например с других континентов, наверняка будут замечать значительные задержки из-за сетевых проблем.

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

Здесь нам на помощь могут прийти сети доставки содержимого (Content Delivery Networks, CDN). Сеть доставки содержимого - это множество веб-серверов, размещенных в разных уголках Земли и обеспечивающих географическую близость вашего веб-приложения к конечному пользователю. При использовании CDN, вы фактически используете единый адрес сети CDN, где бы ни находились, а местный сервер имен DNS будет преобразовывать этот адрес в фактический адрес ближайшего сервера CDN. Различные Интернет-компании, такие как Microsoft, Amazon и Google, имеют собственные сети CDN, которыми можно пользоваться.

Ниже описывается типичная последовательность действий по настройке сети CDN:

  1. В настройках сети CDN вы указываете, где находится оригинальное содержимое.

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

  3. В ответ па последующие запросы сервер CDN будет возвращать кешированное содержимое, не обращаясь к вашему веб-серверу, что обеспечит более быстрый отклик на запросы и, возможно, более высокую скорость передачи.

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

На первом шаге вам следует выбрать поставщика услуг CDN и настроить доставку своего содержимого, как описывается в инструкции поставщика. Когда у вас появится адрес CDN, просто замените ссылки на статическое содержимое (изображения, стили, сценарии) так, чтобы они ссылались на адрес CDN. Например:

<script src="//ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>
Пройди тесты
Лучший чат для C# программистов