【免费下载链接】FrameworkBenchmarks
Source for the TechEmpower Framework Benchmarks project
本文以 webmachine 基准测试说明文档 为主体,结合仓库内的完整源码,系统讲解如何在 OCaml 中基于 Webmachine(RESTful 资源路由库)、Cohttp(HTTP 服务器)、Caqti(数据库访问库)与 Lwt(异步运行时)实现 TechEmpower 基准测试的 JSON、PLAINTEXT、DB、QUERY 四类端点。读完本文,你将掌握 Webmachine 资源对象模型的写法、Caqti 连接池的接入方式、Cohttp 多进程服务器的启动流程,以及该项目从构建到本地验证的完整链路。
一、测试概况与仓库结构
frameworks/OCaml/webmachine/README.md 是本篇的核心文档,它明确了该框架在 TechEmpower 基准套件中的定位:使用 OCaml 生态中的 Webmachine 库实现一个 HTTP 服务,覆盖四类标准测试:
| 测试类型 | 实现位置(相对仓库根目录) | 功能说明 |
|---|---|---|
| JSON | frameworks/OCaml/webmachine/src/src/bin/tfb.ml(hello 类) | 返回 {"message":"Hello, World!"} |
| PLAINTEXT | 同上(hello 类) | 返回纯文本 Hello, World! |
| DB | 同上(db 类) | 从 PostgreSQL 随机取一行 World 记录并返回 JSON |
| QUERY | 同上(queries 类) | 批量随机查询并返回 JSON 数组 |
整个框架目录结构如下:
frameworks/OCaml/webmachine/
├── README.md # 测试说明文档(本文主体)
├── benchmark_config.json # 基准测试元数据(端口、分类、数据库等)
├── config.toml # TFB 新版配置(与 JSON 等价的信息)
├── webmachine.dockerfile # 容器镜像定义
└── src/
├── Makefile # install / build / run 构建入口
├── dune-project # dune 工程定义 + opam 依赖声明
├── tfb.opam / lib.opam # opam 包描述
└── src/
├── bin/tfb.ml # 全部端点实现与服务器启动逻辑
└── lib/
├── db.atd # World 记录的 atd 类型定义
├── message.atd # JSON 响应的 atd 类型定义
├── time.ml # HTTP Date 头的缓存刷新模块
└── dune # 库构建规则(含 atdgen 代码生成)
值得留意的是:README 中提到的 src/tfb.ml 在仓库中的实际路径为 src/src/bin/tfb.ml,阅读源码时请以此为准。
二、技术栈与关键依赖库
README 的 "Important Libraries" 一节列出了六个核心依赖,它们共同构成了这套实现的技术底座:
- Webmachine:RESTful 资源路由库,采用“资源对象 + 决策流程”模型,通过继承
resource类并覆写方法来完成路由与内容协商。 - Cohttp:OCaml 的 HTTP 库,此处使用
Cohttp_lwt_unix(与 Lwt 结合的 Unix 后端)承担底层 HTTP 协议与服务器职责。 - Caqti:数据库访问抽象库,本实现使用
Caqti_lwt与caqti-driver-postgresql驱动连接 PostgreSQL。 - Lwt:轻量级协作式并发运行时,为服务器与数据库访问提供
Lwt/let%lwt异步语法。 - Ezjsonm:JSON 工具库(README 列为依赖,实际序列化由 atdgen 生成代码完成,见第六节)。
- Ptime:POSIX 时间库(README 列为依赖,实际日期头由自实现的
Lib.Time模块生成,见第七节)。
依赖的版本约束在 dune-project 中声明,并被生成到 tfb.opam:
dune >= 2.8.5
webmachine >= 0.7.0
lwt >= 5.4.1
conf-libev >= 4-12
caqti >= 1.6.0
caqti-lwt >= 1.6.0
caqti-driver-postgresql >= 1.6.0
cohttp-lwt-unix >= 4.0.0
lwt_ppx >= 2.0.2
其中 conf-libev 表明 Lwt 使用 libev 事件循环后端;lwt_ppx 为 let%lwt 异步语法提供预处理支持。
三、核心路由实现:Webmachine 资源对象模型
tfb.ml 的入口先定义一个 Wm 模块,将 Webmachine 库实例化到 Cohttp_lwt_unix__Io 上:
module Wm = struct
module Rd = Webmachine.Rd
module UnixClock = struct
let now () = 0
end
include Webmachine.Make (Cohttp_lwt_unix__Io) (UnixClock)
end
接着定义路由表(tfb.ml#L185-L192):
let routes =
[
("/plaintext", fun () -> new hello);
("/json", fun () -> new hello);
("/db", fun () -> new db);
("/queries", fun () -> new queries);
("/queries/:id", fun () -> new queries);
]
每一条路由都对应一个 Webmachine 资源类实例。/json 与 /plaintext 共用 hello 类,/db 对应 db 类,/queries 与带参路由 /queries/:id 共用 queries 类。请求到达后,通过 Wm.dispatch' 分发(tfb.ml#L196),未命中时返回 404:
(Wm.dispatch' routes ~body ~request >|= function
| None -> (`Not_found, Header.init (), `String "Not found", [])
| Some result -> result)
3.1 资源类的标准覆写方法
每个资源类继承 [Cohttp_lwt.Body.t] Wm.resource,并覆写三个方法(hello 类):
class hello =
object (self)
inherit [Cohttp_lwt.Body.t] Wm.resource
method! allowed_methods rd = Wm.continue [ `GET ] rd
method content_types_provided rd =
Wm.continue
[ ("text/plain", self#to_text); ("application/json", self#to_json) ]
rd
method content_types_accepted rd = Wm.continue [] rd
method private to_text rd =
let text = "Hello, World!" in
Wm.continue (`String text) rd
method private to_json rd =
let json = Lib.Message_t.{ message = "Hello, World!" } in
Wm.continue (`String (Lib.Message_j.string_of_message json)) rd
end
allowed_methods:声明该资源仅接受GET。content_types_provided:声明可提供的内容类型及其对应的处理器;content_types_accepted返回空表,表示不接受请求体内容。Wm.continue:Webmachine 决策流程的标准返回构造,将控制权交还给路由框架。to_text/to_json:分别用普通字符串与 atdgen 生成的序列化函数构造响应体。
3.2 JSON / PLAINTEXT 端点
- JSON 端点
http://localhost:8080/json:构造Lib.Message_t.{ message = "Hello, World!" }记录,经Lib.Message_j.string_of_message序列化为 JSON 输出。类型定义见 message.atd。 - PLAINTEXT 端点
http://localhost:8080/plaintext:直接返回固定字符串Hello, World!。
3.3 DB 端点
db 类(tfb.ml#L67-L91)只提供一个 JSON 处理器,流程为:生成随机 id → 从连接池取出连接执行查询 → 序列化结果:
method private read_db rd =
let read_db' (module C : Caqti_lwt.CONNECTION) =
C.find select_random (Random.int 10000 + 1)
in
let%lwt id, randomNumber =
Caqti_lwt.Pool.use read_db' pool |> or_error >|= function
| Ok (x, y) -> (x, y)
| Error _ -> failwith "whoops"
in
let json =
Lib.Db_t.
{ id = float_of_int id; randomNumber = float_of_int randomNumber }
in
Wm.continue (`String (Lib.Db_j.string_of_query json)) rd
3.4 QUERY 端点
queries 类(tfb.ml#L93-L138)是四个端点中最复杂的:
id方法从路径参数:id解析查询数量,并做边界钳制:小于 1 取 1,大于 500 取 500,解析失败(Failure/Not_found)时默认取 1:
method private id rd =
try
let _id = int_of_string (Wm.Rd.lookup_path_info_exn "id" rd) in
match _id with
| x when x < 1 -> 1
| x when x > 500 -> 500
| x -> x
with
| Failure _ -> 1
| Not_found -> 1
read_query先生成n个随机 id,为每个 id 发起一次独立的连接池查询,然后用Lwt.all并发等待全部完成,最后映射为 JSON 数组:
let query_ids = List.init (self#id rd) (fun _ -> Random.int 10000 + 1) in
let read_query' x (module C : Caqti_lwt.CONNECTION) = C.find select_random x in
let response =
List.map
(fun id -> Caqti_lwt.Pool.use (read_query' id) pool |> or_error >|= ...)
query_ids
in
let%lwt resolved_response = Lwt.all response in
...
Wm.continue (`String (Lib.Db_j.string_of_queries json)) rd
这套并发批查模式对应 TechEmpower 的 query 测试要求:返回长度等于查询数量的 JSON 数组,每个元素包含 id 与 randomNumber 两个整数字段(验证逻辑见 query.py)。
四、数据库访问:Caqti 连接池与预处理查询
4.1 连接池的建立
连接池在进程启动时创建一次,使用标准基准数据库凭据(与 create-postgres.sql 中 benchmarkdbuser 的授权一致):
let pool =
let connection_url =
"postgresql://benchmarkdbuser:benchmarkdbpass@tfb-database:5432/hello_world?connect_timeout=15"
in
match Caqti_lwt.connect_pool ~max_size:4 (Uri.of_string connection_url) with
| Ok pool -> pool
| Error err ->
Printf.eprintf "%s" (Caqti_error.show err);
flush stderr;
failwith (Caqti_error.show err)
要点:
max_size:4:连接池最大连接数为 4,所有查询都通过Caqti_lwt.Pool.use f pool借还连接;- 连接 URL 使用主机名
tfb-database,这是基准运行环境中 PostgreSQL 容器的默认服务名; - 建池失败会打印错误并直接
failwith退出,便于快速定位配置问题。
4.2 预处理查询语句
查询使用 Caqti_request.Infix 定义类型安全的预处理语句(tfb.ml#L41-L44):
let select_random =
let open Caqti_request.Infix in
Caqti_type.(int ->! tup2 int int)
"SELECT id, randomNumber FROM World WHERE id = $1"
int ->! tup2 int int 表示:接收一个 int 参数,期望返回 (int, int) 二元组(对应 World 表的 id 与 randomNumber 两列)。
4.3 统一错误处理
or_error 包装了查询结果的错误路径(tfb.ml#L33-L39),将 Caqti 错误打印到 stderr 后转为 Database_error:
let or_error m =
match%lwt m with
| Ok a -> Ok a |> Lwt.return
| Error err ->
Printf.eprintf "%s" (Caqti_error.show err);
flush stderr;
Error (Database_error (Caqti_error.show err)) |> Lwt.return
查询失败时上层会 failwith "whoops" 快速终止,保证基准运行期不会返回错误的响应体。
五、HTTP 服务器:Cohttp + 多进程 fork 模型
5.1 启动前的环境检测
main 函数(tfb.ml#L163-L233)在启动前做了几项准备:
- 将 Lwt 缓冲默认值设为
0x10000(64 KB)——该改动来自 Cohttp 的 issue #328 讨论,用于提升吞吐; - 读取环境变量
APP_INSTANCES确定 worker 进程数,未设置则回退到getconf _NPROCESSORS_ONLN(CPU 核数); - 读取
PORT环境变量确定监听端口,默认8080; - 通过
dump_lwt打印 Lwt 各系统调用的检测结果(libev、epoll相关能力等),便于排查运行时后端问题。
5.2 请求回调与响应头
回调统一为每个响应附加 Server: webmachine 与缓存后的 Date 头(tfb.ml#L194-L203):
let callback (_ch, _conn) request body =
let open Cohttp in
(Wm.dispatch' routes ~body ~request >|= function
| None -> (`Not_found, Header.init (), `String "Not found", [])
| Some result -> result)
>>= fun (status, headers, body, _) ->
let headers = Header.add headers "Server" "webmachine" in
let headers = Header.add headers "Date" Lib.Time.(!memo_date) in
Server.respond ~headers ~body ~status ()
5.3 多进程监听模型
服务器采用经典的“父进程建 socket、子进程共享监听”的 fork 模型:
let socket =
Lwt_unix.socket (Unix.domain_of_sockaddr sockaddr) Unix.SOCK_STREAM 0
in
Lwt_unix.setsockopt socket Unix.SO_REUSEADDR true;
Lwt_main.run @@ (Lwt_unix.bind socket sockaddr >|= fun () ->
Lwt_unix.listen socket (Lwt_unix.somaxconn () [@ocaml.warning "-3"]));
for i = 1 to nproc do
Lwt.async (fun () -> Lwt_io.flush_all ());
if Lwt_unix.fork () = 0 then (
(* child *)
Lib.Time.refresh_date ();
Lwt.async (fun () -> Lwt_io.eprintf "Listening on %s:%d (child %d)\n" ...);
let config = Server.make ~callback () in
Lwt.async (fun () -> Server.create ~mode:(`TCP (`Socket socket)) config);
let forever, _ = Lwt.wait () in
Lwt_main.run forever;
exit 0)
done;
while true do
Unix.pause ()
done
关键点:
- 先创建 socket、
bind、listen,再逐fork出nproc个子进程共享同一监听 socket; - 每个子进程用
Cohttp_lwt_unix.Server.create ~mode:(TCP (Socket socket))启动事件循环并永久挂起(Lwt.wait); - 父进程随后进入
Unix.pause空闲循环,仅承担进程管理职责; SO_REUSEADDR保证端口可被快速重启复用。
六、JSON 序列化:atdgen 代码生成
虽然 README 将 Ezjsonm 列为依赖库,本项目实际采用 atd + atdgen 自动生成类型与序列化代码,这是 lib 库 的核心职责。
类型定义文件:
type message = {
message : string;
}
type queries = query list
type query = {
id : float;
randomNumber : float;
}
构建时由 dune 中的规则调用 atdgen 生成四组文件(message_t/ml/mli、message_j/ml/mli、db_t/ml/mli、db_j/ml/mli):
(rule
(targets message_j.ml message_j.mli)
(deps message.atd)
(action (run atdgen -j %{deps})))
(rule
(targets db_j.ml db_j.mli)
(deps db.atd)
(action (run atdgen -j %{deps})))
生成的 Db_j.string_of_query、Db_j.string_of_queries、Message_j.string_of_message 即被 tfb.ml 直接调用。由于 db.atd 将字段定义为 float,源码在构造记录前会先用 float_of_int 做转换(见 DB 端点实现)。
七、Date 头的缓存优化:Lib.Time 模块
HTTP 响应的 Date 头需要 RFC 1123 格式(如 Wed, 17 Apr 2013 12:00:00 GMT)。为避免每个请求都调用 gmtime 格式化,time.ml 实现了缓存方案:
let date () =
let d = get_date () in
Format.sprintf "%s, %02d %s %4d %02d:%02d:%02d GMT" (dow d.tm_wday) d.tm_mday
(month d.tm_mon) (1900 + d.tm_year) d.tm_hour d.tm_min d.tm_sec
let memo_date = ref @@ date ()
let refresh_date () =
let f _ =
memo_date := date ();
ignore @@ Unix.alarm 1
in
(ignore @@ Sys.(signal sigalrm (Signal_handle f)));
f ()
机制说明:
- 启动时用
memo_date记录当前格式化结果; - 子进程 fork 后调用
refresh_date:注册SIGALRM处理器,处理器每次触发就刷新memo_date并重新设置Unix.alarm 1(即每秒刷新一次); - 请求回调只读取
!memo_date,把日期格式化的开销从每次请求摊薄为每秒一次,避免成为性能瓶颈。
八、构建与运行
8.1 本地构建
Makefile 提供四个目标:
BINARY = tfb
PROFILE ?= release
install:
opam install --yes --deps-only .
gen-opam:
dune build @install
build:
opam exec -- dune build --profile $(PROFILE)
run:
./_build/default/src/bin/$(BINARY).exe
本地构建步骤为:make install(按 opam 安装依赖)→ make build(dune 构建,默认 release profile)→ make run(执行生成的二进制)。
8.2 Docker 镜像
webmachine.dockerfile 基于预编译依赖镜像 rbjorklin/techempower-ocaml-image:5.3.0-e8949511,通过 OCAMLRUNPARAM 环境变量调优 OCaml 运行时:
FROM rbjorklin/techempower-ocaml-image:5.3.0-e8949511
ENV DIR project
ENV OCAMLRUNPARAM a=2,o=240
WORKDIR /${DIR}
COPY ./src /${DIR}
RUN make build
EXPOSE 8080
ENTRYPOINT ["_build/default/src/bin/tfb.exe"]
OCAMLRUNPARAM a=2:控制运行时参数中的分配策略(a参数);OCAMLRUNPARAM o=240:设置最大栈大小相关的运行时参数(o参数),此类调优目的均为减少 GC 停顿、提升热路径吞吐;- 容器入口直接运行构建产物,监听 8080 端口。
8.3 测试端点
按 README 提供的测试 URL,服务启动后可验证四个端点:
http://localhost:8080/json # 返回 {"message":"Hello, World!"}
http://localhost:8080/plaintext # 返回 Hello, World!
http://localhost:8080/db # 返回 {"id":N,"randomNumber":M}
http://localhost:8080/queries/ # 返回 [{"id":..,"randomNumber":..}, ...]
/queries/ 还可携带数量参数,例如 /queries/5,数量超出 1~500 范围时会被钳制到边界值(见 3.4 节)。
九、基准配置元数据与验证机制
9.1 benchmark_config.json
benchmark_config.json 声明了该框架在基准套件中的注册信息:
{
"framework": "webmachine",
"tests": [{
"default": {
"json_url": "/json",
"plaintext_url": "/plaintext",
"db_url": "/db",
"query_url": "/queries/",
"port": 8080,
"approach": "Realistic",
"classification": "Micro",
"database": "postgres",
"framework": "webmachine",
"language": "OCaml",
"flavor": "None",
"orm": "Raw",
"platform": "cohttp",
"webserver": "None",
"os": "Linux",
"database_os": "Linux",
"display_name": "webmachine",
"notes": "",
"versus": "None"
}
}]
}
关键字段含义:
port: 8080:服务监听端口;approach: "Realistic"/classification: "Micro":框架的基准分类;database: "postgres"+orm: "Raw":使用原始 SQL 访问 PostgreSQL;platform: "cohttp":底层 HTTP 平台为 Cohttp;webserver: "None":应用自身即为 HTTP 服务器,未前置独立 Web 服务器。
config.toml 是 TFB 新版配置格式,内容与上述 JSON 等价(urls.*、approach、database、orm、platform 等字段一一对应)。
9.2 对应测试类型的验证逻辑
仓库的 toolset/test_types 目录定义了每个测试的自动校验规则,可据此理解各端点的行为要求:
- json(json.py):校验响应为包含
message字段的 JSON 对象、HTTP 状态码与Content-Type头; - plaintext(plaintext.py):校验响应体包含
hello, world!(大小写不敏感),且多余字节数越少越好; - db(db.py):校验响应为包含
id、randomNumber整数字段的 JSON 对象,并通过verify_queries_count检查实际 SQL 查询次数是否符合并发模型; - query(query.py):依次请求
2、0、foo、501、空串等用例,验证数组长度与字段类型,并校验/queries/路由至少 9 个字符。
十、小结
从 README 的四行测试清单出发,结合 tfb.ml 的完整源码可以看到:这套 OCaml webmachine 基准实现,用 Webmachine 的资源对象模型把路由、内容协商与响应生成解耦成清晰的类结构,用 Caqti 连接池 + 预处理查询保证数据库访问的类型安全,用 Cohttp 的 TCP socket fork 模型摊满多核 CPU,再用 atdgen 自动生成 JSON 序列化代码、以 Lib.Time 缓存 Date 头规避格式化开销——每一处细节都直接服务于基准测试的高吞吐场景。若要在本地复现,按第八节的 make install && make build && make run 即可启动,并通过第九节的端点逐一验证。
【免费下载链接】FrameworkBenchmarks
Source for the TechEmpower Framework Benchmarks project
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



