Обходим ограничения браузера на число соединений

Активное (англ. keep-alive) соединение стало настоящим прорывом в спецификации HTTP 1.1: оно позволяло использовать уже установленный канал для повторной передачи информации от клиента к серверу и обратно (в HTTP 1.0 соединение закрывалось сразу же после передачи информации от сервера, что добавляло задержки, связанные с трехступенчатой передачей пакетов). В том случае, если проблема свободных ресурсов стоит довольно остро, можно рассмотреть выставление небольшого тайм-аута для таких соединений (5-10 секунд).

Однако HTTP 1.1 добавил веб-разработчикам головной боли по другому поводу. Давайте будем разбираться, как нам устранить и эту проблему.

Издержки на доставку объектов

Средняя веб-страница содержит более 50 объектов, и издержки на число объектов доминируют над всеми остальными задержками при загрузке большинства веб-страниц. Браузеры, следуя рекомендациям спецификации HTTP 1.1, обычно устанавливают не более 2 одновременных соединений с одним хостом. При увеличении числа HTTP- запросов, требуемых для отображения страницы, с 3 до 23 — время, затрачиваемое именно на «чистую» загрузку объектов, от общего времени загрузки падает с 50% до всего 14%.

Если число объектов на странице превышает 4, то издержки на ожидание доступных потоков и разбор чанков для присланных объектов превалируют над общим временем загрузки страницы (от 80% до 86% для 20 и 23+ объектов, соответственно) по сравнению со временем, которое уходит на действительную загрузку данных. Время инициализации плюс время ожидания, вызванное ограничением на параллельные соединения, занимают 50-86% от общего времени загрузки страницы.

При увеличении числа подключаемых объектов сверх 10 время, затрачиваемое на инициализацию соединения, возрастает до 80% и более от общего времени, уходящего на получение объектов. Стоит отметить, что можно существенно уменьшить издержки на доставку большого числа объектов (более чем 12 на страницу) включением для сервера keep-alive режима и распределением запросов по нескольким хостам.

Ограничения спецификации HTTP/1.1

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

Спецификация HTTP, приблизительно 1999 года, рекомендует, чтобы браузеры и серверы ограничивали число параллельных запросов к одному хосту — двумя. Эта спецификация была написана задолго до существенного расширения каналов загрузки и была рассчитана на соединения с маленькой скоростью загрузки. Большинство браузеров поддерживают это ограничение на число потоков в спецификации, хотя переход на HTTP 1.0 увеличивает число параллельных загрузок до 4. Поэтому большинство браузеров серьезно ограничено этим параметром, если им приходится загружать большое число объектов с одного хоста (по словам Алекса Могилевского, в IE8 это число будет равно 6 из-за определенных издержек на установление нового соединения). Существует два основных пути для обхода этого ограничения (не считая, конечно, тонкой настройки используемого клиентами браузера, о которой рассказывается в восьмой главе):

  • Отдавать ваши объекты с нескольких серверов
  • Создать несколько поддоменов для нескольких хостов

Чтобы найти подходящий баланс, IE до версии 7 включительно ограничивают пользователей всего восемью одновременными соединениями или двумя соединениями на хост для протокола HTTP 1.1. HTTP 1.0 немного отличается в этом плане, но это уже совсем другая история, потому что все выгоды от постоянных соединений доступны только, если мы будем использовать HTTP 1.1 (или уже так делаем).

Времена меняются

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

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

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

«Режем» соединения

Большинство сайтов обладают всего одним хостом, поэтому все запросы вынуждены бороться за 2 доступных соединения к этому хосту. Одним из наиболее эффективных методов для увеличения числа параллельных потоков будет распределение содержания по нескольким хостам. Это не так сложно сделать, потому что браузеры обращают внимание только на название хоста, а не на IP-адрес. Таким образом, к каждому из хостов images1.yoursite.ru и images2.yoursite.ru браузеры смогут установить по два соединения.

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

SpeedUpYourWebsite.v1.2_img_23

Рис. 23. Загрузка изображений при двух соединениях. Источник: www.ajaxperformance.com

На этом графике хорошо видно, что для musicstore.ajaxperformance.com открыто только 2 соединения (данная диаграмма является модельной и справедлива только для IE, во всех остальных браузерах, по умолчанию, открывается большие соединений): C0 и C2. Мы используем протокол HTTP 1.1, поэтому нам не нужно открывать отдельное соединение для каждой картинки, но мы по-прежнему теряем кучу времени на обслуживание индивидуальных запросов к объектам. Время на установление соединения (время до получения первого байта, голубая полоска на диаграмме) явно доминирует над временем загрузки данных, которое не так велико (красная полоска на диаграмме).

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

