☰
基于 libev 的 Common Lisp 非阻塞 HTTP 服务器:Woo 在 FrameworkBenchmarks 中的基准测试实现深度解析
2026/10/15 3:41:05 网站建设 项目流程
  • 性能测试
  • 测试
  • 后端
  • 开发者工具

【免费下载链接】FrameworkBenchmarks

Source for the TechEmpower Framework Benchmarks project

项目地址:https://gitcode.com/gh_mirrors/fr/FrameworkBenchmarks
点击查看免费下载

导读

Woo 是一个构建在 libev 之上的 Common Lisp 非阻塞 HTTP 服务器,其设计目标是在任何编程语言实现的 Web 服务器中追求极限性能。本文以 frameworks/Lisp/woo/ 目录下的实现为骨架,完整剖析 Woo 在 TechEmpower FrameworkBenchmarks 中的参与方式:从测试元数据(benchmark_config.json、config.toml)、完整可运行的应用源码(woo.ros)、命令行参数解析(helpers/parse-argv.lisp)到多阶段 Docker 构建(woo.dockerfile),并对照 toolset 中的验证逻辑解释每个端点的实现要点。读完本文,你将掌握如何在 Common Lisp 中编写高性能基准测试服务、如何通过 Postmodern 批量更新 PostgreSQL、以及 FrameworkBenchmarks 如何验证与压测这些端点。

项目背景与定位

Woo 的官方定位非常精简:"A fast non-blocking HTTP server on top of libev"(一个构建在 libev 之上的快速非阻塞 HTTP 服务器)。虽然 Woo 由 Common Lisp 编写,但其目标是成为任何编程语言中最快的 Web 服务器之一。在本仓库中,这一目标通过 woo.ros 开头的注释明确表达:

"Although Woo is written in Common Lisp, it aims to be the fastest web server written in any programming language."

Woo 使用 libev 事件循环实现非阻塞 I/O,并通过 SBCL(Steel Bank Common Lisp)的编译器优化策略追求极致吞吐。在 FrameworkBenchmarks 的分类体系中,它属于Micro级(无重型框架),采用Stripped风格(去掉多余功能的精简实现)、Raw ORM(直接手写 SQL)并搭配 PostgreSQL 数据库。

目录结构总览

frameworks/Lisp/woo/ ├── README.md # 项目简介 ├── benchmark_config.json # 旧版测试元数据(FrameworkBenchmarks 主配置) ├── config.toml # 新版测试元数据 ├── woo.dockerfile # 多阶段 Docker 构建(环境准备 + 编译 + 运行) ├── woo.ros # 基于 Roswell 的应用脚本(可执行入口) └── helpers/ ├── parse-argv.lisp # 命令行参数解析 └── starts-with.lisp # 字符串前缀匹配工具

其中woo.ros是真正的"应用本体"——它既是一个 Roswell 脚本(以exec ros -Q -- $0 "$@"作为 shebang),也是完整的服务端实现,负责全部六个基准测试端点。

测试元数据:两个配置文件的对应关系

仓库中同时存在新旧两套配置。旧式配置 benchmark_config.json 定义了完整的测试矩阵:

配置项值说明
plaintext_url/plaintext纯文本测试路由
json_url/jsonJSON 序列化测试路由
fortune_url/fortunesHTML 模板测试路由
db_url/db单条数据库查询路由
query_url/queries?queries=多行查询路由(?queries=为查询参数前缀)
update_url/updates?queries=数据库更新路由
port8080服务监听端口
debug_port4005Slynk(SLIME 的现代继任者)调试端口
approachStripped精简风格实现
classificationMicro微框架分类
databasePostgres依赖数据库
frameworkWoo框架名
languageCommon Lisp实现语言
ormRaw手写 SQL,不使用 ORM
platformLisp平台分类
webserverWoo内嵌 Web 服务器

新版 config.toml 与该 JSON 完全对应,采用 TOML 的[framework]、[main]区块表达同样的 URL 与分类信息。两者的路由定义必须与woo.ros中handler的实际分发逻辑保持一致,否则验证阶段会失败。

应用入口:woo.ros 与 SBCL 编译策略

woo.ros的头部设置了 Roswell 的脚本执行头,并立即对 SBCL 施加激进的编译器优化策略:

(sb-ext:restrict-compiler-policy 'speed 3) (sb-ext:restrict-compiler-policy 'safety 0) (sb-ext:restrict-compiler-policy 'debug 0) (sb-ext:restrict-compiler-policy 'compilation-speed 0)

