1. 这不是“部署教程”而是一份 Clojure Web 应用在 Ubuntu 14.04 上的生产级落地方案你搜到的标题里写着“How To Deploy...”但如果你真把它当成一个照着敲几行命令就能跑起来的“入门指南”那大概率会在凌晨三点盯着supervisorctl status的输出发呆或者反复刷新 Nginx 的 502 Bad Gateway 页面。我做过不下二十个 Clojure 生产服务的上线和迁移其中七次是在 Ubuntu 14.04 这个被官方标记为“EOLEnd of Life”却仍在大量老旧金融、教育、政企内网中服役的系统上。它不是过时的代名词而是一个需要你亲手校准每一颗螺丝的精密工作台。Clojure 本身是 JVM 语言它的部署本质不是“发布代码”而是构建一个可复现、可监控、可回滚、能扛住真实流量冲击的 Java 进程生命周期管理体系。Ubuntu 14.04 提供的不是便利而是一套明确的约束OpenJDK 7 是默认且最稳定的 JVMSystem V init 是唯一可靠的进程管理基础而apt-get源里打包的 Nginx 版本是 1.4.6——这个数字意味着你不能指望stream模块做 TCP 转发也不能用map指令做高级变量映射。所有“现代”部署方案在这里都必须向下兼容而不是向上适配。关键词里的Leiningen是你的构建中枢但它不是万能的——它生成的uberjar在 14.04 上可能因glibc版本差异而无法加载本地库Supervisor是你的进程守夜人但它不处理 JVM 的 OOM 自愈也不懂 Clojure 的热重载Nginx是你的流量前哨但它在 1.4.6 版本下对 WebSocket 的Upgrade头支持有已知缺陷必须手动补丁或绕过。这整套组合不是拼凑而是协同Leiningen 负责把代码变成一个确定性的二进制包Supervisor 负责让这个包在崩溃后 3 秒内复活Nginx 负责把外部世界的混沌请求翻译成 Clojure Ring Handler 能理解的干净 HTTP 流。适合谁看第一类是正在维护一套运行在物理服务器上的老 Clojure 系统的运维工程师你手头没有 Docker没有 Kubernetes只有一台装着 Ubuntu 14.04 的 Dell R720第二类是 Clojure 开发者你刚写完一个基于 Compojure 的 API 服务老板说“明天上线”而你连supervisord.conf里autorestart和startsecs的区别都说不清楚第三类是技术决策者你在评估是否值得为这套系统升级 OS那么本文会告诉你哪些问题是 OS 升级能解决的哪些问题是你换到 Ubuntu 22.04 也依然要亲手写的 Shell 脚本。这不是教你怎么“启动一个服务”而是带你走一遍从lein uberjar输出的那个.jar文件开始到它真正监听在0.0.0.0:3000再到 Nginx 把https://api.example.com/v1/users的请求精准转发过去并在日志里留下一条带毫秒时间戳和响应码的记录——中间每一步的原理、陷阱与实操细节我都拆开给你看。2. 整体架构设计与核心组件选型逻辑2.1 为什么是 Leiningen Uberjar而不是 Boot 或 tools.depsClojure 社区有三套主流构建工具Leiningen、Boot 和clojureCLI即 tools.deps。在 Ubuntu 14.04 这个环境里Leiningen 是唯一经过大规模验证的选择。原因很实际它的project.clj是一个完整的 Clojure 数据结构你可以用eval动态生成依赖版本这对需要在不同客户环境里切换 JDK 7/8 兼容性的项目至关重要它的插件生态如lein-ring、lein-ancient在 2014–2017 年间最为成熟而 Ubuntu 14.04 的黄金维护期恰恰覆盖了这一阶段。Boot 工具链依赖较新的clojure.core.async在 OpenJDK 7u80 下偶发线程挂起问题我们曾在一个实时日志聚合服务中复现过三次clojureCLI 则要求JAVA_HOME指向一个支持java -version输出格式为1.7.0_XX的 JDK而某些定制版 Oracle JDK 7 的输出是1.7.0-XX短横线而非点号导致clj命令直接退出。Leiningen 对此做了兼容性兜底——它会尝试多种正则匹配。lein uberjar生成的 fat jar 是关键。它把所有依赖包括ring-core、jetty9-adapter、cheshire全部打包进一个 JAR彻底规避了CLASSPATH环境变量在不同 shellbash vs dash下的解析差异。Ubuntu 14.04 的/bin/sh默认是dash它不支持 Bash 的数组语法而很多自动生成的启动脚本会误用$(ls *.jar)这种写法导致 classpath 拼接失败。Uberjar 一根筋到底一个文件一个入口java -jar app.jar就完事。注意lein uberjar默认使用:aotAhead-of-Time编译这对 Ring Handler 是必要的。如果你的 handler 定义在src/clj/myapp/handler.clj那么:aot [myapp.handler]必须显式写入project.clj否则 JVM 启动时会报ClassNotFoundException。这不是 bug是 Clojure 的设计哲学运行时编译带来灵活性AOT 编译保障启动确定性。2.2 为什么 Supervisor 是不可替代的进程管理器你可能会想“我直接写个 systemd service 不就行了吗”——不行。Ubuntu 14.04 的默认 init 系统是 Upstart而 systemd 是从 15.04 才开始引入的。Upstart 的配置文件.conf虽然也能管理进程但它缺乏 Supervisor 的三个核心能力进程组管理、输出流重定向控制、以及优雅重启的原子性保证。Supervisor 的program配置项里stopwaitsecs10意味着发送SIGTERM后它会等待最多 10 秒再发SIGKILL。这对 Clojure 应用至关重要Ring/Jetty 服务器收到SIGTERM后会先拒绝新连接再等待已有请求完成默认超时 30 秒最后关闭线程池。如果 Upstart 在 2 秒后就强行kill -9正在写数据库事务的请求就会被截断造成数据不一致。更关键的是日志。Supervisor 可以把stdout和stderr分别重定向到两个文件并自动轮转logfile_maxbytes1MB,logfile_backups5。而 Upstart 的console log指令只是把输出追加到/var/log/upstart/myapp.log没有任何轮转机制。一个高并发的 Clojure API 服务一天就能写满 20GB 日志撑爆根分区。还有一个隐藏优势Supervisor 的rpcinterface。你可以用 Python 脚本调用supervisorctl的 XML-RPC 接口实现“灰度发布”——比如先停掉 50% 的 worker 进程更新 JAR 包再启动它们同时监控 Nginx 的 upstream health check 状态。这种细粒度控制在 14.04 的原生工具链里只有 Supervisor 能提供。2.3 为什么是 Nginx 1.4.6而不是自己编译新版Ubuntu 14.04 的apt-get install nginx安装的是 1.4.6这是经过 Canonical 严格测试、与系统内核3.13.0深度集成的版本。自行编译 Nginx 1.20 看似先进但会立刻撞上三个墙PCRE 版本冲突14.04 自带的libpcre3是 8.31而新版 Nginx 要求 8.32。强行升级 PCRE 会导致apt-get upgrade时apache2、postfix等依赖它的软件包被标记为“broken”系统包管理器会拒绝操作。SSL/TLS 握手失败1.4.6 使用 OpenSSL 1.0.1f它支持 TLS 1.2但不支持 TLS 1.3那是 1.11 的事。这看似落后实则是好事——很多政府、银行的旧客户端如 Windows XP SP3 上的 IE8只认 TLS 1.0/1.1新版 Nginx 强制 TLS 1.2 会导致这些客户端完全无法连接。内存泄漏风险我们曾在一个客户现场编译 Nginx 1.16 并启用http_v2模块结果在持续 72 小时的压力测试后worker 进程 RSS 内存从 20MB 涨到 1.2GBvalgrind显示是ngx_http_v2_state_headers函数的 buffer 未释放。这个问题在 1.4.6 的http_ssl模块里不存在。所以我们的策略是拥抱 1.4.6 的限制并把它变成优势。比如用location ~* \.(js|css|png|jpg|gif|ico)$配置静态资源缓存用proxy_buffering onproxy_buffer_size 4k控制反向代理缓冲区大小用upstream的ip_hash实现最朴素的会话保持——这些功能在 1.4.6 里都稳定得像一块石头。3. 核心细节解析与实操要点3.1 Leiningen 构建环节从 project.clj 到可部署的 uberjarproject.clj是整个构建过程的源头活水。一个生产就绪的配置绝不是lein new compojure myapp生成的模板。以下是我在 Ubuntu 14.04 上验证过的最小可行project.clj(defproject myapp 0.1.0-SNAPSHOT :description My production Clojure web app :url http://example.com/myapp :min-lein-version 2.5.0 :dependencies [[org.clojure/clojure 1.7.0] [compojure 1.4.0] [ring/ring-defaults 0.1.5] [ring/ring-jetty-adapter 1.4.0] [cheshire 5.5.0] [org.clojure/java.jdbc 0.4.2] [mysql/mysql-connector-java 5.1.38]] :plugins [[lein-ring 0.9.7] [lein-ancient 0.6.10]] :ring {:handler myapp.handler/app :init myapp.handler/init :destroy myapp.handler/destroy} :aot [myapp.handler] :profiles {:uberjar {:aot :all :jvm-opts [-Dfile.encodingUTF-8 -server -Xms512m -Xmx1024m -XX:UseParallelGC]}})逐条解释关键点:min-lein-version 2.5.0Leiningen 2.5.0 是第一个正式支持 JDK 7u80 的版本低于此版本在 14.04 上运行lein uberjar会因java.lang.invoke.MethodHandles类缺失而报错。:dependencies中的mysql/mysql-connector-java 5.1.38这是最后一个兼容 JDK 7 的 MySQL 驱动版本。5.1.40开始要求 JDK 8强行使用会导致java.lang.UnsupportedClassVersionError。:ring配置块里的:init和:destroyinit函数在 JVM 启动后、Handler 接收请求前执行常用于初始化数据库连接池如 HikariCPdestroy在 JVM 关闭前执行用于优雅关闭连接池。这两个钩子是实现“零宕机部署”的基础。:aot [myapp.handler]必须显式指定 AOT 编译的命名空间。如果 handler 里引用了另一个命名空间myapp.db而你没把它加入 AOT 列表uberjar会打包字节码但运行时仍会尝试动态编译而dashshell 下的java命令可能找不到clojure.main的 classpath导致启动失败。:profiles {:uberjar {...}}-serverJVM 参数告诉 HotSpot 这是一个长期运行的服务启用服务端 JIT 编译器-Xms512m -Xmx1024m设定堆内存初始值和最大值避免运行时频繁 GC-XX:UseParallelGC是 JDK 7 下吞吐量最高的垃圾收集器比默认的UseSerialGC快 3 倍以上。构建命令不是简单的lein uberjar。正确流程是# 1. 清理旧构建产物避免缓存污染 lein clean # 2. 生成生产环境的 uberjar注意不是 lein ring uberjar lein with-profile uberjar uberjar # 3. 验证 JAR 包结构关键 jar -tf target/myapp-0.1.0-SNAPSHOT-standalone.jar | head -20jar -tf的输出里你必须看到myapp/handler__init.class和myapp/handler$fn__1234.class这样的文件证明 AOT 编译成功。如果只看到myapp/handler.clj说明 AOT 没生效启动时必报错。实操心得在 CI/CD 流水线里我强制加入一个检查步骤jar -tf target/*.jar | grep -q handler__init.class || (echo AOT compilation failed! exit 1)。这个 10 行 Shell 脚本帮我们拦截了 83% 的部署失败。3.2 Supervisor 配置详解进程守护、日志与信号处理Supervisor 的配置文件/etc/supervisor/conf.d/myapp.conf是整个服务稳定性的基石。一个典型的配置如下[program:myapp] commandjava -Dfile.encodingUTF-8 -server -Xms512m -Xmx1024m -XX:UseParallelGC -jar /opt/myapp/myapp-0.1.0-SNAPSHOT-standalone.jar directory/opt/myapp usermyapp autostarttrue autorestarttrue startsecs10 startretries3 stopwaitsecs15 stopasgrouptrue killasgrouptrue redirect_stderrtrue stdout_logfile/var/log/myapp/myapp.stdout.log stdout_logfile_maxbytes1MB stdout_logfile_backups5 stderr_logfile/var/log/myapp/myapp.stderr.log stderr_logfile_maxbytes1MB stderr_logfile_backups5 environmentJAVA_HOME/usr/lib/jvm/java-7-openjdk-amd64,LANGen_US.UTF-8核心参数解析command这里重复写了 JVM 参数是为了确保 Supervisor 启动时的环境与lein uberjar测试时完全一致。-Dfile.encodingUTF-8是必须的否则中文路径或 JSON 字符串会乱码。usermyapp绝对不要用 root 用户运行 Clojure 应用。创建专用用户sudo adduser --disabled-password --gecos myapp。这能防止应用漏洞被利用后获得系统最高权限。startsecs10Supervisor 认为进程“启动成功”的条件是它在startsecs秒内没有退出。Ring/Jetty 默认启动时间约 2–3 秒设为 10 是为了留出数据库连接、缓存预热等耗时操作的余量。stopwaitsecs15如前所述这是给 Jetty 优雅关闭留的时间。stopasgrouptrue和killasgrouptrue是关键——它们确保 Supervisor 发送SIGTERM时不仅杀主进程还杀掉它 fork 出的所有子进程如异步日志写入线程避免僵尸进程。environmentJAVA_HOME必须精确指向update-alternatives --config java显示的 OpenJDK 7 路径。LANGen_US.UTF-8防止java.text.SimpleDateFormat解析日期时因 locale 不同而抛异常。日志目录/var/log/myapp/必须提前创建并授权sudo mkdir -p /var/log/myapp sudo chown myapp:myapp /var/log/myapp sudo chmod 755 /var/log/myappSupervisor 本身不会帮你创建日志目录也不会自动修复权限。如果目录不存在或权限不对supervisord会静默失败supervisorctl status显示FATAL但journalctl -u supervisor里找不到任何线索——这是新手踩坑最多的点。注意supervisord的主配置文件/etc/supervisor/supervisord.conf里[inet_http_server]段落可以开启 Web UIport127.0.0.1:9001但生产环境必须禁用。Ubuntu 14.04 的supervisor包没有内置 Basic Auth暴露在公网等于把进程控制权送给黑客。3.3 Nginx 配置实战反向代理、SSL 终结与安全加固Nginx 配置/etc/nginx/sites-available/myapp是流量的总闸门。针对 Ubuntu 14.04 的 1.4.6 版本我们采用“功能克制配置精准”的原则upstream myapp_backend { server 127.0.0.1:3000 fail_timeout0; # 如果你有多个 Clojure 实例可以加更多 server 行 # server 127.0.0.1:3001; } server { listen 80; server_name api.example.com; # 强制跳转 HTTPS如果启用了 SSL return 301 https://$server_name$request_uri; } server { listen 443 ssl; server_name api.example.com; ssl_certificate /etc/ssl/certs/myapp.crt; ssl_certificate_key /etc/ssl/private/myapp.key; # Ubuntu 14.04 的 OpenSSL 1.0.1f 支持的 TLS 版本 ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_ciphers ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA256:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA; ssl_prefer_server_ciphers on; # WebSocket 支持关键 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; # 超时设置匹配 Jetty 的默认值 proxy_connect_timeout 15; proxy_send_timeout 60; proxy_read_timeout 60; # 静态资源缓存如果 Clojure 应用也托管静态文件 location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { expires 1y; add_header Cache-Control public, immutable; } # API 主路由 location / { proxy_pass http://myapp_backend; proxy_redirect off; } }重点说明upstream块里的fail_timeout0告诉 Nginx即使后端返回 500也不要把它标记为“宕机”。因为 Clojure 应用的 500 往往是业务逻辑错误如数据库查询超时不是进程死亡。fail_timeout0确保流量始终打到后端由 Clojure 层面的熔断器如 Hystrix来决定是否降级。ssl_ciphers字符串是经过 OpenSSL 1.0.1f 实测可用的完整列表。它剔除了所有已知存在 CVE 的加密套件如EXPORT、RC4同时保留了对 WinXP/IE8 的兼容性。你可以用openssl ciphers -v ECDHE...命令验证。WebSocket 支持的两行proxy_set_header是硬编码在 Nginx 1.4.6 里的。Connection upgrade必须是字面量字符串upgrade不能写成$connection_upgrade否则会失效。proxy_read_timeout 60这个值必须大于 Ring/Jetty 的:max-header-size默认 8KB和:max-body-size默认 10MB的读取耗时。一个 10MB 的文件上传在千兆内网里大约耗时 0.1 秒设为 60 是为了应对慢速网络或大 payload 的 POST 请求。启用配置后务必执行# 1. 语法检查永远不要跳过 sudo nginx -t # 2. 创建软链接启用站点 sudo ln -sf /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/myapp # 3. 重新加载配置不中断连接 sudo service nginx reloadnginx -t是你的最后一道防线。我见过太多人因为少了一个分号或引号导致reload失败Nginx 进程退出整个网站 502。-t命令能在 0.02 秒内告诉你错误在哪一行。4. 完整实操流程与核心环节实现4.1 环境准备从裸机到可部署状态假设你拿到一台全新的 Ubuntu 14.04 服务器物理机或 VMIP 为192.168.1.100。以下是零误差的初始化步骤第一步系统更新与基础工具安装# 切换到 root后续操作均需 root 权限 sudo su - # 更新 apt 源14.04 EOL 后源已迁移到 old-releases sed -i s/archive.ubuntu.com/old-releases.ubuntu.com/g /etc/apt/sources.list sed -i s/security.ubuntu.com/old-releases.ubuntu.com/g /etc/apt/sources.list # 更新系统这会安装最新的内核补丁和安全更新 apt-get update apt-get -y upgrade # 安装基础工具curl下载 Leiningen、vim编辑配置、unzip解压 JDK apt-get -y install curl vim unzip wget gnupg2 # 安装 OpenJDK 7Ubuntu 14.04 默认就是它但确认一下 apt-get -y install openjdk-7-jdk java -version # 输出应为 java version 1.7.0_80第二步安装 LeiningenLeiningen 2.9.10 是最后一个支持 JDK 7 的稳定版本# 下载并安装 curl -O https://raw.githubusercontent.com/technomancy/leiningen/stable/bin/lein chmod x lein mv lein /usr/local/bin/ # 第一次运行会自动下载依赖需要耐心等待约 5 分钟 lein version # 输出 Leiningen 2.9.10 on Java 1.7.0_80 Java HotSpot(TM) 64-Bit Server VM第三步安装 Supervisor 和 Nginx# Supervisor 会自动安装 Python 2.714.04 默认 apt-get -y install supervisor # 启动 Supervisor 并设为开机自启 service supervisor start update-rc.d supervisor defaults # Nginx apt-get -y install nginx # 启动 Nginx service nginx start # 此时访问 http://192.168.1.100 应该看到 Welcome to nginx! 页面第四步创建应用用户与目录结构# 创建专用用户 adduser --disabled-password --gecos myapp # 创建标准目录结构 mkdir -p /opt/myapp/{bin,conf,lib,log} chown -R myapp:myapp /opt/myapp chmod 755 /opt/myapp # 创建日志目录Supervisor 需要 mkdir -p /var/log/myapp chown myapp:myapp /var/log/myapp至此系统层面的准备工作完成。整个过程约 8 分钟所有命令均可复制粘贴执行无交互提示。4.2 应用部署从源码到线上服务现在假设你的 Clojure 项目源码已经通过 Git Clone 到了本地或你有一个myapp-0.1.0-SNAPSHOT-standalone.jar文件。以下是部署流水线步骤一上传与验证 JAR 包# 假设 JAR 包在本地电脑用 scp 上传 scp myapp-0.1.0-SNAPSHOT-standalone.jar myapp192.168.1.100:/opt/myapp/ # 切换到 myapp 用户验证 JAR sudo su - myapp cd /opt/myapp java -jar myapp-0.1.0-SNAPSHOT-standalone.jar --help # 如果输出帮助信息说明 JAR 可执行 # 如果报错立即停止检查 AOT 编译步骤二配置 Supervisor# 编辑 Supervisor 配置 sudo vim /etc/supervisor/conf.d/myapp.conf # 粘贴前面给出的完整配置注意修改 JAR 路径和 user # 重新加载 Supervisor 配置 sudo supervisorctl reread sudo supervisorctl update # 启动服务 sudo supervisorctl start myapp sudo supervisorctl status # 输出应为 myapp RUNNING pid 1234, uptime 0:00:05步骤三配置 Nginx 并启用 SSL# 创建 SSL 证书目录 sudo mkdir -p /etc/ssl/{certs,private} # 生成自签名证书仅用于测试生产请用 Lets Encrypt sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ -keyout /etc/ssl/private/myapp.key \ -out /etc/ssl/certs/myapp.crt \ -subj /CCN/STBeijing/LBeijing/OMyApp/CNapi.example.com # 编辑 Nginx 站点配置 sudo vim /etc/nginx/sites-available/myapp # 粘贴前面的完整配置 # 启用站点并重载 sudo ln -sf /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/myapp sudo nginx -t sudo service nginx reload步骤四最终验证# 1. 检查 Clojure 进程是否在监听 3000 端口 sudo netstat -tuln | grep :3000 # 应该看到 tcp6 0 0 :::3000 :::* LISTEN # 2. 检查 Nginx 是否在监听 443 sudo netstat -tuln | grep :443 # 应该看到 tcp6 0 0 :::443 :::* LISTEN # 3. 用 curl 测试本地环回 curl -k https://127.0.0.1:443/health # 假设你的 handler 有 /health 端点 # 4. 从外部机器测试替换为你的服务器 IP curl -k https://192.168.1.100/health # 成功返回 JSON 即表示全链路打通整个部署过程从上传 JAR 到对外提供服务熟练操作可在 3 分钟内完成。关键在于每一步都有明确的验证点任何一个环节失败都能立刻定位到具体命令或配置行。4.3 日常运维启动、停止、日志与升级生产环境不是部署完就结束了而是进入持续运维阶段。以下是高频操作启动/停止/重启服务# 启动整个服务栈 sudo supervisorctl start myapp sudo service nginx start # 停止优雅 sudo supervisorctl stop myapp # 会触发 stopwaitsecs15 sudo service nginx stop # 重启推荐用于配置变更后 sudo supervisorctl restart myapp sudo service nginx reload # 注意是 reload不是 restart不中断连接查看与追踪日志# 查看 Supervisor 管理的日志应用 stdout/stderr sudo tail -f /var/log/myapp/myapp.stdout.log sudo tail -f /var/log/myapp/myapp.stderr.log # 查看 Nginx 访问日志默认在 /var/log/nginx/access.log sudo tail -f /var/log/nginx/access.log | awk {print $1,$4,$7,$9} # 查看 Nginx 错误日志 sudo tail -f /var/log/nginx/error.log # 一个实用技巧实时监控 500 错误 sudo tail -f /var/log/nginx/access.log | grep 500 无缝升级新版本这是最考验架构的地方。目标是用户无感知API 请求不丢失数据库事务不中断。准备新 JAR在另一台机器上构建好myapp-0.1.1-SNAPSHOT-standalone.jar上传到/opt/myapp/new/。停止旧进程但不 killsudo supervisorctl stop myapp # 此时旧进程还在 graceful shutdown替换 JAR 并更新配置sudo mv /opt/myapp/new/myapp-0.1.1-SNAPSHOT-standalone.jar /opt/myapp/ # 修改 /etc/supervisor/conf.d/myapp.conf 中的 command 行指向新 JAR 名 sudo supervisorctl reread sudo supervisorctl update启动新进程sudo supervisorctl start myapp验证新版本用curl测试/health和关键业务接口。确认无误后清理旧日志sudo supervisorctl tail myapp stderr | head -20确认旧进程已完全退出。整个升级过程从 stop 到新服务 ready通常在 20 秒内完成。旧请求在stopwaitsecs时间内完成新请求由新进程处理实现了真正的“零宕机”。5. 常见问题与排查技巧实录5.1 “Supervisor status 显示 FATAL但日志为空”这是 Ubuntu 14.04 上最经典的“幽灵故障”。现象是$ sudo supervisorctl status myapp FATAL - Exited too quickly (process log may have details)但tail /var/log/myapp/*.log是空的journalctl -u supervisor也找不到线索。根本原因Supervisor 的stdout_logfile目录权限不对或者usermyapp指定的用户不存在。排查步骤检查supervisord.conf的