Оптимизация приоритезации HTTP/2 с помощью BBR и tcp_notsent_lowat

Для достижения наилучшей производительности конечного пользователя при использовании HTTP/2 требуется хорошая поддержка приоритезации ресурсов. Хотя большинство веб-серверов поддерживают приоритезацию HTTP/2, для того, чтобы она хорошо работала на всем пути к браузеру, требуется довольно много координации в сетевом стеке. В этой статье мы расскажем о некоторых взаимодействиях между веб-сервером, операционной системой и сетью и о том, как настроить сервер для оптимизации производительности для конечных пользователей.

На ядрах Linux 4.9 и более поздних версий включите контроль перегрузки BBR и установите tcp_notsent_lowat на 16KB, чтобы приоритезация HTTP/2 работала надежно. Это можно сделать в файле /etc/sysctl.conf:

Браузеры и приоритезация запросов

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

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

Большинство современных браузеров используют схожие схемы расстановки приоритетов, которые обычно выглядят следующим образом:

  1. Загружать похожие ресурсы (скрипты, изображения, стили) в порядке их перечисления в HTML.
  2. Стили/CSS загружаются раньше всего остального, потому что контент не может быть отображен до завершения работы над стилями.
  3. Загружать блокирующие скрипты/JavaScript следующими, потому что блокирующие скрипты не дают браузеру перейти к следующей инструкции в HTML, пока они не будут загружены и выполнены.
  4. Загружайте изображения и неблокирующие скрипты (async/defer).

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

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

Приоритетность HTTP/1.x

В HTTP/1.x каждое соединение с сервером может поддерживать один запрос за раз (практически в любом случае, поскольку ни один браузер не поддерживает конвейерную обработку), а большинство браузеров открывают до 6 соединений одновременно с каждым сервером. Браузер ведет приоритетный список необходимого ему контента и выполняет запросы к каждому серверу по мере появления соединения. Когда обнаруживается высокоприоритетный фрагмент контента, он перемещается в начало списка, и когда следующее соединение становится доступным, он запрашивается.

Расстановка приоритетов в HTTP/2

При использовании HTTP/2 браузер использует одно соединение, а запросы мультиплексируются через это соединение в виде отдельных "потоков". Все запросы отправляются на сервер сразу после их обнаружения вместе с информацией о приоритетах, чтобы сервер знал предпочтительный порядок ответов. Затем сервер должен сделать все возможное, чтобы доставить сначала наиболее важные ответы, а затем ответы с более низким приоритетом. Когда на сервер поступает запрос с высоким приоритетом, он должен немедленно опередить ответы с более низким приоритетом, даже в середине ответа. Фактическая схема приоритетов, реализованная в HTTP/2, допускает параллельную загрузку с взвешиванием между ними и более сложные схемы. На данный момент проще всего думать об этом просто как об упорядочивании приоритетов ресурсов.

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

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

Буферы в Интернете

Чрезмерная буферизация - это практически заклятый враг HTTP/2, поскольку она напрямую влияет на способность сервера оперативно реагировать на смену приоритетов. Нередко буферизация между сервером и браузером составляет мегабайты, что больше, чем у большинства веб-сайтов. Практически это означает, что ответы будут доставляться в том порядке, в котором они становятся доступными на сервере. Нередко критически важный ресурс (например, шрифт или блокирующий рендеринг скрипт в <head> документа) задерживается мегабайтами изображений с более низким приоритетом. Для конечного пользователя это выражается в секундах или даже минутах задержки рендеринга страницы.

Буферы отправки TCP

Первый уровень буферизации между сервером и браузером находится в самом сервере. Операционная система поддерживает буфер отправки TCP, в который сервер записывает данные. Как только данные попадают в буфер, операционная система заботится о доставке данных по мере необходимости (извлекая их из буфера по мере отправки и сигнализируя серверу, когда буферу требуется больше данных). Большой буфер также снижает нагрузку на процессор, поскольку уменьшает количество записей, которые серверу приходится делать в соединение.

