• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Экстремальная настройка производительности HTTP: 1,2 млн API-запросов в инстансе EC2 с 4 виртуальными ЦП

вавилонец

CPU register
Пользователь
Регистрация
17.06.2021
Сообщения
1 116
Реакции
1 265
ОРИГИНАЛЬНАЯ СТАТЬЯ
ПЕРЕВЕДЕНО СПЕЦИАЛЬНО ДЛЯ xss.pro
КАШИ В ТАРЕЛКУ Jolah Molivski


Запуск 10-секундного теста @ http://server.tfb:8080/json
16 потоков и 256 соединений
Статистика потоков Avg Stdev Max Min +/- Stdev
Латентность 204.24us 23.94us 626.00us 70.00us 68.70%
Запрос/сек 75.56k 587.59 77.05k 73.92k 66.22%
Распределение задержки
50.00% 203.00us
90.00% 236.00us
99.00% 265.00us
99.99% 317.00us
12031718 запросов за 10,00 с, 1,64 ГБ чтения
Запросы/сек: 1203164.22
Передача/сек: 167.52MB

Обзор​

В этом посте вы познакомитесь с настройками производительности, которые я предпринял для обслуживания 1,2 миллиона запросов JSON «API» в секунду из экземпляра AWS EC2 . Для целей этого воссозданного квеста мы проигнорируем большинство тупиков и темных переулков, через которые мне пришлось продираться в моей одиночной экспедиции. Вместо этого мы в основном будем придерживаться счастливого пути, неуклонно двигаясь от обслуживания 224 000 запросов в секунду в начале с конфигурацией по умолчанию к умопомрачительным 1,2 мил/секунду.

Достижение более 1 млн запросов в секунду не входило в мои первоначальные намерения. Я начал работать над совершенно не относящейся к теме записью в блоге, но каким-то образом обнаружил, что спускаюсь в эту кроличью нору оптимизации. Глобальная пандемия дала мне дополнительное время, поэтому я решил погрузиться с головой. В таблице ниже перечислены девять категорий оптимизации, которые я рассмотрю, и ссылки на соответствующие flame graphs. Он показывает процентное улучшение для каждой оптимизации и совокупную пропускную способность в запросах в секунду. Это довольно убедительная иллюстрация силы компаундирования при выполнении работы по оптимизации.


OptimizationFlame GraphGainReq/s
Ground Zeroinitial.svg-224k
Оптимизация приложений p.svg 55%347к
Предотвращение спекулятивного исполненияpec-exec.svg28%446к
Аудит/блокировка системных вызововiptables.svg11%495k
Отключение iptables/netfilterperfect-locality.svg22%603k
Perfect Localityperfect-locality.svg38%834k
Оптимизация прерыванийinterrupt.svg28%1.06M
The Case of the Nosy Neighbornosy-neighbor.svg6%1.12M
Битва со спин-блокировкойspin-lock.svg2%1.15M
This Goes to Twelvefinal.svg4%1.20M

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

Базовая настройка эталона

Это базовый обзор настройки эталонного теста на AWS. Если вас интересуют подробности, см. раздел « Полная настройка тестов ». Я использовал тест сериализации JSON от Techempower в качестве эталона для этого эксперимента. Для реализации я использовал простой API-сервер, созданный с помощью libreactor , управляемой событиями.

Код:
twrk -t 16 -c 256 -D 2 -d 10 --latency --pin-cpus "http://server.tfb:8080/json" -H 'Host: server.tfb' -H 'Accept: application/json,text/html;q=0.9,application/xhtml+xml;q=0.9,application/xml;q=0.8,*/*;q=0.7' -H 'Connection: keep-alive'

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

Граунд зироу

Код:
unning 10s test @ http://server.tfb:8080/json
  16 threads and 256 connections
  Thread Stats   Avg     Stdev       Max       Min   +/- Stdev
    Latency     1.14ms   58.95us    1.45ms    0.96ms   61.61%
    Req/Sec    14.09k   123.75     14.46k    13.81k    66.35%
  Latency Distribution
  50.00%    1.14ms
  90.00%    1.21ms
  99.00%    1.26ms
  99.99%    1.32ms
  2243551 requests in 10.00s, 331.64MB read
Requests/sec: 224353.73
Transfer/sec:     33.16MB

В начале нашего пути первоначальная реализация libreactor способна обслуживать 224 тыс. запросов в секунду . Тут нечего насмехаться; большинству приложений не требуется ничего близкого к такой скорости. Вывод twrk выше показывает нашу отправную точку. На приведенной ниже гистограмме сравнивается пропускная способность (треб./с) начальной реализации libreactor (раунд 18) с текущими реализациями нескольких популярных серверов/фреймворков (раунд 20), работающих на сервере c5n.xlarge в конфигурации по умолчанию.

1661227641400.png


Я модифицировал nginx.conf, чтобы он отправлял обратно жестко заданный ответ JSON. Это не является частью реализации Techempower.

Actix, NGINX и Netty являются хорошо известными высокопроизводительными HTTP-серверами, и libreactor не отстает от них. Просто взглянув на гистограмму, вы можете подумать, что на самом деле не так много возможностей для улучшения, но, конечно же, вы ошибаетесь. Понимание вашего положения относительно остальных полезно, но это не должно останавливать вас от попыток увидеть, что еще можно улучшить.

Flame Graphs

Flame Graphs предоставляют уникальный способ визуализации использования ЦП и определения наиболее часто используемых путей кода вашего приложения. Они являются мощным инструментом оптимизации, так как позволяют быстро выявлять и устранять узкие места. По всему документу вы будете видеть диаграммы пламени, иллюстрирующие вехи достигнутого прогресса и проблемы, которые необходимо решить. Начальный Flame Graphs дает нам снимок внутренней работы нашего приложения. Я настроил Flame Graphs в этом посте так, чтобы user-land функции были выделены синим цветом, а функции ядра оставались окрашенными в цвет «пламя». Мы уже можем быстро определить, что большая часть процессорного времени тратится в ядре на отправку/получение сообщений по сети. Это означает, что наше приложение уже довольно эффективно; в основном он не мешает, пока ядро перемещает данные. Пользовательский код разделен между синтаксическим анализом входящего HTTP-запроса и отправкой ответа. Высокие, тонкие, похожие на иглы стеки, разбросанные по всему графику, представляют собой обработку входящих запросов, связанную с прерываниями. Эти стеки распределены случайным образом, потому что прерывания могут произойти где угодно.

1661227857545.png


Щелчок по изображению выше откроет исходный файл SVG, созданный инструментом Flamegraph . Эти SVG интерактивны . Вы можете щелкнуть сегмент, чтобы перейти к более подробному представлению, или вы можете выполнить поиск (Ctrl + F или щелкните ссылку в правом верхнем углу) для имени функции. При поиске он выделяет соответствующие кадры стека фиолетовым цветом и показывает относительный процент, который представляют эти кадры. Поиск на графике ret_from_intrдаст вам представление о времени процессора, затраченном на прерывания. Подробности о том, как генерировались графики пламени, см. в приложении .

Отказ от ответственности

Этот захватывающий поиск возмутительной производительности предназначен для развлекательных целей (в основном). Не пытайтесь делать это дома... хорошо, попробуйте дома, но не на работе, если вы действительно не знаете, что делаете. Любой код C, который я лично написал, в лучшем случае следует рассматривать как доказательство концепции. В последний раз я писал C, прежде чем заново изучать его для этого проекта, 20 лет назад. Я знаю, что libreactor используется его создателем в производстве, но в настоящее время я не использую этот код в производственной среде.

1. Оптимизация приложений

Я начал с кода libreactor из раунда 18 * серии тестов Techempower . Я внес свои изменения реализации теста непосредственно в репозиторий Techempower и открыл вопросы в репозитории libreactor для изменений на уровне фреймворка . Затем эти изменения фреймворка были разрешены в ветке libreactor 2.0. Реализация 20-го раунда содержит все оптимизации уровня реализации и фреймворка, описанные в этом посте.

* Я обновил код раунда 18, чтобы использовать Ubuntu 20.04, gcc 10, libdynamic 1.3.0 и libreactor 1.0.1, прежде чем приступить к тестированию.

Оптимизация реализации


vCPU Usage

Как оказалось, самая первая оптимизация, которую я нашел, была чертовски проста, и я наткнулся на нее без какого-либо сложного анализа. Я запустил htop-сервер во время выполнения теста и заметил, что приложение libreactor работает только на 2 из 4 доступных виртуальных ЦП.

1661227897562.png


Верно, реализация бенчмарка в libreactor фактически сражалась с одной рукой, связанной за спиной, так что это было самое первое, к чему я обратился, и это улучшило производительность более чем на 25%! Если вы ожидали большего улучшения, вы должны иметь в виду, что (1) «неиспользуемые» логические ядра все еще обрабатывали часть обработки IRQ * и (2) это виртуальные ЦП с поддержкой Hyper-Threading , что означает, что есть 2 физических ядра, которые представлены в виде 4-х логических ядер. Использование всех 4 логических ядер, безусловно, быстрее, но неизбежно будет некоторая конкуренция за ресурсы, поэтому вы не можете ожидать, что производительность удвоится.

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

