构建世界上最快的 Web 服务器
一份分步教程。你将在 SuperJ 的内置 HTTP 栈上写一个完整的 HTTP 服务器,把它编译成一个单一原生二进制,直接运行那个二进制,并用 wrk 做压测。无需安装框架,无需附带运行时 — 最终产物是一个你可以拷到任何地方运行的可执行文件。
完成的程序随 SuperJ 发布于 demo/sj/demo/WebServer.sj(sj.demo.WebServer)— 你可以直接编译它(第 4 步),或在这里一步步构建。
前置条件:已安装 SuperJ(superj --help 可用,$SJ_HOME 已设置 — 见 快速开始),以及用于基准测试步骤的 wrk。
第 1 步 — 创建文件
SuperJ 遵循 Java 的包约定:package sj.demo 位于 sj/demo/ 目录。创建 sj/demo/WebServer.sj,以包声明和我们需要的 import 开头:
package sj.demo;
import sj.http.HttpRequest;
import sj.http.HttpRequestHandler;
import sj.http.HttpSessionProvider;
import sj.lang.Integer;
import sj.net.NioAdapter;
import sj.net.NioConfig;
import sj.net.SessionWriter;
import sj.util.ByteArray;
这些都来自 SDK — sj.http(HTTP 解析/服务)、sj.net(非阻塞套接字适配器),以及 sj.util.ByteArray(一个可复用的字节缓冲)。
第 2 步 — 编写请求处理器
一个处理器实现 HttpRequestHandler.handle(request, writer) — 每个 HTTP 请求调用一次。我们在构造器中一次性构建 200 OK 响应并复用它,所以服务一个请求不分配任何东西:
class HelloHandler implements HttpRequestHandler {
private final ByteArray response;
HelloHandler() {
string body = "Hello from SuperJ!\n";
this.response = new ByteArray(
"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: "
+ body.length() + "\r\n\r\n" + body);
}
public void handle(HttpRequest request, SessionWriter writer) {
this.response.rewind(); // 复用预构建的缓冲
writer.write(this.response);
}
}
第 3 步 — 启动服务器
WebServer.main 打开一个监听套接字并运行一个事件循环。HttpSessionProvider 把进来的连接通过 HTTP 解析接到你的处理器;adapter.poll(0) 在单线程上驱动套接字:
public class WebServer {
public static void main(String[] args) {
int port = 8080;
if (args.length > 0) { port = Integer.parseInt(args[0]); }
NioConfig cfg = new NioConfig(65536, 16384, 64, 262144, true, 1024);
NioAdapter adapter = new NioAdapter(cfg);
adapter.open("127.0.0.1", port, new HttpSessionProvider(new HelloHandler()));
System.out.println("SuperJ web server listening on http://127.0.0.1:" + port + "/");
while (true) { adapter.poll(0); } // 一个事件循环,一个线程
}
}
这就是整个服务器:一行包声明、一个处理器、一个 main。
第 4 步 — 编译成原生二进制
superj compile sj/demo/WebServer.sj --sdk-path "$SJ_HOME/sdk" --link --output webserver
--link 产出一个成品可执行文件,不只是目标代码。结果是 webserver:一个单一、自包含的原生二进制 — 没有 JVM,没有解释器,没有需要随附安装的运行时。把它拷到另一台相同 OS/架构的机器上就能直接跑。
不想手敲?同样的程序随 SuperJ 发布 — 编译随附的副本:
superj compile "$SJ_HOME/demo/sj/demo/WebServer.sj" --sdk-path "$SJ_HOME/sdk" --link --output webserver。
第 5 步 — 运行二进制
./webserver 8080
SuperJ web server listening on http://127.0.0.1:8080/
你直接运行这个可执行文件 — 没有其他东西参与。
第 6 步 — 检查它
在另一个终端:
curl http://127.0.0.1:8080/
Hello from SuperJ!
第 7 步 — 用 wrk 做基准测试
wrk -t2 -c100 -d30s --latency http://127.0.0.1:8080/
-c100 连接、-d30s 持续时间(wrk 默认是 20s — 至少跑这么长,理想情况下在短暂预热之后,以获得稳态数值),--latency 输出百分位分布。读 Requests/sec 看吞吐,读 Latency 百分位看尾部。
在 Ryzen 9 9950X3D 上通过回环,这个单文件服务器大约能持续:
Latency Distribution
50% 156.00us
75% 159.00us
90% 161.00us
99% 167.00us
Requests/sec: 406762
要点就在这里:p99 在 p50 的 ~1.1× 以内 — 本质上是一条平坦的尾部(167µs 对 156µs)— 因为没有垃圾回收器,没有停顿来拉长它。绝对数值取决于你的硬件、NIC 与负载生成器,所以对你关心的机器做基准测试;但你测量的会是一条请求与底层之间别无他物的服务路径。
但那 ~407k 不是服务器的上限 — 它是 wrk 的上限。见下一步。
第 8 步 — wrk 是瓶颈:用 SuperJ 自带的客户端测量
wrk 是单进程的,面对这么快的服务器它自身就成了瓶颈 — 你最终测的是 wrk,不是服务器。SuperJ 正为此附带了一个原生负载生成器:demo/sj/demo/WebClient.sj,一个跑在同一个内置栈上的事件循环客户端。它打开 keep-alive 连接并流水线化请求,所以一个进程能驱动远超 wrk 的负载。
也编译它,然后指向正在运行的服务器 — webclient <port> [connections] [pipeline] [seconds]:
superj compile "$SJ_HOME/demo/sj/demo/WebClient.sj" --sdk-path "$SJ_HOME/sdk" --link --output webclient
./webclient 8080 64 16 15 # 64 个连接,每个 16 个流水线请求,15 秒
在同一台机器上(Ryzen 9 9950X3D,服务器固定一个核,负载生成器固定另一个核),单个 wrk 进程最高约 423k req/s,而一个 WebClient 进程把同一个服务器推到 ~515k req/s — 服务器从来不是上限;客户端才是。运行多个 WebClient 进程(或增加流水线)还能更高。教训:当你在给这么快的东西做基准测试时,确认你的负载生成器不是你实际在测的东西。
第 9 步 — 跨核扩展
单线程不意味着单核:用进程而非线程扩展。 NioConfig 中的 true 是 reusePort — 多个进程可以共享同一个端口,在 Linux 上内核会在它们之间均衡进来的连接。运行多个副本:
for i in 1 2 3 4; do ./webserver 8080 & done
无需改代码 — 同一个二进制,更多副本,更多核在服务。
为什么它这么快
- 提前编译为原生 — 通过 LLVM 编译成原生二进制;没有 JIT 预热,从第一个请求起就是全速。
- 没有垃圾回收器 — arena 内存被确定性回收,所以没有 GC 停顿;这就是延迟曲线在负载下保持平坦的原因。
- 零分配热路径 — 响应缓冲构建一次,每个请求 rewind 复用;稳态服务不分配任何东西。
- 一个事件循环,一个线程 — 热路径上无锁、无上下文切换、无跨核缓存流量。
更进一步
同一套栈能做的远不止明文:
- 路由 —
sj.http.Router把路径/方法映射到处理器。 - 静态文件 —
sj.http.StaticFileHandler带缓存地服务一个目录。 - HTTP/2 —
sj.http.SimpleH2WebServer(prior-knowledge h2c)。 - WebSockets —
sj.http.WebSocketHandler。
用 superj doc --list(或 superj doc sj.http.Router)浏览完整的 HTTP API。