Хабрахабр

[Перевод] Протокол QUIC в деле: как его внедрял Uber, чтобы оптимизировать производительность

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

Приятного чтения! Картинки кликабельны.

Uber – это мировой масштаб, а именно 600 городов присутствия, в каждом из которых приложение полностью полагается на беспроводной интернет от более чем 4500 сотовых операторов. Пользователи ожидают, что приложение будет работать не просто быстро, а в реальном времени – чтобы обеспечить это, приложению Uber нужны низкие задержки и очень надежное соединение. Увы, но стек HTTP/2 плохо себя чувствует в динамичных и склонных к потерям беспроводных сетях. Мы уяснили, что в данном случае низкая производительность напрямую связана с реализациями TCP в ядрах операционных систем.

В данный момент рабочая группа IETF стандартизирует QUIC как HTTP/3. Чтобы решить проблему, мы применили QUIC, современный протокол с мультиплексированием каналов, который дает нам больше контроля над производительностью транспортного протокола.

Мы наблюдали снижение в диапазоне 10-30% для HTTPS-трафика на примере водительского и пассажирского приложений. После подробных тестов, мы пришли к выводу, что внедрение QUIC в наше приложение сделает «хвостовые» задержки меньше по сравнению с TCP. Также QUIC дал нам сквозной контроль над пользовательскими пакетами.

В этой статье мы делимся опытом по оптимизации TCP для приложений Uber с помощью стека, который поддерживает QUIC.

Последнее слово техники: TCP

Сегодня TCP – самый используемый транспортный протокол для доставки HTTPS-трафика в сети Интернет. TCP обеспечивает надежный поток байтов, тем самым справляясь с перегрузкой сети и потерями канального уровня. Широкое применение TCP для HTTPS-трафика объясняется вездесущностью первого (почти каждая ОС содержит TCP), доступностью на бОльшей части инфраструктуры (например, на балансировщиках нагрузки, HTTPS-прокси и CDN) и функциональностью «из коробки», которая доступна почти в большинстве платформ и сетей.

Проще говоря, с этим сталкивались пользователи по всему миру – на Рисунке 1 отражены задержки в крупных городах: Большинство пользователей используют наше приложение на ходу, и «хвостовые» задержки TCP были далеки от требований нашего HTTPS-трафика в реальном времени.

Рисунок 1. Величина «хвостовых» задержек варьируется в основных городах присутствия Uber.

И это так даже для США и Великобритании. Несмотря на то, что задержки в индийских и бразильских сетях были больше, чем в США и Великобритании, хвостовые задержки значительно больше чем задержки в среднем.

Производительность TCP по воздуху

TCP был создан для проводных сетей, то есть с упором на хорошо предсказуемые ссылки. Однако у беспроводных сетей свои особенности и трудности. Во-первых, беспроводные сети чувствительны к потерям из-за помех и затухания сигнала. Например, сети Wi-Fi чувствительны к микроволнам, bluetooth и прочим радиоволнам. Сотовые сети страдают от потери сигнала (потери пути) из-за отражения/поглощения сигнала предметами и строениями, а также от помех от соседних сотовых вышек. Это приводит к более значительным (в 4-10 раз) и разнообразным круговым задержкам (RTT) и потерям пакетов по сравнению с проводным соединением.

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

На Рисунке 2 мы собрали медианные задержки HTTPS-трафика по сотам в диапазоне 2 километров. Наконец, производительность сотовой сети меняется в зависимости от оператора связи, региона и времени. Как можно заметить, производительность меняется от соты к соте. Данные собраны для двух крупнейших операторов сотовой связи в Дели, Индия. На это влияют такие факторы как паттерны входа в сеть с учетом времени и локации, подвижность пользователей, а также сетевая инфраструктура с учетом плотности вышек и соотношения типов сети (LTE, 3G и т.д.). Также производительность одного оператора отличается от производительности второго.

Рисунок 2. Задержки на примере 2-километрового радиуса. Дели, Индия.

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

Рисунок 3. Хвостовые задержки могут значительно меняться в разные дни, но у того же оператора.

