FlashProxy Logo

FlashProxy

ПроксіТехнологіяНовиниПосібникиПосібникиОголошення

3× більше з'єднань. Те саме ядро. Дослідження продуктивності Accept Engine FlashProxy зсередини

3× більше з'єднань. Те саме ядро. Дослідження продуктивності Accept Engine FlashProxy зсередини

Як FlashProxy використав автономний AI-оптимізатор, щоб виявити TCP accept engine, який використовує в 3× менше інструкцій CPU, ніж Go. Open-source та ліцензований за MIT.

К
Команда FlashProxy
21 червня 2026 р.
9 хв читання

Існує клас проблем продуктивності, які проявляються лише на масштабі, — і це справжній урок смирення, коли вони дають про себе знати. Вузьке місце — не логіка маршрутизації. Не шифрування. Не те, що ваш додаток власне має робити. Вузьке місце — інфраструктура навколо нього. Сантехніка.

Саме це з нами й сталося.

На рівні приблизно 40 000 з'єднань на секунду наш production Go-проксі почав упиратися в ліміти CPU у accept path. Винуватцем була стандартна модель Go для мережевих сервісів: одна горутина на з'єднання, один системний виклик на операцію. Кожне короткочасне з'єднання (перевірка стану, проба балансувальника навантаження, невеликий редирект) звертається до ядра чотири рази: accept, read, write, close. При високій інтенсивності з'єднань ці чотири round-trip перестають бути накладними витратами й стають усією вартістю цілком. Код додатка майже безкоштовний. Передача байтів у ядро та з ядра — ні.

Виправлення, до якого всі вдаються, — це io_uring, асинхронний I/O-інтерфейс Linux, який групує операції ядра й різко зменшує кількість переходів між простором користувача та ядром. Ми це знали. Складнішим питанням було, як його налаштувати.

Чому ми не налаштували його самі

io_uring має довгий список задокументованих оптимізацій: multishot accept, зареєстровані файлові дескриптори, DEFER_TASKRUN, ланцюги completion, MSG_MORE coalescing. Проблема в тому, що ці техніки взаємодіють одна з одною способами, які справді важко передбачити. Деякі комбінації підсилюють одна одну. Деякі нівелюють одна одну. Деякі регресії залишаються цілком невидимими, доки ви не виміряєте продуктивність під реалістичним навантаженням на реальному обладнанні.

Розробник зі perf stat може протестувати кілька комбінацій за день. Але реальний простір пошуку, що охоплює комбінації, порядки, значення параметрів та взаємодії версій ядра, набагато більший. Що ще важливіше, людська інтуїція тут є слабким місцем. Ми схильні триматися теорій, які виглядають правильно на папері, навіть коли дані свідчать про інше.

Тому ми побудували замкнений вимірювальний цикл і передали задачу пошуку AI-агенту.

Як працював цикл досліджень

Ми розгорнули дві реалізації сервера, що працювали паралельно. Контрольною була Go-реалізація, яка точно відтворювала accept path нашого production-проксі: горутина на з'єднання, SO_REUSEPORT fan-out. Вона була заморожена на весь час експерименту. Дослідною була реалізація на C із liburing, що починалася з базового single-shot io_uring accept-циклу. Це був єдиний код, який міг змінюватися.

Обидва сервери виконували один контракт: прийняти з'єднання, прочитати байти запиту, надіслати фіксовану HTTP 200-відповідь, закрити. Те саме обладнання, те саме ядро, те саме навантаження. Єдиною змінною був спосіб обробки.

Claude Code запускався у headless-режимі як оптимізатор. На кожній ітерації він зчитував поточного чемпіона, повну історію попередніх мутацій, збережених як git-коміти, базу знань уроків, накопичених упродовж запусків, та базу даних профілювання. Він формував єдину гіпотезу й вносив свою правку. Після цього повністю передавав управління.

Окремий bash-harness, якого AI не міг торкатися, збирав мутацію, прив'язував її до ізольованого ядра CPU, запускав генератор навантаження та оцінював результат. Формула оцінювання:

score = 1 000 000 000/mean(instructions per connection)

Кількість інструкцій CPU — це точне апаратне вимірювання. На відміну від показників пропускної здатності, воно не залежить від теплового дроселювання та варіацій тактової частоти. Воно точно показує, скільки роботи CPU виконує на одне з'єднання, — а саме це й визначає реальну ємність при масштабуванні.

