В 3 раза больше соединений. Одно ядро. За кулисами исследования производительности Accept Engine от FlashProxy

Как FlashProxy использовала автономный AI-оптимизатор, чтобы обнаружить TCP accept engine, использующий в 3 раза меньше инструкций CPU, чем Go. С открытым исходным кодом и лицензией MIT.
Существует класс проблем производительности, которые проявляются только в масштабе, и это по-настоящему отрезвляет, когда они дают о себе знать. Вы не упираетесь в логику маршрутизации. Не в шифрование. Не в то, что ваше приложение должно делать по своей основной задаче. Вы упираетесь в инфраструктуру вокруг него. В трубопровод.
Это именно то, что произошло с нами.
Примерно при 40 000 соединений в секунду наш production-прокси на Go начал упираться в пределы CPU на пути accept. Виновник — стандартная модель Go для сетевых сервисов: одна goroutine на соединение, один syscall на операцию. Каждое короткоживущее соединение (проверка работоспособности, проба балансировщика нагрузки, небольшой редирект) обращается к ядру четыре раза: accept, read, write, close. При высокой интенсивности смены соединений эти четыре перехода перестают быть накладными расходами и становятся основной стоимостью. Код приложения почти бесплатен. Передача байт в ядро и из него — нет.
Решение, к которому все прибегают, — это io_uring, асинхронный интерфейс ввода-вывода Linux, который группирует операции ядра и резко снижает количество переходов через границу пользователь/ядро. Мы это знали. Более сложный вопрос был в том, как его настроить.
Почему мы не просто настроили это сами
io_uring имеет длинный список документированных оптимизаций: multishot accept, зарегистрированные файловые дескрипторы, DEFER_TASKRUN, completion chains, MSG_MORE coalescing. Проблема в том, что эти техники взаимодействуют друг с другом способами, которые действительно трудно предсказать. Одни комбинации усиливают эффект. Другие нейтрализуют друг друга. Некоторые регрессии остаются полностью невидимыми, пока вы не проведёте нагрузочное тестирование на реальном железе.
Разработчик с perf stat может проверить несколько комбинаций за день. Но реальное пространство поиска, охватывающее комбинации, порядок применения, значения параметров и взаимодействия версий ядра, значительно больше. Что важнее, человеческая интуиция здесь скорее мешает. Мы склонны держаться за теории, которые выглядят правильно на бумаге, даже когда данные говорят обратное.
Поэтому мы создали замкнутый цикл измерений и передали задачу поиска AI-агенту.
Как работал цикл исследования
Мы запустили две реализации сервера параллельно. Контрольная — сервер на Go, точно моделирующий путь accept нашего production-прокси: goroutine на соединение, SO_REUSEPORT fan-out. Он был заморожен на всё время эксперимента. Экспериментальная — C-сервер с использованием liburing, начиная с базового цикла accept io_uring с одиночным выстрелом. Только этот код можно было изменять.
Оба сервера выполняли одинаковый контракт: принять соединение, прочитать байты запроса, отправить фиксированный ответ HTTP 200, закрыть. Одинаковое оборудование, одинаковое ядро, одинаковая нагрузка. Единственной переменной был способ их обработки.
Claude Code запускался в режиме headless как оптимизатор. На каждой итерации он читал текущего чемпиона, полную историю предыдущих мутаций, сохранённую как git commits, базу знаний уроков, накопленных за все запуски, и базу данных профилировочных данных. Он формулировал единственную гипотезу и вносил своё изменение. Затем полностью передавал управление.
Отдельный bash-harness, недоступный для изменения AI, собирал мутацию, привязывал её к изолированному ядру CPU, запускал генератор нагрузки и оценивал результат. Формула оценки была:
score = 1 000 000 000/mean(инструкций на соединение)
Количество инструкций CPU — это точное аппаратное измерение. В отличие от показателей пропускной способности, оно не зависит от теплового троттлинга и вариаций тактовой частоты. Оно точно показывает, сколько работы выполняет CPU на одно соединение, что и определяет реальную ёмкость в масштабе.
Если мутация улучшала оценку более чем на 3 % по сравнению с текущим чемпионом, она повышалась до нового чемпиона. Если нет — удалялась с помощью git reset --hard, и цикл продолжался. AI формулировал гипотезы. Harness принимал каждое решение о сохранении или откате. Ни один не вмешивался в работу другого.
Чтобы оптимизатор не мог «обыграть» бенчмарк, каждый оцениваемый запуск проверялся: байты ответа должны быть строго верными, соединения должны завершаться от начала до конца, а доля отказов должна оставаться ниже 0,01 %. Любое нарушение давало нулевую оценку.
Что мы обнаружили
Оптимизатор работал два дня и сошёлся на шести изменениях, которые вместе объясняют весь разрыв в производительности.
Флаги кольца DEFER_TASKRUN и SINGLE_ISSUER. Они переносят completion task-work в собственный цикл событий рабочего потока, устраняя межпроцессорные пробуждения. Это был самый большой скачок за весь запуск — прямое снижение стоимости CPU ядра на операцию, а не трюк с пропускной способностью.
Зарегистрированные файловые дескрипторы. При использовании прямых дескрипторов принятые соединения хранятся в собственной таблице кольца, а не в таблице файловых дескрипторов процесса. Это исключает установку fd-таблицы при accept и поиск при каждой последующей операции. Оптимизатор протестировал несколько размеров таблицы и выяснил, что оптимальной конфигурацией является 4 096 записей.
Multishot accept. Вместо повторного взвода операции accept после каждого соединения вы взводите её один раз, и ядро автоматически публикует completion для каждого нового соединения. Это снизило количество вызовов io_uring_enter на соединение до 0,34.
Фрилист соединений для каждого рабочего потока. Предварительное выделение 128 объектов соединения на рабочий поток полностью устранило вызовы malloc на горячем пути. Измеренная доля инструкций CPU, приходящаяся на libc, снизилась с 1,32 % до 0,92 %.
Ответ с MSG_MORE и FIN fusion. Отправка ответа с флагом MSG_MORE удерживает его в очереди записи TCP, так что FIN соединения может «прицепиться» к нему, и оба уходят как один TCP-сегмент. Один NIC doorbell вместо двух. Оптимизатор обнаружил это, заметив, что низкоуровневая функция записи ядра потребляет вдвое больше ожидаемой доли инструкций, и отследил причину — ненужное разделение сегмента.
Пакетная обработка завершений с CQE_SKIP_SUCCESS. Пометка операций send и close так, чтобы они не генерировали completion-события при успехе, означает, что completion создают только accept и receive — примерно по два на соединение вместо четырёх. Один вызов submit-and-wait обслуживает множество соединений одновременно.
Отказ, который многому научил
В какой-то момент оптимизатор попытался связать операции receive, send и close в цепочку — технику, при которой каждая операция запускается автоматически, когда завершается предыдущая. Цель состояла в сокращении kernel round-trips, и это сработало: количество вызовов enter на соединение упало с 1,90 до 1,40.
Оценка упала на 34 %. Откат был немедленным.
Урок, занесённый в базу знаний: минимизация kernel entries — не тот рычаг. Стоимость сериализации цепочки превышает экономию от сокращения числа входов в ядро.
Это именно то, что ломает человеческую интуицию. Метрика, выглядевшая как узкое место, не была реальным узким местом. Инженер, вероятно, дольше отстаивал бы эту оптимизацию. Цикл измерил её, отклонил и двинулся дальше.
Где поиск остановился
После шести побед оптимизатор исследовал ещё около дюжины кандидатов. Все были откатаны — не потому что давали регрессию, а потому что шум измерений превышал порог повышения в 3 %. Находить было больше нечего.
При конфигурации чемпиона примерно 94 % оставшегося CPU принадлежит TCP-стеку ядра Linux. Около 1 % — код приложения. Около 4 % — внутренние механизмы. Пользовательского кода, поддающегося осмысленной оптимизации, больше не осталось. Исследование правильно определило нижний предел и остановилось на нём.
Результаты
На одном привязанном ядре, в условиях CPU-bound, с 512 фиксированными соединениями в полёте на loopback:
Go goroutine-per-connection: 83 250 инструкций на соединение
Стартовый базовый уровень vanilla io_uring: 59 931 инструкций на соединение
Accept engine от FlashProxy: 27 363 инструкций на соединение
Это в 3,04 раза меньше инструкций CPU на соединение, чем у Go, и в 2,19 раза меньше, чем у базового уровня io_uring. На одном насыщенном ядре это даёт примерно шестикратный прирост пропускной способности по соединениям по сравнению с моделью goroutine.
Это бенчмарки на loopback, разработанные для чистой изоляции стоимости CPU. Важны соотношения, а не абсолютные числа. Базовые техники (multishot accept, DEFER_TASKRUN, зарегистрированные дескрипторы) задокументированы в идиомах io_uring. Результатом исследования стало доказательство того, какие комбинации действительно работают вместе, а какие выглядят хорошо на бумаге, но обходятся дороже на практике.
Библиотека с открытым исходным кодом
Победившая конструкция теперь доступна как flashaccept — C-библиотека с открытым исходным кодом, которая объединяет все шесть оптимизаций за простым API. Вы передаёте ей порт и обработчик запросов — она автоматически запускает оптимизированный цикл accept io_uring для каждого ядра.
Требуются Linux и liburing 2.3 или новее. На более старых ядрах библиотека корректно деградирует. Основной сценарий использования — высокоинтенсивная смена короткоживущих соединений: проверки работоспособности, редиректоры, пробы балансировщика нагрузки, небольшие RPC-ответы. Полная исследовательская установка, включая базовый уровень, harness, конфигурацию оптимизатора и полную накопленную базу знаний, включена в репозиторий и полностью воспроизводима.
Лицензия MIT. Доступна прямо сейчас на github.com/thealonlevi/flashaccept.
Это исследование появилось непосредственно в процессе разработки прокси-инфраструктуры FlashProxy. Если вы запускаете Linux-сервис с высокоинтенсивной сменой соединений и путь accept является вашим узким местом — мы создали это именно для такой задачи. Если вы хотите расширить библиотеку или самостоятельно запустить исследовательскую установку, в репозитории есть всё необходимое.
FAQ
Что такое Flashaccept?
Flashaccept — это C-библиотека с открытым исходным кодом, созданная FlashProxy, которая обеспечивает высокопроизводительный TCP accept engine для Linux. Под капотом она использует io_uring и принимает соединения, задействуя в 3,04 раза меньше инструкций CPU, чем стандартный Go-сервер с моделью goroutine-per-connection. Распространяется под лицензией MIT и доступна на GitHub.
Для каких рабочих нагрузок это разработано?
Flashaccept разработан для высокоинтенсивной смены короткоживущих соединений, где цикл request-reply-close происходит с большим объёмом: эндпоинты проверки работоспособности, пробы балансировщика нагрузки, HTTP-редиректоры и небольшие RPC-ответы. В версии v1 он не предназначен для keep-alive-соединений или сессий с несколькими обменами.
Какая версия Linux и какая версия liburing мне нужна?
Вам нужен Linux с liburing версии 2.3 или новее, которая поставляется начиная с Ubuntu 24.04. Быстрый путь (multishot accept, прямые дескрипторы) требует ядра версии 5.19 или новее. На более старых ядрах библиотека корректно переключается на одиночный accept и обычные файловые дескрипторы.
Что такое io_uring и почему это важно для производительности прокси?
io_uring — это интерфейс ядра Linux, представленный в версии ядра 5.1, который позволяет приложениям асинхронно отправлять и получать операции ввода-вывода через кольцевые буферы в общей памяти, резко сокращая необходимое количество системных вызовов. Для прокси-инфраструктуры, обрабатывающей десятки тысяч короткоживущих соединений в секунду, стоимость многократного пересечения границы пользователь/ядро становится доминирующей статьёй расходов CPU. io_uring значительно снижает эту стоимость.
Это то, что FlashProxy использует в production?
Исследование появилось непосредственно в ходе нашей production-работы по масштабированию. Прокси-инфраструктура FlashProxy работает в более чем 190 странах и обрабатывает значительный объём соединений. Оптимизация пути accept была реальным инженерным требованием, а не исследовательским упражнением. flashaccept — это дистиллированный результат этой работы, открытый для использования всеми желающими.
Могу ли я использовать flashaccept уже сейчас?
Да. Это версия 1.0.1, распространяется под лицензией MIT и доступна через GitHub, vcpkg, Conan и Arch AUR. Библиотека протестирована под AddressSanitizer и UBSan на более чем 400 000 соединениях по всем четырём путям конфигурации.
Где можно узнать больше об инфраструктуре FlashProxy?
Вы можете прочитать о том, как мы строим и масштабируем прокси-инфраструктуру, в блоге FlashProxy или изучить нашу прокси-сеть напрямую.

