Skip to content

systemd 单元 ​

把一个 jar 或脚本变成"开机自启、崩溃自动重启、日志可查"的服务,只需要一个 unit 文件。

一、最小单元 ​

ini
# /etc/systemd/system/myapp.service
[Unit]
Description=My Application
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=app
Group=app
WorkingDirectory=/opt/app
Environment="JAVA_HOME=/usr/local/jdk-17"
ExecStart=/opt/app/bin/start.sh
Restart=on-failure
RestartSec=5
LimitNOFILE=65535

[Install]
WantedBy=multi-user.target
bash
systemctl daemon-reload
systemctl enable --now myapp
systemctl status myapp --no-pager
text
● myapp.service - My Application
     Loaded: loaded (/etc/systemd/system/myapp.service; enabled; vendor preset: disabled)
     Active: active (running) since Tue 2026-10-27 22:01:12 CST; 5s ago
   Main PID: 32811 (java)
      Tasks: 42 (limit: 19485)
     Memory: 512.4M
        CPU: 1.204s
     CGroup: /system.slice/myapp.service
             └─32811 java -jar /opt/app/app.jar

三段结构:

段放什么
[Unit]描述、依赖关系、启动顺序
[Service]怎么启动、以谁运行、怎么重启
[Install]systemctl enable 时装到哪个 target

二、Type 怎么选 ​

Type行为适用
simple(默认)ExecStart 一执行就算启动成功前台常驻进程(Java、Go、Node)
forking父进程 fork 后退出,子进程继续传统守护进程(nginx 默认)
notify进程调用 sd_notify(READY=1) 才算就绪Redis、部分现代服务
oneshot执行完就退出,不算"运行中"初始化脚本、一次性任务
idle等其他任务都启动完再跑非紧急任务

选错 Type 的典型症状:

  • 用 simple 跑 forking 的程序 → systemd 认为主进程已退出,反复重启。
  • 用 forking 跑前台程序 → systemd 一直等到超时(90 秒)才报失败。
  • 用 notify 但程序不发通知 → 一直 activating,超时后失败。
bash
# 看程序到底是不是前台
/opt/app/bin/start.sh &        # 如果它立即返回但进程还在,就是 forking

三、依赖与顺序 ​

ini
[Unit]
After=network-online.target mysql.service redis.service
Wants=network-online.target
Requires=mysql.service
指令作用
After=B在 B 之后启动(只管顺序)
Before=B在 B 之前启动
Wants=B想要 B,但 B 失败也继续
Requires=B必须有 B,B 停止则自己也停
BindsTo=B更严格,B 消失就立即停

只有 After 不会"等数据库就绪"

After=mysql.service 只保证"mysql 的启动脚本已经执行过",不保证 mysqld 能接受连接。应用起来时数据库可能还在初始化。

两个办法:

  1. 应用自己加重试(推荐)。
  2. 在 unit 里加 ExecStartPre 轮询探测:
ini
ExecStartPre=/bin/sh -c 'until nc -z 127.0.0.1 3306; do sleep 2; done'

network-online.target 需要 systemd-networkd-wait-online 或 NetworkManager-wait-online 支持。只写 network.target 时,网卡可能还没拿到 IP。

四、常用指令 ​

ini
[Service]
User=app
Group=app
WorkingDirectory=/opt/app

Environment="PROFILE=prod" "LOG_LEVEL=info"
EnvironmentFile=-/etc/app/app.env        # 开头的 - 表示文件不存在也不报错

ExecStartPre=/bin/mkdir -p /data/app/logs
ExecStart=/opt/app/bin/start.sh
ExecReload=/bin/kill -HUP $MAINPID
ExecStop=/bin/kill -TERM $MAINPID
ExecStartPost=/bin/sh -c 'echo started >> /var/log/app-boot.log'

Restart=on-failure
RestartSec=5
StartLimitIntervalSec=60
StartLimitBurst=5

TimeoutStartSec=120
TimeoutStopSec=30
KillSignal=SIGTERM
SendSIGKILL=yes

LimitNOFILE=65535
LimitNPROC=4096
OOMScoreAdjust=-500

SuccessExitStatus=143

逐项:

指令说明
EnvironmentFile=-path- 前缀表示文件缺失不报错
ExecStart只能有一个(oneshot 可以有多个)
Restart=on-failure非 0 退出或被杀时重启
StartLimitBurst60 秒内超过 5 次就不再重启(防止无限重启)
TimeoutStopSecSIGTERM 后等多久发 SIGKILL
LimitNOFILE文件句柄上限,覆盖 limits.conf
OOMScoreAdjust内存紧张时让内核优先杀别人
SuccessExitStatus=143把 143(SIGTERM)当正常退出

ExecStart 的写法细节:

ini
# 不经过 shell,参数按引号拆分(推荐)
ExecStart=/usr/local/jdk-17/bin/java -Xms1g -Xmx1g -jar /opt/app/app.jar

# 需要 shell 特性(管道、重定向、变量展开)时显式调用
ExecStart=/bin/sh -c 'exec /opt/app/bin/start.sh >> /var/log/app.log 2>&1'

ExecStart 不要用 shell 形式启动常驻进程

ExecStart=/bin/sh -c '/opt/app/start.sh &' 会让 systemd 跟踪 sh,而不是真正的 Java 进程。systemctl stop 只杀掉 sh,Java 变成孤儿进程继续跑。

要么让脚本 exec 替换自己(exec java -jar ...),要么直接 ExecStart 指向 java。

