Одна из самых «ходовых» оптимизаций в вычислительной технике — это предусматривать для программы «быстрый» и «медленный» путь выполнения. В общем случае эта оптимизация работает. Техники оптимизации применяют на программном или аппаратном уровне. Цель — добиться, чтобы выполнение по быстрому пути было нормальным сценарием и шло «по умолчанию» — работаем быстро и очень эффективно. Выполнение по медленному пути предусматривается для необычных случаев, при исключениях, выбросах. Такой вариант работы выполняется в безопасном, но сравнительно медленном программном окружении, где можно позволить себе не спешить. На первый взгляд выглядит отлично, но, как оказывается, в реальности всё совсем иначе.

Практикующий инженер постепенно убеждается на собственном опыте, что дихотомия быстрый/медленный путь — это зачастую просто привлекательный мираж. Снова и снова мы видим, что попытка внедрить быстрый/медленный путь в реальной системе не даёт результата. Именно в этой области практика вступает в острое противоречие с теорией.

Если чрезмерно полагаться на альтернативу быстрый/медленный путь, то возникающие проблемы проникают повсюду. Я даже возьмусь утверждать, что разделение трафика на быстрый и медленный путь в роутерах в значительной степени подорвало работу Интернета, так как серьёзно ограничило наши возможности развёртывать новые протоколы или инновационные возможности. Можно возразить, что лежащие в основе современного Интернета системы были спроектированы более четверти века назад, и в тот период они были сделаны «настолько хорошо, насколько было возможно». Но тогда — это тогда, а сейчас — это сейчас. Можно приступать к решению проблемы быстрого и медленного пути. Спойлер: наилучшее её решение — просто устранить эту альтернативу :-).

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

❯ Суть проблемы

Альтернатива быстрый/медленный путь сопряжена с некоторыми неотъемлемыми проблемами.

Неумолимый закон Амдала

Если вы работаете над производительностью систем, то вам просто необходимо понимать  закон Амдала. Это принцип, объясняющий, каков предел эффективности при улучшении производительности, и он неумолим — как говорится, «закон суров, но закон»! Одна из формулировок закона Амдала гласит, что любые оптимизации ограничены тем, какая часть задачи определённо не улучшится от вносимых изменений. Это касается и альтернативы быстрый/медленный путь.

Допустим, мы разрабатываем систему обработки заказов, и у нас есть выбор. Либо мы реализуем альтернативу быстрый/медленный путь, где второй вдесятеро медленнее первого, либо нам удаётся обойтись без такой альтернативы, просто сделав все операции на 50% медленнее, чем они выполнялись бы по сценарию быстрого пути. Какой вариант лучше? Ну, зависит от того, какая доля времени будет тратиться при выполнении по быстрому пути.

Например, по быстрому пути операция выполняется за 1 мкс, а по медленному пути — в 10 раз дольше, то есть, за 10 мкс. Если мы будем действовать по быстрому пути в 90% случаев, а по медленному в 10% случаев, то средняя производительность составит 0,9 1 мкс + 0,1 10 мкс = 1,9 мкс. В качестве альтернативы, если замедлим на 50% все операции, то получим среднюю производительность в 1,5 мкс. Таким образом, замедление в результате даст нам суммарное ускорение ;-).

Всё дело в хвостовой задержке, дурачок

Продолжая вышеприведённый пример, допустим, что мы схитрили и выбираем выполнение по медленному пути лишь в 1% случаев. Теперь средняя производительность составит 0,99 1 мкс + 0,01 10 мкс = 1,09 мкс. Супер! Уже гораздо лучше, чем те 1,5 мкс, которые у нас получались при отказе от разделения на быстрый и медленный путь, так что сдаём! Но вынужден попросить вас не торопиться — ведь существует ещё и проблема хвостовых задержек!