Наконец, когда была создана ветка libreactor 2.0, -march-nativeфлаг GCC был добавлен в Makefile фреймворка, и я заметил, что его добавление в приложение также улучшило производительность. Я подозреваю, что это связано с обеспечением того, чтобы один и тот же набор опций использовался для всех компонентов при построении с помощью Link Time Optimizations .

Оптимизация фреймворка
отправить/получить

libreactor 1.0 использует Linux readи writeфункции для связи на основе сокетов. Использование чтения/записи при работе с сокетами эквивалентно более специализированным функциям recvи send, но прямое использование recv/send все же немного более эффективно. Как правило, разница незначительна, однако, когда вы выходите за пределы 50 000 запросов в секунду, она начинает складываться. Вы можете взглянуть на проблему GitHub, которую я создал для получения более подробной информации, включая графики пламени до и после. Эта проблема была решена в ветке libreactor 2.0.

Избегайте накладных расходов pthread

В том же духе, хотя pthreads в Linuxиметь очень мало накладных расходов, если вы действительно проворачиваете вещи, накладные расходы становятся видимыми. Я заметил, что libreactor создает пул потоков для облегчения асинхронного разрешения имен. Это полезно, если вы пишете HTTP-клиент, который подключается к множеству разных доменов, и хотите избежать блокировки при поиске DNS. Это гораздо менее важно при запуске HTTP-сервера, которому нужно только разрешить свой собственный адрес перед привязкой к сокету. Обычно это то, о чем вы бы не подумали, поскольку пул потоков создается только при запуске и больше никогда не используется. Однако в этом случае все еще есть некоторые функции управления потоками и соответствующие накладные расходы. Даже в экстремальных условиях этого теста накладные расходы составляют всего около 3%, но 3% — это много для того, что не используется.

Если сервер построен без оптимизации времени соединения ( -flto), накладные расходы отображаются на пламенных графиках как __pthread_enable_asynccancelи __pthread_disable_asynccancel. Накладные расходы не видны на графиках пламени в этом посте, потому что все эти графики пламени сгенерированы из включенной -fltoсборки . Вы можете проверить проблему libreactor GitHub , которую я открыл, для получения более подробной информации, включая график пламени, где __pthread_enable_asynccancelи __pthread_disable_asynccancelвидны. Эта проблема также была решена в ветке libreactor 2.0.

Добавление всего

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

  • Запуск на всех виртуальных ЦП: 25–27 %
  • Использовать gcc -O3 при сборке фреймворка: 5-10%
  • Использование March=native при создании приложения: 5-10%
  • Использовать send/recv вместо записи/чтения: 5-10%
  • Избегайте накладных расходов pthread: 2-3%

Цифры неточны по нескольким причинам:

  • Изменения происходили не в том порядке, который описан выше, но для того, чтобы составить связный пост в блоге, я решил сгруппировать их.
  • Оптимизации не только кумулятивны, но и дополняют друг друга. Каждая последующая оптимизация может извлекать выгоду из узкого места, устраненного предыдущей оптимизацией, и в результате быть еще более эффективной.
Например, изменение запуска libreactor на всех 4 виртуальных ЦП увеличивает пропускную способность чуть более чем на 25% , если это самая первая оптимизация, однако, если бы это была последняя из всех оптимизаций в этом посте, ее вклад составил бы 40+ . % , потому что ранее «простаивающие» виртуальные ЦП теперь могут вносить более эффективный вклад. Я также должен отметить, что libreactor претерпел значительный рефакторинг между версиями 1.0 и 2.0. Возможно, это также способствовало повышению производительности, но я не пытался изолировать влияние этих изменений.
Эти изменения почти полностью синхронизируют нашу реализацию libreactor с кодом раунда 20 . Единственный недостающий элемент — это enable SO_ATTACH_REUSEPORT_CBPF, но об этом позже.

Результат

В целом это дает нам прирост производительности примерно на 55% . Пропускная способность увеличилась с 224 тыс. запросов/с до 347 тыс. запросов/с .

Код:
Running 10s test @ http://server.tfb:8080/json
  16 threads and 256 connections
  Thread Stats   Avg     Stdev       Max       Min   +/- Stdev
    Latency   735.43us   99.55us    4.26ms  449.00us   62.05%
    Req/Sec    21.80k   727.56     23.42k    20.32k    62.06%
  Latency Distribution
  50.00%  723.00us
  90.00%    0.88ms
  99.00%    0.94ms
  99.99%    1.08ms
  3470892 requests in 10.00s, 483.27MB read
Requests/sec: 347087.15
Transfer/sec:     48.33MB

Flame Graph Analysis

Наиболее очевидным изменением по сравнению с первоначальным графиком пламени является уменьшение ширины и высоты кадров, представляющих код пользовательской земли (синий) -O3, благодаря включенному флагу оптимизации gcc. Переключатель с read/ writeна recv/ sendтоже хорошо виден. Наконец, общее увеличение пропускной способности влечет за собой увеличение частоты прерываний, поскольку теперь мы видим гораздо большую плотность ret_from_intr«иголок» на графике. Поиск ret_from_intrпоказывает, что он переместился с 15% исходного графика на 27% текущего.

1661228391121.png


2. Предотвращение спекулятивного исполнения​

Следующая оптимизация является важной и противоречивой: отключение спекулятивных мер по предотвращению выполнения в ядре Linux. Теперь, прежде чем бежать за факелами и вилами, сначала сделайте глубокий вдох и медленно сосчитайте до десяти. Производительность — это главное в этом эксперименте, и, как оказалось, эти меры по снижению риска сильно влияют на производительность, когда вы пытаетесь выполнять миллионы системных вызовов в секунду. Помимо производительности, хотя эти меры по умолчанию являются разумными настройками по умолчанию, которые в большинстве случаев следует просто оставить в покое, я думаю, что все еще есть место для здорового обсуждения их отключения в сценариях, где преимущества перевешивают риски. Предположим, с одной стороны, что у вас есть многопользовательская система, которая полагается исключительно на разрешения пользователей и пространства имен Linux для установления границ безопасности. Возможно, вам следует оставить включенными меры по смягчению последствий для этой системы . С другой стороны, предположим, что вы запускаете сервер API отдельно на единственном целевом экземпляре EC2. Давайте также предположим, что он не запускает ненадежный код и что экземпляр использует Nitro Enclaves для защиты дополнительной конфиденциальной информации. Если экземплярявляется границей безопасности, а Nitro Enclave обеспечивает глубокую защиту, значит ли это, что mitigations=off возвращается на стол?

AWS, кажется, довольно уверен в подходе «экземпляры как граница безопасности». Их стандартный ответ на уязвимости классов Spectre/Meltdown:

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

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

Спекулятивное выполнение — это не просто отдельная атака, это класс уязвимостей, причем новые атаки еще предстоит обнаружить. Мне кажется, что если вы начнете с предположения, что эти средства защиты отключены, и смоделируете свой подход к использованию экземпляра/ВМ в качестве границы безопасности, вы, вероятно, получите лучшую долгосрочную позицию безопасности. Те, у кого есть время и ресурсы для такого подхода, скорее всего, опередят игру. Мне искренне интересно услышать мнение других экспертов по безопасности по этому поводу. Во всяком случае, для целей этого эксперимента производительность=хорошо, смягчение=выключено; Итак, давайте перейдем к деталям, какие меры по смягчению последствий я фактически отключил. Вот список параметров ядра, которые я использовал:

Код:
nospectre_v1 nospectre_v2 pti=off mds=off tsx_async_abort=off

Отключенные Mitigations
Spectre v1 + SWAPGS
Смягчения для исходных обходов проверки границ v1 нельзя отключить, однако я все же отключил смягчения SWAPGS с помощью параметра ядра nospectre_v1. В моем тестировании отключение барьеров SWAPGS привело к небольшому (1-2%) увеличению производительности.

Spectre v2
Я отключил средства защиты Spectre v2, используя параметр ядра nospectre_v2. Влияние на производительность было значительным, около 15-20%.

Spectre v3/Meltdown
Я отключил KPTI, используя параметр ядра pti=off. Это привело к увеличению производительности примерно на 6%.

MDS/Zombieload и TSX Asynchronous Abort
Я отключил MDS с помощью mds=off и TAA с помощью tsx_async_abort=off. Для обеих уязвимостей используется одно и то же средство смягчения. Производительность улучшилась примерно на 10%, когда оба были отключены.

Mitigations оставлены без изменений

L1TF/Foreshadow
Инверсия PTE включена постоянно. l1tf=flush — это параметр по умолчанию, но он не имеет значения, поскольку мы не используем вложенную виртуализацию. l1tf=off не имеет никакого эффекта, поэтому я оставил значение по умолчанию как есть.

iTLB multihit
Многократное попадание iTLB актуально только в том случае, если вы используете KVM, поэтому оно не применяется, поскольку AWS не поддерживает запуск KVM на экземпляре EC2.

Speculative Store Bypass
Похоже, что в ядре нет средств для устранения этой уязвимости; вместо этого он решается обновлением микрокода Intel. Согласно AWS, их базовая инфраструктура не пострадала , и было выпущено обновление со ссылкой на этот CVE . Ядро по-прежнему считает spec_store_bypass уязвимым, поэтому это может быть связано с тем, что ОС не имеет доступа для проверки микрокода.