五、日志 ​

bash
journalctl -u myapp -f
journalctl -u myapp --since '10 min ago'
journalctl -u myapp -p err -n 50
journalctl -u myapp -o json-pretty | tail
text
Oct 27 22:01:12 kafka-1 systemd[1]: Started My Application.
Oct 27 22:01:13 kafka-1 java[32811]: 2026-10-27 22:01:13 INFO  Started in 8.4s
参数作用
-f跟随
--since today / '2 hours ago'时间范围
-p err优先级(emerg/alert/crit/err/warning/notice/info/debug)
-b本次启动
-b -1上次启动
-o cat只输出消息体(去掉时间戳前缀)
--no-pager不分页(脚本里必加)

限制日志占用:

ini
# /etc/systemd/journald.conf
SystemMaxUse=2G
SystemMaxFileSize=200M
MaxRetentionSec=30day
bash
systemctl restart systemd-journald
journalctl --disk-usage
journalctl --vacuum-size=1G
text
Archived and active journals take up 1.8G in the file system.

应用自己写日志文件时 journalctl 可能是空的

journalctl -u 只收 stdout/stderr。应用写到 /opt/app/logs/app.log 的日志要看文件,或者用 StandardOutput=file:/path 重定向(但这样一来 journald 又收不到了)。

六、常用操作 ​

bash
systemctl daemon-reload            # 改了 unit 文件后必须执行
systemctl enable --now myapp       # 开机自启 + 立即启动
systemctl disable myapp
systemctl restart myapp
systemctl reload myapp             # 发 SIGHUP / 执行 ExecReload
systemctl stop myapp
systemctl status myapp
systemctl cat myapp                # 看最终生效的 unit(含 drop-in)
systemctl edit myapp               # 创建 override,不改原文件
systemctl list-units --failed
systemctl list-unit-files --state=enabled

systemctl edit myapp 会创建 /etc/systemd/system/myapp.service.d/override.conf,内容只需写要覆盖的部分:

ini
[Service]
Environment="PROFILE=staging"

改完自动 daemon-reload。

七、排错 ​

启动失败 ​

bash
systemctl status myapp --no-pager
journalctl -u myapp -n 50 --no-pager
systemctl cat myapp
text
● myapp.service - My Application
     Active: failed (Result: exit-code) since Tue 2026-10-27 22:05:02 CST
    Process: 33120 ExecStart=/opt/app/bin/start.sh (code=exited, status=203/EXEC)

常见状态码:

状态含义处置
203/EXECExecStart 路径不存在或无执行权限检查绝对路径和 chmod +x
200/CHDIRWorkingDirectory 不存在先建目录
1/FAILURE程序自己退出 1看应用日志
timeout启动超过 TimeoutStartSec调大,或改对 Type
signal=KILL / 9被 SIGKILL(OOM 或超时)查内存与 dmesg
status=143收到 SIGTERM若属正常停止可加 SuccessExitStatus=143

命令路径问题 ​

ExecStart 不会读 /etc/profile、.bashrc,也不会展开 $PATH 里的别名。

ini
# 错
ExecStart=java -jar /opt/app/app.jar

# 对
ExecStart=/usr/local/jdk-17/bin/java -jar /opt/app/app.jar

或者:

ini
Environment="PATH=/usr/local/jdk-17/bin:/usr/local/bin:/usr/bin:/bin"
ExecStart=/usr/local/jdk-17/bin/java -jar /opt/app/app.jar

改了配置没生效 ​

bash
systemctl daemon-reload && systemctl restart myapp

忘了 daemon-reload 是最常见的"改了没反应"。

八、完整示例:Spring Boot ​

ini
# /etc/systemd/system/order-api.service
[Unit]
Description=Order API
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=app
Group=app
WorkingDirectory=/opt/order-api
EnvironmentFile=-/etc/order-api/env
ExecStart=/usr/local/jdk-17/bin/java \
  -Xms1g -Xmx1g \
  -XX:+UseG1GC -XX:MaxGCPauseMillis=200 \
  -Xlog:gc*:file=/var/log/order-api/gc.log:time,uptime:filecount=5,filesize=50m \
  -jar /opt/order-api/app.jar \
  --spring.profiles.active=prod

SuccessExitStatus=143
Restart=on-failure
RestartSec=5
TimeoutStopSec=30
LimitNOFILE=65535
OOMScoreAdjust=-500

[Install]
WantedBy=multi-user.target

配套的优雅停机(application.yml):

yaml
server:
  shutdown: graceful
spring:
  lifecycle:
    timeout-per-shutdown-phase: 25s

TimeoutStopSec=30 > timeout-per-shutdown-phase=25s,保证应用有机会自己收尾。反过来的话 systemd 会先 SIGKILL。

九、踩过的坑 ​

现象根因处置
203/EXEC路径错/无执行权限绝对路径 + chmod +x
改了 unit 不生效没 daemon-reload跑一次
服务显示 active 但端口没监听Type 选错(该 notify 用了 simple)改 Type
stop 后进程还在ExecStart 用了 sh -c 带 &用 exec 或直接指向程序
启动超时Type=forking 用在后台程序上改 simple
频繁重启应用启动就崩看 journalctl,加 StartLimitBurst 防止刷屏
日志找不到应用写文件而非 stdout看应用日志路径
重启机器后服务没起没 enablesystemctl enable
端口被占导致启动失败有残留进程ss -lntp,必要时 pkill