
Vũ Khôi
Tác giả tại Wotech Electronics Blog

12 美元服务器(1 vCPU、2GB 内存)扛住 28 万用户:218 名开发者如何优化到最深层?
一台 12 美元、只有 1 个 vCPU 和 2GB 内存的服务器,到底能同时服务多少用户?在 Arjay McCandless 组织的一场挑战中,218 名开发者在正是这个"贫民配置"上提交了各自的社交媒体 API 实现——成绩最好的 10 份作品处理了 24 万到 28 万并发用户,约合每秒 23,000–26,800 个请求,P95 延迟低于 500 毫秒。本文梳理了这些不可思议的数字背后的深层优化技术。
快速回答
- 赛题:在一台 1 vCPU、2GB 内存的服务器(月租约 12 美元)上构建社交媒体 API,强制使用 SQLite,禁止缓存 feed 结果、禁止缓存 token 验证状态——每个写请求都必须从头重新验证 token,并成功提交到数据库后才能返回响应。
- 最高成绩:280,000 并发用户(约 26,800 请求/秒),在 500,000 篇帖子、200 万点赞的种子数据上,持续 5 分钟负载下 P95 低于 500ms、P99 低于 1 秒、错误率低于 1%。
- Top 10 的共同公式:用
io_uring加 Direct Descriptors 突破连接数限制,从 I/O 层到事务层全程批处理(batching),复用缓冲区避免内存分配,并把 SQLite 的 WAL checkpoint 从主处理循环中分离出去。 - 两位并列冠军走了极端路线:一份用定理证明语言 Lean 生成经数学验证的 C 代码;另一份干脆自己重写了一个读写 SQLite 文件格式的存储引擎——P99 仅 23ms,但被主办方判为超出规则精神,现实生产环境无人敢用。
赛题是什么?
参赛者要构建一个社交媒体 API,包含这些操作:加载 feed(最新 20 篇帖子,附作者和点赞数)、打开帖子、发帖、点赞和健康检查。规则被刻意收紧,防止有人靠缓存"作弊":
- 强制使用 SQLite,种子数据为 500,000 篇帖子、2,000,000 个点赞。
- 禁止缓存 feed 结果,禁止缓存 token 验证状态——每个写请求仍须从头验证 token。
- 写操作必须成功提交到数据库后才能返回响应。
- 系统必须持续承受 5 分钟负载,P95 低于 500ms、P99 低于 1 秒、错误率低于 1%。
为什么常规做法会立刻崩溃?
在普通 Web 应用里,单是一个 GET /feed 请求就要走完一长串流程:通过 socket 接收网络包、HTTP 服务器把字节解析成方法/路径/请求头、经过中间件、调用 SQLite 驱动执行查询扫描 B-Tree 和数据库页、把每一行数据映射成对象、序列化成 JSON、再从 socket 推回去。在只有 1 个 vCPU 上把这个流程每秒重复几万次,根本扛不住。
还有操作系统的硬墙:每个进程最多只有 65,000 个文件描述符。当几十万用户同时保持长连接(keep-alive)时,如果靠不停开关连接来绕开限制,光是 accept 和销毁连接的 CPU 开销就足以把服务器压垮。
Top 10 的共同武器:io_uring 与批处理
出现在全部 10 份最高分作品中的方案,是 Linux 的 io_uring 结合 Direct Descriptors:socket 不再占用进程的普通文件描述符,而是直接存放在 io_uring 队列环附属的表中,并分摊到多个 ring 上,从而突破 65,000 连接上限。同时 io_uring 允许批量提交 I/O 操作、批量回收完成结果,把上下文切换开销降到最低。
在这个共同地基上,每位作者用自己的语言即兴发挥——从纯汇编、C、Rust、F#、Crystal 到 Ada,甚至还有专为证明数学定理而生的 Lean。
Top 10 中最值得一看的路线
| 路线 | 成绩 | 关键技术 |
|---|---|---|
| 整个 Web 服务器用 x86_64 汇编手写 | 240,000 用户,约 23,000 RPS,内存占用最低(545MB) | HTTP 解析、路由、JSON、鉴权、事件循环全手写;给每轮处理的 completion 数设硬上限以保护 P99 |
| F#"劫持"ASP.NET Core 的 Kestrel | 247,000 用户,约 24,000 RPS,P99 仅 72ms | 用手写 io_uring 替换整个传输层,绕过 Kestrel 的 HTTP 管线;JSON 直接写入缓冲区;WAL checkpoint 独占一个线程 |
| 同一作者的 C 版与汇编版 | 两版同达 250,000 用户,约 24,100 RPS | 主动等 6ms 攒够 128 个请求的批次再一次性提交;C 版 P99 更好(294ms vs 397ms)且内存更省 |
| Rust 聪明地取 feed 数据 | 264,000 用户,约 25,000 RPS | 无框架、无异步运行时;帖子 ID 相邻时做小范围扫描(range scan),ID 分散时回退到标准查询 |
| C:SIMD 做鉴权 + 直捣 B-Tree | 266,000 用户,约 25,500 RPS | 令牌按批次用 SIMD 同时验证多个签名;读操作绕过 SQL 层,直接调用 SQLite 内部的 B-Tree 游标 |
| Crystal 直接关掉垃圾回收器 | 270,000 用户,约 25,700 RPS | 全手动内存管理:原地解析、共享缓冲区池、每批次固定 Arena 内存区;SQLite 编译时启用 PGO |
| Ada 的 checkpoint 协调机制 | 270,000 用户,约 25,900 RPS,P99 177ms | 主进程与辅助进程之间明确协调 WAL checkpoint;在内核层精确指定要取消的操作,海量关闭连接时不再全表扫描 |
一些细节说明性能往往赢在极小之处:一位作者的汇编版起初把同时在途的 accept 数(in-flight accepts)常量设为 16,只跑出 125,000 用户——仅把这个数字改成 256,分数立刻翻倍到 250,000。F# 版则只是让 checkpoint 线程能被立即唤醒、而不是睡满 50ms,就把写等待从 57ms 降到了 6ms。
两位并列冠军:数学路线对"重写数据库引擎"
在 280,000 用户(约 26,800 RPS)这个成绩上,有两条非常特殊的路线:
- 用定理证明语言 Lean 生成 C 代码(P99 316ms)。作者在 Lean 中定义 API 和数据的不变式(invariant):例如帖子 ID 连续无缺口、时间戳随 ID 递增。随后用数学证明优化后的读取计划与规范给出完全相同的结果,再由此生成 C 代码运行。凭借这些不变式,服务器可以沿 B-Tree 直接逆向读取数据行,无需查询时间戳索引;一旦运行时发现数据不再满足这些性质,立即回退到标准 SQL。
- 用 C 手写一个读写 SQLite 文件格式的存储引擎(P99 仅 23ms)。这份作品甚至不链接 SQLite 库,而是自己实现引擎、直接读写 SQLite 文件的 B-Tree 和 WAL 结构,维护一张直指每个数据库页(在内存或 WAL frame 中)的指针表。这是全场最低的 P99——但主办方本人也判定这种做法超出了"使用 SQLite"这一规则的精神,而且没人敢在真实生产环境里自己重写整个数据库引擎:数据丢失风险和维护负担都太大了。
对日常工作有什么启发?
很少有开发者在工作中需要重写 HTTP 服务器或掏 SQLite 的内脏。但 10 份作品——不分语言——最终汇聚到同一处,揭示了最核心的性能原则:
- 严格控制每个请求上真正发生的工作量:从查询及其执行计划,到内存分配次数,再到序列化成本。
- 善用批处理(batching)和缓冲区复用,给 CPU 和垃圾回收器减负——开销被摊到多个请求上,而不是每个请求单独付一次。
- 绝不让服务用户的主路径被系统"家务"阻塞:数据库 checkpoint 这类清理与磁盘同步任务,必须从请求路径上移走。
原文分析最后还给后端社区留了一个开放问题:如果给你一台 1 vCPU、2GB 内存的 VPS,用你日常的技术栈(Java/Spring Boot、Go、Node.js 或 .NET)和默认配置,它在触顶之前能扛多少 RPS——你第一个会去优化哪里?
常见问题
io_uring 是什么?为什么高分作品全用它?
io_uring 是 Linux 内核的异步 I/O 机制:应用通过与内核共享的环形队列,批量提交 I/O 操作、批量回收结果。结合 Direct Descriptors(把 socket 直接存放在 ring 的表中,而非进程文件描述符),单个进程可以服务远超常规 65,000 文件描述符上限的连接数,并大幅削减上下文切换开销——正是这道题的两个最大瓶颈。
目标是扛最高负载,为什么反而禁止缓存?
因为缓存会把比赛变成内存之争而非处理能力之争:如果 feed 结果和 token 状态都能缓存,服务器几乎不用碰数据库。禁缓存迫使每份作品都在深层优化——I/O、查询、内存分配——且每个写请求仍要真实验证 token 并真实提交到 SQLite。
手写存储引擎那份作品真的"赢"了吗?
看数字是的:P99 23ms 全场最低,并以 280,000 用户并列第一。但主办方认为它超出了"使用 SQLite"规则的精神,而且这种做法无法迁移到现实:手写数据库文件格式的引擎,数据丢失风险和维护成本都极高。它是作为技术演示获胜,而不是作为解决方案获胜。
P95、P99 是什么意思?
它们是延迟的百分位数:P95 低于 500ms 表示 95% 的请求在 500ms 内完成;P99 低于 1 秒表示 99% 的请求在 1 秒内完成。用百分位而不是平均值,是因为平均值会掩盖最慢的那批请求——而那正是真实用户感受最强烈的部分。
这些技术能用在真实系统里吗?
原则可以:批处理、复用缓冲区、减少热路径上的内存分配、把 checkpoint 和清理移出请求路径、仔细审视查询计划——在普通技术栈里都做得到。而极端部分——汇编手写服务器、关掉 GC、自研存储引擎——是比赛框架内的演示,用可靠性和可维护性换性能。
结语
这场挑战说明,便宜硬件的极限远比我们默认以为的要远——瓶颈通常在没优化过的软件,而不在 1 vCPU 和 2GB 内存这两个数字本身。而最大的收获不是某个具体技巧,而是这种"汇聚"本身:被逼到极限时,所有语言、所有流派都会回到同样的几条地基原则上——每个请求少做无用功、批量处理、复用内存,并且永远不要让系统的家务活堵住用户的路。
来源:Lữ Minh Khương 在 Facebook 社群 VGU Tech Alumni 的分析帖(2026 年 11 月 4 日),内容为 Arjay McCandless 组织的优化挑战;数据为原帖汇总的比赛结果,由 Wotech Electronics 编辑整理。插图:原帖附带的赛题示意图。
Bài viết liên quan