SRBDS
ЦП, используемый семейством c5, не подвержен этой уязвимости.

Результат
Отключение этих средств защиты дает нам прирост производительности примерно на 28% . Пропускная способность увеличивается с 347 тыс. запросов/с до 446 тыс. запросов/с .

Код:
Running 10s test @ http://server.tfb:8080/json
  16 threads and 256 connections
  Thread Stats   Avg     Stdev       Max       Min   +/- Stdev
    Latency   570.37us   49.60us    0.88ms  398.00us   66.72%
    Req/Sec    28.05k   546.57     29.52k    26.97k    62.63%
  Latency Distribution
  50.00%  562.00us
  90.00%  642.00us
  99.00%  693.00us
  99.99%  773.00us
  4466617 requests in 10.00s, 621.92MB read
Requests/sec: 446658.48
Transfer/sec:     62.19MB

Анализ Flame Graph
Несмотря на то, что повышение производительности было значительным, изменения в графике пламени довольно малы, поскольку эти меры по смягчению в значительной степени невидимы для профилирования. Тем не менее, если вы сравните результаты поиска по __entry_trampoline_startи __indirect_thunk_start в предыдущем флейм-графе и текущем, то увидите, что сейчас они либо совсем исчезли, либо значительно сократились.



3. Аудит/блокировка системных вызовов

Накладные расходы, связанные с аудитом/блокировкой системных вызовов по умолчанию, выполняемой Linux/Docker, незаметны для большинства рабочих нагрузок; но когда вы пытаетесь выполнять миллионы системных вызовов в секунду, история меняется, и эти функции начинают отображаться на вашем графике пламени. Если вы поищите в предыдущем плам-графе «audit|seccomp», вы поймете, что я имею в виду.
Отключить аудит системных вызовов
Подсистема аудита ядра Linux предоставляет механизм для сбора и регистрации событий безопасности, таких как доступ к конфиденциальным файлам или системным вызовам. Это может помочь вам устранить неожиданное поведение или собрать доказательства в случае нарушения безопасности. Подсистема аудита включена по умолчанию в Amazon Linux 2, но она не настроена для регистрации системных вызовов.
Несмотря на то, что ведение журнала системных вызовов по умолчанию отключено, подсистема аудита по-прежнему добавляет небольшую нагрузку на каждый системный вызов. Хорошая новость заключается в том, что это можно переопределить с помощью относительно простого правила: auditctl -a never,task, поэтому я создал собственный файл конфигурации, который делает именно это. Если вы на самом деле используете подсистему аудита для регистрации системных вызовов, это не лучший вариант, но я подозреваю, что большинство пользователей этого не делают. Согласно этой проблеме Bugzilla , Fedora по умолчанию использует то же правило.

Отключить блокировку системных вызовов

По умолчанию Docker применяет ограничения на процессы, работающие в контейнере, используя пространства имен , контрольные группы и ограниченный набор возможностей Linux . В дополнение к этому фильтр seccomp используется для ограничения списка системных вызовов, которые может выполнять приложение.
Большинство контейнерных приложений могут работать с этими ограничениями без проблем, но есть небольшие накладные расходы, связанные с контролем системных вызовов. Ваш первый порыв может состоять в том, чтобы запустить докер с --privilegedопцией, и хотя это сработает, это также даст контейнеру больше возможностей Linux, чем ему нужно. Вместо этого мы можем использовать --security-optопцию, чтобы отключить только фильтр seccomp. Наша новая команда запуска докера выглядит так:
Код:
docker run -d --rm --network host --security-opt seccomp=unconfined --init libreactor

Результат

В совокупности это дает нам прирост производительности примерно на 11% . Пропускная способность увеличивается с 446 тыс. запросов/с до 495 тыс. запросов/с .

Код:
Running 10s test @ http://server.tfb:8080/json
  16 threads and 256 connections
  Thread Stats   Avg     Stdev       Max       Min   +/- Stdev
    Latency   514.02us   39.05us    1.65ms  134.00us   67.34%
    Req/Sec    31.09k   433.78     32.27k    30.01k    65.97%
  Latency Distribution
  50.00%  513.00us
  90.00%  565.00us
  99.00%  604.00us
  99.99%  696.00us
  4950091 requests in 10.00s, 689.23MB read
Requests/sec: 495005.93
Transfer/sec:     68.92MB

Анализ FFlame Graph

Это полностью устраняет накладные расходы на трассировку/выход системного вызова. Оба эти изменения применяются syscall_trace_enterи syscall_slow_exit_workисчезают с графика пламени.


syscall.svg


Еще одно замечание: несмотря на то, что в документации по докеру говорится, что apparmor также имеет профиль по умолчанию , он, похоже, не включен в Amazon Linux 2. Не было заметного изменения производительности (или графика пламени), когда я использовал - -security-opt apparmor=неограниченный параметр.

4. Отключение iptables/netfilter

iptables/netfilter * — это основной компонент, используемый традиционными брандмауэрами Linux для контроля доступа к сети. Это также чрезвычайно мощный и гибкий сетевой инструмент, от которого зависят многие другие приложения для таких вещей, как преобразование сетевых адресов (NAT). При экстремальной нагрузке этого теста накладные расходы iptables значительны, поэтому это следующая цель в наших поисках. На предыдущем графике пламени служебные данные iptables отображаются как на стороне отправки, так и на стороне приема в виде nf_hook_slowфункции ядра; быстрый поиск по флейм-графу показывает, что на него приходится почти 18% от общего числа кадров.

* netfilter — это имя модуля ядра, который фактически выполняет всю работу. iptables — это пользовательская программа, используемая для изменения правил сетевого фильтра, но большинство людей называют их собирательно iptables.

Отключение iptables не так спорно с точки зрения безопасности , как могло бы быть раньше . С появлением облачных вычислений стратегия брандмауэра для многих развертываний сместилась с iptables на облачные примитивы, такие как группы безопасности AWS. Тем не менее, многие из тех же развертываний по-прежнему неявно используют iptables для NAT, особенно в средах, где широко используются контейнеры Docker. Недостаточно просто отключить iptables, вы также должны пройти и обновить/заменить любые приложения, которые от него зависят.

Для целей этого теста мы отключим поддержку iptables в ядре и в демоне docker . Нам это сойдет с рук, потому что мы запускаем один контейнер, напрямую подключенный к хост-сети, поэтому нет необходимости в трансляции сетевых адресов. Главное помнить, что после того, как вы отключили поддержку iptables в Docker, вам нужно будет использовать эту --network hostопцию с любой командой, взаимодействующей с сетью, включая docker build.

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

Недостатком nftables является то, что поддержка дистрибутивов Linux является относительно новой, а поддержка сторонних инструментов находится в стадии разработки. Docker — прекрасный пример отсутствия поддержки со стороны инструментов. Что касается дистрибутивов, Debian 10 , Fedora 32 и RHEL 8все переключились на nftables в качестве бэкенда по умолчанию. Они используют уровень iptables-nft, чтобы действовать как (в основном) совместимая замена iptables на пользовательском уровне. Разработчики Ubuntu пытались переключиться как на версии 20.04, так и на версии 20.10, но похоже, что они оба раза сталкивались с проблемами совместимости. Amazon Linux 2 по-прежнему использует iptables по умолчанию. Если ваш дистрибутив поддерживает nftables, у вас должна быть возможность получить свой пирог и съесть его, оставив модуль ядра на месте и просто внеся изменения в конфигурацию вашего докера. Надеюсь, однажды Docker предложит встроенную поддержку nftables с минимальным набором правил, необходимых для баланса производительности и функциональности.

Результат

Отключение iptables дает прирост производительности примерно на 22% . Пропускная способность увеличивается с 495 тыс. запросов/с до 603 тыс. запросов/с .

Код:
Running 10s test @ http://server.tfb:8080/json
  16 threads and 256 connections
  Thread Stats   Avg     Stdev       Max       Min   +/- Stdev
    Latency   420.68us   43.25us  791.00us  224.00us   63.70%
    Req/Sec    37.88k   687.33     39.54k    36.35k    62.94%
  Latency Distribution
  50.00%  419.00us
  90.00%  479.00us
  99.00%  517.00us
  99.99%  575.00us
  6031161 requests in 10.00s, 839.76MB read
Requests/sec: 603112.18
Transfer/sec:     83.98MB

Анализ Flame Graph
Эти изменения полностью устраняют накладные расходы iptables. nf_hook_slowнигде не находится на графике пламени.

iptables.svg



