外观
jar 的 systemd 托管与优雅停机
nohup java -jar & 的问题不是不能用,而是没人管它:不自动重启、日志无限增长、停机靠 kill -9、开机不自启。生产服务应该交给 systemd。
一、目录约定
text
/opt/order/
├─ current -> /opt/order/releases/20261027-1610 # 软链,发布只换它
├─ releases/
│ ├─ 20261027-1610/app.jar
│ └─ 20261020-1015/app.jar # 留 5 份用于回滚
├─ config/application.yml
├─ logs/
└─ run/bash
useradd -r -s /sbin/nologin appuser
chown -R appuser:appuser /opt/order
mkdir -p /var/log/order /data/dump && chown appuser /var/log/order /data/dump二、unit 文件
ini
# /etc/systemd/system/order.service
[Unit]
Description=Order Service
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=appuser
Group=appuser
WorkingDirectory=/opt/order/current
Environment=JAVA_HOME=/opt/java/jdk-17.0.11
Environment=TZ=Asia/Shanghai
EnvironmentFile=-/opt/order/config/env.sh
ExecStart=/opt/java/jdk-17.0.11/bin/java \
-server -Xms4g -Xmx4g -XX:MaxGCPauseMillis=200 \
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/dump/ \
-Xlog:gc*:file=/var/log/order/gc.log:time,uptime:filecount=5,filesize=50m \
-jar /opt/order/current/app.jar \
--spring.config.location=file:/opt/order/config/application.yml
ExecStop=/bin/kill -TERM $MAINPID
SuccessExitStatus=143
TimeoutStopSec=60
Restart=on-failure
RestartSec=5
LimitNOFILE=65535
StandardOutput=append:/var/log/order/stdout.log
StandardError=inherit
[Install]
WantedBy=multi-user.targetbash
systemctl daemon-reload
systemctl enable --now order
systemctl status order --no-pager输出:
text
● order.service - Order Service
Loaded: loaded (/etc/systemd/system/order.service; enabled; vendor preset: disabled)
Active: active (running) since Tue 2026-10-27 16:12:04 CST; 12s ago
Main PID: 21874 (java)
Tasks: 214 (limit: 100000)
Memory: 4.3G三、逐字段解释
| 字段 | 为什么这么写 |
|---|---|
Environment=JAVA_HOME= | systemd 不读 /etc/profile,不写就用不到你配的全局环境 |
EnvironmentFile=-/...env.sh | 前缀 - 表示文件不存在也不报错;密码类配置放这里,别写进 unit |
Type=simple | java 前台运行就是 simple。若脚本包了一层 shell,用 Type=forking + PIDFile= 或直接改成 exec 调用 |
SuccessExitStatus=143 | SIGTERM 退出的码是 143,systemd 默认视为"失败"会触发 Restart。不加这行,systemctl stop 后服务会自己又起来 |
TimeoutStopSec=60 | 给应用 60 秒完成优雅停机,超时才 SIGKILL |
Restart=on-failure | 崩溃自动拉起;always 会连手动 stop 也拉起(配合上面的坑) |
LimitNOFILE=65535 | systemd 默认 1024,Netty/DB 连接池多了直接 Too many open files |
StandardOutput=append: | 比 >file 好:追加而非覆盖,且 systemd 240+ 支持。旧版本用 journal + journalctl 取 |
WorkingDirectory= | 相对路径(日志、hprof、临时文件)都落在这里,不设会落到 / |
143 这个坑非常常见
漏了 SuccessExitStatus=143 时,你执行 systemctl stop order,Java 正常响应 SIGTERM 退出,systemd 判定"异常退出"→ 按 Restart=on-failure 重新拉起。表现就是"停不掉的 Java 进程",很多人会误以为是应用没退干净。
四、优雅停机链路
bash
systemctl stop order背后发生的事:
- systemd 发
SIGTERM给MAINPID - JVM shutdown hook 触发 → Spring 收到 context close
- Web 容器停止接收新请求,等待进行中请求完成
- 连接池、MQ 消费者、定时任务依次关闭
- 进程退出,systemd 记录 exit code
Spring Boot 侧必须配合:
yaml
server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 30s验证是否真的优雅:
bash
time systemctl stop order
journalctl -u order --since "2 min ago" | grep -iE 'shutdown|stopped|graceful'输出:
text
real 0m8.412s
... Commencing graceful shutdown. Waiting for active requests to complete
... graceful shutdown complete解释:8 秒是"等在途请求跑完"的时间。如果 TimeoutStopSec 设成 5 秒,这个应用永远是被 SIGKILL 的——数据源没关、消息可能重复消费。两者要匹配:TimeoutStopSec > timeout-per-shutdown-phase + 实际资源关闭时间。
五、日志别双写
StandardOutput=append: 写文件的同时,journald 也留了一份。要么只用 journald:
bash
journalctl -u order -f
journalctl -u order --since "10 min ago" --until "now" > /tmp/order.log要么在 unit 里 StandardOutput=append: 且应用自己配 logback 滚动。最糟的组合是应用写一份、systemd 再写一份、journald 又留一份——磁盘被同一份日志吃三遍。
六、多实例(同机跑多个端口)
用模板 unit,一份配置管 N 个实例:
ini
# /etc/systemd/system/order@.service 注意文件名里的 @
[Unit]
Description=Order Service instance %i
[Service]
User=appuser
ExecStart=/opt/java/jdk-17.0.11/bin/java -jar /opt/order/current/app.jar --server.port=%ibash
systemctl start order@8081 order@8082
systemctl status 'order@*' --no-pager解释:%i 是 @ 后面那段的原样替换。适合单机多实例压测或分片任务,别拿它替代真正的水平扩容。
七、常用运维命令
bash
systemctl daemon-reload # 改完 unit 必须先跑这个
systemctl restart order
systemctl status order --no-pager -l
journalctl -u order -n 200 --no-pager
systemctl show order -p MainPID -p ExecMainStartTimestamp -p MemoryCurrent
systemctl edit order --full # 交互式编辑(会做语法检查)
systemd-analyze verify order.service # 校验 unit 语法改配置不重启的技巧
systemctl edit order 生成 drop-in 片段(/etc/systemd/system/order.service.d/override.conf),改完 daemon-reload + restart 即生效。好处是不覆盖原始 unit 文件,回滚只需删 override 目录。