如何编写一个健壮的系统服务安装脚本(Systemd/Init.d 实战指南)
目录导读
- 为什么需要服务安装脚本? —— 运维自动化的基石
- 脚本的核心架构 —— 前置检查、安装、配置、启动、验证五段式
- 针对 Systemd 与 Init.d 的差异化处理
- 常见陷阱与规避策略(含权限、路径、日志轮转)
- 脚本健壮性增强:幂等性、回滚机制与日志记录
- 问答精华 —— 运维老手最关心的 5 个问题
手把手构建你的服务安装脚本

为什么需要服务安装脚本?
当你在 50 台服务器上手动敲 systemctl start nginx 时,效率与一致性已经失败,一个系统服务安装脚本能实现:一键部署、环境统一、可重复执行,它不仅仅是把二进制文件拷贝到 /usr/local/bin,更是一个状态机——负责处理依赖、权限、端口冲突、开机自启等复杂逻辑。
脚本的核心架构:五段式设计
第一段:前置检查(Pre-flight Checks)
- 检查是否 root 权限(关键!
EUID必须为 0)。 - 检查操作系统发行版(
/etc/os-release)以决定使用systemctl还是service。 - 检查端口冲突(
ss -lntp | grep :8080)。 - 检查依赖包(
command -v wget或dpkg -l)。
第二段:安装核心文件
- 定义变量:
SERVICE_NAME="myapp",INSTALL_DIR="/opt/myapp"。 - 使用
install -d -m 755 $INSTALL_DIR创建目录。 - 使用
install -m 644 config.yaml $INSTALL_DIR/拷贝文件(相比cp,install能精确控制属主与权限)。
第三段:注册系统服务
- Systemd 系统:写
/etc/systemd/system/$SERVICE_NAME.service文件。[Unit] Description=My Custom Service After=network.target
[Service] Type=simple User=myapp ExecStart=/opt/myapp/run.sh Restart=on-failure RestartSec=5
[Install] WantedBy=multi-user.target
- **Init.d 系统**:写 `/etc/init.d/$SERVICE_NAME` 脚本,并包含 `start|stop|restart|status` 函数,然后执行 `update-rc.d $SERVICE_NAME defaults`。
**第四段:启动与验证**
- 先 `daemon-reload`(systemd),再 `enable` 开机自启,`start`。
- 验证:`systemctl is-active` 或 `pgrep -f yourapp`,或者 `curl -f http://localhost:8080/health`。
**第五段:收尾与优雅退出**
- 设置 `trap` 捕获错误,若启动失败,自动回滚(删除文件、恢复备份)。
### 3. 差异化处理:Systemd vs Init.d
**在脚本中检测 init 系统**:
```bash
if [ -d /run/systemd/system ]; then
INIT_SYS="systemd"
else
INIT_SYS="initd"
fi
- Systemd:注意
Type的选择。forking用于启动后父进程退出的程序;simple适用于前台运行。 - Init.d:必须处理
pidfile约定,并确保脚本开头有#!/bin/sh且具有执行权限。
常见陷阱与规避策略
- 权限错误:服务运行用户尽量不使用 root,单独创建
useradd -r -s /sbin/nologin $SERVICE_USER。 - 路径硬编码:绝对路径与环境变量(
$JAVA_HOME)混用易出错,优先使用readlink -f解析脚本自身位置。 - 日志处理:不要手动重定向输出到
/dev/null,应配置StandardOutput=append:/var/log/myapp.log。 - 卸载残留:脚本应提供
uninstall参数,删除服务文件并执行daemon-reload。
脚本健壮性增强:幂等性、回滚与日志
- 幂等:每次执行前检查
if [ -f "$INSTALL_DIR/bin/app" ]; then echo "已安装,跳过安装步骤"; fi。 - 回滚:安装前备份
conf文件为.bak,若启动失败,cp .bak恢复。 - 全量日志:将整个脚本输出写入
$INSTALL_DIR/install.log,方便远程排查。
问答精华(你关心的 5 个问题)
Q1: 脚本中如何优雅地停止旧服务版本?
A: 使用
service $NAME stop || true忽略错误,但更好的方式是先查 PID,kill -TERM,等待 3 秒后kill -KILL,配合trap确保优雅退出。
Q2: 如何处理服务配置文件中的敏感信息(如数据库密码)?
A: 将密码写入
/etc/$SERVICE_NAME.conf并设置chmod 600,服务运行时通过EnvironmentFile读取,避免明文出现在进程参数中。
Q3: systemd 服务启动后立即退出(exit code 1),如何排查?
A: 不要只看
journalctl -u的尾部错误,在脚本中捕获 变量,并在启动失败时自动输出journalctl -xe --no-pager | tail -50到安装日志。
Q4: 如何确保脚本跨 CentOS 7 与 Ubuntu 20.04 兼容?
A: 对于包管理器,使用
yum || apt二选一;对于服务管理,抽象systemctl和service为函数变量:start_cmd="systemctl start $SERVICE"。
Q5: 安装脚本本身需要加密保护吗?
A: 不要加密,采用
shfmt规范格式,并用shellcheck静态检查,若包含敏感数据,可在脚本内调用外部加密的secrets.env文件。
一个好的服务安装脚本,是运维人品的直接体现,它不在于技巧炫技,而在于面对未知环境时的防御性思维,建议你写完后,在干净的虚拟机里反复执行三次,每次删除不同配置项,观察脚本的报错提示是否清晰,最终把脚本提交到内部 Git,用 CI 工具定时做冒烟测试,这才是工程化精神。