Тем не менее, прежде чем искать альтернативы TCP, мы хотели выработать точное понимание по следующим пунктам:
Все вышеупомянутое приводит к тому, что производительность TCP неэффективна в беспроводных сетях.

  • является ли TCP главным виновником хвостовых задержек в наших приложениях?
  • Имеют ли современные сети значительные и разнообразные круговые задержки (RTT)?
  • Каково влияние RTT и потерь на производительность TCP?

Анализ производительности TCP

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

RTO рассчитывается динамически, на основании разных факторов, например, на ожидаемой задержке RTT между отправителем и получателем. Если потерян пакет или ACK, отправитель делает ретрансмит после таймаута (RTO, retransmission timeout).

Рисунок 4. Обмен пакетами по TCP/TLS включает механизма ретрансмита.

Затем мы проанализировали TCP-соединения с помощью tcptrace. Чтобы определить, как TCP работал в наших приложениях, мы отслеживали TCP-пакеты с помощью tcpdump в течение недели на боевом трафике, идущем с индийских пограничных серверов. Смартфоны с этим приложением были розданы нескольким сотрудникам, кто собирал логи на протяжении нескольких дней. Дополнительно мы создали Android-приложение, которое шлет эмулированный трафик на тестовый сервер, максимально подражая реальному трафику.

Мы увидели высокие RTT-задержки; хвостовые значения были почти в 6 раз выше медианного значения; среднее арифметическое значение задержек – более 1 секунды. Результаты обоих экспериментов были сообразны друг другу. В районах с перегрузкой, например, аэропорты и вокзалы, мы наблюдали 7%-ные потери. Многие соединения были с потерями, что заставляло TCP ретрансмитить 3,5% всех пакетов. Ниже – результаты тестов из приложения-«симулянта»:
Такие результаты ставят под сомнение расхожее мнение, что используемые в сотовых сетях продвинутые схемы ретрансмиссии значительно снижают потери на транспортном уровне.

Почти в половине этих соединений была как минимум одна потеря пакетов, по большей части это были SYN и SYN-ACK-пакеты. Большинство реализаций TCP используют значение RTO в 1 секунду для SYN-пакетов, которое увеличивается экспоненциально для последующих потерь. Время загрузки приложения может увеличиться за счет того, что TCP потребуется больше времени на установку соединений.

Мы выяснили, что среднее время ретрансмита – примерно 1 секунда с хвостовой задержкой почти в 30 секунд. В случае пакетов данных, высокие значения RTO эффективно снижают нагрузку на сеть при наличии временных потерь в беспроводных сетях. Такие высокие задержки на уровне TCP вызывали HTTPS-таймауты и повторные запросы, что еще больше увеличивало задержку и неэффективность сети.

Это намекает на то, что потери заставляли TCP делать 7-10 проходов чтобы успешно передать данные. В то время как 75-й процентиль измеренных RTT был в районе 425 мс, 75-й процентиль для TCP был почти 3 секунды. Ниже – результаты тестов потерь TCP:
Это может быть следствием неэффективного расчета RTO, невозможности TCP быстро реагировать на потерю последних пакетов в окне и неэффективности алгоритма управления перегрузкой, который не различает беспроводные потери и потери из-за сетевой перегрузки.

Применение QUIC

Изначально спроектированный компанией Google, QUIC – это мультипоточный современный транспортный протокол, который работает поверх UDP. На данный момент QUIC в процессе стандартизации (мы уже писали, что существует как бы две версии QUIC, любознательные могут пройти по ссылке – прим. переводчика). Как показано на Рисунке 5, QUIC разместился под HTTP/3 (собственно, HTTP/2 поверх QUIC – это и есть HTTP/3, который сейчас усиленно стандартизируют). Он частично заменяет уровни HTTPS и TCP, используя UDP для формирования пакетов. QUIC поддерживает только безопасную передачу данных, так как TLS полностью встроен в QUIC.

Рисунок 5: QUIC работает под HTTP/3, заменяя TLS, который раньше работал под HTTP/2.