Якщо мутація покращувала оцінку більш ніж на 3 % порівняно з поточним чемпіоном, вона отримувала підвищення. Якщо ні — видалялася командою git reset --hard, і цикл продовжувався. AI писав гіпотези. Harness приймав кожне рішення — зберегти чи відкотити. Жоден не втручався в роботу іншого.

Щоб запобігти підгонці оптимізатора під benchmark, кожен оцінений запуск проходив валідацію: байти відповіді мали бути точно правильними, з'єднання мали завершуватися end-to-end, а рівень відмов мав залишатися нижче 0,01 %. Будь-яке порушення давало нульовий результат.

Що ми знайшли

Оптимізатор працював два дні й зійшовся на шести змінах, які разом пояснюють увесь розрив у продуктивності.

  • Прапори ring DEFER_TASKRUN та SINGLE_ISSUER. Вони переміщують completion task-work до власного циклу подій worker-потоку, усуваючи міжпроцесорні пробудження. Це був найбільший одиничний стрибок за весь запуск — пряме зменшення витрат CPU ядра на операцію, а не трюк із пропускною здатністю.

  • Зареєстровані файлові дескриптори. З прямими дескрипторами прийняті з'єднання зберігаються у власній таблиці ring, а не в таблиці файлових дескрипторів процесу. Це виключає встановлення fd-table під час accept і пошук при кожній подальшій операції. Оптимізатор протестував кілька розмірів таблиць і встановив, що 4 096 записів — оптимальна конфігурація.

  • Multishot accept. Замість того щоб переозброювати операцію accept після кожного з'єднання, ви озброюєте її один раз, і ядро автоматично публікує completion для кожного нового з'єднання. Це скоротило кількість викликів io_uring_enter на з'єднання до 0,34.

  • Per-worker connection freelist. Попереднє виділення 128 об'єктів з'єднання на worker повністю усунуло malloc на гарячому шляху. Виміряна частка інструкцій CPU, що припадає на libc, впала з 1,32 % до 0,92 %.

  • MSG_MORE-відповідь та FIN fusion. Надсилання відповіді з MSG_MORE утримує її в черзі запису TCP, щоб FIN з'єднання міг приєднатися до неї й обидва вирушили як один TCP-сегмент. Один NIC doorbell замість двох. Оптимізатор знайшов це, помітивши, що низькорівнева функція запису ядра споживала вдвічі більше інструкцій, ніж очікувалося, і відстеживши причину до зайвого розбиття сегментів.

  • Пакетні completion з CQE_SKIP_SUCCESS. Позначення операцій send та close так, щоб вони не генерували completion-подій при успіху, означає, що лише accept та receive породжують completion — приблизно два на з'єднання замість чотирьох. Один виклик submit-and-wait обслуговує багато з'єднань одночасно.

Провал, який навчив найбільшому

На певному етапі оптимізатор спробував з'єднати операції receive, send та close в ланцюг — техніка, яка змушує кожну запускатися автоматично після завершення попередньої. Метою було зменшити kernel round-trip, і це спрацювало: кількість викликів enter на з'єднання впала з 1,90 до 1,40.

Оцінка впала на 34 %. Негайно відкочено.

Урок, який потрапив до бази знань: мінімізація кількості входів у ядро — не той важіль. Серіалізація ланцюга коштує більше, ніж заощаджені входи.

Це саме той тип результату, який руйнує людську інтуїцію. Метрика, яка виглядала як вузьке місце, не була справжнім вузьким місцем. Інженер, імовірно, довше захищав би цю оптимізацію. Цикл виміряв її, відхилив і рухався далі.

Де пошук зупинився

Після шести перемог оптимізатор дослідив ще приблизно дюжину кандидатів. Усі були відкочені — не тому, що вони спричиняли регресію, а тому, що шум вимірювання перевищував поріг підвищення у 3 %. Більше нічого не було знайти.

У конфігурації чемпіона приблизно 94 % решти CPU належить стеку TCP ядра Linux. Приблизно 1 % — код додатка. Приблизно 4 % — внутрішні механізми. Коду у просторі користувача, який можна було б суттєво оптимізувати, не залишилося. Дослідження правильно визначило нижню межу й зупинилося на ній.

Результати

На одному закріпленому ядрі, CPU-bound, із 512 фіксованими in-flight з'єднаннями на loopback:

  • Go горутина-per-connection: 83 250 інструкцій на з'єднання

  • Базова лінія Vanilla io_uring: 59 931 інструкцій на з'єднання

  • Accept engine FlashProxy: 27 363 інструкцій на з'єднання

