Картинки кликабельны. Приятного чтения!
Uber – это мировой масштаб, а именно 600 городов присутствия, в каждом из которых приложение полностью полагается на беспроводной интернет от более чем 4500 сотовых операторов. Пользователи ожидают, что приложение будет работать не просто быстро, а в реальном времени – чтобы обеспечить это, приложению Uber нужны низкие задержки и очень надежное соединение. Увы, но стек HTTP/2 плохо себя чувствует в динамичных и склонных к потерям беспроводных сетях. Мы уяснили, что в данном случае низкая производительность напрямую связана с реализациями TCP в ядрах операционных систем.
Чтобы решить проблему, мы применили QUIC, современный протокол с мультиплексированием каналов, который дает нам больше контроля над производительностью транспортного протокола. В данный момент рабочая группа IETF стандартизирует QUIC как HTTP/3.
После подробных тестов, мы пришли к выводу, что внедрение QUIC в наше приложение сделает «хвостовые» задержки меньше по сравнению с TCP. Мы наблюдали снижение в диапазоне 10-30% для HTTPS-трафика на примере водительского и пассажирского приложений. Также QUIC дал нам сквозной контроль над пользовательскими пакетами.
В этой статье мы делимся опытом по оптимизации TCP для приложений Uber с помощью стека, который поддерживает QUIC.
Последнее слово техники: TCP
Сегодня TCP – самый используемый транспортный протокол для доставки HTTPS-трафика в сети Интернет. TCP обеспечивает надежный поток байтов, тем самым справляясь с перегрузкой сети и потерями канального уровня. Широкое применение TCP для HTTPS-трафика объясняется вездесущностью первого (почти каждая ОС содержит TCP), доступностью на бОльшей части инфраструктуры (например, на балансировщиках нагрузки, HTTPS-прокси и CDN) и функциональностью «из коробки», которая доступна почти в большинстве платформ и сетей.
Большинство пользователей используют наше приложение на ходу, и «хвостовые» задержки TCP были далеки от требований нашего HTTPS-трафика в реальном времени. Проще говоря, с этим сталкивались пользователи по всему миру – на Рисунке 1 отражены задержки в крупных городах:
Рисунок 1. Величина «хвостовых» задержек варьируется в основных городах присутствия Uber.
Несмотря на то, что задержки в индийских и бразильских сетях были больше, чем в США и Великобритании, хвостовые задержки значительно больше чем задержки в среднем. И это так даже для США и Великобритании.
Производительность TCP по воздуху
TCP был создан для проводных сетей, то есть с упором на хорошо предсказуемые ссылки. Однако у беспроводных сетей свои особенности и трудности. Во-первых, беспроводные сети чувствительны к потерям из-за помех и затухания сигнала. Например, сети Wi-Fi чувствительны к микроволнам, bluetooth и прочим радиоволнам. Сотовые сети страдают от потери сигнала (потери пути) из-за отражения/поглощения сигнала предметами и строениями, а также от помех от соседних сотовых вышек. Это приводит к более значительным (в 4-10 раз) и разнообразным круговым задержкам (RTT) и потерям пакетов по сравнению с проводным соединением.
Чтобы бороться с флуктуациями в полосе пропускания и потерях, сотовые сети обычно используют большие буферы для всплесков трафика. Это может приводить к чрезмерной очередности, что означает бОльшие задержки. Очень часто TCP трактует такую очередность как потерю из-за увеличенного таймаута, поэтому TCP склонен делать ретрансляцию и тем самым заполнять буфер. Это проблема известна как bufferbloat (излишняя сетевая буферизация, распухание буфера), и это очень серьезная проблема современного интернета.
Наконец, производительность сотовой сети меняется в зависимости от оператора связи, региона и времени. На Рисунке 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), чтобы обеспечить надежную доставку.
Если потерян пакет или ACK, отправитель делает ретрансмит после таймаута (RTO, retransmission timeout). RTO рассчитывается динамически, на основании разных факторов, например, на ожидаемой задержке RTT между отправителем и получателем.
Рисунок 4. Обмен пакетами по TCP/TLS включает механизма ретрансмита.
Чтобы определить, как TCP работал в наших приложениях, мы отслеживали TCP-пакеты с помощью tcpdump в течение недели на боевом трафике, идущем с индийских пограничных серверов. Затем мы проанализировали TCP-соединения с помощью tcptrace. Дополнительно мы создали Android-приложение, которое шлет эмулированный трафик на тестовый сервер, максимально подражая реальному трафику. Смартфоны с этим приложением были розданы нескольким сотрудникам, кто собирал логи на протяжении нескольких дней.
Результаты обоих экспериментов были сообразны друг другу. Мы увидели высокие RTT-задержки; хвостовые значения были почти в 6 раз выше медианного значения; среднее арифметическое значение задержек – более 1 секунды. Многие соединения были с потерями, что заставляло TCP ретрансмитить 3,5% всех пакетов. В районах с перегрузкой, например, аэропорты и вокзалы, мы наблюдали 7%-ные потери. Такие результаты ставят под сомнение расхожее мнение, что используемые в сотовых сетях продвинутые схемы ретрансмиссии значительно снижают потери на транспортном уровне. Ниже – результаты тестов из приложения-«симулянта»:
Сетевые метрики | Значения |
---|---|
RTT, миллисекунды [50%,75%, 95%,99%] | [350, 425, 725, 2300] |
RTT-расхождение, секунды | В среднем ~1,2 с |
Потеря пакетов в неустойчивых соединениях | В среднем ~3.5% (7% в районах с перегрузкой) |
Почти в половине этих соединений была как минимум одна потеря пакетов, по большей части это были SYN и SYN-ACK-пакеты. Большинство реализаций TCP используют значение RTO в 1 секунду для SYN-пакетов, которое увеличивается экспоненциально для последующих потерь. Время загрузки приложения может увеличиться за счет того, что TCP потребуется больше времени на установку соединений.
В случае пакетов данных, высокие значения RTO сильно снижают полезную утилизацию сети при наличии временных потерь в беспроводных сетях. Мы выяснили, что среднее время ретрансмита – примерно 1 секунда с хвостовой задержкой почти в 30 секунд. Такие высокие задержки на уровне TCP вызывали HTTPS-таймауты и повторные запросы, что еще больше увеличивало задержку и неэффективность сети.
В то время как 75-й процентиль измеренных RTT был в районе 425 мс, 75-й процентиль для TCP был почти 3 секунды. Это намекает на то, что потери заставляли TCP делать 7-10 проходов чтобы успешно передать данные. Это может быть следствием неэффективного расчета RTO, невозможности TCP быстро реагировать на потерю последних пакетов в окне и неэффективности алгоритма управления перегрузкой, который не различает беспроводные потери и потери из-за сетевой перегрузки. Ниже – результаты тестов потерь TCP:
Статистика потери пакетов TCP | Значение |
---|---|
Процент соединений с как минимум 1 потерей пакета | 45% |
Процент соединений с потерями во время установления соединения | 30% |
Процент соединений с потерями во время обмена данными | 76% |
Распределение задержек в ретрансмиссии, секунды [50%, 75%, 95%,99%] | [1, 2.8, 15, 28] |
Распределение количества ретрансмиссий для одного пакета или TCP-сегмента | [1,3,6,7] |
Применение 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.
Первым делом мы попробовали развернуть TPC PoPs (Points of Presence), чтобы завершать TCP-соединения ближе к пользователям. По сути, PoPs завершает TCP-соединение с мобильным устройством ближе к сотовой сети и проксирует трафик до изначальной инфраструктуры. Завершая TCP ближе, мы потенциально можем уменьшить RTT и быть уверенными, что TCP будет более активно реагировать на динамичное беспроводное окружение. Однако наши эксперименты показали, что по большей части RTT и потери приходят из сотовых сетей и использование PoPs не обеспечивает значительного улучшения производительности.
Мы также смотрели в сторону тюнинга параметров TCP. Настройка TCP-стека на наших неоднородных пограничных серверах была трудной, так как TCP имеет несопоставимые реализации в разных версиях ОС. Было трудно это реализовать и проверить различные сетевые конфигурации. Настройка TCP непосредственно на мобильных устройствах была невозможна из-за отсутствия полномочий. Что еще более важно, фишки вроде соединений с 0-RTT и улучшенным предсказанием RTT критично важны для архитектуры протокола и поэтому невозможно добиться существенного преимущества, лишь настраивая TCP.
Наконец, мы оценили несколько основанных на UDP протоколов, которые устраняют неполадки в видеостриминге – мы хотели узнать, помогут ли эти протоколы в нашем случае. Увы, в них сильно не хватало многих настроек безопасности, а также им требовалось дополнительное TCP-подключение для метаданных и управляющей информации.
Наши изыскания показали, что QUIC – едва ли не единственный протокол, который может помочь с проблемой Интернет-трафика, при этом учитывая как безопасность, так и производительность.
Интеграция QUIC в платформу
Чтобы успешно встроить QUIC и улучшить производительность приложения в условиях плохой связи, мы заменили старый стек (HTTP/2 поверх TLS/TCP) на протокол QUIC. Мы задействовали сетевую библиотеку Cronet из Chromium Projects, которая содержит оригинальную, гугловскую версию протокола – gQUIC. Эта реализация также постоянно совершенствуется, чтобы следовать последней спецификации IETF.
Сперва мы интегрировали Cronet в наши Android-приложения, чтобы добавить поддержку QUIC. Интеграция была осуществлена так, чтобы максимально снизить затраты на миграцию. Вместо того, чтобы полностью заменить старый сетевой стек, который использовал библиотеку OkHttp, мы интегрировали Cronet ПОД фреймворком OkHttp API. Выполнив интеграцию таким способом, мы избежали изменений в наших сетевых вызовах (который используют Retrofit) на уровне API.
Подобно подходу к Android-устройствам, мы внедрили Cronet в приложения Uber под iOS, перехватывая HTTP-трафик из сетевых API, используя NSURLProtocol. Эта абстракция, предоставленная iOS Foundation, обрабатывает протокол-специфичные URL-данные и гарантирует, что мы можем интегрировать Cronet в наши iOS-приложения без существенных миграционных затрат.
Завершение 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. В целом, мы увидели значимое снижение хвостовых задержек в разрезе как регионов, так и операторов связи и типа сети.
На графиках ниже показаны процентные улучшения хвостов (95 и 99 процентили) по макрорегионам и разным типам сети – LTE, 3G, 2G.
Рисунок 9. В боевых тестах QUIC превзошел TCP по задержкам.
Только вперед
Пожалуй, это только начало – выкатка QUIC в продакшн дала потрясающие возможности улучшить производительность приложений как в стабильных, так и нестабильных сетях, а именно:
Увеличение покрытия
Проанализировав производительность протокола на реальном трафике, мы увидели, что примерно 80% сессий успешно использовали QUIC для всех запросов, в то время как 15% сессий использовали сочетание QUIC и TCP. Мы предполагаем, что сочетание появилось из-за того, что библиотека Cronet переключается обратно на TCP по таймауту, так как она не может различать реальные UDP-сбои и плохие условия сети. Сейчас мы ищем решение этой проблемы, так как мы работаем над последующим внедрением QUIC.
Оптимизация QUIC
Трафик из мобильных приложений чувствителен к задержкам, но не к полосе пропускания. Также наши приложения преимущественно используются в сотовых сетях. Основываясь на экспериментах, хвостовые задержки все еще велики, даже несмотря на использование прокси для завершения TCP и QUIC близко к пользователям. Мы активно ищем способы улучшить управление перегрузкой и повысить эффективность QUIC-алгоритмов восполнения потерь.
С этими и некоторыми другими улучшениями мы планируем улучшить пользовательский опыт вне зависимости от сети и региона, сделав удобный и бесшовный транспорт пакетов более доступным по всему миру.
Комментарии (9)
phantasm1c
12.08.2019 14:01«Прерывание QUIC на балансировщиках Google Cloud»
Надмозг. Скорее всего речь идёт о терминации QUIC-соединения:
- Прерывание: соединение работало, мы его прервали
- Терминация: приём соединения сервером/сервисом/демоном
Aingis
12.08.2019 14:25Можно было бы дать и ссылки на русском языке. Так, про BBR был перевод на Хабре: «Система BBR: регулирование заторов непосредственно по заторам», а про CUBIC, как и про многое другое, есть статья в русской Википедии.
maxzhurkin
12.08.2019 20:43Ужасный перевод: termination сначала переводят как «прерывания», а потом как «завершения», кроме того, как «прерывания» переведены так же и interrupting, что совсем не облегчает понимания. Часть предложений в результате перевода вообще поменяли смысл на противополжный.
Все свои замечания со всей конкретикой сообщил автору переводаnvpushkarskiy2 Автор
12.08.2019 23:49Большое спасибо за комментарии про termination и про снижение полезной утилизации — внёс правки. Шлю вам лучи добра за внимательность и уделенное время, благодаря вам хороший материал стал ещё лучше.
erty
Объясните для непонятливых, когда в TLSv1.3 найдут уязвимости, придётся менять весь HTTP/3 и QUIC целиком? Без обратной совместимости?
domix32
Насколько я понимаю то только слой TLS и придется поменять. Правда в случае с QUIC не очень понятно в каком месте непосредственно шифрование происходит исходя из схемы выше.
С другой стороны непонятно обратной совместимости с чем вы ожидаете? С серверами с поддерживающими небезопасные старые версии TLS/SSL? Тогда тут либо шашечки, либо ехать.