5 tháng 11, 2026 · 1 phut doc
Knowledge Engine:为办公工作打造"第二大脑"
一名办公室职员每天从几十个渠道接收信息:群聊消息、邮件、会议、共享文件、走廊里的只言片语。等到要用时,却想不起数字在哪份文件里、决策出自哪场会议、说明在哪条消息里。Knowledge Engine(知识引擎)——"第二大脑"——正是为这个场景而生:一个统一的地方,帮你存储、组织并检索全部工作知识。本文介绍个人知识管理(PKM)的理念,以及如何为自己搭建这样一套系统。

4 tháng 11, 2026 · 2 phut doc
NAS 疑似感染勒索病毒怎么办?六阶段应急流程:隔离、保全证据、安全恢复
发现 NAS 上的文件被成批加密、出现勒索信(ransom note),或者设备行为异常——很多人的第一反应是"重装了事"。但越南 IT 社群里一篇被广泛收藏的技术分享指出,这恰恰是最该避免的动作:NAS 勒索事件的应对是一套有纪律的六阶段流程——隔离、保全证据、确定范围、封堵根源、恢复、复盘——其中任何一步做错,都可能让数据永久丢失,或让恶意软件卷土重来。

4 tháng 11, 2026 · 1 phut doc
把 Raspberry Pi 变成完全本地运行的对话式 AI 智能体:隐私与成本
在所有 AI 助手都"栖身"于科技巨头云端的当下,一个开源项目正在逆流而行:Be More Agent 把 Raspberry Pi——一块巴掌大小、价格低廉的电脑板卡——变成一个完全在本地运行的对话式 AI 智能体(AI Agent)。语音不上传服务器,不按调用次数付 API 费,也不需要联网。本文解析这样一款本地对话助手的架构如何运作、它为何重要,以及必须了解的局限。