Systemd 服务管理实战:从 Unit 编写到日常运维

把一套应用从”手动 nohup 启动”改成 systemd 托管,是运维规范化里回报最高的一件事——开机自启、崩溃拉起、日志集中、权限隔离一次全解决。这篇把从零手写 .service 到日常运维的完整过程捋一遍。

一、先理解三个角色

  • .service 文件 = 一份”岗位说明书”:写明怎么启动、怎么停止、什么情况下重启;
  • systemctl = 操作指令:对服务执行 start / stop / enable / status
  • systemd = 管理进程:负责读懂说明书、监控所有服务。

现代主流 Linux 全部用 systemd,老掉牙的 SysV init 那套(/etc/inittabrc.local 塞脚本)只在新装系统时偶尔遇见,不建议再往里写东西。

二、.service 文件的三段式

每个 Unit 文件由 [Unit][Service][Install] 三部分构成,缺一不可。放在 /etc/systemd/system/(管理员自定义,优先级最高)或 /usr/lib/systemd/system/(软件包自带)。

[Unit]:声明依赖

1
2
3
4
5
[Unit]
Description=支付网关服务
Documentation=https://wiki.internal/payment
After=network.target mysql.service # 启动顺序:网络和 MySQL 之后
Wants=mysql.service # 弱依赖:mysql 挂了不影响本服务

生产里最常见的坑:Java 应用在 MySQL 还没就绪时就启动了,连接池初始化报错。所以 After 一定要把依赖关系写清楚。

[Service]:定义怎么干活

1
2
3
4
5
6
7
8
9
10
[Service]
Type=simple # 默认值:ExecStart 启动的进程就是主进程
ExecStart=/usr/bin/java -jar /app/payment/app.jar
Restart=on-failure # 异常退出才重启(MySQL 等数据库推荐)
RestartSec=10 # 重启间隔,防止故障时疯狂重启
TimeoutSec=60 # 启动/停止超时
User=appuser # 绝不用 root 跑对外服务
Group=appgroup
LimitNOFILE=65535 # 文件句柄上限,高并发服务必须调
PrivateTmp=true # 独立临时目录,防 /tmp 提权

Type 的选择直接决定 systemd 能不能正确跟踪进程:

Type 适用场景
simple 前台运行的程序(Node.js、Python Web 服务)
forking 会分叉后台的传统服务(Nginx、MySQL)
oneshot 执行一次就退出的脚本,配合 RemainAfterExit=yes
notify 支持 sd_notify 的现代程序,就绪后主动通知

[Install]:挂靠开机流程

1
2
[Install]
WantedBy=multi-user.target

systemctl enable 的实质,就是在 /etc/systemd/system/multi-user.target.wants/ 下创建指向本文件的符号链接,开机进入多用户模式时自动拉起。

三、两个真实案例

案例 1:手工安装的 MySQL 5.7(Type=forking)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[Unit]
Description=MySQL 5.7 Database Server
After=network.target

[Service]
Type=forking
User=mysql
Group=mysql
PIDFile=/data/mysql/data/mysqld.pid
ExecStart=/usr/local/mysql/bin/mysqld_safe --defaults-file=/etc/my.cnf
ExecStop=/usr/local/mysql/bin/mysqladmin -u root shutdown
Restart=on-failure
RestartSec=10
LimitNOFILE=65535
PrivateTmp=true

[Install]
WantedBy=multi-user.target

mysqld_safe 会分叉出真正的 mysqld 进程,所以必须 Type=forking 并指定 PIDFile,否则 systemd 会误判服务已退出。

案例 2:Java 微服务(Type=simple)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[Unit]
Description=Order Service
After=network.target mysql.service redis.service
Wants=mysql.service redis.service

[Service]
Type=simple
User=appuser
Group=appgroup
WorkingDirectory=/app/order
ExecStart=/opt/jdk11/bin/java -jar /app/order/order-service.jar --spring.profiles.active=prod
Restart=always
RestartSec=15
EnvironmentFile=/etc/sysconfig/order-service
StandardOutput=journal
StandardError=journal
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target

敏感配置(数据库密码)绝不写在 Unit 的 Environment 里明文,而是用 EnvironmentFile 指向权限 600 的配置文件,或交给密钥管理系统注入。

四、日常运维命令

1
2
3
4
5
6
7
8
systemctl start/stop/restart/status 服务名
systemctl daemon-reload # 修改 Unit 后必须重载
systemctl enable --now 服务名 # 开机自启 + 立即启动
systemctl cat 服务名 # 查看服务实际生效的配置
systemctl list-units --type=service --state=failed # 列出所有启动失败的服务(排查必用)
systemctl is-enabled 服务名 # 是否开机自启
systemd-analyze verify xxx.service # 校验 Unit 语法
systemd-analyze security xxx.service # 服务安全加固评分

五、两个排障经验

  1. 改了单位文件不生效:九成是忘了 systemctl daemon-reload,新文件 systemd 根本不认识。
  2. Restart=always 的服务疯狂重启:启动即崩的程序会不停刷日志,光看 status 一时半会看不出来,要注意结合 RestartSecStartLimitIntervalSec/StartLimitBurst 限制重启次数,同时去 journal 里看崩溃原因——这就是下一篇日志排查的内容。