Хвостовая задержка — ещё один фактор, который необходимо учитывать, работая над повышением производительности систем. При разработке крупномасштабных распределённых приложений, в том числе, связанных с искусственным интеллектом и машинным обучением, общая производительность обычно коррелирует с хвостом задержек. Например, если я по частям распределю задачу на 1000 машин, и нам придётся дождаться отклика от каждой из них, то производительность приложения будет не выше, чем на самой медленной машине. Таким образом, если 999 серверов отвечают за 1 мкс, а последний отвечает за 10 мкс, то на всю операцию уходит 10 мкс :-(. Обычно хвостовая задержка измеряется как 90-я, 99-я или 99,9-я перцентиль. Хвостовая задержка настолько важна, что зачастую нас даже не слишком интересует средняя задержка, а минимальная задержка (для случая, когда всё сложится наилучшим образом) вообще практически не рассматривается.

Возвращаясь к нашему примеру, задержка по 99-й перцентили при разделении на быстрый и медленный путь составит 10 мкс, но при отсутствии разделения на быстрый и медленный путь задержка по 99-й перцентили будет всего 1,5 мкс. Поэтому, систему нужно проектировать без альтернативы быстрый/медленный путь, если важна величина хвостовой задержки.

Ваш медленный путь – это мой быстрый

Как понятно из вышеприведённых примеров, производительность зависит именно от того, какова пропорция времени, затрачиваемого на быстрый и медленный путь (как минимум, с учётом средней производительности). Хммм, звучит знакомо, а где мы уже могли сталкиваться с такими эффектами? Ну, конечно же — при работе с кэшами. Успешное обращение к кэшу можно расценивать, как операцию по быстрому пути, а кэш-промах — как операцию по медленному пути. Общую среднюю производительность работы кэша можно выразить как соотношение успешных попаданий в кэш приложения и кэш-промахов. Естественно, для обеспечения наилучшей производительности мы стремимся довести до максимума процент успешных попаданий в кэш.

При проектировании системы, в которой предусмотрена альтернатива быстрый/медленный путь, можно спрогнозировать производительность, если учесть два фактора: 1) производительность на медленном и на быстром пути и 2) процент времени, уходящего на выполнение кода по медленному пути. Таким образом, можно количественно выразить производительность вот так:

Производительность = p  slow_path + (1 — p)  fast_path

Обычно производительность slow_path и fast_path жёстко зависит от того, как спроектирована и реализована система. Например, задержка на попадание в кэш ЦП и на кэш-промах зависит от аппаратной архитектуры и не сильно варьируется при разных рабочих нагрузках. С другой стороны, p, то есть, процент времени, затрачиваемого на выполнение по быстрому пути, может значительно варьироваться при разных рабочих нагрузках.

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

Когда при проектировании системы предусматривается альтернатива быстрый/медленный путь, архитектору зачастую приходится гадать, что представляет собой медленный путь. Классический пример — ситуация с «TCP offload» (снижением нагрузки системы через вынос некоторых моментов обработки TCP соединений на плечи сетевых карт), когда уже при проектировании приходится допустить, что типичная установка и разрыв соединения — это медленный путь, а нормальный обмен данными по надёжно налаженному протоколу TCP – это быстрый путь. Такое допущение справедливо только до тех пор, пока это решение используется на серверах с низкоскоростным соединением. Если мы имеем дело с сервером, обращённым в Интернет, и на этом сервере настроено высокоскоростное соединение, то оказывается, что, пожалуй, быстрый путь распространяется и на операции установки и разрыва соединения. Если неправильно спрогнозировать, как именно распределяется работа между быстрым и медленным путём, можно настолько всё запустить, что впоследствии мы будем только сами себя корить, что вообще взялись за такую оптимизацию.

Если существует альтернатива быстрый/медленный путь — это раздолье для злоумышленников

Альтернатива быстрый/медленный путь — это заготовка для DoS-атак (отказ в обслуживании). Всё, что требуется злоумышленнику — перегрузить систему потоком запросов, чтобы жертва большую часть времени вынужденно проводила на медленном пути. В данном случае речь о так называемом SYN-флуде. Не прошло и пяти минут после того, как первые коммерческие серверы поступили на рынок, их принялись атаковать SYN-флудом. Это очень, очень просто, злоумышленник просто посылает на сервер рой фальшивых пакетов TCP SYN. Тем самым он загружает сервер массой работы и создаёт в памяти обширное бесцельно существующее состояние. Атака срабатывает, поскольку невозможно отличить пакеты злоумышленника от нормальных пакетов, а эффективна она, во-первых, потому, что невозможно отличить вредоносные пакеты от нормальных, а во-вторых — потому, что крайне истощает ресурсы. До такой степени, что SYN-пакеты обычных пользователей отбрасываются и не могут попасть на сервер.