Фактический размер буфера отправки должен быть достаточно большим, чтобы сохранить копию всех данных, которые были отправлены браузеру, но еще не подтверждены, на случай, если пакет будет потерян и некоторые данные придется передавать повторно. Слишком маленький буфер не позволит серверу максимально использовать пропускную способность соединения с клиентом (и является распространенной причиной медленной загрузки на больших расстояниях). В случае HTTP/1.x (и многих других протоколов), данные доставляются в массовом порядке, и настройка буферов на максимально возможный размер не имеет никаких недостатков, кроме увеличения использования памяти (обмен памяти на процессор). Увеличение размера буфера отправки TCP является эффективным способом повышения пропускной способности веб-сервера.

Для HTTP/2 проблема с большими буферами отправки заключается в том, что они ограничивают гибкость сервера в корректировке данных, отправляемых по соединению, по мере поступления высокоприоритетных ответов. Как только данные ответа записаны в буфер отправки TCP, они не зависят от сервера и должны быть доставлены в том порядке, в котором они были записаны.

Оптимальный размер буфера отправки для HTTP/2 - это минимальный объем данных, необходимый для полного использования доступной пропускной способности браузера (которая различна для каждого соединения и меняется со временем даже для одного соединения). Практически вы хотите, чтобы буфер был немного больше, чтобы обеспечить некоторое время между сигналом серверу о необходимости дополнительных данных и моментом, когда сервер записывает дополнительные данные.

TCP_NOTSENT_LOWAT

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

TCP_NOTSENT_LOWAT может быть настроен в коде для каждого сокета, если программное обеспечение веб-сервера поддерживает его, или для всей системы с помощью sysctl net.ipv4.tcp_notsent_lowat:

Экспериментально, значение 16,384 (16K) оказалось хорошим балансом, при котором соединения используются полностью с незначительными дополнительными накладными расходами процессора. Это означает, что не более 16 КБ данных с низким приоритетом будут буферизированы, прежде чем ответ с более высоким приоритетом сможет прервать их и быть доставлен.

Bufferbloat

Помимо буферизации на сервере, буфером может служить сетевое соединение между сервером и браузером. Все чаще сетевое оборудование имеет большие буферы, которые поглощают данные, отправляемые быстрее, чем принимающая сторона может их потреблять. Это явление обычно называют "буферизацией" (Bufferbloat). Я немного смягчил свое объяснение эффективности tcp_notsent_lowat, сказав, что оно основано на текущем окне перегрузки, которое является оценкой оптимального количества необходимых данных в полете, но не обязательно фактического оптимального количества данных в полете.

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

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

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

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

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

Управление перегрузкой BBR

BBR - это новый алгоритм контроля перегрузки от Google, который использует изменения в задержках пакетов для моделирования перегрузки вместо того, чтобы ждать, пока пакеты упадут. Как только он видит, что подтверждение пакетов занимает больше времени, он предполагает, что соединение насыщено и пакеты начали буферизироваться. В результате окно перегрузки часто очень близко к оптимальному, необходимому для поддержания полного использования соединения и избежания Bufferbloat. BBR был включен в ядро Linux в версии 4.9 и может быть настроен через sysctl:

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

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

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

Технически, любой контроль перегрузки, который держит Bufferbloat под контролем и поддерживает точное окно перегрузки, будет работать для поддержания буферов отправки TCP под контролем, BBR просто оказывается одним из них (с множеством хороших свойств).

Соединяем все вместе

Комбинация TCP_NOTSENT_LOWAT и BBR снижает количество буферизации в сети до абсолютного минимума и имеет решающее значение для хорошей производительности конечного пользователя при использовании HTTP/2. Это особенно актуально для NGINX и других HTTP/2 серверов, которые не реализуют собственное дросселирование буфера.

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

Даже при кабельном соединении 5 Мбит/с правильное распределение ресурсов может привести к тому, что страница будет отображаться значительно быстрее (а при более медленном соединении эта разница может вырасти до десятков секунд или даже минут).

Понравилась статья? Поделиться с друзьями:
Добавить комментарий