多个服务依赖怎么搞?测试脚本教你合理排序
在实际运维和开发环境中,我们经常遇到这样的问题:系统启动时需要按特定顺序启动多个服务——比如数据库必须先于应用服务启动,消息队列要早于消费者进程加载,缓存服务得在业务逻辑之前就绪。一旦顺序错乱,轻则服务启动失败,重则整个系统卡死、日志刷屏、排查耗时数小时。
这个问题看似简单,实则暗藏玄机。很多人以为只要把脚本丢进/etc/init.d/就万事大吉,结果一重启,应用报“连接拒绝”,查半天才发现 MySQL 还没起来;或者发现 Redis 启动了,但应用却连不上,最后发现是网络配置脚本被排在了后面……
别急,这不是玄学,而是有章可循的工程实践。本文不讲抽象理论,不堆概念术语,就用一个真实可用的测试开机启动脚本镜像,手把手带你理清依赖关系、看懂启动顺序逻辑、写出可验证的排序方案。全程基于 Linux 常见发行版(CentOS 和 Ubuntu 均适用),所有操作均可直接复现,无需额外安装工具。
你不需要是系统专家,只要会写 shell 脚本、能敲几条命令,就能掌握这套方法。读完后,你会清楚知道:
- 为什么有些脚本启动快、有些总报错;
- 怎么一眼看出哪个服务该先起、哪个得等;
- 如何用最简方式验证你的排序是否生效;
- 遇到依赖冲突时,该改哪一行、调哪个数字。
准备好了吗?我们从最基础的脚本准备开始,一步步拆解这个“多服务依赖”的硬骨头。
1. 先写一个能跑起来的测试脚本
别一上来就碰复杂的业务服务,我们先造一个干净、可控、带日志输出的“测试脚本”,专门用来观察启动行为。它不干别的事,只做三件事:打时间戳、写日志、假装自己是个服务。
1.1 创建脚本文件
打开终端,执行以下命令创建/etc/init.d/mytest.sh:
sudo tee /etc/init.d/mytest.sh << 'EOF' #!/bin/bash # chkconfig: 2345 99 01 # description: MyTest service for dependency testing case "$1" in start) echo "$(date '+%H:%M:%S') - [mytest] Starting..." | tee -a /var/log/mytest.log sleep 1 echo "$(date '+%H:%M:%S') - [mytest] Started successfully." | tee -a /var/log/mytest.log ;; stop) echo "$(date '+%H:%M:%S') - [mytest] Stopping..." | tee -a /var/log/mytest.log sleep 0.5 echo "$(date '+%H:%M:%S') - [mytest] Stopped." | tee -a /var/log/mytest.log ;; restart) $0 stop $0 start ;; *) echo "Usage: $0 {start|stop|restart}" exit 1 ;; esac exit 0 EOF1.2 设置执行权限并验证
sudo chmod +x /etc/init.d/mytest.sh sudo /etc/init.d/mytest.sh start检查日志是否生成:
tail -n 3 /var/log/mytest.log你应该看到类似这样的输出:
14:22:05 - [mytest] Starting... 14:22:06 - [mytest] Started successfully.成功!这个脚本现在就是一个“可观察的服务”——它不依赖任何外部组件,但能清晰告诉你:它什么时候启动、有没有卡住、是否真的被执行了。
小贴士:为什么不用
systemd?因为本文聚焦传统 SysV init 的启动顺序机制,这是理解依赖排序最底层、最直观的入口。Ubuntu 16.04+ 和 CentOS 7+ 虽默认用systemd,但/etc/init.d/兼容层依然完整,且rcN.d的软链逻辑完全一致,不影响学习本质。
2. 看懂系统启动级别和执行目录
很多同学卡在这一步:明明写了脚本、加了软链,重启后却没反应。根本原因,是没搞清“系统到底在哪一刻、从哪个目录里找你的脚本”。
2.1 查当前运行级别
执行命令:
runlevel输出类似:
N 5这表示:系统当前运行级别是5(图形界面模式)。注意,这里的5就是关键线索——它决定了系统启动时去哪个目录加载脚本。
小白友好解释:
Linux 启动不是“一股脑全拉起来”,而是分阶段、按“级别”来。级别3是纯命令行多用户模式,5是带图形界面的多用户模式。每个级别对应一个专属目录:/etc/rc3.d/、/etc/rc5.d/……系统启动时,就去对应目录里,按名字顺序执行所有以S开头的脚本。
2.2 理解/etc/rc5.d/目录的命名规则
进入该目录看看:
ls -l /etc/rc5.d/ | head -10你会看到一堆类似这样的文件:
S10rsyslog S20network S50apache2 S99mytest K01mysql重点来了——这些名字不是随便起的,它们自带两层含义:
- 首字母:
S表示 Start(启动),K表示 Kill(停止) - 紧跟的两位数字:表示执行顺序,范围
01到99,数字越小越早执行,越大越晚
所以S10rsyslog一定比S50apache2先跑,而S99mytest是这一批里最后一个启动的。
为什么要有 K 开头的?
这是为了关机或切换运行级别时用的。比如从5切到3,系统会去/etc/rc3.d/找K开头的脚本,按数字从小到大执行停止逻辑。你暂时不用管它,专注S就行。
2.3 关键结论:依赖 = 启动序号的大小关系
到这里,你就掌握了核心逻辑:
如果 A 服务依赖 B 服务(比如应用依赖数据库),那么 A 的启动序号必须大于B 的启动序号。
比如数据库叫S20mysql,你的应用就得叫S80myapp或S99myapp,不能叫S15myapp。
这就是“合理排序”的全部秘密——没有魔法,只有数字大小。
3. 给测试脚本加个“依赖伙伴”,模拟真实场景
光一个脚本看不出依赖效果。我们再加一个“数据库模拟脚本”,让它启动得早一点,然后让mytest.sh显式等待它——这样就能验证排序是否真起作用。
3.1 创建数据库模拟脚本
sudo tee /etc/init.d/mydb.sh << 'EOF' #!/bin/bash # chkconfig: 2345 20 80 # description: MyDB mock service (starts early) case "$1" in start) echo "$(date '+%H:%M:%S') - [mydb] Starting database mock..." | tee -a /var/log/mydb.log sleep 2 echo "$(date '+%H:%M:%S') - [mydb] Database ready." | tee -a /var/log/mydb.log ;; stop) echo "$(date '+%H:%M:%S') - [mydb] Stopping database..." | tee -a /var/log/mydb.log sleep 0.5 echo "$(date '+%H:%M:%S') - [mydb] Database stopped." | tee -a /var/log/mydb.log ;; restart) $0 stop $0 start ;; *) echo "Usage: $0 {start|stop|restart}" exit 1 ;; esac exit 0 EOF sudo chmod +x /etc/init.d/mydb.sh注意看chkconfig行里的20—— 这就是它的启动序号,比mytest.sh的99小得多,意味着它会先启动。
3.2 修改 mytest.sh,加入依赖检查逻辑
编辑/etc/init.d/mytest.sh,在start)分支开头加一段等待代码:
sudo sed -i '/start)/a\ # Wait for mydb to be ready\n timeout=30\n while [ $timeout -gt 0 ]; do\n if grep -q "Database ready." /var/log/mydb.log 2>/dev/null; then\n echo "$(date \'+%H:%M:%S\') - [mytest] Detected mydb is ready." | tee -a /var/log/mytest.log\n break\n fi\n sleep 1\n timeout=$((timeout - 1))\n done\n if [ $timeout -eq 0 ]; then\n echo "$(date \'+%H:%M:%S\') - [mytest] ERROR: mydb did not start in time!" | tee -a /var/log/mytest.log\n exit 1\n fi' /etc/init.d/mytest.sh这段代码的意思是:启动mytest时,最多等 30 秒,反复检查/var/log/mydb.log里有没有 “Database ready.” 这行字。如果超时没等到,就直接退出报错。
现在,两个脚本有了明确的依赖关系:mydb.sh(S20)必须先于mytest.sh(S99)启动,且mytest.sh会主动确认依赖就绪。
4. 创建软链接,正式“排序”
前面都是准备,现在进入最关键的一步:把脚本放进正确的rcN.d目录,并用数字控制顺序。
4.1 进入对应 rc 目录
根据runlevel输出的级别(比如5),进入:
cd /etc/rc5.d/4.2 创建软链接(带序号)
为mydb.sh创建启动链接(序号20):
sudo ln -sf /etc/init.d/mydb.sh S20mydb为mytest.sh创建启动链接(序号99):
sudo ln -sf /etc/init.d/mytest.sh S99mytest注意
ln -sf中的-f参数:它会强制覆盖已存在的同名链接,避免因重复操作报错。
4.3 验证链接是否正确
执行:
ls -l S*my*你应该看到:
S20mydb -> /etc/init.d/mydb.sh S99mytest -> /etc/init.d/mytest.sh链接创建成功,序号清晰,指向无误。
5. 不用重启,也能验证排序是否生效
等等——难道每次都要reboot才能测?当然不用。那样效率太低,还容易干扰生产环境。
我们用一个更聪明的办法:手动模拟系统启动流程。
5.1 按序号顺序手动执行 S 开头的脚本
在/etc/rc5.d/目录下,执行:
for script in $(ls S* | sort); do echo "=== Running $script ===" sudo ./$script start 2>/dev/null || echo "[FAIL] $script failed" sleep 0.5 done这个循环会按字母顺序(也就是按数字顺序)依次执行所有Sxx*脚本。你将实时看到:
S20mydb先打印启动日志,2秒后显示 “Database ready.”- 然后
S99mytest启动,先等几秒,检测到mydb就绪后,才继续自己的启动流程。
5.2 检查日志,确认依赖成立
查看两个日志文件的末尾:
echo "--- mydb.log ---"; tail -n 3 /var/log/mydb.log echo "--- mytest.log ---"; tail -n 5 /var/log/mytest.log理想输出应类似:
--- mydb.log --- 14:35:10 - [mydb] Starting database mock... 14:35:12 - [mydb] Database ready. --- mytest.log --- 14:35:12 - [mytest] Starting... 14:35:12 - [mytest] Detected mydb is ready. 14:35:13 - [mytest] Started successfully.看到时间戳对得上:mydb在14:35:12就绪,mytest在同一秒就检测到了——说明排序和等待逻辑都工作正常。
你已经用最小成本,验证了整套依赖排序机制。
6. 实战建议:如何给真实服务排好序?
上面是测试,现在回归现实。当你面对 N 个真实服务(Nginx、MySQL、Redis、你的 Python 应用……)时,该怎么动手?
6.1 三步法快速梳理依赖
列出所有服务及其默认启动序号
ls -l /etc/rc5.d/S* | awk '{print $9, $11}' | grep -v "\-> /etc/init.d/"这会显示当前已启用的所有
S脚本及其目标路径,帮你摸清现状。画一张依赖图(纸上或白板)
- 圆圈代表服务(MySQL、Redis、App)
- 箭头从依赖方指向被依赖方(App → MySQL,App → Redis)
- 标出你希望的相对顺序(MySQL < Redis < App)
分配序号,留足余量
- 不要用满
01-99,推荐区间:- 基础服务(syslog、network):
01-20 - 中间件(MySQL、Redis、RabbitMQ):
21-50 - 业务应用(Web、API、Worker):
51-85 - 测试/监控/自定义脚本:
86-99
- 基础服务(syslog、network):
- 每类之间留 5~10 的空档,方便后续插入新服务。
- 不要用满
6.2 避坑指南:那些年踩过的排序雷区
❌雷区1:序号相同导致竞态
两个S50xxx脚本,系统按字母顺序执行(S50apache2在S50mysql前),但你无法保证。永远不要让关键依赖服务共享同一序号。❌雷区2:只靠序号,不加运行时检查
序号只能保证“谁先执行”,不能保证“谁先就绪”。MySQL 脚本可能已启动,但端口还没监听完成。务必在应用脚本中加入健康检查(如nc -z localhost 3306或curl -f http://localhost:8080/health)。❌雷区3:忽略停止顺序
启动是Sxx,停止是Kyy。如果你的应用S80app依赖S20mysql,那么停止时,应该让K20app在K80mysql之后执行(即K20app数字更小),确保应用先停、数据库后停。chkconfig行第二个数字就是停止序号(如chkconfig: 2345 80 20)。正解:用
update-rc.d(Debian/Ubuntu)或chkconfig(CentOS)管理
它们会自动处理软链和序号,比手敲ln更安全:
# Ubuntu/Debian sudo update-rc.d mydb defaults 20 80 sudo update-rc.d mytest defaults 99 01 # CentOS sudo chkconfig --add mydb sudo chkconfig mydb on sudo chkconfig mydb priority 207. 总结:排序不是玄学,是可验证的工程动作
我们从一个简单的测试脚本出发,一路走到了真实服务的排序实践。回顾一下,你真正掌握的是什么?
- 不是记住
rc5.d这个路径,而是理解“运行级别决定执行目录”这一设计逻辑; - 不是死记
S99代表最后,而是明白“依赖 = 启动序号的严格大小关系”这一本质; - 不是学会
ln -s命令,而是建立了“先建脚本 → 再定序号 → 最后验日志”的闭环验证习惯; - 不是为了兼容老系统,而是获得一种底层、稳定、不依赖特定工具链的依赖治理能力。
在容器和 Kubernetes 时代,rc.d机制看似过时,但它所承载的思想——显式声明依赖、有序执行、可观测验证——从未过时。Docker Compose 的depends_on、K8s 的 Init Container、甚至现代 CI/CD 的 job 依赖,都是这一思想的延伸。
所以,别把它当成历史遗迹。把它当作一把尺子,用来衡量任何依赖方案是否足够清晰、是否经得起验证。
下次再遇到“服务起不来”的告警,别急着翻文档、查日志、重启大法。先问一句:它的启动序号,配得上它的依赖关系吗?
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。