Linux — фантастическое многоцелевое ядро ОС, которое хорошо работает в самых разных случаях использования. По умолчанию ядро старается максимально равномерно распределять ресурсы, автоматически распределяя нагрузку между несколькими сетевыми очередями, процессами и процессорами. В большинстве случаев это работает очень хорошо, но когда вы хотите перейти от хорошей производительности к экстремальной производительности, вы должны более жестко контролировать то, как все делается. Один из методов , который появился с появлением серверов с несколькими очередями/ЦП, заключается в создании отдельных хранилищ (аналогичных сегментам базы данных), где каждая сетевая очередь связана с ЦП, чтобы каждая пара работала как можно более независимо от других. . Как ОС, так и приложение должны быть настроены таким образом, чтобы после поступления сетевого пакета в любую очередь вся дальнейшая обработка выполнялась одним и тем же хранилищем виртуального ЦП/очереди как для входящих, так и для исходящих данных. Такой акцент на локальность пакетов/данных повышает эффективность за счет сохранения теплоты кэш-памяти ЦП, уменьшения переключения контекста/режима, минимизации обмена данными между ЦП и устранения конфликтов блокировок.

Закрепление ЦП

Первым шагом к достижению идеальной локальности является создание и привязка отдельного серверного процесса libreactor к каждому из доступных виртуальных ЦП в экземпляре. В нашем случае этим занимается fork_workers(). Технически мы все время использовали привязку ЦП, но я хотел выделить ее здесь из-за ее важности для создания хранилища виртуальных ЦП/очередей.

Масштабирование стороны приема (RSS)

Следующим шагом является установление фиксированных пар между сетевыми очередями и виртуальными ЦП для входящих данных (исходящие должны обрабатываться отдельно). Масштабирование на стороне приема — это аппаратный механизм для согласованного распределения сетевых пакетов по нескольким очередям приема. Драйвер AWS ENA поддерживает RSS и включен по умолчанию. Хеш-функция (Toeplitz) используется для преобразования фиксированного хеш-ключа (автоматически сгенерированного при запуске) и src/dst/ip/port соединения в хешированное значение, затем объединяются 7 младших битов этого хэша. с таблицей косвенности RSS, чтобы определить, в какую очередь приема будет записан пакет. Эта система гарантирует, что входящие данные от данного соединения всегдаотправлены в ту же очередь. В c5n.xlarge таблица косвенных RSS-адресов по умолчанию распределяет соединения/данные по четырем доступным очередям получения, что нам и нужно, поэтому мы оставляем ее без изменений.
Как только сетевой адаптер записывает пакет в область ОЗУ, зарезервированную для очереди приема, ОС необходимо уведомить о наличии данных, ожидающих обработки; это событие известно как аппаратное прерывание. Каждой сетевой очереди назначается номер IRQ, который по сути является выделенным аппаратным каналом прерывания для этой очереди. Чтобы определить, какой ЦП будет обрабатывать прерывание, каждый номер IRQ сопоставляется с ЦП на основе значения /proc/irq/$IRQ/smp_affinity_list. По умолчанию служба irqbalance обновляет значения smp_affinity_list для динамического распределения нагрузки. Чтобы поддерживать наши бункеры, нам нужно отключить irqbalanceи вручную установить значения smp_affinity_list так, чтобы очередь 0 -> ЦП 0, очередь 1 -> ЦП 1 и т. д.

Код:
systemctl stop irqbalance.service

export IRQS=($(grep eth0 /proc/interrupts | awk '{print $1}' | tr -d :))
for i in ${!IRQS[@]}; do echo $i > /proc/irq/${IRQS[i]}/smp_affinity_list; done;

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

SO_ATTACH_REUSEPORT_CBPF


Реализация libreactor использует параметр сокета SO_REUSEPORT , чтобы позволить нескольким серверным процессам прослушивать соединения на одном и том же порту. Это отличный вариант для распределения соединений между несколькими процессами. По умолчанию он использует простую хеш-функцию , которая также основана на src/dst/ip/port, но не имеет отношения к той, которая используется для RSS. К сожалению, поскольку это распределение является случайным, оно нарушает наш разрозненный подход. Входящий пакет может быть сопоставлен ЦП 2 для обработки softirq, а затем передан процессу приложения, прослушивающему ЦП 0. К счастью, начиная с ядра 4.6 у нас теперь есть возможность балансировать нагрузку этих соединений более контролируемым образом. Параметр сокета SO_ATTACH_REUSEPORT_CBPF позволяет нам указать собственный пользовательскийПрограмма BPF * , которую можно использовать для определения того, как распределяются соединения. Скажем, например, у нас есть четыре серверных процесса с сокетами, прослушивающими один и тот же порт; следующая программа BPF будет использовать идентификатор процессора, обрабатывающего softirq для пакета, чтобы решить, какой из четырех прослушивающих сокетов/процессов должен получить входящее соединение.
Код:
{{BPF_LD | BPF_W | BPF_ABS, 0, 0, SKF_AD_OFF + SKF_AD_CPU}, {BPF_RET | BPF_A, 0, 0, 0}}
* Технически эта программа написана в формате Classic BPF, но она автоматически транслируется в представление eBPF.

Так, например, если обработка softirq была обработана ЦП 0, то данные для этого соединения направляются в сокет 0. Позвольте мне быстро пояснить то, что было не сразу понятно мне, когда я впервые попытался использовать параметр SO_ATTACH_REUSEPORT_CBPF. Сокет 0 не обязательно является сокетом, подключенным к процессу, работающему на ЦП 0, на самом деле он относится к первому сокету, который начал прослушивать комбинацию ip/port для группы SO_REUSEPORT.

Сначала я не особо задумывался об этом, так как считал, что запускаю серверные процессы (и открываю сокеты) в том же порядке, в котором я прикреплял их к соответствующим процессорам. Я просто использовал простой цикл for для fork()нового рабочего процесса, прикрепил его к процессору и запустил сервер libreactor. Однако, если вы вызываете fork()такой цикл for, порядок создания нового процесса/сокета не является детерминированным, что нарушает сопоставление между сокетами и процессорами. Короче говоря, при использовании SO_ATTACH_REUSEPORT_CBPF вам нужно быть особенно осторожным с порядком создания сокетов. Дополнительные сведения см. в коммите GitHub, в котором я исправил проблему .

XPS: управление передачей пакетов

Управление передачей пакетов по сути делает для исходящих пакетов то, что RSS делает для входящих пакетов; это позволяет нам поддерживать нашу разрозненность, гарантируя, что, когда наше приложение будет готово отправить ответ, будет использоваться та же самая пара vCPU/queue. Это делается путем установки значения /sys/class/net/eth0/queues/tx-<n>/xps_cpus(где n — идентификатор очереди) в шестнадцатеричное растровое изображение, содержащее соответствующий ЦП.

Код:
export TXQUEUES=($(ls -1qdv /sys/class/net/eth0/queues/tx-*))
for i in ${!TXQUEUES[@]}; do printf '%x' $((2**i)) > ${TXQUEUES[i]}/xps_cpus; done;

Результат

Благодаря этим изменениям мы добились идеальной локализации и значительного повышения производительности примерно на 38% . Пропускная способность увеличивается с 603 тыс. запросов/с до 834 тыс. запросов/с .

Код:
Running 10s test @ http://server.tfb:8080/json
  16 threads and 256 connections
  Thread Stats   Avg     Stdev       Max       Min   +/- Stdev
    Latency   301.65us   23.43us  692.00us  115.00us   68.96%
    Req/Sec    52.41k   640.10     54.25k    50.52k    68.75%
  Latency Distribution
  50.00%  301.00us
  90.00%  332.00us
  99.00%  361.00us
  99.99%  407.00us
  8343567 requests in 10.00s, 1.13GB read
Requests/sec: 834350.86
Transfer/sec:    116.17MB

Анализ Flame Graph

1661230985124.png

Изменения на этом графике пламени интересны. По сравнению спредыдущим пламенным графиком блок user-land (синий) заметно шире. Он переместился примерно с 12% до 17% графика пламени. Профилирование на основе графа пламени не является точной наукой, и следует ожидать колебания в 1-2% между захватами, но это было постоянное изменение. Сначала я подумал, что это может быть просто неравномерное распределение прерываний, но дальнейший анализ показал, что это была просто корреляция. Чем больше процессорного времени тратится в пользовательской области, тем больше прерываний будет происходить в пользовательской области. Моя теория состоит в том, что идеальные изменения местоположения позволили коду ядра выполнять свою работу более эффективно. Теперь обрабатывается большее количество пакетов, используя еще меньше процессорного времени. Дело не в том, что пользовательский код стал медленнее, просто код ядра стал намного быстрее. В recvчастности, стек (представленный SYSC_recvfrom) уменьшается с 17% до 13% графика пламени. Это проверено, потому что теперь, когда у нас есть идеальная локальность, recvвсегда происходит на том же процессоре, который обрабатывал прерывание, что действительно ускоряет работу. Преимущества идеального локального подхода очевидны, но он не лишен ограничений; самая большая сила также является самой заметной слабостью. Предположим, например, что вы обслуживаете лишь небольшое количество соединений, и распределение этих соединений создает непропорционально большую нагрузку на ЦП/очередь 0. Никакой другой ЦП не сможет обрабатывать обработку IRQ, никакой другой процесс не будут назначены некоторые из соединений очереди 0, и никакая другая очередь не будет использоваться для исходящих передач. Каждый виртуальный ЦП/очередь — сам за себя. Также имейте в виду, что вам нужно быть очень осторожным, выполняя такие вещи, как запуск на ограниченном количестве процессоров с использованием опции tasksetDocker . --cpuset-cpusОн будет нормально работать с текущей программой BPF, если вы используете непрерывный диапазон ЦП, но если вы попытаетесь проявить фантазию, вам может потребоваться написать более конкретную программу. В зависимости от вашей архитектуры и вашей рабочей нагрузки эти ограничения могут не иметь значения, но вы все равно должны полностью о них знать, поскольку архитектуры и рабочие нагрузки могут меняться со временем. Даже с учетом этих предостережений я думаю, что SO_ATTACH_REUSEPORT_CBPF — это мощная оптимизация при эффективном использовании, и я надеюсь увидеть больше основных серверов и фреймворков, использующих ее в качестве дополнительной функции. Я думаю, что одна из причин, по которой он так долго оставался незамеченным, заключается в том, что трудно найти хорошие практические примеры того, как его использовать. Надеемся, что этот пост может послужить «реальным» примером его использования для улучшения локальности пакетов. Я видел обсуждения в списке рассылки NGINX о добавлении поддержки SO_ATTACH_REUSEPORT_CBPF, так что в недалеком будущем она может стать гораздо более популярной. Как и реализация libreactor, используемая в этом тесте, NGINX также имеет архитектуру «процесс на ядро» и поддерживает привязку ЦП., поэтому добавление поддержки SO_ATTACH_REUSEPORT_CBPF потенциально является большой победой, если ОС настроена для соответствия.
6. Оптимизация прерываний

