From 83de4ed398f9558feeb2f41acc0f96ac3cea8fc5 Mon Sep 17 00:00:00 2001 From: sercebr Date: Sun, 20 Sep 2026 22:07:20 +0800 Subject: [PATCH 1/4] =?UTF-8?q?docs(benchmark):=20=E5=8A=A0=E4=B8=80?= =?UTF-8?q?=E9=A1=B5=E5=AE=9E=E6=B5=8B=20=E2=80=94=E2=80=94=20=E6=AF=8F?= =?UTF-8?q?=E8=BF=9E=E6=8E=A5=204.62=20KB=20/=20=E5=8D=95=E6=A0=B8=2075=20?= =?UTF-8?q?k=20RPS=EF=BC=8C=E5=B9=B6=E4=B8=8E=20Hical=20=E5=AF=B9=E7=85=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在 1.1.33(`534e4ed`)上用靶场实测的一组读数,落成 `doc/benchmark.md`: - 每连接内存 **4.62 KB**(八档 0–54 852 条连接,最小二乘 R² = 0.999987) → 外推 100 万连接 ≈ 4.42 GiB - 单事件循环 75 791 RPS(流水线 10–50 深到 88 134);静态 44 379 RPS / 107 MB/s - 10 分钟长跑 38 404 681 请求 0 错误,RSS 漂移 0.2% 源起是回答「百万空闲长连接吃多少内存」。真造不满百万(54 852 条即 `ENOBUFS`, 端口/内核墙),所以是外推 —— 判据落在 **R² 与每点残差**上,而不是"斜率好看"。 ## 对照 对照对象是 **Hical**(`Hical61/Hical`,同为 Asio 系、同样面向百万长连接),取它自己 `docs/performance_report.md` §2.8/§2.9 的自报数据:**~17.44 KB(优化前)→ ~8 KB** (v2.6.5;v2.6.6 另 +256 B 协程帧栈)。据此 4.62 KB 分别是 **3.8×** 与 **1.7×**。 两处口径问题在文中写明,不选对本库有利的一边: - Hical 的 ~8 KB **是否含它自己提到的「内核侧 TCP 缓冲 ~4 KB/连接」没有写清**, 而本文的 4.62 KB 只含用户态。若含,则同口径下两者接近持平(差别是本库机制里 压根没有常驻读缓冲,它需要一套 `ReadBufferPool` 借还来达到同一格)。 优化前那一格(17.44 KB)没有这个歧义。 - 单位口径不同(它十进 GB,本文 GiB),按各自原文引用,没有替对方换算。 ## 「读数对后续版本是否成立」单列一节 `534e4ed..d46b6eb` 之间 **`src/` 只动了三个文件** —— 版本号,以及压缩变体表的字节账 (`d56c2a4`)。后者只在压缩缓存的冷路径上(变体表超上限时淘汰谁),不在本文测量的 连接内存/吞吐/长跑路径上。所以 **1.1.33 – 1.1.35-dev 这条线共用这组读数**。 README 与 README.zh.md 各加一行文档索引,并在「性能」小节指向这一页。 --- README.md | 7 ++ README.zh.md | 6 ++ doc/benchmark.md | 234 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 247 insertions(+) create mode 100644 doc/benchmark.md diff --git a/README.md b/README.md index 600f2428..137e1119 100644 --- a/README.md +++ b/README.md @@ -424,6 +424,7 @@ libuvcpp/ │ └── expand/ # Memory pool tests ├── examples/ # Runnable examples (webapp_demo) ├── doc/ # Documentation +│ ├── benchmark.md # Measured per-connection memory, throughput, stability │ ├── ci-guide.md # CI maintenance guidelines │ └── webapp-guide.md # Web app framework guide ├── cmake/ # CMake config templates @@ -508,6 +509,12 @@ contributor, [@sercebr](https://github.com/sercebr). blocks (`nbufs = 2`) (`1.1.28`); the variant table stores and returns handles instead of whole bodies (`1.1.31`) +**Measured, not estimated** — see [doc/benchmark.md](doc/benchmark.md): **4.62 KB per idle +connection** (least squares over eight tiers, R² = 0.999987 → 1 M connections ≈ 4.42 GiB), +75 k RPS on a single event loop, and a 10-minute soak at 38 M requests / 0 errors. That page +also compares the per-connection figure against [Hical](https://github.com/Hical61/Hical)'s +own report, and states the caveats that comparison carries. + ### Configuration, packaging & CI - Enable macros ship with the package, and a macro set that disagrees with the DLL is now a diff --git a/README.zh.md b/README.zh.md index 43acd5a4..e6faddd1 100644 --- a/README.zh.md +++ b/README.zh.md @@ -443,6 +443,7 @@ libuvcpp/ │ └── expand/ # 内存池测试 ├── examples/ # 可运行示例(webapp_demo) ├── doc/ # 文档 +│ ├── benchmark.md # 每连接内存、吞吐与稳定性实测 │ ├── ci-guide.md # CI 维护指南 │ └── webapp-guide.md # web 应用框架指南 ├── cmake/ # CMake 配置模板 @@ -518,6 +519,11 @@ libuvcpp/ - 响应体零拷贝接管,并与头一起作为两块写出去(`nbufs = 2`)(`1.1.28`); 变体表按句柄存/取,不再整份拷体(`1.1.31`) +**实测数据(不是估算)** —— 见 [doc/benchmark.md](doc/benchmark.md):每条空闲连接 +**4.62 KB**(八档最小二乘,R² = 0.999987,外推 100 万连接 ≈ 4.42 GiB)、单事件循环 +75 k RPS、10 分钟长跑 3 840 万请求 0 错误。那一页还把每连接这个数与 +[Hical](https://github.com/Hical61/Hical) 自己的报告做了对照,并写明了该对照带的口径问题。 + ### 配置、打包与 CI - 使能宏随包发出;宏集与 DLL 不一致从静默垃圾值改为编译期硬失败(`1.1.27`) diff --git a/doc/benchmark.md b/doc/benchmark.md new file mode 100644 index 00000000..a483e3cc --- /dev/null +++ b/doc/benchmark.md @@ -0,0 +1,234 @@ +# libuvcpp 每连接内存与吞吐实测 + +这一页是一次**可复现的靶场实测记录**,被测对象是**用本库构建出来的一个真实 HTTP 服务**, +不是微基准,也不是估算值。写它的直接动机是回答一个具体问题: + +> 一台机器上一百万条空闲长连接,要吃掉多少内存? + +| 项 | 值 | +|---|---| +| **测量版本** | **1.1.33**(`534e4ed`) | +| 撰写时 `master` | `1.1.35-dev`(`d46b6eb`) | +| 被测服务 | 靶场:静态文件 + 动态路由 + 上传下载 + WebSocket + TLS 的完整 webapp | +| 平台 | Windows 10 Pro 1903(18363)/ MSVC x64 / Release | +| 运行时 | 单事件循环线程(下文另注明) | + +**三个读数**(方法、分档与原始数据见下文各节): + +- **每连接内存 4.62 KB**(最小二乘,**R² = 0.999987**)⇒ 外推 100 万连接 ≈ **4.42 GiB** +- 单核动态路由 **75 k RPS**(流水线 10–50 深可到 **88 k**);静态 **44 k RPS / 107 MB/s** +- 10 分钟长跑 **3 840 万请求、0 错误**,RSS 漂移 **0.2%** + +--- + +## 1. 为什么这是「1.1.33 的读数」,以及它为什么对后面几版仍然成立 + +测量发生在 `534e4ed`(1.1.33)。写明这件事是**必要条件**:一个不带 commit 的读数值 +不是结论,是传闻。 + +`534e4ed` 之后到本文撰写时的 `d46b6eb`(1.1.35-dev)之间,**`src/` 下只动了三个文件**: + +``` +src/uvcpp/uvcpp_version.h | 2 +- ← 版本号 +src/web/uvcpp_http_server.cpp | 31 +++++----- ← 压缩变体表的字节账 +src/web/uvcpp_http_server.h | 10 ++++++- +``` + +其余全是文档、CI、测试与发布打包。逐个说清它们为什么不在本文的测量路径上: + +| commit | 动的东西 | 在本文测量的路径上吗 | +|---|---|---| +| `4710479` | 版本号 1.1.33 → 1.1.34 | 不涉及 | +| `d56c2a4` | 压缩变体表的字节账:把会漂移的计数字段换成从容器派生的求和,并补上「字节上限」那条腿 | **不在** —— 见下 | +| `d46b6eb` | 发布包同时提供 Debug 档动态库与 PDB(`uvcppd.dll` / `libuvcppd.so`),只改打包器与 CI 判据 | 不涉及 Release 运行时 | + +`d56c2a4` 只影响「变体表超过上限时淘汰谁」,且只在**压缩缓存的冷路径**上;本文测的是连接 +内存、动态/静态吞吐、长跑稳定性,都不经过那条路径(本文这一版连压缩都没开)。 + +**因此:下面的读数对 1.1.33 – 1.1.35-dev 这条线成立**;版本号只升不改功能时(1.2.0 属此 +列)同样成立。真去动了上面那三条路径的话,这一页需要重测 —— 复现方法在第 8 节。 + +--- + +## 2. 方法:为什么是外推,而不是真的造满一百万 + +**真造不满。**本机在 **54 852** 条时开始出现 `ENOBUFS` —— 那是动态端口与内核缓冲到顶, +不是服务端拒绝。换平台也一样有这条墙,只是位置不同(对照项目在自己的 Linux 环境里撞到的 +是同一量级的另一个数)。所以百万级只能外推,而外推要立得住,就必须有判据。 + +做法是**多档位建立连接 + 最小二乘回归 + R²**:每档记「建立 N 条连接后的进程 RSS」,对 N +做一次线性拟合,斜率就是每连接内存,再外推到 1e6。判据不是"斜率好看",是**R² 必须接近 +1**,且每档残差要小 —— 否则那条直线根本不存在,外推就是编。 + +- 采样对象是**进程 RSS**(`WorkingSet64`),即**用户态**每连接开销;内核侧的 TCP 缓冲 + (每连接约 4 KB 量级)不在其中,也不受本库控制 —— 第 6 节比较时会回到这一点。 +- 每档建立连接后**逐条复检**该连接可服务(不是"连上就算")。 +- 关闭后复检句柄数回落,确认没有把连接泄漏成别的形状。 + +--- + +## 3. 结果一:每连接内存 + +八档采样(0 与 7 个非零档): + +| 目标并发 | 实际建立 | 连接失败 | 探测成功 | 关闭后 RSS / 句柄 | +|---:|---:|---:|---:|---| +| 8 000 | 8 000 | 0 | 8 000 | 18.2 MB / 184 | +| 16 000 | 16 000 | 0 | 16 000 | 25.5 MB / 184 | +| 24 000 | 24 000 | 0 | 24 000 | 32.8 MB / 184 | +| 32 000 | 32 000 | 0 | 32 000 | 40.4 MB / 184 | +| 40 000 | 40 000 | 0 | 40 000 | 47.3 MB / 184 | +| 48 000 | 48 000 | 0 | 48 000 | 55.3 MB / 184 | +| 55 000 | **54 852** | 148(`ENOBUFS`) | 全部 54 852 | 61.9 MB / **184** | + +- **零连接失败**直到 48 000。 +- 每一档都逐条复检过 8 000 – 54 852 条连接**全部可服务**。 +- 全部关闭后句柄回落到 **184**(与基线一致,无泄漏)。 + +拟合(8 个点): + +``` +RSS(MB) = 0.0045145 × N + 9.95 R² = 0.999987 + = 4.62 KB/连接 × N + 9.95 MB +``` + +- 除截距点外,**每点残差 ≤ 0.51%**。 +- 截距 9.95 MB 是进程的固定开销(静态缓存、会话表、已初始化的协议层等),不是每连接的。 +- **外推 100 万连接 ≈ 4.41 GiB + 0.01 GiB ≈ 4.42 GiB。** + +**为什么能这么低**:本库**没有每连接常驻的读缓冲**。读是 +`read_start([](uvcpp_buf* b){ … })` 的按需形状 —— 有数据才给缓冲,空闲连接上不预置任何 +一块固定大小的读缓冲。这一条是第 6 节那个对照的全部原因。 + +--- + +## 4. 结果二:吞吐与延迟 + +动态路由(内存型响应),单事件循环: + +| 用例 | 并发 | 流水线 | RPS | p50 | p99 | max | 错误/超时/503 | +|---|---:|---:|---:|---:|---:|---:|---:| +| json | 1 | 1 | 44 946 | 0 ms | 0 ms | 1 ms | 0 | +| json | 10 | 1 | **75 791** | 0 ms | 0 ms | 1 ms | 0 | +| json | 400 | 1 | 71 614 | 5 ms | 6 ms | 27 ms | 0 | +| json | 1 600 | 1 | 63 278 | 25 ms | 28 ms | 101 ms | 0 | +| json | 100 | **10** | **88 134** | 10 ms | 16 ms | 28 ms | 0 | +| json | 100 | **50** | 87 874 | 56 ms | 65 ms | 94 ms | 0 | + +静态文件(走工作线程池):`44 379 RPS / 107 MB/s`(并发 100)、`43 154 RPS / 104 MB/s` +(并发 400)。256 KB 大响应:`1 608 RPS / 402 MB/s`。 + +**要点**:到并发 1 600 仍零错误、零超时、零 503,延迟线性劣化但不掉请求;流水线 +(10–50 深)把单核推到 88 k RPS。 + +**静态服务没有并发悬崖**:1 → 200 并发共 12 档打静态文件,**每一档 503 占比都是 0%**, +成功 RPS 从 15 879 升到峰值 **45 044**(并发 12)后平在 43 563(并发 200)。作为对照, +同一台机器上纯内存路由是 44 501 → 73 480。 + +--- + +## 5. 结果三:稳定性 + +10 分钟持续压测(动态路由 + 连接 churn + WebSocket 混合): + +| 项 | 读数 | +|---|---| +| 总请求 | **38 404 681,0 错误** | +| RPS | 64 008 | +| 延迟 | p50 **1 ms** / p90 1 ms / p99 **2 ms** / max **14 ms** | +| RSS 漂移 | **0.2%**(90.3 → 90.5 MB) | +| 句柄 | 252 → 254 | +| 连接 churn | 9 810 次建连 / **0 失败** | +| WebSocket | **7 804 条会话、23 412 帧 / 0 失败**,回收计数与建连数完全对上 | + +另外单独做了**分段归因**(每段只做一件事,各自算 RSS 斜率):只发内存型请求 +(每请求新建连接)**−0.01 MB/min**、只建连/断开(3 757 次)**+0.01 MB/min**、只做 +WebSocket 建连/回显/断开(2 733 轮)**+0.07 MB/min**、完全不发请求 **0.00 MB/min**。 +四段全平 ⇒ 没有随时间累积的增长,长跑那条 0.2% 是分配器膨胀后收敛,不是斜坡。 + +> 这份归因第一次跑出来是 `19.52 MB/min` 的"持续增长"。真因不在被测对象:采样是按 +> **进程名**取第一条匹配,而当时机器上还跑着另外两台同名的被测进程,样本里不存 pid ⇒ +> 采样在进程之间跳了。证据是那条序列呈**单次 34 MB 台阶后永久平**(跳变,不是斜坡), +> 换成**按 pid 锚定**的采样后全程 9.47 MB 不动。写在这里是因为这类装置缺陷会让读数 +> 骗人,而它长得和真泄漏一模一样。 + +--- + +## 6. 与 Hical 的对照 + +对照对象是 **[Hical61/Hical](https://github.com/Hical61/Hical)** —— 一个 C++20/26 的 +Asio 框架,同样面向「百万空闲长连接」。它自己维护了一份性能报告(该仓库 +`docs/performance_report.md`),其中 **§2.8「空闲长连接内存优化」** 给出的正是同一量: +**每条空闲连接的内存**。所以两边不用在两套口径之间换算,可以直接比。 + +Hical 自己在 §2.8 里给的账(v2.6.4 → v2.6.5): + +| 优化项 | 优化前 | 优化后 | 每连接节省 | +|---|---|---|---| +| `readBuf` 借还 | 连接常驻 8 KB `std::string` | `ReadBufferPool` 按需借还,空闲时不持有 | ~7.5 KB | +| `inputBuffer_` 懒分配 | 连接建立即分配 `PmrBuffer`(~2 KB) | `std::optional`,第一次读才创建 | ~2 KB | +| **合计** | **~17.44 KB** | **~8 KB**(省约 55%) | | + +同表给出百万连接量级:**~16.6 GB → ~7–8 GB**。 +§2.9(v2.6.6)又加了一档:空闲连接 **+256 B**(协程帧栈),换来 `epoll_ctl` 调用数降 71.8%。 + +| 实现 | 每空闲连接 | 百万连接 | +|---|---|---| +| Hical v2.6.4(优化前) | ~17.44 KB | ~16.6 GB | +| Hical v2.6.5 / v2.6.6 | ~8 KB(v2.6.6 再 +256 B) | ~7–8 GB | +| **libuvcpp 1.1.33** | **4.62 KB** | **4.42 GiB** | + +- 对本库 vs Hical **优化前**:**轻约 3.8×**(每连接少约 12.8 KB)。 +- 对本库 vs Hical **优化后**:**轻约 1.7×**(4.62 KB vs ~8 KB)。 + +**⚠ 这个 1.7× 有一处口径不确定,写清楚免得被读成强结论。** Hical 在那句话里同时注明 +「内核侧 TCP 缓冲 ~4 KB/连接不可优化」。**本文的 4.62 KB 是进程 RSS 增量,只含用户态**; +如果 Hical 的 ~8 KB 里已经含了那 ~4 KB 内核缓冲,那么同一口径下它剩下的用户态部分约 +4 KB,两者**接近持平** —— 差别是本库的机制里压根没有常驻读缓冲,而它需要一套 +`ReadBufferPool` 借还来达到同一格。该文档没有把这个口径写明,所以两种读法都列在这里, +不单选对本库有利的那个。**优化前那一格(17.44 KB)没有这个歧义。** + +**其余可比性说明**: + +- 两边都是**外推**(都撞端口/内核墙),所以都是"斜率可信、截距不可跨机器比"。截距差异 + 明显(本机 9.95 MB,因为这台还带着静态缓存、会话表与已初始化的协议层)。**斜率才是 + 可比的量**,上表据此下结论。 +- **单位口径不同**:Hical 写的是十进 GB(~16.6 GB / ~7–8 GB),本文写 GiB(4.42 GiB + ≈ 4.75 GB)。上表按各自原文引用,没有替对方换算。 +- **平台不同**:Hical 的数据出自它自己文档所述环境(Linux VM),本文是 Windows / MSVC + / 环回。跨平台比较请带上这一条。 + +--- + +## 7. 这一页不是什么 + +- **不是跨平台的定论**,也不是跨项目的绝对排名 —— 见第 6 节最后三条。 +- **不是单核之外的成绩**。上面所有吞吐都是**一个事件循环线程**(静态那几条走工作池)。 + 要横向扩就起 N 个进程。别把 75 k 当成"每核 75 k 且不可再扩"。 +- **不是 TLS 读数**。TLS 的稳态代价另有一组(约 −23% ~ −30% RPS,256 KB 响应受 AES-GCM + 单核吞吐限制降到 119 MB/s);那一组的基线对照**归因未闭合**,所以没有放进这一页。 + +--- + +## 8. 复现方法 + +测量装置是一组独立脚本(Node + Python,跑在靶场进程之外),不随库发布。方法本身足够 +简单,按下面三步就能复刻: + +1. **建连**:分档建立 N 条 TCP 连接(N = 0 / 8k / 16k / 24k / 32k / 40k / 48k / 55 000), + 每条建好后发一个请求并读回响应 —— **逐条复检**,失败的连接不计入 N 且要单独报数。 +2. **采样**:每档稳定后读被测进程的 RSS(Windows 是 `WorkingSet64`,Linux 读 + `/proc//status` 的 `VmRSS`)。**按 pid 采样,不要按进程名** —— 见第 5 节那个坑。 +3. **拟合**:对 (N, RSS) 做最小二乘,报**斜率、截距、R²、每点残差**。R² 不接近 1 就说明 + 这条直线不成立,此时不要外推。 + +吞吐那一组用 `autocannon`(15 s/档,同机);长跑那组是 10 分钟的自建压测循环,同时统计 +错误、延迟分位、句柄数与 RSS 漂移。 + +--- + +## 9. 出处 + +- **libuvcpp 侧**的全部数字:本库在 `534e4ed`(1.1.33)上构建出的靶场,方法如上。 +- **Hical 侧**的数字:`Hical61/Hical` 仓库的 `docs/performance_report.md`,§2.8 / §2.9。 + 引用的是该项目的**自报**数据,本文没有在其平台上复跑过;其测试环境见该文档 §1。 From 2c6b6db3de07f88e1aa1bf537812cf159c87e81d Mon Sep 17 00:00:00 2001 From: sercebr Date: Sun, 20 Sep 2026 22:18:38 +0800 Subject: [PATCH 2/4] =?UTF-8?q?docs(benchmark):=20=E8=A1=A5=E4=B8=8A?= =?UTF-8?q?=E3=80=8C=E5=BB=BA=E7=AB=8B=E5=90=8E=20RSS=E3=80=8D=E4=B8=8E?= =?UTF-8?q?=E9=80=90=E7=82=B9=E6=AE=8B=E5=B7=AE=20=E2=80=94=E2=80=94=20?= =?UTF-8?q?=E8=A1=A8=E9=87=8C=E5=8E=9F=E6=9D=A5=E5=8D=B0=E7=9A=84=E6=98=AF?= =?UTF-8?q?=E3=80=8C=E5=85=B3=E9=97=AD=E5=90=8E=E3=80=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 评审指出:§2 说明拟合用的是「建立 N 条连接后的 RSS」,但 §3 那张表印的是 「关闭后 RSS / 句柄」—— 两个不同的量,所以拿表里那列验不出那条拟合式:对表里 那列**自己**回归得到 930 B/连接,斜率差 4.7 倍。这页把「判据是 R² 与每点残差」 立成脊椎,那这两样就得让人自己能算,否则"实测数据(不是估算)"这句在最关键的 一步上是不可验的。 改法: - §3 表里加 **「建立后 RSS」** 列(拟合真正用的那列,8 档全列,含原点) - 印出**逐点残差表**(建立后 RSS / 拟合预测 / 残差 / 残差%),让人能自己验 - 原点那档写明是**空转基线 9 859 072 B**(8 个点里的第 8 个),并交代它自己偏 −5.86% 是"这一次单点读数不落在拟合线上"(拟合截距 9.954 MiB 比它高 0.55 MiB), 不再用一句"除截距点外"带过 - 「关闭后」那列**留着**,但单列一小节说清它是**另一条线**(930 B/连接、 R² = 0.9997352、截距 10.120 MiB):价值在它的**截距离空转基线 9.402 MiB 只差 0.72 MiB**、且句柄一律回 184 —— 那是"连接被回收干净"的证据,不是斜率 顺带把单位写准:拟合的 y 是 MiB(0.0045143 MiB/N = 4 733.6 B = 4.6226 KiB), 所以"4.62 KB/连接"应为 **4.62 KiB**。 用 9 859 072 B 作原点重算,复现出 0.0045143 / 9.954 / R² 0.9999869(原报告记的 是 0.0045145 / 9.95 / 0.999987)。 --- doc/benchmark.md | 82 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 63 insertions(+), 19 deletions(-) diff --git a/doc/benchmark.md b/doc/benchmark.md index a483e3cc..8cf10723 100644 --- a/doc/benchmark.md +++ b/doc/benchmark.md @@ -15,7 +15,7 @@ **三个读数**(方法、分档与原始数据见下文各节): -- **每连接内存 4.62 KB**(最小二乘,**R² = 0.999987**)⇒ 外推 100 万连接 ≈ **4.42 GiB** +- **每连接内存 4.62 KiB**(4 734 B,最小二乘,**R² = 0.999987**)⇒ 外推 100 万连接 ≈ **4.42 GiB** - 单核动态路由 **75 k RPS**(流水线 10–50 深可到 **88 k**);静态 **44 k RPS / 107 MB/s** - 10 分钟长跑 **3 840 万请求、0 错误**,RSS 漂移 **0.2%** @@ -69,37 +69,81 @@ src/web/uvcpp_http_server.h | 10 ++++++- ## 3. 结果一:每连接内存 -八档采样(0 与 7 个非零档): +下表有**两列 RSS,是两个不同的量**,别混: -| 目标并发 | 实际建立 | 连接失败 | 探测成功 | 关闭后 RSS / 句柄 | -|---:|---:|---:|---:|---| -| 8 000 | 8 000 | 0 | 8 000 | 18.2 MB / 184 | -| 16 000 | 16 000 | 0 | 16 000 | 25.5 MB / 184 | -| 24 000 | 24 000 | 0 | 24 000 | 32.8 MB / 184 | -| 32 000 | 32 000 | 0 | 32 000 | 40.4 MB / 184 | -| 40 000 | 40 000 | 0 | 40 000 | 47.3 MB / 184 | -| 48 000 | 48 000 | 0 | 48 000 | 55.3 MB / 184 | -| 55 000 | **54 852** | 148(`ENOBUFS`) | 全部 54 852 | 61.9 MB / **184** | +- **建立后 RSS** —— 连接全部建好、攥住时读的。**拟合用的就是这一列。** +- **关闭后 RSS** —— 全部断开之后再读的。它是**另一条线**,只用来佐证连接被回收干净 + (本节末单说)。 + +八档采样(原点 + 7 个非零档): + +| 目标并发 | 实际建立 | 连接失败 | 探测成功 | **建立后 RSS**(拟合用) | 关闭后 RSS / 句柄 | +|---:|---:|---:|---:|---:|---| +| 0(基线) | — | — | — | **9.402 MiB** | — | +| 8 000 | 8 000 | 0 | 8 000 | 46.277 MiB | 17.398 MiB / 184 | +| 16 000 | 16 000 | 0 | 16 000 | 82.602 MiB | 24.320 MiB / 184 | +| 24 000 | 24 000 | 0 | 24 000 | 118.477 MiB | 31.324 MiB / 184 | +| 32 000 | 32 000 | 0 | 32 000 | 154.238 MiB | 38.531 MiB / 184 | +| 40 000 | 40 000 | 0 | 40 000 | 190.652 MiB | 45.133 MiB / 184 | +| 48 000 | 48 000 | 0 | 48 000 | 226.727 MiB | 52.715 MiB / 184 | +| 55 000 | **54 852** | 148(`ENOBUFS`) | 全部 54 852 | 257.270 MiB | 59.094 MiB / **184** | - **零连接失败**直到 48 000。 - 每一档都逐条复检过 8 000 – 54 852 条连接**全部可服务**。 - 全部关闭后句柄回落到 **184**(与基线一致,无泄漏)。 +- 原点那档是**进程空转时的基线**(9 859 072 B),即 8 个点里的第 8 个。 -拟合(8 个点): +**拟合(8 个点,y 单位 MiB)**: ``` -RSS(MB) = 0.0045145 × N + 9.95 R² = 0.999987 - = 4.62 KB/连接 × N + 9.95 MB +RSS(MiB) = 0.0045143 × N + 9.954 R² = 0.9999869 + = 4.62 KiB/连接 × N + 9.95 MiB ``` -- 除截距点外,**每点残差 ≤ 0.51%**。 -- 截距 9.95 MB 是进程的固定开销(静态缓存、会话表、已初始化的协议层等),不是每连接的。 -- **外推 100 万连接 ≈ 4.41 GiB + 0.01 GiB ≈ 4.42 GiB。** +逐点残差(**这一列要能让人自己算**): + +| N | 建立后 RSS (MiB) | 拟合预测 (MiB) | 残差 | 残差 % | +|---:|---:|---:|---:|---:| +| 0 | 9.402 | 9.954 | −0.551 | −5.86% | +| 8 000 | 46.277 | 46.068 | +0.210 | +0.45% | +| 16 000 | 82.602 | 82.182 | +0.420 | **+0.51%** | +| 24 000 | 118.477 | 118.296 | +0.180 | +0.15% | +| 32 000 | 154.238 | 154.410 | −0.172 | −0.11% | +| 40 000 | 190.652 | 190.525 | +0.128 | +0.07% | +| 48 000 | 226.727 | 226.639 | +0.088 | +0.04% | +| 54 852 | 257.270 | 257.571 | −0.301 | −0.12% | + +- **除原点外每点 |残差| ≤ 0.51%**;而上表能看出这些残差**没有走向**(在零点附近交替), + 不是单向偏。 +- 原点那点自己偏 −5.86%:它是**空转基线**这一次单独的读数,不落在拟合线上 —— 拟合出的 + 截距 9.954 MiB 比它高 0.55 MiB。写出来是因为"除原点外"这句话要是没有上面那张表,读者 + 没法知道偏多少。 +- 截距 9.95 MiB 是进程的固定开销(静态缓存、会话表、已初始化的协议层等),不是每连接的。 +- **外推 100 万连接 ≈ 4.4085 GiB(斜率)+ 0.0097 GiB(截距)≈ 4.42 GiB。** +- 每连接 **4 733.6 B = 4.6226 KiB**,本文按 4.62 KiB 记。 **为什么能这么低**:本库**没有每连接常驻的读缓冲**。读是 `read_start([](uvcpp_buf* b){ … })` 的按需形状 —— 有数据才给缓冲,空闲连接上不预置任何 一块固定大小的读缓冲。这一条是第 6 节那个对照的全部原因。 +### 「关闭后」那一列是另一条线,不能拿去验上面的拟合式 + +上表最右列(关闭后 RSS)**自己对 N 回归**,出来的是: + +``` +RSS(MiB) = 0.0008870 × N + 10.120 R² = 0.9997352 + = 930 B/连接 +``` + +斜率是 **4.73×** 的差(930 B vs 4 734 B),R² 也不是上面那个 0.9999869。**这两个数都对, +但它们量的不是同一件事**:关闭后读到的是一条正在被回收的服务端连接表的残余占用,不是 +「多一条连接要多多少内存」。**要判定容量,看的是「建立后」那条线。** + +这一列自己的价值在另一头:它的**截距 10.120 MiB 与空转基线 9.402 MiB 只差 0.72 MiB** +(约 7.7%)。这才是"连接被回收干净"的证据 —— 8 000 到 54 852 条连接全关掉之后,进程回到 +离基线不远的地方,且**句柄一律回到 184**。所以这一列留着,但它是**证据**,不是**斜率**。 + + --- ## 4. 结果二:吞吐与延迟 @@ -176,10 +220,10 @@ Hical 自己在 §2.8 里给的账(v2.6.4 → v2.6.5): |---|---|---| | Hical v2.6.4(优化前) | ~17.44 KB | ~16.6 GB | | Hical v2.6.5 / v2.6.6 | ~8 KB(v2.6.6 再 +256 B) | ~7–8 GB | -| **libuvcpp 1.1.33** | **4.62 KB** | **4.42 GiB** | +| **libuvcpp 1.1.33** | **4.62 KiB**(4 734 B) | **4.42 GiB** | - 对本库 vs Hical **优化前**:**轻约 3.8×**(每连接少约 12.8 KB)。 -- 对本库 vs Hical **优化后**:**轻约 1.7×**(4.62 KB vs ~8 KB)。 +- 对本库 vs Hical **优化后**:**轻约 1.7×**(4.62 KiB vs ~8 KB)。 **⚠ 这个 1.7× 有一处口径不确定,写清楚免得被读成强结论。** Hical 在那句话里同时注明 「内核侧 TCP 缓冲 ~4 KB/连接不可优化」。**本文的 4.62 KB 是进程 RSS 增量,只含用户态**; From 6cb2ca8128f69b7d06e0601f9dad1f6b3453acb5 Mon Sep 17 00:00:00 2001 From: sercebr Date: Sun, 20 Sep 2026 22:18:38 +0800 Subject: [PATCH 3/4] =?UTF-8?q?docs(readme):=20=E6=96=87=E6=A1=A3=E7=B4=A2?= =?UTF-8?q?=E5=BC=95=E8=A1=A5=E4=B8=8A=20doc/http2-status.md=EF=BC=9B?= =?UTF-8?q?=E6=AF=8F=E8=BF=9E=E6=8E=A5=E6=95=B0=E5=AD=97=E6=94=B9=E6=88=90?= =?UTF-8?q?=20KiB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `doc/` 下一直有三个文件,而两处 README 的索引里只有 `ci-guide.md` 与 `webapp-guide.md`,`http2-status.md` 缺席。1.2.0 的重点是 HTTP/2,发版时索引里 少这一页不好看,趁这次改的是同样那两行一起补掉。 另:`4.62 KB` 改成 **`4.62 KiB`(4 734 B)** —— 拟合的 y 单位是 MiB,写成 KB 会让 十进制/二进制两种读法差 2.4%。 --- README.md | 3 ++- README.zh.md | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 137e1119..ce934530 100644 --- a/README.md +++ b/README.md @@ -426,6 +426,7 @@ libuvcpp/ ├── doc/ # Documentation │ ├── benchmark.md # Measured per-connection memory, throughput, stability │ ├── ci-guide.md # CI maintenance guidelines +│ ├── http2-status.md # HTTP/2 support status │ └── webapp-guide.md # Web app framework guide ├── cmake/ # CMake config templates ├── .github/workflows/ # CI pipeline @@ -509,7 +510,7 @@ contributor, [@sercebr](https://github.com/sercebr). blocks (`nbufs = 2`) (`1.1.28`); the variant table stores and returns handles instead of whole bodies (`1.1.31`) -**Measured, not estimated** — see [doc/benchmark.md](doc/benchmark.md): **4.62 KB per idle +**Measured, not estimated** — see [doc/benchmark.md](doc/benchmark.md): **4.62 KiB per idle connection** (least squares over eight tiers, R² = 0.999987 → 1 M connections ≈ 4.42 GiB), 75 k RPS on a single event loop, and a 10-minute soak at 38 M requests / 0 errors. That page also compares the per-connection figure against [Hical](https://github.com/Hical61/Hical)'s diff --git a/README.zh.md b/README.zh.md index e6faddd1..ddfb0820 100644 --- a/README.zh.md +++ b/README.zh.md @@ -445,6 +445,7 @@ libuvcpp/ ├── doc/ # 文档 │ ├── benchmark.md # 每连接内存、吞吐与稳定性实测 │ ├── ci-guide.md # CI 维护指南 +│ ├── http2-status.md # HTTP/2 支持现状 │ └── webapp-guide.md # web 应用框架指南 ├── cmake/ # CMake 配置模板 ├── .github/workflows/ # CI 流水线 @@ -520,7 +521,7 @@ libuvcpp/ 变体表按句柄存/取,不再整份拷体(`1.1.31`) **实测数据(不是估算)** —— 见 [doc/benchmark.md](doc/benchmark.md):每条空闲连接 -**4.62 KB**(八档最小二乘,R² = 0.999987,外推 100 万连接 ≈ 4.42 GiB)、单事件循环 +**4.62 KiB**(4 734 B,八档最小二乘,R² = 0.999987,外推 100 万连接 ≈ 4.42 GiB)、单事件循环 75 k RPS、10 分钟长跑 3 840 万请求 0 错误。那一页还把每连接这个数与 [Hical](https://github.com/Hical61/Hical) 自己的报告做了对照,并写明了该对照带的口径问题。 From 461cfa8868c4a5cf84c66e88dc18177038071516 Mon Sep 17 00:00:00 2001 From: sercebr Date: Sun, 20 Sep 2026 22:34:09 +0800 Subject: [PATCH 4/4] =?UTF-8?q?docs(benchmark):=20=E4=BF=AE=204.73=C3=97?= =?UTF-8?q?=20=E2=86=92=205.09=C3=97=E3=80=81=E6=9C=AB=E5=A4=84=204.62=20K?= =?UTF-8?q?B=20=E2=86=92=20KiB=EF=BC=8C=E5=B9=B6=E6=8A=8A=E5=8D=95?= =?UTF-8?q?=E4=BD=8D=E5=8F=A3=E5=BE=84=E9=82=A3=E6=9D=A1=E6=9F=A5=E5=AE=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 评审指出两处: 1. §3 末「斜率是 **4.73×** 的差」—— `4 733.6 ÷ 930.1 = 5.09`。我那个 4.73 是把分母当成 1000 了(`4 733.6/1000 = 4.7336`),而括号里自己给的「930 B vs 4 734 B」分明就是 5.09 的分子分母。改成 **5.09×**,并把算式写进括号,免得再犯。 2. §6 口径那一段里「**本文的 4.62 KB** 是进程 RSS 增量」漏改成 KiB —— §0 / §3 / §6 表格 与两份 README 都改了,偏偏漏掉这句,而它正是讲口径的那一句。 另外把评审提的第三个问题(3.8× 要不要按 KB/KiB 折算)**查实**,而不是加一句"近似": ``` 17.44 x 1024 x 10^6 B = 16.63 GiB == 它写的「~16.6 GB」 17.44 x 1000 x 10^6 B = 16.24 GiB != 它写的 ``` Hical 标的是 `KB` / `GB`,但那几个数**只在二进读法下自洽**,所以它的 KB 就是 KiB、GB 就是 GiB,与本文本来就同口径,**3.8× / 1.7× 不用折算**。§6 那条从"单位口径不同"改成 "其实同一个"并附上这个判据;表格下加一行说明 Hical 那两格沿用它的原文标法。 --- doc/benchmark.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/doc/benchmark.md b/doc/benchmark.md index 8cf10723..fc96b43b 100644 --- a/doc/benchmark.md +++ b/doc/benchmark.md @@ -135,7 +135,7 @@ RSS(MiB) = 0.0008870 × N + 10.120 R² = 0.9997352 = 930 B/连接 ``` -斜率是 **4.73×** 的差(930 B vs 4 734 B),R² 也不是上面那个 0.9999869。**这两个数都对, +斜率是 **5.09×** 的差(4 733.6 ÷ 930.1),R² 也不是上面那个 0.9999869。**这两个数都对, 但它们量的不是同一件事**:关闭后读到的是一条正在被回收的服务端连接表的残余占用,不是 「多一条连接要多多少内存」。**要判定容量,看的是「建立后」那条线。** @@ -222,11 +222,13 @@ Hical 自己在 §2.8 里给的账(v2.6.4 → v2.6.5): | Hical v2.6.5 / v2.6.6 | ~8 KB(v2.6.6 再 +256 B) | ~7–8 GB | | **libuvcpp 1.1.33** | **4.62 KiB**(4 734 B) | **4.42 GiB** | -- 对本库 vs Hical **优化前**:**轻约 3.8×**(每连接少约 12.8 KB)。 -- 对本库 vs Hical **优化后**:**轻约 1.7×**(4.62 KiB vs ~8 KB)。 +> Hical 那两格沿用**它的原文标法**(`KB` / `GB`);它实际是二进单位,见下面第一条。 + +- 对本库 vs Hical **优化前**:**轻约 3.8×**(每连接少约 12.8 KiB)。 +- 对本库 vs Hical **优化后**:**轻约 1.7×**(4.62 KiB vs ~8 KiB)。 **⚠ 这个 1.7× 有一处口径不确定,写清楚免得被读成强结论。** Hical 在那句话里同时注明 -「内核侧 TCP 缓冲 ~4 KB/连接不可优化」。**本文的 4.62 KB 是进程 RSS 增量,只含用户态**; +「内核侧 TCP 缓冲 ~4 KB/连接不可优化」。**本文的 4.62 KiB 是进程 RSS 增量,只含用户态**; 如果 Hical 的 ~8 KB 里已经含了那 ~4 KB 内核缓冲,那么同一口径下它剩下的用户态部分约 4 KB,两者**接近持平** —— 差别是本库的机制里压根没有常驻读缓冲,而它需要一套 `ReadBufferPool` 借还来达到同一格。该文档没有把这个口径写明,所以两种读法都列在这里, @@ -237,8 +239,10 @@ Hical 自己在 §2.8 里给的账(v2.6.4 → v2.6.5): - 两边都是**外推**(都撞端口/内核墙),所以都是"斜率可信、截距不可跨机器比"。截距差异 明显(本机 9.95 MB,因为这台还带着静态缓存、会话表与已初始化的协议层)。**斜率才是 可比的量**,上表据此下结论。 -- **单位口径不同**:Hical 写的是十进 GB(~16.6 GB / ~7–8 GB),本文写 GiB(4.42 GiB - ≈ 4.75 GB)。上表按各自原文引用,没有替对方换算。 +- **单位口径其实同一个**(这里先前写成"不同",是我没查):Hical 标的是 `KB` / `GB`,但它 + 那几个数**只在二进读法下自洽** —— `17.44 × 1024 × 10⁶ B = 16.63 GiB`,正好对上它写的 + **~16.6 GB**;按十进读成 `17.44 KB` 算出来是 `16.24`,对不上。所以它的 `KB` 是 KiB、 + `GB` 是 GiB,与本文同口径,**上面那两个倍数不用折算**。 - **平台不同**:Hical 的数据出自它自己文档所述环境(Linux VM),本文是 Windows / MSVC / 环回。跨平台比较请带上这一条。