首页 · ← SuperJ 手册 中文|EN

构建世界上最快的 Web 服务器

一份分步教程。你将在 SuperJ 的内置 HTTP 栈上写一个完整的 HTTP 服务器,把它编译成一个单一原生二进制,直接运行那个二进制,并用 wrk 做压测。无需安装框架,无需附带运行时 — 最终产物是一个你可以拷到任何地方运行的可执行文件。

完成的程序随 SuperJ 发布于 demo/sj/demo/WebServer.sjsj.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 中的 truereusePort — 多个进程可以共享同一个端口,在 Linux 上内核会在它们之间均衡进来的连接。运行多个副本:

for i in 1 2 3 4; do ./webserver 8080 & done

无需改代码 — 同一个二进制,更多副本,更多核在服务。


为什么它这么快

更进一步

同一套栈能做的远不止明文:

superj doc --list(或 superj doc sj.http.Router)浏览完整的 HTTP API。

SuperJ — manual · generated from fastest-web-server.md at pack time · Powered by superJ — this site is served by superj_web 中文|EN