Interrupt Moderation

Когда пакет данных поступает по сети, сетевая карта сигнализирует операционной системе о наличии входящих данных для обработки. Он делает это с помощью аппаратного прерывания, и, как следует из названия, он прерывает все, что делает ОС. Это делается для того, чтобы буферы данных сетевой карты не переполнялись и не приводили к потере данных. Однако, когда каждую секунду приходят тысячи пакетов, это постоянное прерывание приводит к большим накладным расходам. Чтобы смягчить это, современные сетевые карты поддерживают модерацию/объединение прерываний. Их можно настроить на задержку прерываний на короткий период времени, а затем инициировать одно прерывание для всех пакетов, пришедших за этот период. Более продвинутые драйверы, такие как драйвер AWS ENA, поддерживают адаптивную модерацию , которая использует преимущества алгоритма динамической модерации прерываний ядра для динамической настройки задержек прерываний. Если сетевой трафик невелик, задержки прерываний сводятся к нулю, а прерывания запускаются сразу после получения пакета, что гарантирует минимально возможную задержку. По мере увеличения сетевого трафика задержка прерывания увеличивается, чтобы не перегружать ресурсы системы; это увеличивает пропускную способность, сохраняя постоянную задержку. Драйвер ENA поддерживает фиксированные значения задержки прерывания (в микросекундах) для входящих и исходящих данных ( tx-usecs, rx-usecs) и динамическую модерацию прерываний только для входящих данных ( adaptive-rxвкл./выкл.). По умолчанию выключено , adaptive-rxравно 0 и равно 64 . В своем тестировании я обнаружил, что настройка на и 256 обеспечивает наилучший баланс пропускной способности и низкой задержки для различных рабочих нагрузок.rx-usecstx-usecsadaptive-rx tx-usecs
Для нашей эталонной рабочей нагрузки включение адаптивного приема повышает пропускную способность с 834 тыс. запросов/с до 955 тыс. запросов/с, т. е. на 14 %. Включение адаптивного rx — одно из немногих изменений конфигурации в этом посте, которые имеют большой потенциал плюсов и очень мало минусов для любого, кто обрабатывает более 10 000 запросов в секунду . Однако, как и в любом другом случае, обязательно проверяйте любые изменения с вашей рабочей нагрузкой, чтобы убедиться в отсутствии непредвиденных побочных эффектов.

Busy Polling
Busy Polling — относительно малоизвестная (и не очень хорошо задокументированная) сетевая функция, существующая с 2013 года (ядро 3.11). Она предназначена для случаев, когда крайне важна низкая задержка. По умолчанию он работает, позволяя заблокированному сокету инициировать опрос входящих данных за счет дополнительных циклов ЦП и энергопотребления. Функцию можно включить несколькими способами:

  1. sysctl net.core.busy_read можно использовать для установки значения по умолчанию для опроса занятости при использовании recv/read, но только для блокировки чтения. Это не помогает нам, так как мы делаем неблокирующие чтения.
  2. Значение net.core.busy_read может быть переопределено для каждого сокета с помощью параметра сокета SO_BUSY_POLL, но, опять же, оно не применяется к неблокирующим операциям чтения.
  3. sysctl — net.core.busy_pollэто третий вариант управления этой функцией, и именно его я использовал. В документации подразумевается, что настройка актуальна только для pollи select(а не для epollтого, что использует libreactor) и явно указано, что только сокеты с установленным SO_BUSY_POLL будут опрашиваться по занятости . Возможно, оба эти утверждения были верны в прошлом, но ни одно из них не верно сегодня. Поддержка опроса занятости была добавлена в epoll в 2017 году (ядро 4.12) , и, исходя из кода и моего тестирования, установка SO_BUSY_POLL для сокета не является обязательным требованием.
При использовании с epoll опрос занятости больше не работает на уровне отдельного сокета, а вместо этого запускается epoll_wait. Если включен опрос занятости и на момент вызова нет доступных событий epoll_wait, то подсистема NAPI собирает все необработанные пакеты из очереди приема сетевой карты и пропускает их через обработку softirq. В идеальном сценарии эти обработанные пакеты окажутся в тех же сокетах, которые отслеживал экземпляр epoll, и заставят epoll вернуться с доступными событиями. Все это может происходить без каких-либо аппаратных прерываний или переключений контекста.
Самым большим недостатком опроса занятости является дополнительная мощность и загрузка ЦП, связанные с опросом новых данных в узком цикле. Рекомендуемое значение net.core.busy_pollсоставляет от 50 мкс до 100 мкс, однако в ходе тестирования я определил, что могу получить все преимущества опроса занятости (почти без недостатков) при значении всего 1 мкс. С net.core.busy_poll=50дополнительной загрузкой ЦП при выполнении теста всего с 8 подключениями более 45%, тогда как с net.core.busy_poll=1дополнительной загрузкой ЦП всего 1-2%.
Важно подчеркнуть, в какой степени модерация прерываний, Busy_Polling и SO_ATTACH_REUSEPORT_CBPF работают вместе в эффективном цикле. Уберите любой из трех, и вы увидите гораздо менее выраженный эффект с точки зрения пропускной способности, задержки и результирующего графика пламени. Без модерации прерываний у опроса занятости вряд ли когда-либо будет шанс запуститься, а без SO_ATTACH_REUSEPORT_CBPF (и других настроек локальности данных) опрос занятости имеет очень небольшой эффект. Обратите внимание, что в сообщении git commit для добавления опроса занятости в epoll специально упоминается использование SO_ATTACH_REUSEPORT_CBPF в сочетании с опросом занятости для повышения эффективности. Этот документ от netdev 2.1 Montreal 2017 содержит еще более подробные сведения.
Результат
При объединении прерываний и опросе занятости мы получаем прирост производительности примерно на 28% . Пропускная способность увеличивается с 834 тыс. запросов/с до 1,06 млн запросов/с , а задержка p99 снижается с 361 мкс до 292 мкс.
Код:
Running 10s test @ http://server.tfb:8080/json
  16 threads and 256 connections
  Thread Stats   Avg     Stdev       Max       Min   +/- Stdev
    Latency   233.13us   24.41us  636.00us   73.00us   70.59%
    Req/Sec    66.96k   675.18     68.80k    64.95k    68.37%
  Latency Distribution
  50.00%  233.00us
  90.00%  263.00us
  99.00%  292.00us
  99.99%  348.00us
  10660410 requests in 10.00s, 1.45GB read
Requests/sec: 1066034.60
Transfer/sec:    148.43MB

Анализ Flame Graph

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

interrupt-optimization.svg


Если вы выполните поиск соответствующих графиков пламени для ena_io_poll, ret_from_intrи napi_busy_loopвы увидите, что общая доля обработки softirq (как указано ena_io_poll) остается примерно такой же, но количество прерываний (все тонкие остроконечные башни) падает с 25% до чуть более 6%, а количество, управляемое busy_poll, подскакивает почти до 22%.


До После
ena_io_poll 29,6% 30,2%
ret_from_intr 24,6% 6,3%
napi_busy_loop 0,0%21,9%

Вы можете увидеть разницу еще более явно, если посмотрите на подробную статистику прерываний, сгенерированную dstat:
Код:
dstat -y -i -I 27,28,29,30 --net-packets

До
Код:
---system-- -------interrupts------ -pkt/total-
 int   csw |  27    28    29    30 |#recv #send
 183k 6063 |  47k   48k   49k   37k| 834k  834k
 183k 6084 |  48k   48k   48k   37k| 833k  833k
 183k 6101 |  47k   49k   48k   37k| 834k  834k

После
Код:
---system-- -------interrupts------ -pkt/total-
 int   csw |  27    28    29    30 |#recv #send
  16k  967 |3843  3848  3849  3830 |1061k 1061k
  16k  953 |3842  3847  3851  3826 |1061k 1061k
  16k  999 |3842  3852  3852  3827 |1061k 1061k

