Skip to content

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.target
bash
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=simplejava 前台运行就是 simple。若脚本包了一层 shell,用 Type=forking + PIDFile= 或直接改成 exec 调用
SuccessExitStatus=143SIGTERM 退出的码是 143,systemd 默认视为"失败"会触发 Restart。不加这行,systemctl stop 后服务会自己又起来
TimeoutStopSec=60给应用 60 秒完成优雅停机,超时才 SIGKILL
Restart=on-failure崩溃自动拉起;always 会连手动 stop 也拉起(配合上面的坑)
LimitNOFILE=65535systemd 默认 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

背后发生的事:

  1. systemd 发 SIGTERM 给 MAINPID
  2. JVM shutdown hook 触发 → Spring 收到 context close
  3. Web 容器停止接收新请求,等待进行中请求完成
  4. 连接池、MQ 消费者、定时任务依次关闭
  5. 进程退出,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=%i
bash
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 目录。