Атака TCP SYN. Злоумышленник отправляет рой SYN-пакетов, IP-адреса которых обеспечивают спуфинг. Поражённый хост обрабатывает пакеты по медленному пути, на котором требуется выделять память в форме состояния соединения для каждого SYN. Хост отправляет SYN-ACK на фиктивный исходный адрес SYN, поэтому никакого отклика не получает. В конце концов, соединение будет разорвано по истечении времени ожидания, но ущерб уже будет нанесён. Злоумышленник уже вынудил жертву истратить процессорное время и выделить память без какого-либо полезного эффекта. Атака состоится, когда поражённый хост начнёт отбрасывать те нормальные SYN-пакеты, для которых должен был бы установить соединение. Источник иллюстрации: Research Gate.
Атака TCP SYN. Злоумышленник отправляет рой SYN-пакетов, IP-адреса которых обеспечивают спуфинг. Поражённый хост обрабатывает пакеты по медленному пути, на котором требуется выделять память в форме состояния соединения для каждого SYN. Хост отправляет SYN-ACK на фиктивный исходный адрес SYN, поэтому никакого отклика не получает. В конце концов, соединение будет разорвано по истечении времени ожидания, но ущерб уже будет нанесён. Злоумышленник уже вынудил жертву истратить процессорное время и выделить память без какого-либо полезного эффекта. Атака состоится, когда поражённый хост начнёт отбрасывать те нормальные SYN-пакеты, для которых должен был бы установить соединение. Источник иллюстрации: Research Gate.

❯ Маршрутизаторы и быстрый/медленный путь

Сетевые маршрутизаторы — эталонный пример, иллюстрирующий проблемы, связанные с альтернативой быстрый/медленный путь. Сетевой маршрутизатор, в особенности, обращённый к Интернету, постоянно работает под нагрузкой, так как на нём приемлемы задержки, исчисляемые считанными наносекундами, а пропускная способность составляет множество терабит в секунду. При таких требованиях к производительности вендорам маршрутизаторов никак не обойтись без альтернативы быстрый/медленный путь. Быстрый путь рассчитан на нормальную переадресацию пакетов и целиком реализуется аппаратно на высокопроизводительных специализированных интегральных схемах (ASIC). Медленный путь предусматривается для исключительных случаев, например, когда приходится обрабатывать сложные пакеты, выполнять поиск по всему маршруту (full route lookup), отслеживать соединения или работать с брандмауэром. Медленный путь может быть реализован программно, работать на ЦП и демонстрировать производительность в 10-100 раз хуже, чем аппаратный быстрый путь.

Вполне понятно, почему в маршрутизаторах требуется предусмотреть альтернативу быстрый/медленный путь, но последствия такого архитектурного решения могут быть разрушительны. Например, чтобы пакеты обрабатывались быстро, аппаратному уровню требуется, чтобы они были просты. В современном Интернете можно рассчитывать на то, что пакеты TCP и UDP, отправляемые по IPv4 и IPv6, будут доставлены успешно. И на этом всё! При отправке любого другого материала существует риск, что пакеты будут отброшены. Этот риск перекликается с законом Амдала в том, что на всём пути передачи достаточно встретить лишь один маршрутизатор, которому пакет не понравится – и тогда этот пакет будет отброшен. Совершенно не важно, что все остальные маршрутизаторы на пути этот пакет бы устроил.

Особая проблема, касающаяся альтернативы быстрый/медленный путь – это конструкции, обеспечивающие расширяемость сетевых протоколов. В частности, если говорить об опциях IPv4 и расширенных заголовках IPv6. В другом моём посте вы можете почитать о тяготах развёртывания расширенных заголовков IPv6 и в особенности о том, как маршрутизаторы любят отводить на медленный путь пакеты, в которых активирован заголовок «Hop-by-Hop Options», требующий обработки всеми промежуточными узлами. Не работает! «Hop-by-Hop Options» — это механизм пути данных, а не пути управления, поэтому при его переходе на медленный путь какое-то приложение просто начинает получать данные с сильным замедлением. В конце концов, возникает такая большая задержка, что пакеты становятся бесполезны (вполне возможно, маршрутизатор предпочтёт отбросить пакет, а не доставлять его со стократной задержкой). Кроме того, на медленный путь будет переброшено так много трафика, что ЦП перестанет с ним справляться и начнёт отбрасывать пакеты только по этой причине — что также служит отличной почвой для DoS-атаки.