Ниже мы приводим причины, которые убедили нас использовать QUIC для усиления TCP:

  • 0-RTT установка соединения. QUIC позволяет повторное использование авторизаций из предыдущих соединений, снижая количество хендшейков безопасности. В будущем TLS1.3 будет поддерживать 0-RTT, однако трехсторонний TCP-хендшейк все еще будет обязательным.
  • преодоление HoL-блокировки. HTTP/2 использует одно TCP-соединение для каждого клиента, чтобы улучшить производительность, но это может привести к HoL (head-of-line) блокировке. QUIC упрощает мультиплексирование и доставляет запросы в приложение независимо друг от друга.
  • управление перегрузкой. QUIC находится на уровне приложений, позволяя проще обновлять главный алгоритм транспорта, который управляет отправкой, основываясь на параметрах сети (количество потерь или RTT). Большинство TCP-реализаций используют алгоритм CUBIC, который не оптимален для трафика, чувствительного к задержкам. Недавно разработанные алгоритмы вроде BBR, более точно моделируют сеть и оптимизируют задержки. QUIC позволяет использовать BBR и обновлять этот алгоритм по мере его совершенствования.
  • восполнение потерь. QUIC вызывает два TLP (tail loss probe) до того как сработает RTO – даже когда потери очень ощутимы. Это отличается от реализаций TCP. TLP ретрансмитит главным образом последний пакет (или новый, если есть таковой), чтобы запустить быстрое восполнение. Обработка хвостовых задержек особо полезна для того, как Uber работает с сетью, а именно для коротких, эпизодических и чувствительных к задержкам передач данных.
  • оптимизированный ACK. Так как каждый пакет имеет уникальный последовательный номер, не возникает проблема различения пакетов при их ретрансмите. ACK-пакеты также содержат время для обработки пакета и генерации ACK на стороне клиента. Эти особенности гарантируют, что QUIC более точно рассчитывает RTT. ACK в QUIC поддерживает до 256 диапазонов NACK, помогая отправителю быть более устойчивым к перестановке пакетов и использовать меньше байтов в процессе. Выборочный ACK (SACK) в TCP не решает эту проблему во всех случаях.
  • миграция соединения. Соединения QUIC идентифицируются с помощью 64-битного ID, так что если клиент меняет IP-адреса, можно дальше использовать ID старого соединения на новом IP-адресе, без прерываний. Это очень частая практика для мобильных приложений, когда пользователь переключается между Wi-Fi и сотовыми соединениями.

Альтернативы QUIC

Мы рассматривали альтернативные подходы к решению проблемы до того, как выбрать QUIC.

По сути, PoPs прерывает TCP-соединение с мобильным устройством ближе к сотовой сети и проксирует трафик до изначальной инфраструктуры. Первым делом мы попробовали развернуть TPC PoPs (Points of Presence), чтобы закрывать TCP-соединения ближе к пользователям. Однако наши эксперименты показали, что по большей части RTT и потери приходят из сотовых сетей и использование PoPs не обеспечивает значительного улучшения производительности. Завершая TCP ближе, мы потенциально можем уменьшить RTT и быть уверенными, что TCP будет более активно реагировать на динамичное беспроводное окружение.

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

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

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

Интеграция QUIC в платформу

Чтобы успешно встроить QUIC и улучшить производительность приложения в условиях плохой связи, мы заменили старый стек (HTTP/2 поверх TLS/TCP) на протокол QUIC. Мы задействовали сетевую библиотеку Cronet из Chromium Projects, которая содержит оригинальную, гугловскую версию протокола – gQUIC. Эта реализация также постоянно совершенствуется, чтобы следовать последней спецификации IETF.

Интеграция была осуществлена так, чтобы максимально снизить затраты на миграцию. Сперва мы интегрировали Cronet в наши Android-приложения, чтобы добавить поддержку QUIC. Выполнив интеграцию таким способом, мы избежали изменений в наших сетевых вызовах (который используют Retrofit) на уровне API. Вместо того, чтобы полностью заменить старый сетевой стек, который использовал библиотеку OkHttp, мы интегрировали Cronet ПОД фреймворком OkHttp API.

Эта абстракция, предоставленная iOS Foundation, обрабатывает протокол-специфичные URL-данные и гарантирует, что мы можем интегрировать Cronet в наши iOS-приложения без существенных миграционных затрат. Подобно подходу к Android-устройствам, мы внедрили Cronet в приложения Uber под iOS, перехватывая HTTP-трафик из сетевых API, используя NSURLProtocol.

Прерывание QUIC на балансировщиках Google Cloud

