- 性能测试
- 测试
- 后端
- 开发者工具
【免费下载链接】FrameworkBenchmarks
Source for the TechEmpower Framework Benchmarks project
导读
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 | /json | JSON 序列化测试路由 |
fortune_url | /fortunes | HTML 模板测试路由 |
db_url | /db | 单条数据库查询路由 |
query_url | /queries?queries= | 多行查询路由(?queries=为查询参数前缀) |
update_url | /updates?queries= | 数据库更新路由 |
port | 8080 | 服务监听端口 |
debug_port | 4005 | Slynk(SLIME 的现代继任者)调试端口 |
approach | Stripped | 精简风格实现 |
classification | Micro | 微框架分类 |
database | Postgres | 依赖数据库 |
framework | Woo | 框架名 |
language | Common Lisp | 实现语言 |
orm | Raw | 手写 SQL,不使用 ORM |
platform | Lisp | 平台分类 |
webserver | Woo | 内嵌 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_HOST | tfb-database | 数据库主机(与部署脚本中的容器名一致) |
DB_PORT | 5432 | PostgreSQL 端口 |
DB_USE_SSL | 不启用 | 设置后启用 SSL(:full) |
DB_NAME | hello_world | 库名 |
DB_USER | benchmarkdbuser | 用户名 |
DB_PASS | benchmarkdbpass | 密码 |
连接由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 采用经典三阶段构建:
- debian 基础阶段:
debian:bullseye-slim,配置 APT 关闭 recommends/suggests; - roswell 工具链阶段:安装 Roswell(版本
22.12.14.113,通过官方 .deb 包安装)、SBCL(sbcl-bin/2.3.4)、ASDF(3.3.6),并固定 Quicklisp 发行版本2023-02-15以保证依赖可复现; - 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。
- 运行时阶段:回到精简的 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/ 目录):
| 端点 | 验证脚本 | 压测脚本 | 核心验证点 |
|---|---|---|---|
| plaintext | plaintext.py | pipeline.sh(管线化) | 响应包含hello, world!、无多余字节 |
| json | json.py | concurrency.sh | JSON 含message键 |
| db | db.py | concurrency.sh | JSON 对象含id/randomNumber整数 |
| queries | query.py | query.sh | 数组长度正确、含 id/randomNumber |
| fortunes | fortune.py | concurrency.sh | FortuneHTMLParser字节级比对 |
| updates | update.py | query.sh | 更新后返回正确对象 |
其中fortune.py的FortuneHTMLParser会忽略<meta>、<link>、<script>、<thead>、<tbody>等标签并规范化字符实体(如"→")——这正是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
相关推荐
Fluent for Steam贡献指南:如何参与皮肤开发与功能改进
Fluent for Steam贡献指南:如何参与皮肤开发与功能改进 Fluent for Steam 是一款让 Steam 客户端呈现 Windows 11
桌面应用awesomo 视角下的 Common Lisp Web 开发:非阻塞服务器 Woo 与 WSGI/Rack 风格环境 Clack
awesomo 视角下的 Common Lisp Web 开发:非阻塞服务器 Woo 与 WSGI/Rack 风格环境 Clack 本篇技术指南以 awesom
文档FrameworkBenchmarks 中的 RestExpress Java 微服务基准测试实现解析
FrameworkBenchmarks 中的 RestExpress Java 微服务基准测试实现解析 导读 RestExpress 是一个组合轻量级工具形成的
性能测试测试后端开发者工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考