Быстрый/медленный путь в маршрутизаторе. Концептуальное представление маршрутизатора, который переадресует операции, выполняемые по быстрому пути, на специализированные интегральные схемы, а операции, выполняемые по медленному пути — на процессор. Получив пакет, маршрутизатор разбирает его заголовки и определяет, по какому пути его отправить — по быстрому или по медленному. Зелёной стрелкой показана обработка пакета по быстрому пути, эта работа полностью выполняется аппаратным движком. Красной стрелкой показана обработка пакета по медленному пути. Аппаратная часть отправляет пакет на процессор для глубокой обработки. Процессор обработает пакет, а затем может его переадресовать. Таким образом, медленный путь может обходиться в 10-100 раз дороже, чем быстрый.
Быстрый/медленный путь в маршрутизаторе. Концептуальное представление маршрутизатора, который переадресует операции, выполняемые по быстрому пути, на специализированные интегральные схемы, а операции, выполняемые по медленному пути — на процессор. Получив пакет, маршрутизатор разбирает его заголовки и определяет, по какому пути его отправить — по быстрому или по медленному. Зелёной стрелкой показана обработка пакета по быстрому пути, эта работа полностью выполняется аппаратным движком. Красной стрелкой показана обработка пакета по медленному пути. Аппаратная часть отправляет пакет на процессор для глубокой обработки. Процессор обработает пакет, а затем может его переадресовать. Таким образом, медленный путь может обходиться в 10-100 раз дороже, чем быстрый.

❯ Как исправить путаницу, связанную с быстрым/медленным путём

Расскажу анекдот. Заходит пациент в кабинет к врачу, помахивает рукой из стороны в сторону и говорит: «Доктор, когда я так делаю — у меня рука болит». А врач ему отвечает: «Ну значит не делайте так». Ладно, шутка плосковатая, но, думаю, вы поняли, о чём я. Чтобы решить проблему с быстрым и медленным путём, нужно просто избавиться от этой альтернативы — пусть у вас будет просто «путь». Да, я немного ёрничаю. Если бы избавиться от альтернативы быстрый/медленный путь было так просто, разумеется, эту проблему давно бы уже устранили. Признаться, избавиться от неё нелегко, но я считаю, что это определённо осуществимо, учитывая, как далеко продвинулись технологии. Рассмотрим, как можно было бы решить эту проблему в маршрутизаторах.

Проблема расширенных заголовков отлично иллюстрирует ситуацию конкурирующих интересов, когда для получения действенного решения разным заинтересованным сторонам требуется удовлетворить свои требования хотя бы наполовину. Когда заголовок «Hop-by-Hop Options» для IPv6 был впервые описан в запросе на спецификацию RFC2460, никаких ограничений для него не предусмотрели. Все промежуточные маршрутизаторы на пути пакета были обязаны обрабатывать все опции, помеченные «для обработки на каждом переходе». Лишь постфактум это требование было признано совершенно нереалистичным! Ни в одной программе невозможно обработать неограниченное количество опций, тем более — в высокопроизводительной. Поэтому вендоры маршрутизаторов отправили всю обработку «Hop-by-Hop Options» на медленный путь или просто принялись отбрасывать такие пакеты. Так или иначе, пользоваться ими стало невозможно.

Есть старая поговорка: «выплеснуть ребёнка вместе с водой». Фактически, именно это и сделали производители маршрутизаторов. Столкнувшись с нереалистичным требованием обрабатывать неограниченное количество опций пакета в режиме «Hop-by-Hop», они выдали решения, при которых опции «Hop-by-Hop» в пакете вообще не обрабатываются. Так мы к этому и пришли: разработчики протоколов переусердствовали, потребовав обеспечить неограниченную поддержку, а вендоры маршрутизаторов упростили себе жизнь, предусмотрев для таких случаев нулевую поддержку. Есть ли здесь золотая середина? :-)

В RFC9673 описаны обновлённые требования к обработке «Hop-by-Hop». В данном случае наиболее интересно, что RFC признаёт наличие альтернативы быстрый/медленный путь в маршрутизаторах (может быть, впервые в истории IETF?). Требования согласованы с этим, и мы приходим к простому выводу, что лучше не определять таких протоколов, которые, скорее всего, будут обрабатываться по медленному пути. Иными словами, при проектировании протоколов нужно иметь в виду альтернативу быстрый/медленный путь, но при этом делать так, чтобы путь обработки данных всегда приравнивался к быстрому. Думаю, именно в этом сейчас заключается задача разработчиков протоколов.

Что касается производителей маршрутизаторов, им следует немного расширить область применения быстрого пути. Например, что касается опций «Hop-by-Hop», нужно предусмотреть возможность обработки относительно небольшого множества опций по быстрому пути. Этот аспект нужно программно прописать в пути переадресации данных. Разумеется, попытка одновременно достичь высокой производительности и программируемости традиционно считается оксюмороном (именно поэтому когда-то и был предусмотрен медленный путь), но сейчас появляются новые технологии, и программирование пути данных в таком ключе теперь кажется осуществимым. Это тема для отдельного разговора.


Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале 

Комментарии (0)