images1.yoursite.ru
images2.yoursite.ru

Загрузка при шести соединенияхimages3.yoursite.ru Однако каждый из этих поддоменов не обязан находиться на отдельном сервере.

Лучше, больше, быстрее

Чтобы улучшить производительность, можно создать CNAME-записи в DNS-таблице для images1.yoursite.ru, images2.yoursite.ru и images3.yoursite.ru, каждая из которых указывает обратно на основной хост.

Стоит обратить внимание, что при проектировании масштабируемых приложений, которые будут распределять объекты по различным хостам, нужно использовать хеш- функцию. Она установит однозначное соответствие между названием изображения и хостом, с которого оно должно загружаться. В качестве простых примеров можно привести остаток от деления md5-суммы или длины строки адреса изображения на число хостов. Также можно рассмотреть использование контрольной суммы CRC32, которую проще посчитать на JavaScript.

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

SpeedUpYourWebsite.v1.2_img_24

Рис. 24. Загрузка при шести соединениях. Источник: www.ajaxperformance.com

Реальный выигрыш

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

Существует масса примеров применения этого метода в реальных AJAX-приложениях. Чтобы утилизировать параллельность соединений, на Google Maps ( http://maps.google.com/ ) картинки поставляются с нескольких хостов, начиная с mt0.google.com и заканчивая mt3.google.com. На Virtual Earth ( http://local.live.com/ ) также используется эта техника.

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

В данном случае, наверное, наиболее практичным решением будет размещение всей статики (кроме, пожалуй, CSS- и JavaScript-файлов, которые влияют на стадию предзагрузки — чтобы максимально избежать на этой стадии задержек на дополнительные DNS-запросы) на отдельном домене, например, static.example.com, а загрузка HTML-страниц, которые требовательны к базе, будет вестись с основного хоста. При этом static.example.com может иметь даже другой IP-адрес и обслуживаться любым «легким» сервером. Этот прием, может быть, и не сильно ускорит загрузку нашей страницы, но определенно улучшит ощущаемую производительность, позволяя пользователю загружать все статические файлы без дополнительных задержек.

Подводим итоги

Сейчас средняя веб-страница состоит более чем из 50 объектов (для Рунета, по статистическим данным webo.in, ситуация весьма похожа: число объектов колеблется в пределах 40-50), поэтому минимизация издержек на доставку объектов является весьма критичной для клиентской производительности. Также можно уменьшить число объектов на странице, если использовать технику CSS Sprites (или data:URI) и объединение текстовых файлов на сервере. Так как в данный момент у пользователей достаточно быстрый канал, то можно достигнуть уменьшения времени загрузки до 40-60% (зависит от общего числа объектов). Можно использовать 2 или 3 хоста для обслуживания объектов с одного сервера, чтобы «обмануть» браузеры в их ограничениях на загрузку нескольких объектов параллельно.

При этом нужно помнить, что увеличение одновременных запросов повлечет задействование дополнительных ресурсов со стороны сервера (это может быть, например, как максимальное число открытых соединений или портов, так и дополнительные объемы оперативной памяти). Поэтому данный подход стоит активно испольовать только при наличии «легкого» сервера, который способен одновременно поддерживать тысячи и десятки тысяч открытых соединений без особого ущерба для производительности (например, nginx или 0W).

Стоит коснуться еще одного, весьма интересного, момента в оптимизации времени загрузки путем увеличения числа параллельных потоков. Заключается он в выравнивании и увеличении размера одновременно загружаемых объектов, чтобы максимально использовать имеющиеся соединения. Например, если у вас есть 40 картинок по 5 Кб, то гораздо выгоднее будет отдавать 10 картинок по 20 Кб с двух хостов, чем 20 (по 10 Кб) с 4 хостов или 40 — с 8. Общие задержки в первом случае будут минимальными в силу максимизации эффективной скорости загрузки данных клиенту.

Можно пойти и дальше и загружать, например, 4 картинки по 50 Кб в 4 потока, достигая просто феноменального ускорения. Однако тут вступает в роль психологический фактор: пользователю будет не комфортно, если он будет видеть страницу вообще без картинок все время, пока грузится 50 Кб, и он может просто уйти с сайта.

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

Posted in Разгони свой сайт.