Це у 3,04× менше інструкцій CPU на з'єднання, ніж у Go, та у 2,19× менше, ніж у baseline io_uring. На одному насиченому ядрі це дає приблизно вшестеро більшу пропускну здатність за кількістю з'єднань порівняно з моделлю горутин.

Це loopback-benchmark, розроблені для чистої ізоляції витрат CPU. Важливі співвідношення, а не абсолютні числа. Базові техніки (multishot accept, DEFER_TASKRUN, зареєстровані дескриптори) задокументовані в ідіомах io_uring. Те, що дало дослідження, — це доказ того, які комбінації справді працюють разом, а які виглядають добре на папері, але коштують вам на практиці.

Бібліотека Open Source

Переможна конструкція тепер доступна як flashaccept, open-source C-бібліотека, яка упаковує всі шість оптимізацій за простим API. Ви передаєте їй порт і обробник запиту. Вона автоматично запускає оптимізований io_uring accept-цикл на кожне ядро.

Вимагає Linux та liburing 2,3 або новіше. На старіших ядрах деградує плавно. Цільовий сценарій використання — high-churn, короткочасні з'єднання: перевірки стану, редиректори, проби балансувальника навантаження, невеликі RPC-відповіді. Повна дослідницька установка, включно з baseline, harness, конфігурацією оптимізатора та повною накопиченою базою знань, входить до складу й повністю відтворюється.

Ліцензовано за MIT. Доступно зараз на github.com/thealonlevi/flashaccept.

Це дослідження виникло безпосередньо з роботи над побудовою проксі-інфраструктури FlashProxy. Якщо ви керуєте high-churn Linux-сервісом і accept path є вашим вузьким місцем — ми будували це саме для такої проблеми. Якщо ви хочете розширити його або самостійно запустити дослідницьку установку, у репозиторії є все необхідне.

FAQ

Що таке Flashaccept?

Flashaccept — це open-source C-бібліотека, створена FlashProxy, яка забезпечує високопродуктивний TCP accept engine для Linux. Вона використовує io_uring під капотом і приймає з'єднання, використовуючи у 3,04× менше інструкцій CPU, ніж стандартний Go-сервер із горутиною на з'єднання. Ліцензована за MIT та доступна на GitHub.

Для яких робочих навантажень вона розроблена?

Flashaccept створена для high-churn, короткочасних з'єднань, де цикл request-reply-close відбувається у великому обсязі: точки перевірки стану, проби балансувальника навантаження, HTTP-редиректори та невеликі RPC-відповіді. У версії v1 вона не розрахована на keep-alive-з'єднання або багатоетапні сеанси обміну.

Яка версія Linux та liburing мені потрібна?

Вам потрібен Linux із liburing версії 2,3 або новіше, яка постачається з Ubuntu 24,04 та новіше. Швидкий шлях (multishot accept, прямі дескриптори) вимагає ядро 5,19 або новіше. На старіших ядрах бібліотека плавно повертається до single-shot accept та звичайних файлових дескрипторів.

Що таке io_uring і чому це важливо для продуктивності проксі?

io_uring — це інтерфейс ядра Linux, введений у ядрі 5,1, який дозволяє додаткам асинхронно надсилати та отримувати I/O-операції через спільні кільцеві буфери пам'яті, різко зменшуючи необхідну кількість системних викликів. Для проксі-інфраструктури, що обробляє десятки тисяч короткочасних з'єднань на секунду, витрати на повторне перетинання межі користувач/ядро стають домінуючими витратами CPU. io_uring суттєво скорочує ці витрати.

Чи є flashaccept тим, що FlashProxy використовує у production?

Дослідження виникло безпосередньо з нашої роботи з масштабування у production. Проксі-інфраструктура FlashProxy працює в 190+ країнах і обробляє значний обсяг з'єднань. Оптимізація accept path була реальною інженерною вимогою, а не дослідницькою вправою. flashaccept — це дистильований результат цієї роботи, відкритий для використання іншими.

Чи можу я використовувати flashaccept вже сьогодні?

Так. Це версія 1,0.1, ліцензована за MIT, доступна через GitHub, vcpkg, Conan та Arch AUR. Бібліотека протестована під AddressSanitizer та UBSan на 400 000+ з'єднаннях по всіх чотирьох шляхах конфігурації.

Де я можу дізнатися більше про інфраструктуру FlashProxy?

Ви можете прочитати більше про те, як ми будуємо та масштабуємо проксі-інфраструктуру, у блозі FlashProxy або безпосередньо дослідити нашу мережу проксі.


flashacceptFlashProxyсервер проксі для підвищення продуктивностіпідвищення продуктивності серверапідвищення продуктивності сервера проксі