2026-09-09-springboot-cloud-deploy-pitfalls

Spring Boot 应用上云:JAR 配置热替换、systemd 托管与三个隐藏故障

背景

把一套 Spring Boot 单体应用(fat JAR,约 200 MB)部署到一台 1.7 GB 内存的云服务器上。数据库和 Redis 都在同一台机器。

看起来是最简单的部署场景:传 JAR、改配置、启动。实际卡了四次,每次原因都不一样。记一笔。


坑一:改一行配置,要重传 200 MB

Spring Boot 的 fat JAR 把 application-dev.yml 打包进了 BOOT-INF/classes/。数据库地址写错了,改完本地 yml 之后------

第一反应是 mvn clean package 重新打包,然后重新上传。但这里有两个问题:

  1. 重新打包依赖本地环境 :这台机器的 JAVA_HOME 指向 JDK 8,项目要 JDK 17,mvn 直接报 无效的目标发行版: 17
  2. 重传 200 MB:家用上行带宽,一次十几分钟

更好的做法:JAR 就是 ZIP,直接原地替换里面的文件。

JDK 自带的 jar 命令支持增量更新:

bash 复制代码
# 把新的 yml 放到 /tmp/application-dev.yml
# 然后原地更新 JAR 内的对应条目
jar uf /opt/app/myapp.jar -C /tmp application-dev.yml

jar uf 会在 JAR 里找到同名条目替换掉,不存在就新增。整个操作在服务器本地完成,网络传输量 = 一个 yml 文件的大小(几 KB)。

如果需要精确控制 JAR 内路径,用 Python 的 zipfile 重写也可以:

python 复制代码
import zipfile

with zipfile.ZipFile("app.jar", "r") as zin, \
     zipfile.ZipFile("app_new.jar", "w", zipfile.ZIP_DEFLATED) as zout:
    for item in zin.infolist():
        if item.filename.endswith("application-dev.yml"):
            zout.writestr(item, new_yml_bytes)
        else:
            zout.writestr(item, zin.read(item.filename))

注意:这个技巧适合改配置文件这种「不影响编译产物」的场景。改 Java 代码当然还是要重新编译。


坑二:nohup 启动的进程,SSH 一断就没了

第一版启动命令:

bash 复制代码
ssh server "cd /opt/app && nohup java -jar app.jar > app.log 2>&1 &"

进程起来了,ss -tln 能看到端口。但下一次 SSH 上去 pkill 再重启,进程就再也起不来了。

试过各种组合:

bash 复制代码
setsid nohup java -jar app.jar > app.log 2>&1 < /dev/null & disown

disown 在非交互 shell 里不生效,setsid 也没能完全解决。根本原因是通过 SSH 的非交互 shell 启动的后台进程,生命周期管理很不确定------尤其是 paramiko 这类库执行完命令就关 channel,进程可能收到 SIGHUP。

正确做法:交给 systemd。

ini 复制代码
# /etc/systemd/system/myapp.service
[Unit]
Description=My Spring Boot App
After=network.target mysqld.service redis.service

[Service]
Type=simple
WorkingDirectory=/opt/app
ExecStart=/usr/bin/java -Xmx256m -jar /opt/app/app.jar --spring.profiles.active=prod
StandardOutput=append:/opt/app/app.log
StandardError=append:/opt/app/app.log
Restart=on-failure
RestartSec=10

[Install]
WantedBy=multi-user.target
bash 复制代码
systemctl daemon-reload
systemctl enable myapp     # 开机自启
systemctl start myapp
systemctl restart myapp    # 以后重启就这一条

顺手拿到三个好处:进程脱离 SSH 会话、崩溃自动重启、开机自启。调试也方便:

bash 复制代码
systemctl status myapp --no-pager
journalctl -u myapp -n 50 --no-pager

坑三:Client sent AUTH, but no password is set

应用起来了,端口在监听,日志里定时任务在正常轮询数据库。但一访问接口就 500:

复制代码
org.springframework.data.redis.RedisConnectionFailureException: Cannot get Jedis connection
Caused by: redis.clients.jedis.exceptions.JedisDataException:
    ERR Client sent AUTH, but no password is set

这个报错方向是反的------不是「密码错了」,是「你发了密码但我根本没设密码」。