Общее количество аппаратных прерываний в секунду резко падает со 183 тыс. до 16 тыс. Также обратите внимание, что общее количество переключений контекста упало с более чем 6000 до менее чем 1000, впечатляюще малое число, учитывая, что сейчас мы обрабатываем более 1 миллиона запросов в секунду .

7. The Case of the Nosy Neighbor /* Дело о любопытном соседе /

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

Отвернувшись от _raw_spin_lock, я нацелился на dev_queue_xmit_nitследующую цель, так как на данный момент она занимает 3,5% графика пламени. Просмотр исходного кода dev_queue_xmit_nit следует вызывать только в том случае, если !list_empty(&ptype_all) || !list_empty(&dev->ptype_all)это примерно переводится как «кто-то прослушивает и получает копию каждого исходящего пакета». Аналогичная проверка происходит и для входящих пакетов внутри __netif_receive_skb_core. Если вы выполните поиск на графике пламени для dev_queue_xmit_nit|packet_rcv, вы также увидите packet_rcvво входящих стеках программных прерываний, и что общее количество совпадений теперь составляет 4,5%.

Было бы совершенно разумно ожидать таких накладных расходов, если бы я запускал программу, которая выполняет низкоуровневый захват пакетов (например, tcpdump), но это не так. Тот факт, что это вызывалось, packet_rcvозначал, что кто-то где-то открыл необработанный сокет, используя AF_PACKET , и это замедляло работу. К счастью, я смог в конечном итоге отследить виновника, используя ss . Я сначала попробовал ss --raw, но оказалось, что этот вариант применим только к сокетам AF_INET/SOCK_RAW, а мы ищем сокет AF_PACKET/SOCK_RAW (не спрашивайте, я особо не копался). Следующая команда находит необработанный сокет AF_PACKET, а также показывает имя прослушивающего процесса: sudo ss --packet --processes. Это разоблачает нашего убийцу (производительности):(("dhclient",pid=3191,fd=5)). Это был DHCP-клиент в библиотеке с необработанным сокетом.

Конечно, найти виновного — это только полдела, решить проблему — совсем другая история. Первое, что меня заинтересовало, это то, почему DHCP-клиент должен в первую очередь прослушивать необработанный сокет. Ответ, который я нашел в базе знаний ISC , приведен ниже. Технически это говорит с точки зрения сервера, но я уверен, что та же логика применима и к клиенту:

[сокет] Передает направленные одноадресные рассылки (без ARP) и специальные ограниченные широковещательные рассылки, соответствующие RFC 2131. Они необходимы на начальной стадии настройки клиентов (когда у клиента еще не настроен адрес).

Это имеет смысл, клиент DHCP должен иметь возможность отправлять и получать сообщения до того, как экземпляру будет выдан IP-адрес. В той же статье базы знаний также упоминается, что стандартный сокет UDP используется для передачи маршрутизируемых одноадресных передач для обновлений DHCP. Я надеялся, что есть какой-то способ заставить dhclient закрыть необработанный сокет после получения начального адреса и просто использовать сокет UDP для обновлений, но, похоже, это не вариант.

Следует отметить, что эти пакеты на самом деле никогда не достигают dhclient, поскольку, вероятно, в сокете есть фильтр BPF, который отбрасывает не-DHCP-пакеты до того, как они покинут ядро. Тем не менее, накладные расходы на действия, выполняемые внутри ядра, все еще остаются, и мы просто не можем этого допустить.

Согласно документам , после того как AWS присвоит экземпляру основной частный IP-адрес, он будет связан с этим экземпляром на весь срок службы, даже при перезагрузках и длительных остановках. Поскольку обновления DHCP не критичны для предотвращения переназначения IP-адреса, я решил отключить dhclient после загрузки. Помимо остановки dhclient также необходимо обновить время жизни приватного адреса на уровне сетевого устройства (eth0) с помощью ip address. По умолчанию время жизни составляет 1 час (и обновления IPv4 происходят каждые 30 минут), поэтому я вручную установил время жизни на «навсегда».
Код:
sudo dhclient -x -pf /var/run/dhclient-eth0.pid
sudo ip addr change $( ip -4 addr show dev eth0 | grep 'inet' | awk '{ print $2 " brd " $4 " scope global"}') dev eth0 valid_lft forever preferred_lft forever

Результат

Отключение dhclient дает прирост производительности чуть менее 6% . Пропускная способность увеличивается с 1,06 млн запросов/с до 1,12 млн запросов/с .
Код:
Running 10s test @ http://server.tfb:8080/json
  16 threads and 256 connections
  Thread Stats   Avg     Stdev       Max       Min   +/- Stdev
    Latency   219.38us   26.49us  598.00us   56.00us   68.29%
    Req/Sec    70.84k   535.35     72.42k    69.38k    67.55%
  Latency Distribution
  50.00%  218.00us
  90.00%  254.00us
  99.00%  285.00us
  99.99%  341.00us
  11279049 requests in 10.00s, 1.53GB read
Requests/sec: 1127894.86
Transfer/sec:    157.04MB

Анализ Flame Graph

Функции dev_queue_xmit_nit и packet_rcv были полностью удалены. Нашего любопытного соседа выселили.

1661231897043.png


ПРИМЕЧАНИЕ. Я хочу, чтобы было абсолютно ясно, что я не тестировал эту конфигурацию на производственных рабочих нагрузках или в течение длительного периода времени. Отключение dhclient может привести к непредвиденным побочным эффектам. Это просто быстрый обходной путь; если кто-нибудь знает более чистый способ заставить dhclient не слушать необработанный сокет, я хотел бы услышать об этом: Hacker News | Реддит | Прямой .

8. Битва с болью в спине / The Battle Against the Spin Lock /

Я потратил значительное количество времени, пытаясь подавить значительный _raw_spin_lockфрейм в верхней части sendtoстека (см. предыдущий график пламени). Ядро использует несколько спин-блокировок для управления выходом сетевых пакетов, проходящих через (или вокруг) qdiscs и выходящих на сетевую карту. Учитывая, что я уже добился идеальной локальности , мне казалось неправильным, что по-прежнему существует так много споров о блокировках, поэтому я решил докопаться до сути.
Я потратил дни (а может и недели) только на этот вопрос. Я тренировался с инструментами трассировки, пытаясь проанализировать это. Я вручную перекомпилировал bpftrace, чтобы можно было небезопасно отслеживать _raw_spin_lockс помощью kretprobe . Я спал, думая об этом. Я мечтал об этом. Я просыпался с новой яркой идеей, но она всегда оказывалась очередным тупиком. Тем не менее я продолжал возвращаться; это был мой белый кит. Я пробовал разные параметры, разные qdisc и разные ядра. Я даже пробовал некоторые хаки, которые я нашел для полного отключения qdisc. Все, что я получил за свою беду, это недостижимый экземпляр. В конце концов, с тяжелым сердцем, я решил сдаться. Технически ядро вело себя так, как ожидалось. Блокировки вращения, связанные с qdisc, являются известным узким местом, и попытки разработчиков ядра решить эту проблему до сих пор были сорваны. Поддержка незаблокированных qdisc была добавлена в ядро 4.16, но это произошло за счет невозможности полностью обойти пустой qdisc (TCQ_F_CAN_BYPASS). Это означает, что для моего сценария (несогласованные отправки/идеальное местоположение) узкое место просто сместится с _raw_spin_lockна __qdisc_run, даже если qdisc всегда будет пуст.
Я немного обрадовался, когда узнал, что поддержка одновременного использования TCQ_F_CAN_BYPASS и незаблокированных qdisc была добавлена в более поздние ядра . Увы, моя радость была недолгой. Как оказалось, исправление вызвало некоторые регрессии, и его пришлось отменить .

Никто не поможет
После того, как я отказался от борьбы со спин-блокировкой и перешел к другим оптимизациям, я вернулся, чтобы записать свои заметки для раздела « Не сработавшие оптимизации » этого блога. Я подумал, что мог бы также поделиться тем, что нашел, надеясь, что кто-то еще может найти что-то, что я пропустил. При поиске одного из безумных способов взлома, которые я пробовал (тот, который заблокировал экземпляр), я наткнулся на то, что раньше упускал из виду: noqueue qdisc. Документации по noqueue qdisc довольно мало, но я нашел несколько неофициальных заметок о tc , которые дают разумный обзор. Как следует из названия, это дисциплина организации очередей, которая на самом деле не занимается очередями. Он предназначен для использования с программными устройствами, такими как петлевой интерфейс (localhost) или виртуальные интерфейсы на основе контейнеров. В то время как реальным устройствам может потребоваться время от времени прекращать прием новых пакетов, чтобы они могли очистить свою задолженность, программные устройства в основном принимают все, что им отправляется; они никогда не применяют противодавление . Обратное давление обычно приводит к тому, что пакет ставится в очередь qdisc, поэтому отсутствие обратного давления = отсутствие постановки в очередь. Обоснование использования noqueue с программными устройствами простое — если qdisc все равно не будет использоваться, то можно вообще избежать дополнительных накладных расходов. Избежать накладных расходов мне показалось отличной идеей, особенно если это позволит избавиться от всех этих спин-блокировок! Вопрос был в том, смогу ли я заставить его работать с аппаратным * устройством?

* Несмотря на то, что экземпляр EC2 является виртуальной машиной, во всех смыслах и целях сетевое устройство на базе Nitro представляет собой сетевую интерфейсную карту, подключенную к PCIe.

Короткий ответ: да, можно заменить pfifo_fast на noqueue в качестве qdisc по умолчанию.
Код:
sudo sysctl net.core.default_qdisc=noqueue
sudo tc qdisc replace dev eth0 root mq
Если вы запустите sudo tc qdisc show dev eth0, после выполнения вышеуказанных команд ваш вывод будет выглядеть так на c5n.xlarge. Имейте в виду, что это настройка с несколькими очередями, поэтому для каждой из очередей передачи сетевой карты существует отдельный qdisc.

qdisc mq 8001: root
qdisc noqueue 0: parent 8001:4
qdisc noqueue 0: parent 8001:3
qdisc noqueue 0: parent 8001:2
qdisc noqueue 0: parent 8001:1

Итак, теперь мы знаем, что это возможно, но мы также знаем, что это не совсем то, для чего он был разработан. Я хотел лучше понять, было ли это чем-то, что вы, вероятно , не должны делать, или это была очень, очень плохая идея. В качестве первого шага я взглянул на код, чтобы увидеть, что на самом деле происходит, когда вы используете noqueue. Внутри noqueue_initфункции значение enqueueфункции устанавливается равным NULL. По сути, это то, как noqueue идентифицирует себя, и это вступает в игру позже, когда enqueueтестируется в __dev_queue_xmit. Если enqueueимеет значение, то используется стандартный путь qdisc (тяжелая спин-блокировка). Если enqueueравно NULL, то выбирается путь без очереди, и перед вызовом dev_hard_start_xmitдля отправки пакета на устройство устанавливается только одна блокировка. Мы должны иметь в виду, что даже без qdisc сетевой интерфейс по-прежнему имеет свою собственную очередь передачи (обычно кольцевой буфер), где исходящие данные сохраняются в ожидании, когда сетевая карта заберет их и отправит. Простые qdisc, такие как pfifo_fast , служат дополнительным буфером, который может ставить данные в очередь, если устройство занято. Более сложные qdisc, такие как sfq, активно планируют передачу пакетов на основе потоков, чтобы обеспечить справедливость. Удаление qdisc из микса означает, что мы должны полагаться только на очередь передачи. Это поднимает два вопроса: (1) Рискуем ли мы заполнить очередь передачи? (2) Что произойдет, если мы это сделаем? Я был уверен, что не заполнил очередь передачи для этого конкретного теста. Несмотря на то, что каждую секунду отправляется более миллиона ответов, (а) всего 256 подключений, (б) каждый ответ умещается в одном пакете и (в) это синхронный эталонный запрос/ответ. Это означает, что в любой момент времени может быть не более 256 исходящих пакетов, ожидающих отправки. Кроме того, каждая из наших 4 пар ЦП/очередь работает независимо, поэтому в каждой очереди должно быть максимум 64 пакета, ожидающих отправки. Емкость очереди передачи драйвера ENA составляет 1024 записи, так что запаса более чем достаточно. Я хотел подтвердить, что мои ожидания совпали с реальностью, но размер очереди передачи не отслеживается ОС или драйвером ENA. Я создал скрипт bpftrace для записи длины очереди передачи устройства при каждом ena_com_prepare_txвызове. Я запустил скрипт bpftrace с параметром qdisc, установленным на pfifo_fast, а затем noqueue. Результаты выглядят почти одинаково, и в обоих случаях длина очереди редко превышает 64. Вот результаты для одной очереди:

pfifo_fast

Код:
@txq[4593, 1]:
[0, 8)            313971 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@                       |
[8, 16)           558405 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[16, 24)          516077 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@    |
[24, 32)          382012 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@                 |
[32, 40)          301520 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@                        |
[40, 48)          281715 |@@@@@@@@@@@@@@@@@@@@@@@@@@                          |
[48, 56)          137744 |@@@@@@@@@@@@                                        |
[56, 64)            9669 |                                                    |
[65, ...)             37 |                                                    |


noqueue

Код:
@txq[4593, 1]:
[0, 8)            338451 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@                     |
[8, 16)           564032 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[16, 24)          514819 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@     |
[24, 32)          380872 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@                 |
[32, 40)          300524 |@@@@@@@@@@@@@@@@@@@@@@@@@@@                         |
[40, 48)          281562 |@@@@@@@@@@@@@@@@@@@@@@@@@                           |
[48, 56)          141918 |@@@@@@@@@@@@@                                       |
[56, 64)           11999 |@                                                   |
[65, ...)            112 |


Таким образом, мы подтвердили, что в нашем конкретном тесте нет риска переполнения очереди передачи, но я все же хотел знать, что произойдет, если она переполнится. В соответствии с этим документом реализация ndo_start_xmitметода в сетевом драйвере должна возвращаться NETDEV_TX_OKв случае успеха и NETDEV_TX_BUSYв случае неудачи. Одним из примеров сбоя, который вызовет NETDEV_TX_BUSYответ, является переполнение очереди передачи. При использовании обычного qdisc, такого как pfifo_fast, этот ответ обрабатывается путем повторного помещения исходящих данных в очередь qdisc . При использовании noqueue возникает ошибка, и пакет отбрасывается. Вместо того, чтобы полагаться на эту теорию, я решил посмотреть исходный код драйвера ENA, чтобы увидеть, что происходит на самом деле. То, что я обнаружил, немного удивило. ena_start_xmit()на самом деле обрабатывает ошибки передачи, отбрасывая пакет и возвращая NETDEV_TX_OKвместо NETDEV_TX_BUSY. Если вы посмотрите на ena_com_prepare_txфункцию, там есть проверка , заполнена ли очередь передачи . Если он заполнен, ненулевой код ответа передается обратно вверх по цепочке , заставляя драйвер отключить DMA, отбросить пакет и вернутьсяNETDEV_TX_OK .NETDEV_TX_BUSYникогда не возвращается в этом сценарии, что означает, что ни pfifo_fast, ни noqueue не будут вызываться для обработки обратного давления. Протоколы более высокого уровня, такие как TCP, должны заметить, что пакет так и не достиг пункта назначения, и отправить его повторно. Я хотел запустить тест, чтобы подтвердить ожидаемое поведение, поэтому я использовал iPerf для передачи как можно большего количества пакетов с тестового сервера на другой экземпляр: iperf3 -c 10.XXX.XXX.XXX -P 10 -t 5 -M 88 -p 5200. iPerf отправляет пакеты асинхронно, что резко увеличивает вероятность заполнения очереди на передачу. Я изменил свой сценарий bpftrace и убедился, что по крайней мере в нескольких случаях очередь передачи сетевой карты была заполнена на полную мощность (1024 записи) во время тестового запуска. Когда я побежал dmesgподтверждать, что не было никаких непредвиденных ошибок или предупреждений от ядра, я с удивлением обнаружил вот это: Virtual device eth0 asks to queue packet!. Оказывается, мой предыдущий анализ кода драйвера ENA был неполным. Немедленно после того , как новый пакет успешно добавлен в очередь на передачу, выполняется второй тест, чтобы увидеть, заполнена ли очередь в этот момент , и если это так, очередь останавливается , чтобы можно было очистить отставание. Это означает, что следующий исходящий пакет может столкнуться с остановленной очередью . Когда нет qdisc, на который можно было бы вернуться, ядро просто регистрирует предупреждение и отбрасывает пакет, оставляя повторные попытки для протоколов более высокого уровня.
Чтобы представить ситуацию в перспективе, я смог смоделировать этот сценарий «полной очереди» только с довольно экстремальным синтетическим тестом, и даже тогда я получил только около 500-1000 отброшенных пакетов в тесте iPerf, где было передано около 9 миллионов пакетов. . Тем не менее, с noqueue могут быть и другие краеугольные случаи, о которых я не знаю, и это определенно не то, для чего он был разработан. Еще раз, пожалуйста, не пробуйте это в продакшене.

Результат

Переход на noqueue дает прирост производительности чуть более чем на 2% . Пропускная способность увеличивается с 1,12 млн запросов/с до 1,15 млн запросов/с .

Код:
Running 10s test @ http://server.tfb:8080/json
  16 threads and 256 connections
  Thread Stats   Avg     Stdev       Max       Min   +/- Stdev
    Latency   213.19us   25.10us  801.00us   77.00us   68.52%
    Req/Sec    72.56k   744.45     74.36k    70.37k    65.09%
  Latency Distribution
  50.00%  212.00us
  90.00%  246.00us
  99.00%  276.00us
  99.99%  338.00us
  11551707 requests in 10.00s, 1.57GB read
Requests/sec: 1155162.15
Transfer/sec:    160.84MB

Анализ Flame Graph
Функция _raw_spin_lockна вершине sendto стека системных вызовов наконец-то побеждена. Небольшой _raw_spin_lockвызов показан выше __dev_queue_xmit, но он весит 0,3% по сравнению с чудовищными 5,4% своего предшественника.

1661232470089.png

The White Whale Rises

Хотя я был рад избавиться от _raw_spin_lock, я все еще не был готов к всеобщему празднованию. Что-то было не так. Несмотря на то, что процентные показатели Flame Graph не связаны с приростом производительности 1:1, я ожидал, что после такого значительного изменения производительность увеличится более чем на 2%. Дальнейшее изучение графика пламени выявило неожиданный поворот. Из ниоткуда tcp_event_new_data_sentфункция выросла с 1,1% до 5,1% флейм-графика. Как только я подумал, что победил его, мой белый кит вновь появился в новой форме.
После того, как я преодолел свое первоначальное смятение и недоверие, я начал атаковать tcp_event_new_data_sentс новой силой. я прочитал через исходный код , но ничего не бросилось мне в глаза. Я проанализировал его с помощью bpftrace, подтвердив, что функция вызывается одинаковое количество раз и при одинаковых условиях. Попытки анализа таймингов не увенчались успехом, поскольку накладные расходы на анализ значительно превышали время выполнения самой функции. В какой-то момент я подумал, не было ли это просто аномальным артефактом того, как perf производит выборку данных. Я уже использовал рекомендуемую частоту 99 Гц для сэмплирования, но решил на всякий случай переключить ее. Я пробовал 49 Гц, 173 Гц и 263 Гц. Не было никаких изменений.
Затем я задался вопросом, может ли проблема быть связана с гиперпоточностью, поэтому я изменил таблицу косвенных адресов, чтобы отправлять данные только в очередь/процессор 0 и очередь/процессор 1 (процессоры 0 и 1 сопоставляются с разными физическими ядрами в этом экземпляре). К моему удивлению, это сработало, tcp_event_new_data_sentсжавшись до чуть более 1% графика. К моему ужасу, на его месте появился новый зверь, release_sockподскочивший с менее чем 1% до почти 7% флейм-графика . Не обращая внимания на все предупреждающие знаки, я продолжил. Я решил, что мне нужно протестировать его с отключенной гиперпоточностью на самом деле , поэтому я использовал параметры ЦП , чтобы запустить экземпляр с 1 потоком на ядро. Я снова провел тесты и создал новый график пламени. tcp_event_new_data_sentвсе было в норме, release_sockбыло в норме, но теперь по какой-то причинеtcp_schedule_loss_probeсоставил 11% от Flame Graph. Какая!?!?
Это превращалось в причудливую греческую трагедию, где я был обречен на вечность игры против китоголовой гидры. В этот момент я решил, что вселенная пытается преподать мне урок. Иногда вам нужно остановиться и оценить все, что у вас уже есть, а не зацикливаться на 5% вещей, которых у вас нет. Поэтому я решил вернуться к завершению этого поста в блоге, а тему «ударь по гидре» оставил на другой день/пост.

Ты хоть представляешь, что, черт возьми, здесь происходит, дорогой читатель? Если да то может своим комментарием ты отрубишь еще одну Гидро-Голову!

9. This Goes to Twelve

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

Disabling Generic Receive Offload (GRO)
GRO — это сетевая функция, предназначенная для оппортунистического объединения входящих пакетов на уровне ядра. Собранные сегменты представляются пользовательскому приложению как единый блок данных. Идея состоит в том, что повторная сборка может выполняться более эффективно в ядре, что повышает общую производительность. Как правило, это параметр, который вы хотите оставить включенным, и он включен по умолчанию в Amazon Linux 2. Однако для нашего эталонного теста мы уже знаем, что все запросы и ответы легко помещаются в один пакет, поэтому нет необходимости в повторной сборке. . Отключение GRO устраняет накладные расходы, связанные с функцией, используемой для проверки необходимости повторной сборки: dev_gro_receive.
Код:
sudo ethtool -K eth0 gro off

Контроль перегрузки TCP

Linux поддерживает ряд подключаемых алгоритмов управления перегрузкой TCP . Каждый из них использует немного отличающуюся стратегию для оптимизации потока данных по сети. На высоком уровне алгоритмы пытаются замедлить работу при обнаружении перегрузки внешней сети и снова ускорить ее, когда перегрузка исчезнет. Это особенно важно для беспроводных сетей (Wi-Fi, мобильных, спутниковых), где производительность сильно варьируется. Это гораздо менее полезно в сети с малой задержкой и без перегрузок, такой как группа размещения кластера AWS, используемая в этом тесте.
Сначала я не стал задумываться о контроле заторов, но потом мне пришла в голову идея. Даже в среде, где перегрузка близка к нулю, алгоритм все равно должен следить за тем, что происходит. Вместо того, чтобы искать лучший алгоритм для адаптации к перегрузкам, возможно, мне следует искать алгоритм с наименьшими накладными расходами в среде без перегрузок.
Amazon Linux 2 поставляется с двумя предустановленными вариантами управления перегрузкой: reno и cube. Reno (он же NewReno) встроен в ядро; он используется, если другой модуль управления перегрузкой недоступен. Reno использует один из самых простых подходов, а многие другие алгоритмы просто предлагают дополнительную функциональность поверх reno. Cubic использует более сложный алгоритм, который лучше работает в более широком числе вариантов использования. Cubic — это алгоритм управления перегрузкой по умолчанию, используемый в Amazon Linux 2.
Если вы посмотрите на список tcp_congestion_ops для рено и кубического , станет ясно, насколько рено проще. Переход с кубического на рено приводит к небольшому, но постоянному увеличению производительности.
Код:
sudo sysctl net.ipv4.tcp_congestion_control=reno

Я также протестировал различные другие алгоритмы управления перегрузкой, включая vegas, highspeed и bbr. Это потребовало от меня сначала вручную загрузить соответствующие модули ядра, например modprobe tcp_bbr. Ни один из них не был быстрее, чем Reno для этого теста.

Модерация статических прерываний

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

Жесткое кодирование rx-usecs будет иметь довольно большое негативное влияние на время отклика для более легких рабочих нагрузок, таких как ping-тест. Это также оказывает негативное влияние, если вы отключите BPF или опрос занятости; кое-что, о чем следует знать, если вы решите поиграть с этим тестом. Обратите внимание, что эта оптимизация не включена в шаблон CloudFormation .
Код:
sudo ethtool -C eth0 adaptive-rx off
sudo ethtool -C eth0 rx-usecs 300
sudo ethtool -C eth0 tx-usecs 300

Результат

Эти последние оптимизации дают нам прирост производительности более чем на 4% . Пропускная способность увеличивается с 1,15 млн запросов/с до 1,2 млн запросов/с .
Код:
Running 10s test @ http://server.tfb:8080/json
  16 threads and 256 connections
  Thread Stats   Avg     Stdev       Max       Min   +/- Stdev
    Latency   204.24us   23.94us  626.00us   70.00us   68.70%
    Req/Sec    75.56k   587.59     77.05k    73.92k    66.22%
  Latency Distribution
  50.00%  203.00us
  90.00%  236.00us
  99.00%  265.00us
  99.99%  317.00us
  12031718 requests in 10.00s, 1.64GB read
Requests/sec: 1203164.22
Transfer/sec:    167.52MB

АнализFlame Graph

Здесь нет больших изменений. dev_gro_receiveпадает с 1,4% до 0,1% графика пламени. bictcp_acked(одна из функций, используемых кубическим алгоритмом) ранее составляла 0,3%, но теперь она полностью исчезла из Flame Graph.

final.svg


Вывод

На этом, дорогие друзья, мы достигли конечного пункта назначения. Это был долгий, извилистый и часто разочаровывающий путь, но в конце концов он того стоил. За это время я многому научился и очень доволен конечным результатом. Увеличение количества запросов в секунду на 436 % при одновременном снижении задержки p99 на 79 % — немалый подвиг, особенно для сервера, который и без того был довольно быстрым.

Первоначальные результаты
Код:
Running 10s test @ http://server.tfb:8080/json
  16 threads and 256 connections
  Thread Stats   Avg     Stdev       Max       Min   +/- Stdev
    Latency     1.14ms   58.95us    1.45ms    0.96ms   61.61%
    Req/Sec    14.09k   123.75     14.46k    13.81k    66.35%
  Latency Distribution
  50.00%    1.14ms
  90.00%    1.21ms
  99.00%    1.26ms
  99.99%    1.32ms
  2243551 requests in 10.00s, 331.64MB read
Requests/sec: 224353.73
Transfer/sec:     33.16MB

Окончательные результаты
Код:
Running 10s test @ http://server.tfb:8080/json
  16 threads and 256 connections
  Thread Stats   Avg     Stdev       Max       Min   +/- Stdev
    Latency   204.24us   23.94us  626.00us   70.00us   68.70%
    Req/Sec    75.56k   587.59     77.05k    73.92k    66.22%
  Latency Distribution
  50.00%  203.00us
  90.00%  236.00us
  99.00%  265.00us
  99.99%  317.00us
  12031718 requests in 10.00s, 1.64GB read
Requests/sec: 1203164.22
Transfer/sec:    167.52MB
 

Вложения

  • 1661229170701.png
    1661229170701.png
    38.2 КБ · Просмотры: 6


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх