- 性能测试
- 测试
- 后端
- 开发者工具
【免费下载链接】FrameworkBenchmarks
Source for the TechEmpower Framework Benchmarks project
本文围绕 TechEmpower Framework Benchmarks 仓库中 Clojure 框架 Compojure 的基准测试实现展开,以 frameworks/Clojure/compojure/ 目录为核心,完整剖析其六大测试场景(JSON、DB、Queries、Fortunes、Updates、Plaintext)的路由组织、HikariCP 连接池配置、next.jdbc 数据访问、Hiccup 模板渲染,以及基于 Leiningen + Resin 的多阶段 Docker 构建与部署方式。读完本文,你将理解一个"微框架 + Servlet 平台"的 Clojure 基准测试应用是如何被组织、构建和验证的,并能直接对照源码复现其中的配置与实现手法。
一、Compojure 基准测试应用概述
Compojure 是 Clojure 生态中经典的轻量级 Web 路由库,其本质是构建在 Ring 之上的路由 DSL(Domain-Specific Language)。在 TechEmpower Framework Benchmarks(TFB)中,Compojure 被归类为Micro(微框架),平台为Servlet,Web 服务器为Resin,数据库使用MySQL,ORM 类型同样标记为Micro(即直接使用 JDBC 层级的 next.jdbc 而非完整 ORM)。这些元数据记录在两个基准配置文件中:benchmark_config.json 与 config.toml。
从项目结构看,整个 Compojure 基准测试由以下文件组成:
| 文件 | 作用 |
|---|---|
| project.clj | Leiningen 项目定义,声明依赖、Ring 插件与打包方式 |
| src/hello/handler.clj | 全部测试接口的处理器实现(路由、SQL、渲染) |
| compojure.dockerfile | 多阶段 Dockerfile:Leiningen 打包 WAR + Resin 运行 |
| resin.xml | Resin 应用服务器配置(HTTP 端口、Web 应用部署路径) |
| src/log4j.xml | log4j 日志级别配置 |
| benchmark_config.json / config.toml | 基准测试元数据与测试 URL 定义 |
二、项目依赖与基础设施软件版本
README.md 明确列出了核心依赖版本,它们全部体现在 project.clj 中:
- Clojure 1.12.4:语言运行时版本;
- Compojure 1.7.2:路由库(weavejester/compojure);
- Ring-JSON 0.5.1:Ring 的 JSON 中间件,其底层使用Cheshire(Clojure 的 JSON 库),而 Cheshire 又基于Jackson完成 Java 层面的 JSON 序列化。
在 project.clj 中,除了上述三个主依赖,还有一组与基准测试场景直接相关的组件:
| 依赖 | 版本 | 用途 |
|---|---|---|
com.mysql/mysql-connector-j | 9.6.0 | MySQL JDBC 驱动,对应handler.clj中的com.mysql.cj.jdbc.Driver |
org.clojars.jj/majavat | 1.19.0 | Java 工具辅助库 |
com.github.seancorfield/next.jdbc | 1.3.1093 | 现代 Clojure JDBC 数据访问层 |
hikari-cp | 4.0.0 | HikariCP 连接池的 Clojure 封装 |
hiccup | 1.0.5 | Clojure 的 HTML 渲染库(Fortunes 测试) |
log4j | 1.2.15 | 日志框架(含排除项避免依赖冲突) |
项目同时声明了lein-ring插件(版本 0.12.5),并在:ring {:handler hello.handler/app}中指定了 Ring 应用入口;devprofile 下加入servlet-api与ring-mock用于本地开发测试。整体构建产物为standalone WAR,由 compojure.dockerfile 中的lein ring uberwar命令生成。
三、六大测试场景的路由与实现剖析
3.1 路由入口:defroutes 与 JSON 中间件
所有基准接口统一由 handler.clj 中的defroutes app-routes定义,再通过wrap-json-response中间件包一层,保证响应体自动以 JSON 序列化输出:
(defroutes app-routes (GET "/" [] "Hello, World!") (GET "/plaintext" [] plaintext) (GET "/json" [] (json-serialization)) (GET "/db" [] (single-query-test)) (GET "/queries/:queries" [queries] (multiple-queries-test queries)) (GET "/queries/" [] (multiple-queries-test 1)) (GET "/fortunes" [] (fortune-test)) (GET "/updates/:queries" [queries] (db-updates queries)) (GET "/updates/" [] (db-updates 1)) (route/not-found "Not Found")) (def app "Format responses as JSON" (-> app-routes (json-middleware/wrap-json-response)))路由设计与 config.toml 中声明的测试 URL 一一对应:/json、/db、/queries/、/updates/、/fortunes、/plaintext。值得注意的是queries与updates同时注册了带路径参数:queries和无参数两个版本,无参数版本固定取1,这与 TFB 压测脚本(query.sh)会以url + c的方式追加查询数量参数的行为兼容。
3.2 Test 1:JSON 序列化(/json)
(defn json-serialization "Test 1: JSON serialization" [] (ring-resp/response {:message "Hello, World!"}))该接口返回{:message "Hello, World!"},经wrap-json-response中间件输出为{"message":"Hello, World!"}。toolset 中的验证逻辑 json/json.py 会调用verify_helloworld_object(定义于 verifications.py)检查:响应必须是包含message键的 JSON 对象,且值大小写不敏感地等于hello, world!;若对象多于 1 个键会给出 warn,多余字节也会被提示优化。
3.3 Test 2/3:单条与多条数据库查询(/db、/queries)
查询的核心是get-random-world与run-queries:
(defn get-random-world "Query a random World record from the database" [] (let [id (inc (rand-int 9999))] (jdbc/execute! (db-mysql) ["select * from world where id = ?" id] {:builder-fn rs/as-unqualified-lower-maps}))) (defn run-queries "Run query repeatedly, return an array" [queries] (flatten (take queries (repeatedly get-random-world))))实现要点:
- 通过
(inc (rand-int 9999))生成 1–10000 的随机主键,对应 TFB 规范中world表的 10000 行数据; - SQL 使用
?占位符 + 参数绑定,走 prepared statement; rs/as-unqualified-lower-maps让结果集以小写、无前缀的 map形式返回,即{:id 2354 :randomNumber 8952}的 Clojure 数据结构,再由 JSON 中间件序列化输出(键名最终呈现为id、randomNumber,与规范一致);run-queries用repeatedly+take执行 N 次查询并flatten成数组,/queries接口返回的是一个 JSON 数组而非单个对象。
/db单查接口single-query-test取查询结果的first元素返回单个 JSON 对象;/queries多查接口multiple-queries-test则先对queries参数做清洗再批量执行。
3.4 queries 参数清洗:clamp 到 1–500
multiple-queries-test与db-updates共用sanitize-queries-param对queries路径参数做归一化:
(defn sanitize-queries-param "Sanitizes the `queries` parameter. Clamps the value between 1 and 500. Invalid (string) values become 1." [queries] (let [n (try (Integer/parseInt queries) (catch Exception _ 1))] (cond (< n 1) 1 (> n 500) 500 :else n)))- 非整数输入(如
foo)或解析失败时取默认值1; - 小于 1 的输入钳制为
1,大于 500 的输入钳制为500。
这一行为与 toolset 的验证脚本高度吻合:query/query.py 和 update/update.py 都会用[('2', 'fail'), ('0', 'fail'), ('foo', 'fail'), ('501', 'warn'), ('', 'fail')]这组用例逐一打向接口,并通过 verifications.py 中的verify_query_cases检查返回数组长度是否与期望值一致(501 被钳制为 500、0 被钳制为 1、foo期望长度为 1)。这说明sanitize-queries-param的 clamp 语义是通过基准验证的必要条件。
3.5 Test 4:Fortunes(/fortunes)
Fortunes 测试包含数据库读取、内存排序与 HTML 渲染三个环节:
(defn get-all-fortunes "Query all Fortune records from the database using next.jdbc." [] (jdbc/execute! (db-mysql) ["select * from fortune"] {:builder-fn rs/as-unqualified-lower-maps})) (defn get-fortunes [query-function] (sort-by #(:message %) (conj (query-function) {:id 0 :message "Additional fortune added at request time."}))) (defn fortunes-hiccup "Render the given fortunes to simple HTML using Hiccup." [fortunes] (html5 [:head [:title "Fortunes"]] [:body [:table [:tr [:th "id"] [:th "message"]] (for [x fortunes] [:tr [:td (:id x)] [:td (escape-html (:message x))]])]]))关键设计:
- 按 TFB 规范,应用层必须自行注入一条
{:id 0 :message "Additional fortune added at request time."}记录后再排序,注入前先query-function拉取全部 12 条,排序后共 13 行; - 排序键是
:message,即 fortune 文案的字典序; - HTML 由 Hiccup 的
html5生成,且对 message 调用escape-html做转义——create.sql 预置数据中恰有一条含<script>alert(...)</script>的 XSS 型记录,验证时 FortuneHTMLParser 期望它被渲染为<script>...转义形式,因此escape-html是 Fortunes 通过的硬性要求; - 响应通过
ring-resp/content-type "text/html"与charset "utf-8"显式指定 Content-Type,toolset 的 verify_headers 会校验Content-Type必须匹配^text/html; ?charset=(UTF|utf)-8$。此外 verifications.py 只允许三种 Content-Type 形态:JSON、HTML(带 utf-8 字符集)与纯文本,这里 HTML 必须携带 charset,因此fortune-test中的 charset 设置不可或缺。
3.6 Test 5:数据库更新(/updates)
(defn update-and-persist "Using next.jdbc: Changes the :randomNumber of a number of world entities. Persists the changes to sql then returns the updated entities" [queries] (let [world (map #(assoc % :randomnumber (inc (rand-int 9999))) (run-queries queries))] (doseq [{:keys [id randomnumber]} world] (jdbc/execute-one! (db-mysql) ["UPDATE world SET randomnumber = ? WHERE id = ?" randomnumber id])) world))流程为:先按queries数量查询随机 World 行 → 将每行的:randomnumber重新随机赋值(同样限制在 1–10000)→ 逐条执行UPDATE持久化 → 返回更新后的实体列表作为 JSON 响应。toolset 的更新验证(verifications.py 的verify_updates与 mysql.py 的get_rows_updated)会对比更新前后数据库内容,确认确实发生了写入,而不是只返回数据。
3.7 Test 6:Plaintext(/plaintext)
(def plaintext "Test 6: Plaintext" (-> (ring-resp/response "Hello, World!") (ring-resp/content-type "text/plain")))该接口返回text/plain类型的固定字符串。toolset 的 plaintext/plaintext.py 会检查响应必须包含hello, world!(大小写不敏感),并对超出必需字节的额外内容给出 warn;压测则使用带流水线(pipeline)的 pipeline.sh 脚本。
四、数据库访问:HikariCP 连接池与 next.jdbc
4.1 连接池配置
make-hikari-data-source构建了整套 HikariCP 连接池(经memoize包装为单例,避免每次请求重建):
(defn make-hikari-data-source [] (make-datasource { :idle-timeout 15000 :max-lifetime 60000 :minimum-idle 0 :maximum-pool-size 1024 :pool-name "db-pool" :driver-class-name "com.mysql.cj.jdbc.Driver" :jdbc-url "jdbc:mysql://tfb-database:3306/hello_world?jdbcCompliantTruncation=false&elideSetAutoCommits=true&useLocalSessionState=true&cachePrepStmts=true&cacheCallableStmts=true&alwaysSendSetIsolation=false&prepStmtCacheSize=4096&cacheServerConfiguration=true&prepStmtCacheSqlLimit=2048&zeroDateTimeBehavior=convertToNull&traceProtocol=false&useUnbufferedInput=false&useReadAheadInput=false&maintainTimeStats=false&useServerPrepStmts=true&cacheRSMetadata=true&useSSL=false" :username "benchmarkdbuser" :password "benchmarkdbpass" :register-mbeans false}))参数含义与基准场景的对应关系:
| 参数 | 值 | 作用 |
|---|---|---|
:maximum-pool-size | 1024 | 池最大连接数,匹配高并发压测需求 |
:minimum-idle | 0 | 空闲最小连接数(0 表示可收缩) |
:idle-timeout | 15000 ms | 空闲连接回收超时 |
:max-lifetime | 60000 ms | 连接最大存活时间 |
:driver-class-name | com.mysql.cj.jdbc.Driver | 对应mysql-connector-j9.6.0 驱动 |
:jdbc-url | jdbc:mysql://tfb-database:3306/hello_world?... | 连接tfb-database主机的hello_world库 |
:username/:password | benchmarkdbuser/benchmarkdbpass | 与 create.sql 创建的账号一致 |
JDBC URL 中携带了大量 MySQL 连接优化参数:cachePrepStmts=true与prepStmtCacheSize=4096(启用并扩大预编译语句缓存)、useServerPrepStmts=true(使用服务端预编译)、useLocalSessionState=true(减少会话状态往返)、elideSetAutoCommits=true、alwaysSendSetIsolation=false、maintainTimeStats=false等,目的都是削减每次查询的网络往返与元数据开销,最大化吞吐——这是基准测试场景下的典型 MySQL JDBC 调优手法。账号benchmarkdbuser对应 create.sql 中创建的 MySQL 用户,数据库名为hello_world。
4.2 测试数据库的初始化
TFB 的 MySQL 容器由 mysql.dockerfile 构建,基于mysql:9.4,将 create.sql 放入/docker-entrypoint-initdb.d/实现首启初始化:创建world(10000 行随机randomNumber)与fortune(12 条预置文案)两张 InnoDB 表,并设置时区为+00:00。这正是handler.clj中world、fortune表结构与随机数范围(1–10000)的出处。
五、构建与部署:Leiningen + Resin 多阶段镜像
5.1 多阶段 Dockerfile
compojure.dockerfile 采用三个构建阶段:
- leiningen 构建阶段:基于
clojure:lein,COPY src src与COPY project.clj project.clj后执行lein ring uberwar,产出target/hello-compojure-standalone.war; - resin 准备阶段:基于
curlimages/curl,下载 Resin 4.0.66 并解压到/resin,清理默认webapps/*后把 WAR 复制为webapps/ROOT.war(即部署为根上下文); - 运行阶段:基于
amazoncorretto:25(Amazon Corretto JDK 25),将 Resin 与自定义的 resin.xml 复制进镜像,暴露 8080 端口后启动:
CMD ["java", "-XX:MaxRAMPercentage=70", "-Dclojure.compiler.direct-linking=true", "-jar", "lib/resin.jar", "console"]启动参数中-XX:MaxRAMPercentage=70让 JVM 堆按容器内存上限自适应分配(而非固定值,便于 TFB 统一分配资源),-Dclojure.compiler.direct-linking=true开启 Clojure 直接调用链接以降低反射/多态分发开销,属于基准场景下的常见 JVM/Clojure 调优项。
5.2 Resin 配置
resin.xml 是一个精简的 Resin 集群配置:
<cluster id=""> <resin:import path="/resin/conf/app-default.xml" /> <log name="" level="config" path="stdout:" timestamp="[%H:%M:%S.%s] " /> <server id=""> <http port="8080" /> </server> <host> <web-app-deploy path="/resin/webapps" expand-preserve-fileset="WEB-INF/work/**"/> </host> </cluster>- HTTP 监听端口 8080,与 benchmark_config.json 中的
"port": 8080一致; - 日志输出到 stdout 并带时间戳,便于容器化收集;
- 通过
web-app-deploy从/resin/webapps部署 WAR,expand-preserve-fileset保留WEB-INF/work/**目录; - 导入 Resin 自带的
app-default.xml提供 Servlet 容器默认行为。
这解释了 benchmark 元数据中"platform": "Servlet"与"webserver": "Resin"的由来:Compojure 应用被打包为标准 WAR,运行在 Servlet 容器 Resin 之上。
六、基准测试配置与验证闭环
6.1 元数据与测试入口
benchmark_config.json 定义了该框架的完整测试描述:
{ "framework": "compojure", "tests": [{ "default": { "json_url": "/json", "db_url": "/db", "query_url": "/queries/", "update_url": "/updates/", "fortune_url": "/fortunes", "plaintext_url": "/plaintext", "port": 8080, "approach": "Realistic", "classification": "Micro", "database": "MySQL", "framework": "compojure", "language": "Clojure", "flavor": "None", "orm": "Micro", "platform": "Servlet", "webserver": "Resin", "os": "Linux", "database_os": "Linux", "display_name": "compojure", "notes": "", "versus": "servlet" } }] }config.toml 是同一信息的 TOML 形态,二者共同定义了测试 URL 与框架分类。其中versus: "servlet"表示 Compojure 被归入 Servlet 平台对照组。
6.2 响应如何被验证
toolset 的测试类型脚本会按端点类型对 Compojure 的响应做严格校验:
- /json(json/json.py):校验为
{message: "Hello, World!"}且键值多余项、多余字节触发 warn; - /db(db/db.py):校验为含
id与randomNumber整数键的 JSON 对象,id须在 1–10000; - /queries 与 /updates(query/query.py、update/update.py):按 2/0/foo/501/空串 五组用例校验数组长度与钳制行为,updates 还需校验数据库真实写入(verifications.py);
- /fortunes(fortune/fortune.py + fortune_html_parser.py):解析 HTML 与内置 13 行规范对比,校验排序、注入记录与 HTML 转义;
- /plaintext(plaintext/plaintext.py):校验文本内容与响应字节数。
所有这些校验都在 verifications.py 中汇总实现,其中verify_headers还会强制要求响应携带Server、Date、Content-Type与Content-Length/Transfer-Encoding等头,并校验 Date 不被缓存、Content-Type 必须匹配 JSON/HTML/plaintext 三种既定形态之一。因此 handler.clj 中 Fortunes 显式设置text/html; charset=utf-8、Plaintext 显式设置text/plain都是通过验证的必要条件。
七、小结:从路由到部署的完整链路
回看整个 Compojure 基准测试实现,可以提炼出一条清晰的技术链路:
- 路由层:
defroutes以简洁的 DSL 映射六个测试端点,路径参数:queries与无参路由互补,保证不同压测方式都能命中; - 数据层:HikariCP(1024 连接池)承载高并发,next.jdbc 提供参数化查询与
as-unqualified-lower-maps结果映射,配合 MySQL JDBC 连接参数优化降低开销; - 渲染层:Hiccup 以数据驱动的向量语法生成 HTML,配合
escape-html满足 Fortunes 的转义校验; - 参数归一化:
sanitize-queries-param的 1–500 钳制直接对应用户侧verify_query_cases的边界用例,是基准通过与否的关键细节; - 构建部署层:
lein ring uberwar产出 WAR,多阶段 Dockerfile 在 Resin 4.0.66 上以根上下文部署,JVM 与 Clojure 编译参数按容器基准环境调优。
这套实现完整覆盖了 TechEmpower 基准的六大场景,其代码组织方式(路由 → 连接池 → SQL → 序列化/渲染)也是 Clojure Web 应用在 Servlet 平台上的一个典型范例。想深入调试或复现,可直接查看 handler.clj(约 160 行)、project.clj 与 compojure.dockerfile,并结合 toolset 的测试类型脚本理解验证口径。
- 性能测试
- 测试
- 后端
- 开发者工具
【免费下载链接】FrameworkBenchmarks
Source for the TechEmpower Framework Benchmarks project
相关推荐
CubeFS 元数据性能评估:基于 mdtest 的目录与文件操作基准测试实战指南
CubeFS 元数据性能评估:基于 mdtest 的目录与文件操作基准测试实战指南 本指南以 CubeFS 官方在 v3.3.1 集群环境下使用 mdtest
存储分布式文件系统对象存储云原生CubeFS 元数据性能评估:基于 mdtest 的目录与文件操作基准测试指南
CubeFS 元数据性能评估:基于 mdtest 的目录与文件操作基准测试指南 导读 本文档是 CubeFS 元数据性能评估的权威参考,基于官方在 v3.3.1
存储分布式文件系统对象存储云原生Sequel Ace单元测试实践:基于SPTestingUtils构建可靠数据库操作测试
Sequel Ace单元测试实践:基于SPTestingUtils构建可靠数据库操作测试 在数据库管理工具开发中,单元测试是保障功能稳定性的关键环节。Seque
数据库桌面应用开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考