
1. 项目概述为什么在 FreeBSD 10.2 上部署 Clojure Web 应用不是“怀旧”而是务实选择Clojure 这门以不可变数据结构、函数式编程范式和 JVM 生态为根基的语言从诞生起就带着一种“反主流但极高效”的气质。它不追求语法糖的堆砌而是用简洁的括号表达力把并发、状态管理、REPL 驱动开发这些现代 Web 后端的核心痛点变成了可推演、可验证的代码逻辑。而 FreeBSD —— 这个被 Netflix、WhatsApp、Yelp 等高并发服务长期信赖的操作系统它的 ZFS 文件系统快照与克隆能力、内核级网络栈优化如 TCP BBR 的早期支持、jail 轻量级虚拟化机制以及对硬件资源近乎苛刻的调度控制让它在稳定性、安全性和 I/O 效率上至今仍是 Linux 发行版难以全面替代的“硬核底座”。当这两者结合你得到的不是一个技术考古现场而是一套面向生产环境的“低熵系统”更少的意外重启、更可预测的 GC 延迟、更干净的进程隔离边界。我第一次在 FreeBSD 10.2 上跑起一个 Ring Compojure 的 Clojure Web 服务时并不是为了标新立异。当时手头是一个需要处理大量传感器上报 JSON 的物联网网关 API要求 7×24 小时不间断且日志必须精确到毫秒级时间戳ZFS 的atime禁用和logbiasthroughput设置直接帮我省去了 NTP 同步抖动带来的日志乱序问题。Linux 上常见的systemd依赖链崩溃、cgroup内存限制误杀 JVM 进程、iptables规则加载延迟导致连接超时等问题在 FreeBSD 的rc.d服务框架和pf防火墙下几乎绝迹。Leiningen 作为 Clojure 社区事实标准的构建工具其lein uberjar生成的 fat jar 在 FreeBSD 的jail中运行得比在 Docker 容器里更轻量——没有 overlayfs 层叠文件系统的写时复制开销也没有runc运行时的额外上下文切换。而 Supervisor 并非必须但它是我在多实例热更新场景下的“保险丝”当需要滚动升级一个包含 3 个 worker 进程的 Ring 服务时Supervisor 的startsecs和stopwaitsecs参数能确保新 JVM 进程真正完成类加载、数据库连接池初始化、缓存预热后才切断旧进程而不是像kill -9那样粗暴。至于 Nginx它在这里的角色远不止是反向代理。FreeBSD 10.2 的kqueue事件驱动模型与 Nginx 的epoll/kqueue自适应机制深度耦合使得单机承载数万长连接成为可能而proxy_buffering off配合proxy_http_version 1.1能让 Clojure 的流式响应比如 Server-Sent Events零缓冲直达客户端这是很多基于 HTTP/1.0 的传统代理无法做到的。所以这个标题不是一份过时的操作系统说明书而是一份面向高 SLA 要求、中等规模 Clojure 服务的“稳态部署手册”。2. 整体架构设计与核心组件选型逻辑2.1 为什么是 FreeBSD 10.2而不是更新的 13.x 或 14.xFreeBSD 10.2 发布于 2015 年 10 月表面看已是“古董”但它的内核 ABIApplication Binary Interface稳定性和用户空间工具链成熟度恰恰是生产环境最看重的。我曾对比过在 10.2、12.4 和 13.2 上部署同一套 Clojure 应用的实测数据在持续 72 小时的压测中10.2 的平均连接建立延迟time_connect比 13.2 低 8.3%原因在于其tcp_slowstart_flightsize默认值为 10而 13.x 已调整为 1这在高丢包率的广域网环境下反而更利于快速恢复吞吐。更重要的是10.2 的pkg包管理器仓库虽已归档但通过配置/etc/pkg/FreeBSD.conf指向http://pkg.FreeBSD.org/freebsd:10:x86:64/latest的镜像源如https://mirrors.ustc.edu.cn/freebsd-pkg/仍能获得经过严格签名验证的二进制包。最关键的一点是兼容性许多企业级 Java 库如 Oracle JDBC Driver对较新 glibc 的符号版本有强依赖而 FreeBSD 使用的是libc其 ABI 在 10.x 系列内保持完全向后兼容。这意味着你编译好的uberjar在 10.2 上跑十年只要 JVM 版本不变行为就绝不会突变。这不是保守而是对“确定性”的极致追求。2.2 Leiningen构建而非编译Clojure 的哲学落地Leiningen 的核心价值从来不是“编译速度”而是“环境一致性”。project.clj文件定义的不仅是依赖坐标更是整个 REPL 开发会话的沙盒边界。当你执行lein uberjar时Leiningen 实际上在做三件事第一解析:dependencies中的 Maven 坐标下载所有 JAR 到~/.m2/repository并校验 SHA-1第二将:source-paths下的所有.clj文件连同:resource-paths下的静态资源如resources/public/css/app.css按:aotAhead-of-Time指定的命名空间进行字节码预编译注意Clojure 的 AOT 不是必须的但对启动速度敏感的服务很有用第三用jar命令将所有内容打包同时在META-INF/MANIFEST.MF中写入Main-Class: myapp.core。这个过程在 FreeBSD 上没有任何特殊之处但有一个关键细节FreeBSD 的默认ulimit -n最大打开文件数是 11096而一个典型的 Clojure Web 应用在高并发下很容易突破此限。因此project.clj中必须显式设置 JVM 参数:jvm-opts [-Xms512m -Xmx2g -XX:UseG1GC -Dfile.encodingUTF-8 -Dsun.jnu.encodingUTF-8]其中-Xmx2g是上限而-XX:UseG1GC是必须的因为 FreeBSD 10.2 的 JVMOpenJDK 8u292对 G1 收集器的支持比 CMS 更完善。如果你跳过这一步应用在启动时就会因java.lang.OutOfMemoryError: Metaspace而失败错误日志里却只显示Could not create the Java virtual machine这是新手最容易踩的坑。2.3 Supervisor进程守护的“最小必要原则”Supervisor 在这里不是为了替代 FreeBSD 原生的rc.d而是为了填补rc.d的空白。rc.d擅长管理“启动即稳定”的服务如sshd,ntpd但它不擅长监控进程的“健康状态”。一个 Clojure 应用可能启动成功ps aux | grep java能看到进程但内部的数据库连接池却因网络抖动而耗尽此时rc.d无动于衷。Supervisor 的autostarttrue和autorestartunexpected组合能确保进程在非预期退出如exit code 137表示 OOM Killer 杀死后自动拉起。但最关键的配置是startretries3和startsecs30前者防止因依赖服务如 PostgreSQL未就绪而导致的无限重启循环后者强制 Supervisor 等待 30 秒确认 JVM 进程不仅存在而且已响应 HTTP 健康检查可通过curl -f http://localhost:3000/health实现。我通常会把 Supervisor 的配置文件/usr/local/etc/supervisord.conf精简到极致[supervisord] nodaemonfalse userroot logfile/var/log/supervisord.log pidfile/var/run/supervisord.pid [program:my-clojure-app] command/usr/local/openjdk8/bin/java -Xms512m -Xmx2g -jar /opt/myapp/myapp-0.1.0-standalone.jar directory/opt/myapp userwww autostarttrue autorestartunexpected startretries3 startsecs30 stopwaitsecs60 redirect_stderrtrue stdout_logfile/var/log/myapp/app.log注意userwwwFreeBSD 的www用户是专为 Web 服务创建的其 UID/GID 为 80权限被严格限制在/var/www和/tmp下这比用root运行安全得多。2.4 Nginx从反向代理到流量整形的中枢Nginx 在此架构中承担了四重角色第一SSL 终结。FreeBSD 10.2 的 OpenSSL 版本1.0.2u已支持 TLS 1.2但不支持 1.3。因此Nginx 的ssl_protocols TLSv1.2;是必须的而ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;则强制使用前向保密PFS算法避免私钥泄露后历史流量被解密。第二连接复用。proxy_http_version 1.1;和proxy_set_header Connection ;组合让 Nginx 与后端 Clojure 应用之间维持长连接池避免每次请求都重建 TCP 连接。第三请求整形。limit_req_zone $binary_remote_addr zoneapi:10m rate10r/s;可以限制单个 IP 每秒最多 10 次请求这对防爬虫和暴力破解至关重要。第四静态资源服务。Clojure 应用本身可以 servepublic/目录但 Nginx 的sendfile on;指令能利用 FreeBSD 的sendfile(2)系统调用实现零拷贝文件传输比 JVM 的FileChannel.transferTo()性能高出 30%。一个典型的server块配置如下server { listen 443 ssl http2; server_name api.example.com; ssl_certificate /usr/local/etc/nginx/ssl/fullchain.pem; ssl_certificate_key /usr/local/etc/nginx/ssl/privkey.pem; # SSL 优化 ssl_protocols TLSv1.2; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256; ssl_prefer_server_ciphers off; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; # 静态资源 location ~ ^/(css|js|img|fonts)/ { alias /var/www/myapp/; expires 1h; add_header Cache-Control public, immutable; } # API 代理 location / { proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_buffering off; proxy_read_timeout 300; } }其中proxy_buffering off;是针对 Clojure 流式响应的关键开关否则 Nginx 会缓存整个响应体再返回破坏 SSE 和 chunked encoding 的实时性。3. 核心细节解析与实操要点3.1 FreeBSD 10.2 系统初始化从裸机到 Clojure 就绪在物理机或 KVM 虚拟机上安装 FreeBSD 10.2第一步是禁用所有非必要服务。安装过程中在sysinstall的Configure - Networking - Interfaces里只启用实际使用的网卡如em0并取消勾选IPv6除非你明确需要。安装完成后首次登录立即执行# 更新系统到 10.2-RELEASE-p29最后一个安全补丁版本 freebsd-update fetch freebsd-update install # 配置 pkg 源为国内镜像USTC echo packagesite: http://mirrors.ustc.edu.cn/freebsd-pkg/102amd64/latest /usr/local/etc/pkg.conf # 升级 pkg 工具本身 pkg update pkg upgrade -y pkg # 安装基础工具 pkg install -y bash git vim-console curl wget sudo接着创建专用用户clojure并赋予其www组权限pw useradd clojure -m -s /usr/local/bin/bash -G www passwd clojure然后配置sudo允许clojure用户无需密码执行supervisorctlecho clojure ALL(ALL) NOPASSWD: /usr/local/bin/supervisorctl /usr/local/etc/sudoers最关键的一步是内核参数调优。编辑/boot/loader.conf添加# 提高网络连接数 kern.ipc.somaxconn1024 net.inet.tcp.delayed_ack0 net.inet.tcp.mssdflt1440 # 提高文件描述符限制 kern.maxfiles65536 kern.maxfilesperproc65536 # ZFS 优化如果根文件系统是 ZFS vfs.zfs.arc_max2G最后重启系统使内核参数生效。此时sysctl kern.maxfiles应返回65536证明配置成功。3.2 OpenJDK 8 与 Leiningen 的离线部署方案FreeBSD 10.2 的官方仓库中openjdk8的版本是8u292这是一个经过充分测试的 LTS 版本。但如果你的网络环境受限如企业内网无外网访问必须采用离线部署。步骤如下在一台有网的 FreeBSD 10.2 机器上执行pkg fetch openjdk8 leiningen会下载openjdk8-8.292.10_1.txz和leiningen-2.10.0.txz两个文件。将这两个.txz文件拷贝到目标机器的/tmp目录。执行pkg add /tmp/openjdk8-8.292.10_1.txz /tmp/leiningen-2.10.0.txz。验证安装java -version应输出openjdk version 1.8.0_292lein version应输出Leiningen 2.10.0。提示Leiningen 的~/.lein/profiles.clj文件是全局配置中心。为避免不同项目间的依赖冲突我强烈建议在每个 Clojure 项目的根目录下创建一个profiles.clj文件与project.clj同级内容为{:dev {:dependencies [[org.clojure/tools.namespace 1.3.0]] :plugins [[com.jakemccrary/lein-test-refresh 0.24.1]]}}这样lein test-refresh命令只在当前项目生效不会污染全局环境。3.3 Supervisor 的精细化进程管理Supervisor 的supervisord.conf配置中stopwaitsecs60是一个经验值。它表示 Supervisor 在发送SIGTERM后会等待 60 秒再发送SIGKILL。这个时间必须大于 Clojure 应用的“优雅关闭”耗时。在你的 Clojure 代码中必须注册 JVM 关闭钩子Shutdown Hook(ns myapp.core (:require [ring.adapter.jetty :as jetty] [clojure.java.io :as io])) (defonce server (atom nil)) (defn start-server [] (reset! server (jetty/run-jetty app {:port 3000 :join? false}))) (defn stop-server [] (when-let [s server] (.stop s) (reset! server nil))) ;; 注册关闭钩子 (.addShutdownHook (Runtime/getRuntime) (Thread. #(do (println Shutting down gracefully...) (stop-server) (println Shutdown complete.)))) (defn -main [ args] (start-server))这样当 Supervisor 执行supervisorctl stop my-clojure-app时JVM 会先执行stop-server关闭 Jetty 连接池、释放数据库连接、清空本地缓存然后再退出。如果stopwaitsecs设得太短如 10 秒SIGKILL会强行终止 JVM导致数据库连接泄漏、文件句柄未关闭等严重后果。3.4 Nginx 配置的“安全红线”与性能陷阱Nginx 的ssl_certificate和ssl_certificate_key必须指向由可信 CA如 Lets Encrypt签发的证书。FreeBSD 10.2 的openssl命令不支持 ACME 协议因此你需要在另一台机器上用certbot获取证书再手动拷贝。证书文件权限必须是600chmod 600 /usr/local/etc/nginx/ssl/privkey.pem chown root:wheel /usr/local/etc/nginx/ssl/privkey.pem否则 Nginx 启动会报错SSL_CTX_use_PrivateKey_file(/usr/local/etc/nginx/ssl/privkey.pem) failed (SSL: error:0200100D:system library:fopen:Permission denied)。另一个常见陷阱是proxy_read_timeout。Clojure Ring 应用的默认 Jetty 超时是 30 秒但如果应用中有耗时的数据库查询或外部 API 调用30 秒可能不够。此时必须同步调整 Nginx 的proxy_read_timeout和 Jetty 的:max-idle-time参数。例如若业务要求最长 5 分钟响应则在project.clj中添加:ring {:handler myapp.handler/app :init myapp.handler/init :destroy myapp.handler/destroy :server-port 3000 :max-idle-time 300000} ; 5 分钟单位毫秒并在 Nginx 配置中对应设置proxy_read_timeout 300;。两者必须一致否则 Nginx 会在 Jetty 还未返回时就断开连接客户端收到504 Gateway Timeout。4. 实操过程与核心环节实现4.1 从零开始一个可运行的 Clojure Web 应用示例我们创建一个极简的健康检查 API用于验证整个链路是否通畅。首先在 FreeBSD 上切换到clojure用户su - clojure然后使用 Leiningen 创建新项目lein new myapp cd myapp编辑src/myapp/core.clj替换为以下内容(ns myapp.core (:require [ring.adapter.jetty :as jetty] [ring.util.response :as response] [ring.middleware.defaults :refer [wrap-defaults api-defaults]] [clojure.java.io :as io]) (:gen-class)) (defn health-handler [request] (response/response {:status 200 :body (str {\status\:\ok\,\timestamp\: (System/currentTimeMillis) }) :headers {Content-Type application/json}})) (def app (- (constantly (health-handler {})) (wrap-defaults api-defaults))) (defn -main [ args] (let [port (Integer/parseInt (or (System/getenv PORT) 3000))] (println (str Starting server on port port)) (jetty/run-jetty app {:port port :join? false})))这个应用只有一个/路径返回 JSON 格式的健康状态。接着修改project.clj确保:main指向正确的命名空间(defproject myapp 0.1.0-SNAPSHOT :description A minimal Clojure web app for FreeBSD :url http://example.com/FIXME :license {:name Eclipse Public License :url http://www.eclipse.org/legal/epl-v10.html} :dependencies [[org.clojure/clojure 1.10.3] [ring/ring-core 1.9.5] [ring/ring-jetty-adapter 1.9.5]] :main ^:skip-aot myapp.core :target-path target/%s :profiles {:uberjar {:aot :all}})现在执行构建lein do clean, uberjar成功后你会在target/uberjar/目录下看到myapp-0.1.0-SNAPSHOT-standalone.jar。将其重命名为myapp-0.1.0-standalone.jar并拷贝到/opt/myapp/sudo mkdir -p /opt/myapp sudo cp target/uberjar/myapp-0.1.0-SNAPSHOT-standalone.jar /opt/myapp/myapp-0.1.0-standalone.jar sudo chown www:www /opt/myapp/myapp-0.1.0-standalone.jar4.2 Supervisor 服务的注册与验证创建 Supervisor 的程序配置文件/usr/local/etc/supervisord.d/myapp.ini[program:myapp] command/usr/local/openjdk8/bin/java -Xms512m -Xmx2g -jar /opt/myapp/myapp-0.1.0-standalone.jar directory/opt/myapp userwww autostarttrue autorestartunexpected startretries3 startsecs30 stopwaitsecs60 redirect_stderrtrue stdout_logfile/var/log/myapp/app.log stdout_logfile_maxbytes10MB stdout_logfile_backups5 environmentPORT3000注意environmentPORT3000这会将环境变量注入 JVM 进程供 Clojure 代码读取。然后重新加载 Supervisor 配置sudo supervisorctl reread sudo supervisorctl update sudo supervisorctl start myapp验证进程是否运行sudo supervisorctl status # 输出应为myapp RUNNING pid 12345, uptime 0:00:10同时检查日志sudo tail -f /var/log/myapp/app.log # 应看到Starting server on port 30004.3 Nginx 的完整配置与 HTTPS 强制跳转创建 Nginx 的站点配置/usr/local/etc/nginx/sites-enabled/myapp.confupstream myapp_backend { server 127.0.0.1:3000; } # HTTP 重定向到 HTTPS server { listen 80; server_name api.example.com; return 301 https://$server_name$request_uri; } server { listen 443 ssl http2; server_name api.example.com; ssl_certificate /usr/local/etc/nginx/ssl/fullchain.pem; ssl_certificate_key /usr/local/etc/nginx/ssl/privkey.pem; ssl_protocols TLSv1.2; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256; ssl_prefer_server_ciphers off; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; # 日志格式 log_format main $remote_addr - $remote_user [$time_local] $request $status $body_bytes_sent $http_referer $http_user_agent $request_time $upstream_response_time; access_log /var/log/nginx/myapp_access.log main; error_log /var/log/nginx/myapp_error.log warn; location / { proxy_pass http://myapp_backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_buffering off; proxy_read_timeout 300; proxy_send_timeout 300; } }然后确保主配置/usr/local/etc/nginx/nginx.conf包含该文件http { include sites-enabled/*.conf; # ... 其他配置 }最后测试配置并重启 Nginxsudo nginx -t # 输出应为nginx: the configuration file /usr/local/etc/nginx/nginx.conf syntax is ok # nginx: configuration file /usr/local/etc/nginx/nginx.conf test is successful sudo service nginx restart4.4 端到端验证从浏览器到日志的全链路追踪现在打开浏览器访问https://api.example.com请将api.example.com替换为你实际的域名或在/etc/hosts中添加127.0.0.1 api.example.com。你应该看到{status:ok,timestamp:1717023456789}接着检查 Nginx 访问日志sudo tail -1 /var/log/nginx/myapp_access.log # 输出类似127.0.0.1 - - [30/May/2024:10:23:45 0000] GET / HTTP/1.1 200 45 - Mozilla/5.0 ... 0.012 0.011其中0.012是 Nginx 处理总时间秒0.011是上游 Clojure 应用的响应时间证明代理链路通畅。最后模拟一次优雅重启验证 Supervisor 的可靠性# 手动杀死 Clojure 进程 sudo pkill -f myapp-0.1.0-standalone.jar # 等待 5 秒检查状态 sudo supervisorctl status # 输出应为myapp STARTING # 再等 30 秒再次检查 sudo supervisorctl status # 输出应为myapp RUNNING pid 67890, uptime 0:00:05此时/var/log/myapp/app.log中会有新的Starting server on port 3000日志证明 Supervisor 成功完成了故障自愈。5. 常见问题与排查技巧实录5.1 “Connection refused” 错误的三层定位法当curl https://api.example.com返回Failed to connect to api.example.com port 443: Connection refused时不要急于重装软件按以下三层顺序排查第一层Nginx 是否监听sudo sockstat -4 | grep :443 # 正常输出www nginx 12345 7 tcp4 *:443 *:* # 如果无输出说明 Nginx 未启动或配置错误 sudo service nginx status sudo nginx -t第二层Supervisor 是否启动了 Clojure 进程sudo supervisorctl status # 如果是 FATAL 或 STARTING查看日志 sudo supervisorctl tail myapp stderr # 常见错误Error: Could not find or load main class myapp.core原因是 project.clj 中 :main 路径写错第三层Clojure 进程是否真的在监听 3000 端口sudo sockstat -4 | grep :3000 # 正常输出www java 67890 123 tcp4 *:3000 *:* # 如果无输出但 Supervisor 显示 RUNNING说明 JVM 启动失败检查 stdout_logfile sudo tail -20 /var/log/myapp/app.log # 常见错误java.lang.OutOfMemoryError: Java heap space需增大 -Xmx 参数5.2 “502 Bad Gateway” 的七种可能与修复Nginx 返回502意味着它无法与上游127.0.0.1:3000建立连接。根据我的实战经验原因及修复如下表序号可能原因排查命令修复方案1Clojure 进程未启动sudo supervisorctl statussudo supervisorctl start myapp2Clojure 进程启动但未监听 3000sudo sockstat -4 | grep :3000检查project.clj中:server-port和:main函数中的:port参数是否一致3防火墙阻止了本地回环sudo pfctl -sr | grep block编辑/etc/pf.conf确保有pass quick on lo0 all规则4Nginx 配置中proxy_pass地址错误sudo nginx -T | grep proxy_pass确认proxy_pass指向http://127.0.0.1:3000而非http://localhost:3000FreeBSD 的localhost解析可能受/etc/hosts影响5Clojure 应用绑定到了127.0.0.1但 Nginx 尝试连接::1sudo sockstat -6 | grep :3000在project.clj的:ring配置中显式指定:host 127.0.0.16ulimit -n不足Clojure 无法 accept 新连接sudo su - www -c ulimit -n在/etc/login.conf中为www用户增加:openfiles65536:然后执行cap_mkdb /etc/login.conf7Supervisor 的startsecs设置过短Nginx 在 Clojure 完全就绪前就发起了请求sudo supervisorctl tail myapp stdout查看日志中是否有Starting server on port 3000之后的请求记录如有增大startsecs5.3 日志分析从海量文本中提取关键信号FreeBSD 的日志分散在多个地方高效分析是运维的基本功。我常用的组合命令如下实时跟踪 Nginx 错误日志中的 5xx 错误sudo tail -f /var/log/nginx/myapp_error.log \| grep --line-buffered 5[0-9][0-9]统计过去一小时的请求来源 IP 及其 404 数量sudo awk -v cutoff$(date -v -1H %s) $4 [ strftime(%d/%b/%Y:%H:%M:%S, cutoff) {print $1} /var/log/nginx/myapp_access.log \| sort \| uniq -c \| sort -nr \| head -10分析 Clojure 应用的 GC 行为需在jvm-opts中添加-XX:PrintGCDetails -XX:PrintGCTimeStampssudo tail -1000 /var/log/myapp/app.log \| grep GC \| awk {print $1, $2, $3, $4, $5} \| column -t如果发现Full GC频繁发生说明-Xmx设置过小或内存泄漏需用jmap工具分析堆转储jmap -dump:formatb,file/tmp/heap.hprof pid。5.4 性能瓶颈的“三板斧”诊断当应用响应变慢时我习惯性地执行以下三个命令它们能覆盖 90% 的性能问题第一板斧top -P查看 CPU 核心占用top -P观察PID列找到占用 CPU 最高的 Java 进程。如果某个核心长期 100%而其他核心空闲说明应用是单线程瓶颈如 Ring 的默认 Jetty 配置只有 1 个工作线程需在project.clj中增加:thread-pool配置。第二板斧iostat -x 1查看磁盘 I/Oiostat -x 1关注%util和await列。如果%util接近 100% 且await 10