应用配置里写了 spring.redis.password: xxx,但服务器上的 Redis 是 yum 装完直接启动的,/etc/redis.confrequirepass 还是注释状态。

bash 复制代码
# 确认当前状态
grep -n "^requirepass\|^# requirepass" /etc/redis.conf
# 480:# requirepass foobared      ← 注释掉的

# 打开并设置
sed -i 's/^# *requirepass .*/requirepass yourpassword/' /etc/redis.conf
systemctl restart redis

# 验证
redis-cli -a yourpassword ping     # 应返回 PONG

教训:应用配置和服务端配置是两头,改了一头要记得对齐另一头。部署清单里应该把「Redis 是否设密码」列成显式检查项,而不是假设默认行为。


坑四:Fontconfig head is null

登录页的验证码接口返回 500:

复制代码
java.lang.RuntimeException: Fontconfig head is null, check your fonts or fonts configuration

验证码是服务端用 Java AWT 画出来的图片,需要系统字体。最小化安装的 Linux 镜像通常不带字体库------桌面环境才需要,服务器版本默认省掉了。

bash 复制代码
yum install -y fontconfig dejavu-sans-fonts

# 验证
fc-list | wc -l     # 装之前是 0,装之后有值

装完重启应用即可。

同类问题 :任何用到 java.awt 的功能(图片处理、图表生成、PDF 渲染、二维码带文字)在最小化服务器镜像上都可能踩这个。如果只是缺字体不想装完整字体包,也可以给 JVM 加 -Djava.awt.headless=true,但那只解决「没有显示设备」的问题,不解决「没有字体」。


内存 1.7 GB 怎么跑

顺带提一下资源限制。这台机器要同时跑 MySQL、Redis、Nginx 和 Spring Boot:

bash 复制代码
java -Xmx256m -jar app.jar

-Xmx256m 是硬约束,不加的话 JVM 默认堆上限是物理内存的 1/4,看起来够,但加上 metaspace、线程栈、堆外内存,很容易把机器挤爆。实测这个应用在 256 MB 堆下稳定运行,RSS 约 580 MB。

MySQL 那边也要调:

ini 复制代码
# /etc/my.cnf
innodb_buffer_pool_size = 128M

排错顺序总结

四个故障的共同点:日志里都有明确答案,只是要往下翻Caused by 链条的最后一层才是真正的根因:

复制代码
RedisConnectionFailureException          ← 表象
  Caused by: JedisConnectionException    ← 中间层
    Caused by: JedisDataException:
      ERR Client sent AUTH, but no password is set    ← 真正原因

所以查日志的过滤条件应该是:

bash 复制代码
grep -A2 "Caused by" app.log | tail -30

而不是只看第一行异常类型。

现象 真正原因 解法
改配置要重传大 JAR fat JAR 打包了配置 jar uf 原地替换
重启后进程起不来 SSH 非交互 shell 的后台进程生命周期不可控 改用 systemd
接口 500,Redis 连接失败 应用配了密码,服务端没设 对齐 requirepass
验证码接口 500 最小化镜像无字体库 fontconfig + 字体包
相关推荐
YWL1 小时前
OpenLayers弹窗Overlay深度实战
前端·vue·openlayers
dozenyaoyida1 小时前
AI与大模型新闻日报 | 2026-09-09
人工智能·ai·chatgpt·大模型·新闻
YWL2 小时前
OpenLayers+ECharts联动:地图点击联动图表,数据可视化大屏方案
前端·信息可视化·vue·echarts·openlayers
驭渊的小故事2 小时前
多线程02
java·开发语言·jvm
芭拉拉小魔仙2 小时前
Vue 2 门诊收费系统中的医保结算流程设计与实践
前端·javascript·vue.js
xqqxqxxq2 小时前
AI Agent学习:主动工具发现(李博杰《深入理解 AI Agent》4.8观后总结)
人工智能·学习
随遇而安zx2 小时前
【多线程】---实战:CompletableFuture编排
java·多线程·并发
moonrailgun2 小时前
用 Node.js 复刻 Codex Astra 的终端星光
前端·javascript·算法
Wang's Blog2 小时前
Java框架快速入门: Spring Security+OAuth2之HTTP请求结构与认证基础
java·spring·http