3 倍连接数。同样的核心。FlashProxy Accept 引擎性能研究背后的故事

FlashProxy 如何利用自主 AI 优化器发现了一个 TCP accept 引擎,其 CPU 指令数比 Go 少 3 倍。开源且采用 MIT 许可证。
存在一类性能问题,只有在规模达到一定程度时才会显现,当它出现时确实令人警醒。你的瓶颈不在路由逻辑上。不在加密上。不在应用程序应该做的任何事情上。你的瓶颈在基础设施上。在管道上。
这正是我们遭遇的情况。
在每秒约 40,000 个连接的规模下,我们的生产级 Go 代理开始在 accept 路径上触及 CPU 限制。罪魁祸首是 Go 网络服务的标准模型:每个连接一个 goroutine,每个操作一次系统调用。每个生命周期短的连接(健康检查、负载均衡器探测、小型重定向)都会接触内核四次:accept、read、write、close。在高连接变动时,这四次往返不再只是开销,而成为了整个成本。应用程序代码几乎是免费的。让字节进出内核才是代价。
大多数人采用的解决方案是 io_uring,Linux 的异步 I/O 接口,它可以批量处理内核操作,大幅减少跨越用户/内核边界的次数。我们知道这一点。更难的问题是如何调优它。
为什么我们没有自己调优它
io_uring 有一长串记录在案的优化技术:multishot accept、registered file descriptors、DEFER_TASKRUN、completion chains、MSG_MORE 合并。问题在于这些技术之间的相互作用方式确实难以预测。某些组合会相互增强。某些会相互抵消。某些性能退化完全不可见,直到你在真实硬件上进行实际负载基准测试。
一名开发者手握 perf stat 一天内可以测试少数几个组合。但真正的搜索空间,涵盖组合、顺序、参数值和内核版本交互,远大于此。更重要的是,人类的直觉在这里是一个障碍。我们倾向于坚持看起来在纸面上正确的理论,即使数据另有说明。
所以我们搭建了一个闭合测量循环,把搜索问题交给了一个 AI 智能体。
研究循环如何工作
我们并排运行了两个服务器实现。对照组是一个 Go 服务器,精确模拟我们生产代理的 accept 路径:每个连接一个 goroutine,SO_REUSEPORT 扇出。它在整个实验期间被冻结。处理组是使用 liburing 的 C 服务器,从一个基础的单次 io_uring accept 循环开始。这是唯一可以更改的代码。
两个服务器遵循同样的约定:接受连接、读取请求字节、写入固定的 HTTP 200 响应、关闭。同样的硬件、同样的内核、同样的负载。唯一的变量是它们如何处理。
Claude Code 作为优化器无头运行。每次迭代时,它读取当前的冠军、存储为 git 提交的完整历史变化、跨运行积累的知识库,以及性能分析数据库。它形成一个假设并进行编辑。然后完全移交。
一个独立的 bash 工具脚本(AI 无法触碰)编译变化、将其固定到隔离的 CPU 核心、运行负载生成器,并评分结果。评分公式为:
score = 1,000,000,000/mean(每个连接的指令数)
CPU 指令计数是精确的硬件测量。与吞吐量数字不同,它们不受热节流和时钟频率变化的影响。它们精确告诉你 CPU 每个连接做了多少工作,这正是决定规模下容量的因素。
如果一个变化相对于当前冠军的得分提高了超过 3%,它就会被晋升。如果没有,它会被用 git reset --hard 删除,循环继续。AI 编写假设。工具脚本进行每一个保留或回滚决策。两者互不干扰。
为了防止优化器作弊基准测试,每次评分运行都要验证:回复字节必须完全正确、连接必须端到端完成,失败率必须保持在 0.01% 以下。任何违反都得零分。
我们发现了什么
优化器运行了两天,收敛到六项变化,这些变化共同解释了整个性能差距。
DEFER_TASKRUN 和 SINGLE_ISSUER ring 标志。 这些将 completion 任务工作移入 worker 线程自己的事件循环,消除跨 CPU 唤醒。这是整个运行中单次最大的跳跃,直接降低了每个操作的内核 CPU 成本,不是吞吐量技巧。
Registered file descriptors。 使用直接描述符,被接受的连接活在 ring 自己的表中而非进程文件描述符表。这跳过了 accept 上的 fd 表安装和后续每个操作上的查找。优化器测试了多个表大小,发现 4,096 条目是最优配置。
Multishot accept。 与其在每个连接后重新武装 accept 操作,不如武装一次,内核为每个新连接自动发布一个 completion。这将每个连接的 io_uring_enter 调用减少到 0.34。
Per-worker 连接空闲列表。 每个 worker 预分配 128 个连接对象完全消除了热路径上的 malloc。测量的 libc 消耗的 CPU 指令份额从 1.32% 降至 0.92%。
MSG_MORE 回复和 FIN 融合。 使用 MSG_MORE 发送回复时会将其保留在 TCP 写队列中,以便连接的 FIN 可以搭载在上面,以单个 TCP 段的形式发送两者。一次 NIC 敲门而非两次。优化器通过注意到一个低级内核写函数消耗的指令数超过预期两倍,并将其追踪回不必要的段分割而发现了这一点。
批量 completion 和 CQE_SKIP_SUCCESS。 标记 send 和 close 操作使其在成功时不生成 completion 事件意味着只有 accept 和 receive 产生 completion,每个连接大约两个而非四个。一次 submit-and-wait 调用驱动许多连接同时进行。
教会最多东西的失败
在某一点,优化器试图将 receive、send 和 close 操作链接在一起,这是一个使每个操作在前一个完成时自动触发的技术。目标是减少内核往返,它确实有效:每个连接的 enter 调用从 1.90 降至 1.40。
得分下降了 34%。立即回滚。
进入知识库的教训:最小化内核条目不是杠杆。链序列化的成本超过了它节省的条目。
这正是打破人类直觉的那种结果。看起来像瓶颈的指标不是实际瓶颈。工程师可能会更长时间地为那个优化辩护。这个循环测量了它、拒绝了它、然后继续前进。
搜索停止的地方
在六个胜利之后,优化器探索了大约十几个更多的候选。全部被回滚,不是因为它们出现退化,而是因为测量噪音超过了 3% 晋升阈值。没有什么可以找的了。
在冠军配置下,剩余 CPU 的大约 94% 属于 Linux 内核的 TCP 栈。大约 1% 是应用程序代码。大约 4% 是利用内部机制。没有用户空间代码留下来有意义地优化。研究正确识别了下限并在那里停止。
结果
在一个固定的核心上,CPU 限制,有 512 个固定的环回上的在途连接:
Go goroutine-per-connection:每个连接 83,250 条指令
Vanilla io_uring 起始基线:每个连接 59,931 条指令
FlashProxy 的 accept 引擎:每个连接 27,363 条指令
这是 比 Go 少 3.04 倍的每连接 CPU 指令数,以及 比 io_uring 基线少 2.19 倍。在单个饱和核心上,这相当于与 goroutine 模型相比大约六倍的连接吞吐量。
这些是环回基准,设计用于精确隔离 CPU 成本。比率很重要,不是绝对数字。底层技术(multishot accept、DEFER_TASKRUN、registered descriptors)记录在 io_uring 习语中。研究产生的是哪些组合实际上可以一起工作,哪些在纸面上看起来不错但在实践中会消耗你成本的证明。
开源库
获胜的设计现在是 flashaccept,一个开源 C 库,在一个简单的 API 后面打包了全部六个优化。你给它一个端口和一个请求处理器。它自动为每个核心运行一个优化的 io_uring accept 循环。
它需要 Linux 和 liburing 2.3 或更新版本。在更旧的内核上它会优雅降级。预期的用例是高变动、短生命周期连接:健康检查、重定向器、负载均衡器探测、小型 RPC 响应。完整的研究套件,包括基线、工具脚本、优化器配置和全部累积的知识库,都包含在内且完全可复现。
MIT 许可证。现已在 github.com/thealonlevi/flashaccept 推出。
这项研究直接来自构建 FlashProxy 的 代理基础设施。如果你正在运行高变动的 Linux 服务,accept 路径是你的瓶颈,我们为正好这个问题构建了它。如果你想扩展它或自己运行研究套件,代码库有你需要的一切。
常见问题
Flashaccept 是什么?
Flashaccept 是 FlashProxy 构建的开源 C 库,为 Linux 提供高性能 TCP accept 引擎。它在底层使用 io_uring,接受连接的 CPU 指令数比标准 Go goroutine-per-connection 服务器少 3.04 倍。它采用 MIT 许可证,可在 GitHub 获得。
它为哪些工作负载设计?
Flashaccept 为高变动、短生命周期连接而构建,其中请求-回复-关闭循环以高容量发生:健康检查端点、负载均衡器探测、HTTP 重定向器和小型 RPC 响应。在 v1 中它不为保活连接或多交换会话设计。
我需要哪个 Linux 版本和 liburing 版本?
你需要 Linux 配合 liburing 版本 2.3 或更新,随 Ubuntu 24.04 及更新版本附带。快速路径(multishot accept、direct descriptors)需要内核 5.19 或更新。在更旧的内核上库会优雅降级到单次 accept 和常规文件描述符。
什么是 io_uring,为什么它对代理性能很重要?
io_uring 是在内核 5.1 中引入的 Linux 内核接口,允许应用程序使用共享内存环缓冲区异步提交和接收 I/O 操作,大幅减少所需的系统调用数。对于处理每秒数万个短生命周期连接的 代理基础设施,重复跨越用户/内核边界的成本成为主要的 CPU 开销。io_uring 显著降低了该成本。
flashaccept 是 FlashProxy 在生产中运行的吗?
研究直接来自我们的生产扩展工作。FlashProxy 的 代理基础设施 跨越 190 多个国家运行并处理大量连接容量。accept 路径优化是真实的工程需求,不是研究练习。flashaccept 是该工作的提炼结果,开源以便其他人可以使用。
我今天可以使用 flashaccept 吗?
可以。它是版本 1.0.1,MIT 许可证,通过 GitHub、vcpkg、Conan 和 Arch AUR 提供。该库已在 AddressSanitizer 和 UBSan 下测试,跨越 400,000 多个连接,跨越所有四个配置路径。
我在哪里可以了解更多关于 FlashProxy 基础设施的信息?
你可以在 FlashProxy 博客 上阅读更多关于我们如何构建和扩展代理基础设施的内容,或直接探索我们的 代理网络。