这是性能导向的典型做法:将速度压到 3 级,同时关闭 safety、debug 和 compilation-speed 的优化空间。代价是运行时错误检查减少,但作为基准测试实现,这一取舍完全服务于吞吐量目标。

依赖管理:Quicklisp 快速加载

通过ql:quickload一次性加载全部依赖(:silent t抑制输出):

(ql:quickload '(postmodern st-json spinneret quri uiop woo alexandria serapeum function-cache cl+ssl slynk slynk/mrepl slynk-macrostep slynk-named-readtables log4cl-extras) :silent t)

各依赖的选型在源码注释中留下了明确依据:

  • st-json:JSON 编码,源码注释指出其比 jonathan 快 10%–15%(引用 json-review 的写性能对比);
  • spinneret:HTML 生成,注释说明其比此前使用的 cl-markup 快 30%,且比需要手动转义的 CL-WHO 更安全;
  • postmodern:PostgreSQL 数据库驱动,提供预编译语句(prepared statements);
  • quri:URI 解析(用于解析?queries=查询参数);
  • serapeum:提供dict便捷哈希表构造;
  • function-cache:为数据库配置提供缓存式读取;
  • slynk:调试用 SLIME 后端,仅在 DEBUG 模式下启用。

数据库连接:环境变量驱动的配置体系

数据库配置通过function-cache:defcached定义,读取环境变量并提供默认值,缓存避免每次请求重复读取环境:

(function-cache:defcached db-host () (or (uiop:getenv "DB_HOST") "tfb-database")) (function-cache:defcached db-port () (parse-integer (or (uiop:getenv "DB_PORT") "5432"))) (function-cache:defcached db-use-ssl () (if (uiop:getenv "DB_USE_SSL") :full :no)) (function-cache:defcached db-name () (or (uiop:getenv "DB_NAME") "hello_world")) (function-cache:defcached db-user () (or (uiop:getenv "DB_USER") "benchmarkdbuser")) (function-cache:defcached db-pass () (or (uiop:getenv "DB_PASS") "benchmarkdbpass"))
环境变量默认值说明
DB_HOSTtfb-database数据库主机(与部署脚本中的容器名一致)
DB_PORT5432PostgreSQL 端口
DB_USE_SSL不启用设置后启用 SSL(:full)
DB_NAMEhello_world库名
DB_USERbenchmarkdbuser用户名
DB_PASSbenchmarkdbpass密码

连接由with-binary-connection宏统一管理,启用了二进制协议(:use-binary t)与连接池(:pooled-p t),这是减少解析开销、复用连接的关键:

(defmacro with-binary-connection (() &body body) `(postmodern:with-connection (list (db-name) (db-user) (db-pass) (db-host) :port (db-port) :use-binary t :use-ssl (db-use-ssl) :pooled-p t) ,@body))

数据库表结构由 toolset/databases/postgres/create-postgres.sql 定义:World表(10000 行id+randomNumber)与Fortune表(12 条谚语记录,含一条日文记录フレームワークのベンチマーク用于验证 UTF-8 输出)。

六个基准端点的实现剖析

1. plaintext:纯文本最短路径

(declaim (ftype (function () list) plaintext) (inline plaintext)) (defun plaintext () "Plaintext handler." '(200 (:content-type "text/plain" :server "Woo") ("Hello, World!")))

这是最轻量的端点:直接返回字符串字面量,declaim inline提示编译器内联以消除调用开销。验证器 toolset/test_types/plaintext/plaintext.py 要求响应体包含hello, world!(大小写不敏感),并检查是否返回多余字节。

2. json:JSON 序列化

(defun json () `(200 (:content-type "application/json" :server "Woo") (,(st-json:write-json-to-string (serapeum:dict "message" "Hello, World!")))))

使用 st-json 将哈希表序列化为 JSON 字符串。

3. db:单行随机查询

(postmodern:defprepared get-a-random-record-query (:select 'randomnumber :from 'world :where (:= 'id '$1)) :single)

postmodern:defprepared在编译期生成预编译语句。get-a-random-record将 id 夹在1..10000内((min id 10000)),并以serapeum:dict构造{"id":..., "randomNumber":...}响应。请求时随机取(+ 1 (random 10000))。

4. queries:N 行随机查询与参数钳制

queries端点的核心在于解析?queries=N参数,并严格限制在 1 到 500 之间:

(defun ensure-integer-is-between-one-and-five-hundreds (n) (max (min n 500) 1)) (defun extract-number-of-records-to-fetch (env) (let ((n (handler-case (parse-integer (cdr (assoc "queries" (quri:url-decode-params (getf env :query-string)) :test #'equal))) (error (c) (values 1 c))))) (ensure-integer-is-between-one-and-five-hundreds n)))

参数解析流程:通过 quri 的url-decode-params解码查询串 → 提取queries参数 →parse-integer解析(异常时回落为 1)→ 钳制到[1, 500]。这一钳制逻辑直接呼应 toolset/test_types/query/query.py 中的测试用例:('2','fail')、('0','fail')、('foo','fail')、('501','warn')、('','fail')——即非法输入应失败、501 超出上限应告警,只有合法值才通过。

(defun get-some-random-records (n) (loop repeat n for id fixnum = (1+ (random 10000)) collect (get-a-random-record id)))

5. fortunes:HTML 模板与转义陷阱

fortunes端点要求返回符合规范字节级匹配的 HTML 表格(12 条记录 + 运行时新增的第 13 条),由 toolset/test_types/fortune/fortune_html_parser.py 中的FortuneHTMLParser逐标签比对验证。源码中的两处注释揭示了两个关键陷阱:

(let ((*print-pretty* nil) ;; Without this setting Spinneret does not close tags when it is possible ;; and benchmark's validator fails. (spinneret:*html-style* :tree)) ... (:tag :name "head" (:title "Fortunes")) ... ;; Here I have to use :tag, ;; because otherwise spinneret inserts ;; <meta charset="UTF-8" /> and benchmark's verification fails.
  • 必须设置spinneret:*html-style* :tree,否则 Spinneret 不会在可省略时闭合标签,导致验证器失败;
  • head 必须用:tag手写而非(:head ...),否则 Spinneret 会自动插入<meta charset="UTF-8" />,而该标签在验证器的忽略列表之外,会破坏字节级匹配。

排序要求按 message 字符串字典序(string-lessp),并附加id=0的 "Additional fortune added at request time." 记录。

6. updates:批量更新与防 SQL 注入的设计演进

updates 是最复杂的端点,其实现经历了明确的安全演进。源码注释指出:

Previous version of the update function formatted SQL query using FORMAT function. In real world application this could lead to SQL injection.

旧版本用format拼接 SQL 存在注入风险,现版本改为宏生成固定数量的预编译语句:define-batch-updater为每个批量长度n(1 到 500)生成独立的 prepared 函数,SQL 形如:

UPDATE world AS ori SET randomnumber = new.randomnumber::integer FROM (VALUES ($1,$2), ($3,$4), ...) AS new (id, randomnumber) WHERE ori.id = new.id::integer

(define-batch-updaters 500)在编译期展开为 500 个预编译函数并存入*batch-updaters*哈希表。auto-batch-update按数据长度查找对应函数分发;get-and-update-some-random-records-batch则实现"攒批"逻辑:当批量超过 500 时(虽然测试上限为 500,代码仍保留此分支),每攒满 500 条立即发送一次update-batch-500,剩余部分在finally中通过auto-batch-update收尾。响应复用原row哈希并原地更新randomNumber键,注释明确说明这是为了减少 Lisp 的 consing(内存分配)。

路由分发与 server-info

所有端点由handler统一分发,基于 helpers/starts-with.lisp 提供的前缀匹配(注意使用string=而非全词匹配,因此/json也会命中/jsonxxx):

(defun handler (env) "Router" (log4cl-extras/error:with-log-unhandled () (let ((path (getf env :path-info))) (cond ((starts-with path "/plaintext") (plaintext)) ((starts-with path "/json") (json)) ((starts-with path "/db") (db)) ((starts-with path "/queries") (queries env)) ((starts-with path "/fortunes") (fortunes)) ((starts-with path "/updates") (updates env)) (t (server-info))))))

未匹配任何基准路由的请求回落到server-info,以纯文本返回 Lisp 实现类型与版本、启动参数,便于人工排查运行环境。

启动流程:worker 数、DEBUG 模式与 SSL

main函数集中管理启动逻辑(woo.ros):

(setf (getf args :worker-num) ;; empirically I found that performance is the best when ;; we have 4 workers per core. (* (getf args :cpu 1) 4))
  • Worker 数:按"每 CPU 核心 4 个 worker"的经验值计算(:cpu来自--cpu参数),随后从参数表中移除:cpu;
  • 随机种子:(setf *random-state* (make-random-state t))用当前时间初始化全局随机状态,避免各 worker 产生相同随机序列;
  • DEBUG 模式:设置环境变量DEBUG时,在 4005 端口启动 Slynk 服务(对应配置中的debug_port),并将日志级别降为:debug;
  • SSL 证书:启用 SSL 时从~/.postgresql/root.crt加载全局验证位置(cl+ssl:ssl-load-global-verify-locations);
  • 最终启动:(apply #'woo:run #'handler :debug nil args)调用 Woo 服务器,禁用其自带 debug 模式,将解析后的参数(address/port/worker-num)传给服务器。

命令行参数由 helpers/parse-argv.lisp 解析,仅接受三个选项:--address(字符串)、--port与--cpu(整数,解析失败时报错),未知或格式错误的选项直接抛出错误。

Docker 构建:多阶段流水线

woo.dockerfile 采用经典三阶段构建:

  1. debian 基础阶段:debian:bullseye-slim,配置 APT 关闭 recommends/suggests;
  2. roswell 工具链阶段:安装 Roswell(版本22.12.14.113,通过官方 .deb 包安装)、SBCL(sbcl-bin/2.3.4)、ASDF(3.3.6),并固定 Quicklisp 发行版本2023-02-15以保证依赖可复现;
  3. builder 编译阶段:安装libev-dev、build-essential,然后——关键的 Hack——克隆三个未合入上游/Quicklisp 的分支到/libs:
git clone --branch http-pipelining https://github.com/svetlyak40wt/fast-http /libs/fast-http git clone --branch fix-error-codes https://github.com/svetlyak40wt/woo /libs/woo git clone --branch fix-defprepared-threadsafety https://github.com/svetlyak40wt/Postmodern /libs/Postmodern

这三个分支分别对应 fast-http 的 HTTP 管线化支持、woo 的错误码修复、Postmodern 的defprepared线程安全修复。随后通过CL_SOURCE_REGISTRY=/libs// ros build woo.ros将应用与补丁库编译为独立可执行文件woo。

  1. 运行时阶段:回到精简的 debian 基础镜像,仅安装libev4,从 builder 复制编译产物woo,EXPOSE 8080与4005,默认启动命令:
CMD ./woo --cpu $(nproc) --address 0.0.0.0 --port 8080

即自动探测 CPU 核数、监听所有网卡的 8080 端口——与配置中的port: 8080完全一致。

验证与压测机制对照

FrameworkBenchmarks 对 Woo 的每个端点都运行验证与压力测试(对应 toolset/test_types/ 目录):

端点验证脚本压测脚本核心验证点
plaintextplaintext.pypipeline.sh(管线化)响应包含hello, world!、无多余字节
jsonjson.pyconcurrency.shJSON 含message键
dbdb.pyconcurrency.shJSON 对象含id/randomNumber整数
queriesquery.pyquery.sh数组长度正确、含 id/randomNumber
fortunesfortune.pyconcurrency.shFortuneHTMLParser字节级比对
updatesupdate.pyquery.sh更新后返回正确对象

其中fortune.py的FortuneHTMLParser会忽略<meta>、<link>、<script>、<thead>、<tbody>等标签并规范化字符实体(如&#34;→&quot;)——这正是woo.ros中刻意使用:tag避开<meta>的原因。

总结

Woo 的基准测试实现是"极简 + 极限性能"设计哲学的完整样本:libev 非阻塞 I/O 打底,SBCL 全速编译策略加持,st-json/spinneret 等经过实测比选的轻量库,加上 Postmodern 预编译语句、二进制协议与连接池,再以宏生成的 500 个批量更新函数绕开 SQL 拼接风险。从 woo.ros 的每一行注释都可以看到为通过验证器与压测所做的针对性优化——包括每核 4 个 worker、钳制查询参数、精确控制 HTML 输出等细节。若要在本地复现,可参照 woo.dockerfile 构建镜像,或直接运行ros build woo.ros编译出可执行文件后以--cpu $(nproc) --address 0.0.0.0 --port 8080启动。

  • 性能测试
  • 测试
  • 后端
  • 开发者工具

【免费下载链接】FrameworkBenchmarks

Source for the TechEmpower Framework Benchmarks project

项目地址:https://gitcode.com/gh_mirrors/fr/FrameworkBenchmarks
点击查看免费下载
上一篇:ESLint 已移除规则 `space-return-throw-case`:`return`、`throw`、`case` 关键字后的空格检查与 `keyword-spacing` 迁移指南
下一篇:ViTPose 中基于 HRNetv2-W18 + UDP 的 RHD 二维手部姿态估计配置全解析

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询