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.conf 里 requirepass 还是注释状态。

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 + 字体包
相关推荐
cu1431 分钟前
细谈GM8229的具体功能与其应用
c语言·c++·人工智能·单片机
Alson_Code3 分钟前
从0到1打造个人专属编程智能体
人工智能·langchain·ai编程
AI Data 搭子5 分钟前
阿里云发布 Agentic Storage 全矩阵产品:面向 AI 到 Agent 负载的全栈演进
人工智能·阿里云·云计算
2601_951092836 分钟前
德国海外仓:跨境电商布局欧洲的核心枢纽与合规指南
大数据·人工智能·其他
智鸟科技GemeOpen开发者智能设备7 分钟前
GemeOpen 智能音箱 GSSM0B - 播放控制(Java示例)
java·开发语言·智能音箱
小蜗 strong12 分钟前
和电脑猜拳(随机程序应用)
服务器·前端·python
百数平台19 分钟前
百数 AI 智能体怎么使用?基础操作与常见问题解答
人工智能
xingyuzhisuan21 分钟前
无限画布实操分享:基于 StarVerse-AI 制作连续国风漫剧分镜
人工智能
FII工业富联科技服务24 分钟前
从 AI for AI 到 AI Factory:大模型重构智能制造技术底座的四层逻辑
人工智能
今天的砖头有点烫手啊26 分钟前
Java IO/NIO/AIO 演进:从 BIO 到 Netty 的底层逻辑
java·jvm·nio