ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Linux开机自启动服务配置详解:从Systemd到rc.local

Linux开机自启动服务配置详解:从Systemd到rc.local 1. 开机自启动的底层逻辑从内核到服务管理器先聊点背景知识。我刚接触 Linux 的时候一直有个困惑开机自启动服务不就是把命令写进/etc/rc.local吗后来在真实生产环境里吃了几次亏才明白这套思路早就不够用了。你想想如果你的服务器上有 Nginx、MySQL、Redis、定时任务脚本甚至还有内网穿透工具全都塞进一个 rc.local 文件里那这个文件会变成什么样启动顺序怎么控制某个服务挂了要不要自动拉起日志记在哪里这些问题靠“往文件里追加命令”这种简单粗暴的方式根本回答不了。所以我现在写这篇东西就是想把这些年配置开机自启动服务的经验一次性讲清楚。不管你是刚把 Linux 装在虚拟机里练手的新手还是在公司服务器上部署业务系统的运维这篇文章都适合你。我会从最底层的启动流程讲起再逐步展开 Systemd、SysV、rc.local、cron 这几种主流做法最后分享一些排查故障的实操经验。先花一分钟把启动流程这事说透。按下电源键之后硬件先跑 BIOS 或 UEFI 自检然后引导加载程序GRUB 之类把内核加载进内存。内核起来之后会启动第一个用户态进程也就是 PID 1。在传统 SysV 系统上PID 1 是 init 进程它按照/etc/inittab里的设定去执行/etc/init.d/下的脚本而在现代 Systemd 系统上PID 1 就是 systemd它会读取/etc/systemd/system/和/usr/lib/systemd/system/下的 Unit 文件按依赖关系把服务逐个拉起来。所谓“开机自启动”本质就是告诉 PID 1这个服务请在系统启动到某个阶段时自动帮我运行起来。明白了这个逻辑你就懂了为什么现在都推荐 Systemd。它把服务、挂载点、设备、定时器都抽象成 Unit支持并行启动、自动重启、资源隔离、日志集中管理还能用一条命令查看所有服务的状态。CentOS 7 之后、Ubuntu 16.04 之后、Debian 8 之后主流发行版默认都是 Systemd。所以我们现在配置开机自启优先用 Systemd 的方案这是最符合当下环境的选择。2. 主流打法手写 Systemd 服务实现开机自启2.1 Service Unit 文件到底长什么样Systemd 的服务管理核心就是 Unit 文件。后缀是.service放在/etc/systemd/system/或/usr/lib/systemd/system/下。两者的区别是前者是系统管理员手工创建的优先级更高适合自定义服务后者是软件包安装时自动生成的属于“出厂配置”。如果你要修改某个安装好的服务的行为应该去/etc/systemd/system/下建一个同名的覆盖文件而不是直接改后者因为软件升级时会把原始文件覆盖掉你的改动就白费了。一个最小的 Service Unit 长这样[Unit] DescriptionMy Custom Service Afternetwork.target [Service] Typesimple ExecStart/usr/local/bin/my-service Restarton-failure [Install] WantedBymulti-user.target[Unit]段里的 Description 是服务描述After声明依赖关系意思是这个服务要在 network.target 之后才启动确保网络已经就绪。[Service]段是核心Type 指定启动类型ExecStart 是实际要运行的命令Restart 配置失败后是否自动重启。[Install]段里的 WantedBy 是关键它定义了服务在哪个 target 下被启用multi-user.target表示系统进入多用户命令行模式时启动这是绝大多数服务使用的目标。Type 这个参数容易被忽略但它直接影响系统判断“服务是否启动成功”。如果你写 TypesimpleSystemd 认为 ExecStart 进程一启动就算成功哪怕这个进程只是个壳真正干活的是它 fork 出去的子进程。如果你的程序是传统的守护进程会自己调 fork 然后父进程退出那应该用 Typeforking而且往往要配合 PIDFile 参数指定 PID 文件位置否则 Systemd 找不到主进程管理就会失控。我第一次写服务时用的 Typesimple结果程序内部自己 fork 了systemctl status 怎么都显示 active (running)但实际进程死活找不到后来才发现是 Type 写错了。2.2 制作服务的完整流程从脚本到 systemctl 管理光看文件结构肯定不够我拿一个实际场景演示一遍。假设我写了一个 Python 脚本/opt/scripts/wechat-alert.py用来监控服务器负载负载高了就通过企业微信机器人发告警。这个脚本我希望开机自动运行而且进程挂了能自动拉起来。第一步写好脚本并赋予执行权限sudo vim /opt/scripts/wechat-alert.py sudo chmod x /opt/scripts/wechat-alert.py脚本第一行一定要写 shebang也就是#!/usr/bin/env python3这行因为 Systemd 的 ExecStart 默认不会帮你解析脚本类型如果没有 shebang它会尝试当作二进制执行直接报 Exec format error。第二步创建 Service Unit 文件sudo vim /etc/systemd/system/wechat-alert.service内容如下[Unit] DescriptionWeChat Alert Service Afternetwork-online.target Wantsnetwork-online.target [Service] Typesimple Userroot WorkingDirectory/opt/scripts ExecStart/usr/bin/python3 /opt/scripts/wechat-alert.py Restartalways RestartSec10 EnvironmentPYTHONUNBUFFERED1 [Install] WantedBymulti-user.target这里我加了几个值得留意的配置项。Wantsnetwork-online.target配合After保证脚本运行时网络已经可用这个对告警脚本特别重要因为发消息要联网。Userroot是指定运行身份如果你脚本只需要普通权限千万别用 root最小权限原则在服务配置里同样适用。Restartalways表示只要进程退出就自动重启RestartSec10是重启前等待 10 秒防止崩溃循环导致 CPU 飙升。EnvironmentPYTHONUNBUFFERED1是为了让 Python 输出不经过缓冲区日志能实时刷出来。第三步重新加载 Systemd 配置并启动sudo systemctl daemon-reload sudo systemctl start wechat-alert.service sudo systemctl status wechat-alert.service很多人配置完直接 start结果报错说服务不存在就是因为忘了 daemon-reload。Systemd 会缓存 Unit 文件的状态新增文件后必须重新加载才能识别。第四步也是主题所在设置开机自启sudo systemctl enable wechat-alert.service这一步会在/etc/systemd/system/multi-user.target.wants/目录下创建一个符号链接指向你的 Unit 文件。这样系统启动时Systemd 扫描到这个链接就会按配置把服务拉起来。执行 enable 之后可以用systemctl is-enabled wechat-alert.service验证输出 enabled 就说明成功了。2.3 禁用和状态管理细节别忽视有开启自然有关闭。systemctl disable wechat-alert.service会移除那个符号链接服务就不再开机自启了但注意它不会停止当前正在运行的服务。想既禁用又立即停止要连用两条命令sudo systemctl disable wechat-alert.service sudo systemctl stop wechat-alert.service还有一个容易踩的坑修改了 Unit 文件之后必须再执行一次daemon-reload否则你改了 ExecStart 的参数服务重启后用的还是旧配置。我见过同事在服务器上调了半天参数发现服务行为完全没变最后发现是没 reload白白浪费半小时。另外systemctl enable只是建立启动链接真正启动要在start之后。有些服务的安装脚本会自动 start有些不会所以配置完一定验证一次完整的“重启流程”先disable再enable然后 reboot看看服务是否真的起来了。生产环境我一般不会直接 reboot而是用systemctl reboot在维护窗口做一次真实的冷启动测试这样最保险。3. 老系统与特殊场景SysV、rc.local、cron 三个备选方案3.1 SysV init 脚本的老派玩法虽然 Systemd 是主流但你一定会遇到老旧的服务器。五六年前部署的 CentOS 6、Ubuntu 14.04 现在还有不少在跑这些系统用的是 SysV init。在这种系统上配置开机自启靠的是/etc/init.d/下的启动脚本和 runlevel 目录里的符号链接。脚本头部有一段特殊的注释叫 LSB header这样写#!/bin/bash ### BEGIN INIT INFO # Provides: my-service # Required-Start: $network # Required-Stop: $network # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: My Custom Service ### END INIT INFO case $1 in start) echo Starting my-service... /usr/local/bin/my-service ;; stop) echo Stopping my-service... pkill -f my-service ;; restart) $0 stop $0 start ;; *) echo Usage: $0 {start|stop|restart} exit 1 ;; esac exit 0这个脚本要写得特别小心start 分支里的不能丢否则脚本会卡住系统启动时就会在你这卡半分钟。写完脚本之后用 update-rc.dDebian/Ubuntu或 chkconfigCentOS/RHEL把它注册进去# Debian 系 sudo update-rc.d my-service defaults # CentOS/RHEL 6 及更早 sudo chkconfig --add my-service sudo chkconfig my-service on说实话这套机制现在已经很少用了处理起来也比较繁琐。但如果你管理的是存量老系统还是得会不然真遇到问题就只能抓瞎。3.2 rc.local这个“万能兜底”还能用多久再讲一个大家都听说过的方案/etc/rc.local。它的运作原理很直接——系统启动到后期init 或 Systemd 会去执行这个文件里的所有命令。在没有 Systemd 的发行版上这是最应该掌握的技巧但即使在现代系统上它依然是一个非常有用的兜底方案特别适合“我就想启动时跑一句命令、又不想为它专门写一个 service”的场景。用法很简单把要执行的命令追加到/etc/rc.local里确保有执行权限sudo chmod x /etc/rc.local echo /usr/local/bin/my-backup-script.sh /etc/rc.local这里的关键坑是现代 Systemd 系统上 rc.local 本身也是被 Systemd 当作一个服务来管理的文件末尾通常有exit 0。如果你把脚本路径放在exit 0之后脚本根本不会被执行。另外rc.local 里的命令默认是在一个极简环境里跑的PATH 可能不包含/usr/local/bin所以最好的做法是用绝对路径或者在 rc.local 开头先 export PATH。我的态度是rc.local 适合几条简单的命令比如设置内核参数、加载模块、启动一个自定义的隧道工具。它不适合拿来管理正经服务因为 Systemd 的依赖管理、自动重启、日志采集它一个都没有。为了省事把它当万能筐到最后就是你根本不知道服务器上哪些服务是怎么被拉起来的这种状态对运维来说很危险。3.3 用 cron reboot 曲线救国最后一个备选方案是 cron 的reboot特殊时间表达。它在每次系统启动时运行指定的命令非常适合那些“不需要持续运行、只需开机后执行一次”的任务。crontab -e # 添加一行 reboot /usr/local/bin/clean-tmp-files.sh这个方案的优势是配置极为简单普通用户就能管理自己的定时任务不需要 sudo不需要维护 systemd 文件。缺点是 cron 守护进程本身要在系统启动后才会运行所以你的命令执行时机比较靠后而且 cron 同样不提供进程守护功能命令跑完就完了挂了不会自动拉起。我一般只用它清理临时目录、发送开机通知这类一次性任务。4. 启动顺序、依赖关系与开机慢的排查4.1 服务多了之后启动顺序怎么管很多人在单机上有五六个自启动服务配置完之后就祈祷开机别出问题。问题是你怎么能确认 MySQL 一定在应用服务之前启动如果你直接用After指定了 MySQLSystemd 能保证顺序但这个依赖一旦写错整个启动链可能就崩了。我对服务排序的建议是先画清楚依赖图。自己的应用依赖数据库、依赖网络、依赖某个挂载点都需要在 Unit 文件的 After 和 Requires/Wants 里体现。区分硬依赖和软依赖。Requires表示强依赖目标服务没起来当前服务就失败Wants是弱依赖目标服务没起来当前服务照常启动。日常配置用 Wants 更稳妥至少不会因为一个辅助服务挂了导致主服务起不来。依赖不能想当然。我遇到过有人给一个纯本机脚本配置了Afterdocker.service结果 Docker 没安装脚本一直处于 waiting 状态后来才发现这个依赖根本没意义。如果你只是想让某个服务晚点启动可以用After配合一个不存在的 target 吗我之前看到有人写Aftermysql.service但只写了个名称没有安装结果服务启动失败。依赖必须真实存在Systemd 自身没有“延迟 5 秒启动”这种直观配置但你可以通过ExecStartPre/bin/sleep 5来达到类似效果或者用 timer 单元 OnBootSec5。不过最好的做法还是把依赖关系理顺而不是靠延时硬凑。4.2 开机启动慢问题到底出在哪去年我调过一台服务器的启动流程systemd-analyze给了三个很关键的排查维度systemd-analyze time # 查看总启动耗时 systemd-analyze blame # 按耗时排序每个服务的启动时间 systemd-analyze critical-chain # 查看关键启动路径有一次我发现开机要 1 分 40 秒用 blame 一看某个服务占用了 50 多秒。点进去发现是脚本里有个sleep 60原因是写脚本的人觉得“等网络完全就绪再执行比较好”。频率这么高的 sleep 就是典型的偷懒方案。正确的做法是用Afternetwork-online.targetWantsnetwork-online.target等待网络真正确认就绪。另一个常见的问题是服务依赖了网络但网络配置本身等了很久才拿到 DHCP 地址。这时候要看是不是没有启用systemd-networkd-wait-online或者是 NetworkManager 的网络等待服务没拉起来。我踩过类似的坑排查到最后发现是网卡固件驱动加载慢跟服务本身没关系。所以看到某个服务一直 waiting别急着改业务代码先用systemctl list-jobs看看当前有哪些任务卡在启动队列里往往能直接找到元凶。5. 常见问题速查表与排查经验5.1 高频坑位清单建议截图收藏配置开机自启这么多年我把高频问题整理成了一张速查表定位问题时照着看就能少走很多弯路。现象可能原因排查命令与解决思路开机后服务没启动Unit 文件未 enablesystemctl is-enabled xxx.service执行 enabledaemon-reload之后还是报错Unit 文件语法问题systemd-analyze verify /etc/systemd/system/xxx.service服务启动成功但立即退出Type 类型配置错误确认程序是否 fork改用 Typeforking PIDFileExec format error脚本缺少 shebang 或没有执行权限补#!/bin/bashchmod x启动顺序不对依赖关系未声明检查 After、Wants、Requires 配置日志里什么都没有服务输出被吞加Environment...或配置 StandardOutputjournal开机自启配置没问题但重启后又没了用了错误路径的 Unit 文件确认 enable 的链接指向/etc/systemd/system/下方有一次我在 CentOS 7 上配置一个服务systemctl enable报错Failed to execute operation: No such file or directory。这个报错很迷惑实际上是因为我在 Unit 文件的 ExecStart 里写了不存在的二进制路径。enable 时 systemd 会做依赖检查发现 ExecStart 可执行文件不存在就直接拒绝创建启动链接。这种问题用systemd-analyze verify可以提前发现但很多人丢了这条命令只知道反复改权限其实方向搞错了。5.2 一次真实的排查过程记录我之前在客户一台 Ubuntu 18.04 服务器上部署了一个 Java 后端服务按要求配置了开机自启。客户过了两天跟我说服务开机后就是起不来但手动systemctl start一切正常。我远程上去排查systemctl status只看到一个冷冰冰的状态inactive (dead)journal 日志里也没留下明显的报错。我先把服务状态打开然后重新 enable再手动执行systemctl start确认没问题后reboot 了一次。重启之后还是一样起不来。这时候我意识到一个问题Afternetwork.target只在网络接口达成“基础就绪”时触发而 Java 应用启动时是需要 DNS 解析才能连接注册中心的。DHCP 虽然快但 DNS 服务加载可能更慢。于是我把After改成了network-online.target并加上Wantsnetwork-online.target重新测试服务终于在开机后稳定启动了。后来我又发现一个问题即使网络就绪了Java 进程还是会偶发启动失败。看日志发现是数据库连接池初始化超时本质是数据库实例启动比应用慢了几秒。这次我用Aftermysql.service把数据库的依赖关系写明让应用等数据库真正就绪再启动问题才算彻底解决。这个案例给我最大的体会是配置开机自启动不只是写一个 Unit 文件还得真正理解你依赖的服务的启动时序。很多时候“服务没起来”不是 Systemd 配置错了而是启动顺序里隐含了一个你没想到的竞态条件。5.3 最后分享几个小技巧配置开机自启每当我把一个服务交给 Systemd 管理时都会顺手做三件事这三个好习惯帮我省了特别多麻烦第一ExecStart里一定用绝对路径。这不是强迫症而是 Systemd 执行命令时默认不会继承当前用户的 PATH 环境变量不同发行版里/usr/local/bin、/usr/bin、/snap/bin的优先级和包含关系差别很大。为了避免“手动跑没问题开机就跑不起来”这种经典事故路径写死是最稳的。第二启动后立即看一下日志。journalctl -u xxx.service -f这个命令可以实时看输出很多隐藏错误会在日志尾部体现比如依赖环境不对、权限不足、配置文件解析失败。养成这个习惯能极大缩短定位问题的时间。第三修改配置后不要盲目重启服务器验证。如果只是改了一个 Unit 文件的参数先systemctl daemon-reload再systemctl restart观察状态确认没问题后再决定要不要做一次完整重启。生产环境下每次 reboot 都是有成本的能用最小代价验证就不要劳师动众。我在实际操作里还有一个习惯——每次配置完一个自启动服务会在一个单独的笔记文件里记录服务名称、Unit 文件路径、启动依赖、常见故障、验证命令。这个笔记看起来不起眼但半年之后你想起来某个服务当时为什么加了某个诡异的配置翻一眼就能找到答案比对着系统里乱七八糟的配置文件瞎猜要高效得多。
返回列表