На стороне бэкенда прерывание QUIC обеспечено инфраструктурой Google Cloud Load balancing, которая использует alt-svc заголовки в ответах, чтобы поддерживать QUIC. В общем случае, к каждому HTTP-запросу балансировщик добавляет заголовок alt-svc и уже он валидирует поддержку QUIC для домена. Когда клиент Cronet получает HTTP-ответ с таким заголовком, он использует QUIC для последующих HTTP-запросов к этому домену. Как только балансировщик прерывает QUIC, наша инфраструктура явно отправляет это действие по HTTP2/TCP в наши дата-центры.

Производительность: результаты

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

Эксперимент 1

Инвентарь для эксперимента:

  • тестовые устройства на Android со стеками OkHttp и Cronet, чтобы убедиться, что мы пускаем HTTPS-трафик по TCP и QUIC соответственно;
  • сервер эмуляции на базе Java, который шлет однотипные HTTPS-заголовки в ответах и нагружает клиентские устройства, чтобы получать от них запросы;
  • облачные прокси, которые физически расположены близко к Индии, чтобы завершать TCP и QUIC-соединения. В то время как для завершения TCP мы использовали обратный прокси на NGINX, было трудно найти опенсорсный обратный прокси для QUIC. Мы собрали обратный прокси для QUIC сами, используя базовый стек QUIC из Chromium и опубликовали его в хромиум как опенсорсный.


Рисунок 6. Дорожный набор для тестов TCP vs QUIC состоял из Android-устройств с OkHttp и Cronet, облачных прокси для завершения соединений и сервера эмуляции.

Эксперимент 2

Когда Google сделал QUIC доступным с помощью Google Cloud Load Balancing, мы использовали тот же инвентарь, но с одной модификацией: вместо NGINX, мы взяли гугловские балансировщики для завершения TCP и QUIC-соединений от устройств, а также для направления HTTPS-трафика в сервер эмуляции. Балансировщики распределены по всему миру, но используют ближайший к устройству PoP-сервер (спасибо геолокации).

Рисунок 7. Во втором эксперименте мы хотел сравнить задержку завершения TCP и QUIC: с помощью Google Cloud и с помощью нашего облачного прокси.

В итоге нас ждало несколько откровений:

  • завершение через PoP улучшило производительность TCP. Так как балансировщики завершают TCP-соединение ближе к пользователям и отлично оптимизированы, это дает меньшие RTT, что улучшает производительность TCP. И хотя на QUIC это сказалось меньше, он все равно обошел TCP в плане снижения хвостовых задержек (на 10-30 процентов).
  • на хвосты влияют сетевые переходы (hops). Хотя наш QUIC-прокси был дальше от устройств (задержка примерно на 50 мс выше), чем гугловские балансировщики, он выдавал схожую производительность – 15%-ное снижение задержек против 20%-ного снижения в 99 процентиле у TCP. Это говорит о том, что переход на последней миле – это узкое место (bottleneck) в работе сети.


Рисунок 8. Результаты двух экспериментов показывают, что QUIC значительно превосходит TCP.

Боевой трафик

Вдохновленные экспериментами, мы внедрили поддержку QUIC в наши Android и iOS-приложения. Мы провели A/B тестирование, чтобы определить влияние QUIC в городах присутствия Uber. В целом, мы увидели значимое снижение хвостовых задержек в разрезе как регионов, так и операторов связи и типа сети.

В боевых тестах QUIC превзошел TCP по задержкам. На графиках ниже показаны процентные улучшения хвостов (95 и 99 процентили) по макрорегионам и разным типам сети – LTE, 3G, 2G.

Рисунок 9.

Только вперед

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

Увеличение покрытия

Проанализировав производительность протокола на реальном трафике, мы увидели, что примерно 80% сессий успешно использовали QUIC для всех запросов, в то время как 15% сессий использовали сочетание QUIC и TCP. Мы предполагаем, что сочетание появилось из-за того, что библиотека Cronet переключается обратно на TCP по таймауту, так как она не может различать реальные UDP-сбои и плохие условия сети. Сейчас мы ищем решение этой проблемы, так как мы работаем над последующим внедрением QUIC.

Оптимизация QUIC

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

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть