简介:面向系统服务与安卓进程管理的开发者,这份资料以双模块工程切入进程常驻实战,从守护进程原理到服务绑定、优先级调整均有覆盖,适合有一定Java基础并希望深入理解应用保活与系统服务的读者。资源在设计上兼顾理论与实践,既有可编译的工程源码,也有直观看懂效果的运行截图。压缩包共含六十九个文件,以Java源码、XML配置与布局、AIDL接口定义及构建脚本为主,配合二十张PNG截图和混淆规则等构建细节,完整呈现从进程注册、优先级设置到生命周期管理的实现脉络,整体仅十六点九兆,离线研读方便。资源由两个模块组成,可分别体验前台服务、后台常驻及进程优先级的差异,配套工程目录结构清晰,便于直接导入开发环境调试与二次开发。内容还对日志监控、自动重启等保活边界作了探讨,能帮助读者规避常见误用。已有三百一十九人学习,说明其内容对同类需求具有实用参考价值。
1. 进程常驻不只是 nohup:这份多 module 常驻包解决什么
进程常驻这个问题,几乎所有做数据采集、消息中转、定时任务的开发者都躲不掉:终端一关脚本就跟着死,服务器重启后没人记得去点启动器,某几个模块各自为战、日志东一处西一处。排障的时候翻三个目录才找得到一份输出文件。这份「进程常驻探索」资源包我完整拆了一遍,里面不是一个单一大程序,而是把多个业务模块拆开来部署的完整思路:每个 module 独立跑、独立给 PID、独立写日志,再用 service 体系统一接管。适合手里有 3~5 个 python 脚本或 shell 任务、想让它们 7x24 小时稳定跑的从业者。这套东西不挑语言,核心是启停、守护、日志、自启动这几个常驻必备环节,对应到脚本和配置里每一步都能直接抄。
2. 常驻方式选型:为什么多数人的 nohup 方案撑不到第三天
2.1 终端、会话与进程之间的绑定关系
先把常驻这件事的原理说透。你在终端里跑一个脚本,这个脚本会挂在当前会话下,当终端退出时,内核会向会话内的进程发送 SIGHUP 信号,默认行为是直接终止进程。这就是关终端进程就消失的根本原因。
nohup 做的事只是忽略 SIGHUP,让进程不至于因为终端退出而被杀。但要注意,nohup 并不能解决程序自身崩溃、内存泄漏、死循环、端口被占用后异常退出这类问题。我常跟身边的人讲一句话:nohup 是让你把任务“丢出去”,而不是把任务“养起来”。真正的常驻需要一个外部机制去监控它、拉起它、记录它的健康状态。
这份资源包里的 module 设计方式比较务实:每个模块是一个独立的目录,目录内有自己的入口脚本和配置,对外统一由一套启动管理脚本来调度。这种结构天然适合后面接 systemd 或 supervisor,因为每个 service 单元管理一个独立进程,不会出现一个挂了拖死一串的情况。
2.2 五种常见常驻手段的取舍对比
常驻方案的选型直接决定后续的翻车概率。整理一下市面上常用的几种手段:
| 方式 | 是否跨终端 | 崩溃自愈 | 开机自启 | 适用场景 |
|---|---|---|---|---|
| 裸后台 & | 否 | 无 | 无 | 临时任务、前后端联调 |
| nohup + & | 是 | 无 | 无 | 一次性长时间任务 |
| screen / tmux | 是 | 弱(需手动恢复) | 无 | 交互式运维会话 |
| supervisor | 是 | 有 | 需自配 | Python 项目常见,依赖 pip 环境 |
| systemd | 是 | 有 | 有 | 生产环境首选 |
我自己在真实项目里一般这样选:临时跑一个晚上的批量处理用 nohup,持续时间超过一周并且希望挂了自动拉起的任务,直接上 systemd。资源包里默认提供了对应的 service 配置模板,不需要额外安装任何东西。supervisor 我很少用在生产机器上,原因是它本身是 Python 包,如果机器上有多个 Python 版本或者 conda 环境,很容易出现 supervisorctl 连不上 supervisord 的诡异情况,排起来比较费劲。
2.3 用最小脚本理解 startsid、PID 与日志重定向
resource 包里第一个能直接用的是 start_all.sh,它做的事情很简单:逐个启动 module 目录下的入口脚本,记录 PID,并把输出重定向到统一的 logs 目录。先看一段简化版:
#!/bin/bash # 最小化常驻启动示例:启动一个名为 collector 的模块 MODULE_NAME="collector" PID_FILE="./run/${MODULE_NAME}.pid" LOG_FILE="./logs/${MODULE_NAME}.log" # 检查 PID 文件是否已存在且进程仍在运行 if [ -f "$PID_FILE" ]; then OLD_PID=$(cat "$PID_FILE") if kill -0 "$OLD_PID" 2>/dev/null; then echo "[skip] $MODULE_NAME 已在运行,PID=$OLD_PID" exit 0 fi fi # 启动新进程:setsid 让进程脱离终端会话 setsid python3 "./modules/${MODULE_NAME}/main.py" >>"$LOG_FILE" 2>&1 & NEW_PID=$! echo "$NEW_PID" > "$PID_FILE" echo "[ok] $MODULE_NAME 已启动,PID=$NEW_PID"这段脚本四个重点。第一,setsid让进程完全脱离当前会话,比单纯 nohup 更彻底,即使父 shell 退出也不会收到 SIGHUP。第二,>>是追加写日志,不会每次启动都覆盖上一次的输出,保留现场对排障太重要了。第三,PID 文件写入 run 目录,这是后面做状态检查和停止操作的基础。第四,kill -0只探测进程是否存在,不发送信号,用于防重复启动。
注意这里有个细节:$!拿到的其实是 setsid 命令的 PID,在大多数 Linux 发行版上 setsid 会 exec 目标程序,所以这个 PID 也就是 python 进程本身。个别系统上如果 setsid 没有 exec,拿到的会是 setsid 的 PID,进程真实 PID 藏在下一个子进程。稳妥的做法是用pgrep -f确认,或者用setsid --wait。资源包里面的实际脚本做得更完整,加了进程名匹配校验,后面有踩坑案例会说。
2.4 怎么验证常驻是否真的成功
启动之后不能光看“报了个 ok”就收工。我在实际项目里一定会做三层验证:
# 1. 进程是否存在 ps -ef | grep "modules/collector/main.py" | grep -v grep # 2. PID 文件里的数字是否与进程一致 cat run/collector.pid ps -p $(cat run/collector.pid) -o pid,stat,cmd # 3. 日志里是否有启动标记 sleep 3 tail -n 20 logs/collector.log第一层验证进程,第二层验证 PID 一致,第三层验证业务确实跑起来了。如果只做第一层,很容易出现进程僵死或者卡在导入阶段,ps 能看到但业务完全没动。第三层最关键,很多常驻脚本的启动日志是写在业务代码里的,比如“connected to server”这类标记,看到它才算真正落地。
3. 多 module 的资源包组织方式:一个任务一个模块,崩溃不互拖
3.1 为什么拆成多个 module 而不是一个大进程
我拿到这套资源的第一反应是看它的目录结构。拆模块而不是堆单体,这个设计值得先聊聊。某个图像处理 Demo 里有三个任务:图片监听、特征提取、结果上报。三者节奏完全不同,监听要实时常驻,提取是 CPU 密集,上报是网络密集,如果写在一个进程里,一个模块卡住 I/O 就把全局拖死。
把任务拆成独立 module 有四个实际好处。第一,崩溃隔离,某个模块内存暴涨不会拖垮其他模块。第二,日志隔离,查问题时只看对应模块的日志文件,不用从一堆混着的时间戳里靠 grep 分辨。第三,独立重启,某个模块发布新版本,只要单独重启它,其他部分继续服务。第四,资源限制,后续可以为每个 module 设置独立的 CPU 或内存配额。
资源包的目录结构大概长这样,直接用 tree 看一下:
├── modules/ │ ├── collector/ │ │ ├── main.py │ │ └── config.ini │ ├── processor/ │ │ ├── main.py │ │ └── config.ini │ └── reporter/ │ ├── main.py │ └── config.ini ├── run/ # PID 文件存放目录 ├── logs/ # 模块日志统一输出目录 ├── bin/ │ ├── start.sh # 支持按模块启动 │ ├── stop.sh │ ├── status.sh │ └── healthcheck.sh ├── service/ # systemd/supervisor 配置模板 └── README.md这个布局有几个值得抄的地方。run 目录专门放 PID 文件,logs 目录专门放日志,bin 目录放管理脚本。pid 和日志不要散落到模块目录里,否则日志切割、备份、权限管理很麻烦。service 目录放 systemd 配置模板,后面会讲怎么用。
3.2 start.sh:幂等启动的逻辑要点
bin/start.sh 是整套资源的入口。它支持三种用法:不带参数启动全部模块,带模块名启动单个模块,带--force强制重启。
#!/bin/bash # start.sh:幂等启动一个或全部模块 BASE_DIR=$(cd "$(dirname "$0")/.." && pwd) RUN_DIR="${BASE_DIR}/run" LOG_DIR="${BASE_DIR}/logs" MODULES_DIR="${BASE_DIR}/modules" # 定义需要常驻的模块列表 MODULES=("collector" "processor" "reporter") start_module() { local name="$1" local pid_file="${RUN_DIR}/${name}.pid" local log_file="${LOG_DIR}/${name}.log" mkdir -p "$RUN_DIR" "$LOG_DIR" if [ -f "$pid_file" ]; then local old_pid old_pid=$(cat "$pid_file") if kill -0 "$old_pid" 2>/dev/null; then echo "[skip] $name 已在运行 (PID=$old_pid)" return 0 else echo "[warn] $name 的 PID 文件存在但进程不存在,清理残留" rm -f "$pid_file" fi fi cd "${MODULES_DIR}/${name}" || return 1 setsid python3 main.py --config config.ini >>"$log_file" 2>&1 & local new_pid=$! echo "$new_pid" > "$pid_file" echo "[started] $name (PID=$new_pid)" } if [ $# -eq 0 ]; then for m in "${MODULES[@]}"; do start_module "$m" done else start_module "$1" fi这段脚本的核心是幂等性。不管你执行多少次 start.sh,同一个模块不会启动第二份进程,这个判断逻辑靠 PID 文件加上kill -0。如果 PID 文件存在但进程不存在,说明上次是异常退出或者机器重启没清干净,直接删掉残留后再启动。
留意脚本里我用了cd到模块目录再启动,这是为了让 python 代码里所有相对路径都以模块目录为基准。很多实际部署中的诡异“文件找不到”问题,都是因为从别处执行脚本导致相对路径发生了偏移。
3.3 stop.sh:优雅退出与超时强杀的配合
停止比启动更容易踩坑。直接kill -9是最粗暴的,进程持有的数据库连接、临时文件、消息队列状态都有可能写坏。资源包里的 stop.sh 用两段式:先发 SIGTERM 让它自己收尾,等 N 秒后还没退出再 SIGKILL。
#!/bin/bash # stop.sh:优雅停止模块,超时后强制终止 BASE_DIR=$(cd "$(dirname "$0")/.." && pwd) RUN_DIR="${BASE_DIR}/run" stop_module() { local name="$1" local pid_file="${RUN_DIR}/${name}.pid" if [ ! -f "$pid_file" ]; then echo "[skip] $name 没有 PID 文件,无需停止" return 0 fi local pid pid=$(cat "$pid_file") if ! kill -0 "$pid" 2>/dev/null; then echo "[warn] $name 进程($pid)不存在,清理 PID 文件" rm -f "$pid_file" return 0 fi echo "[stop] 向 $name (PID=$pid) 发送 SIGTERM" kill -TERM "$pid" # 每 0.5 秒检查一次,最多等 10 秒 local waited=0 while kill -0 "$pid" 2>/dev/null; do sleep 0.5 waited=$((waited + 1)) if [ $waited -ge 20 ]; then echo "[warn] $name 未在 10 秒内退出,发送 SIGKILL" kill -KILL "$pid" break fi done rm -f "$pid_file" echo "[ok] $name 已停止" } if [ $# -eq 0 ]; then for pidfile in "$RUN_DIR"/*.pid; do [ -f "$pidfile" ] || continue mod_name=$(basename "$pidfile" .pid) stop_module "$mod_name" done else stop_module "$1" fi为什么先 TERM 再 KILL?因为业务模块可能在退出前需要写状态文件、关闭数据库连接池、或者通知下游“我要下线了”。有些模块需要处理 SIGTERM 信号来触发收尾逻辑,比如 python 代码里注册了signal.signal(signal.SIGTERM, handler)。如果直接 KILL,这些逻辑全部被跳过。
超时时间我习惯给到 10 秒。太短的话,模块收尾逻辑没跑完就强行杀,比如 MySQL 事务没提交完就断连,数据回滚了。太长的话,运维等得着急,还可能掩盖死锁问题。10 秒是个相对均衡的值。
3.4 日志切割:常驻模块最容易被忽视的隐性故障
常驻模块的日志文件如果不做切割,会无限制增长。日志文件达到几个 GB 之后,磁盘被打满,系统开始出现各种奇怪的现象:文件写不进去、数据库返回磁盘空间不足、甚至整个服务无响应。最常见的原因是进程一直持有文件句柄,你用rm删掉日志文件后,空间并不释放,进程还在往那个已删除的 inode 里写。
资源包里带了一份 logrotate 配置模板:
# /etc/logrotate.d/daemon-modules 的参考配置 ${BASE_DIR}/logs/*.log { daily rotate 7 compress delaycompress missingok notifempty copytruncate create 0644 root root }copytruncate是常驻场景下的关键参数。普通日志切割方式是 rename 旧文件再建新文件,但常驻进程还持有旧文件句柄,会继续往旧的 inode 写,新文件始终是空的。copytruncate 先复制当前内容到新文件,再把原文件截断,进程继续往原文件写,不会断流。代价是复制过程中可能有少量日志丢,但对排障来说完全可接受。
日志切割之后记得观察一下切割是否真的触发。logrotate 多数发行版是每天由 cron 跑一次,跑之前先看一眼/etc/cron.daily/里有没对应脚本。如果你发现日志文件几天没被切割,先手动执行/usr/sbin/logrotate -f /etc/logrotate.d/daemon-modules验证配置,而不是改完就干等。
4. 把 module 纳入 service:systemd 单元的模板与参数细节
4.1 为什么最终要迁到 systemd
脚本管常驻有一个天然缺陷:机器重启后脚本不会自动执行,除非你额外写 crontab @reboot 或者 rc.local。而 systemd 作为系统级服务管理器,天然解决了开机自启、崩溃重启、日志统一采集三个问题。资源包里的 service 模板就是把前面那些 module 脚本转换成标准的 systemd unit。
有人问 supervisor 不也能做吗?能,但 systemd 不用装。它跟内核同源,启动顺序由 init 接管,比第三方工具更可靠。唯一要注意的是,systemd unit 文件写法有它自己的约束,直接套脚本的写法会翻车,主要坑在 Type 和 Restart 的选择上。
4.2 单模块 unit 文件逐项拆解
# /etc/systemd/system/daemon-collector.service [Unit] Description=Daemon Collector Module After=network-online.target Wants=network-online.target [Service] Type=simple WorkingDirectory=/opt/daemon/modules/collector ExecStart=/usr/bin/python3 /opt/daemon/modules/collector/main.py --config config.ini Restart=always RestartSec=3 StartLimitIntervalSec=0 StandardOutput=journal StandardError=journal [Install] WantedBy=multi-user.target逐项拆开讲。Type=simple是说 ExecStart 启动的进程本身就是服务主进程,不 fork。对于 python 脚本来说这是最合适的,因为不需要额外 spawn 子进程。WorkingDirectory必须设置成模块目录,否则脚本里相对路径找不到 config.ini。Restart=always代表不管什么原因退出都重启,注意它和on-failure的区别:如果进程杀了之后退出码是 0,on-failure 会认为“干净退出”,不重启。但常驻模块正常不该退出,一旦退出说明业务中断了,所以用 always。
RestartSec=3是重启前等 3 秒,防止崩溃循环里 CPU 被打满。StartLimitIntervalSec=0这一行很多人不知道它的含义:它关掉了 systemd 对启动失败的次数限制。如果没有这一行,默认配置下 10 分钟内启动失败超过 5 次,systemd 会拒绝继续尝试,你必须手动systemctl reset-failed才能再拉起来。对无人值守的生产模块来说,这个限制会导致服务静默死掉,所以直接置 0。
日志不用再重定向到文件了,StandardOutput=journal让 systemd 把进程的标准输出和标准错误都收进 journal,用journalctl -u集中查看。
4.3 用模板实例减少重复:%i 的妙用
三个模块就要写三个近乎一样的 unit 文件,有没有办法合并?systemd 提供了模板单元,文件名带@符号,比如daemon@.service,启动时用systemctl start daemon@collector来指定实例名。
# /etc/systemd/system/daemon@.service [Unit] Description=Daemon Module %i After=network-online.target [Service] Type=simple WorkingDirectory=/opt/daemon/modules/%i ExecStart=/usr/bin/python3 /opt/daemon/modules/%i/main.py --config config.ini Environment=MODULE_NAME=%i Restart=always RestartSec=3 StartLimitIntervalSec=0 [Install] WantedBy=multi-user.target%i在这里会被替换成实际启动的实例名,也就是模块目录名。启动方式变成systemctl start daemon@collector,开机自启用systemctl enable daemon@collector。
模板实例适合模块启动方式一致的场景。如果你的某个模块需要额外传环境变量或者读不同的端口,模板会变得别扭。我一般这样把握:模块数多且启动参数一致,用模板;只有两三个模块且差异大,老老实实分开写。资源包里两种模板都给了,可以直接对照着改。
4.4 systemd 下管多模块的常用命令
服务配好后,日常操作集中在几条命令上。列个表方便粘贴:
| 操作 | 命令 |
|---|---|
| 重载配置 | systemctl daemon-reload |
| 启动 | systemctl start daemon@collector |
| 停止 | systemctl stop daemon@collector |
| 重启 | systemctl restart daemon@collector |
| 开机自启 | systemctl enable daemon@collector |
| 查看状态 | systemctl status daemon@collector |
| 看实时日志 | journalctl -u daemon@collector -f |
| 按时间查日志 | journalctl -u daemon@collector --since "10 min ago" |
改完 unit 文件之后必须执行daemon-reload,否则 systemd 还在用旧配置。我见过不止一次有人改完 RestartSec 不 reload,然后反复问“为什么没生效”。另外,restart和stop && start有细微差别,restart 会保留环境变量,后者容易因为目录上下文不同产生路径问题。日常发布用 restart,排查问题时用 stop 再 start,因为能看到干净的启动日志。
5. 避坑清单:常驻服务翻车最常见的 5 个现场
5.1 进程明明在跑,业务却静默停机
现象:ps 能看到 python 进程还在,但日志已经几个小时没有新内容,下游反馈没数据了。
原因:业务代码内部有个死循环或者异常被吞了,线程卡死但主进程还活着。这类问题 PID 文件和进程存活都发现不了,因为从系统角度看进程是“活着”的。
解决:给业务代码加心跳。最简单的做法是在日志里定期打一个心跳标记,比如每 30 秒输出一行heartbeat ok。更好一点是模块内部起一个线程维护状态文件,把最近一次成功执行的时间戳写进去。healthcheck 脚本去检查这个时间戳距离当前时间是否超过阈值,超过就把进程杀掉让 systemd 重启。资源包里的 healthcheck.sh 就是干这个的:
#!/bin/bash # 检查模块心跳文件是否仍在更新 HEARTBEAT_FILE="/opt/daemon/run/collector.heartbeat" MAX_AGE=120 # 秒,超过 120 秒视为心跳停止 if [ ! -f "$HEARTBEAT_FILE" ]; then echo "[fail] 心跳文件不存在" exit 1 fi last_modified=$(stat -c %Y "$HEARTBEAT_FILE") now=$(date +%s) age=$(( now - last_modified )) if [ $age -gt $MAX_AGE ]; then echo "[fail] 心跳超时,age=${age}s" exit 1 else echo "[ok] 心跳正常,age=${age}s" exit 0 fi5.2 开机自启“没生效”,连不上的 email 才提醒我
现象:服务器断电重启后,模块没有自动恢复,需要手动执行 start.sh 才起来。
原因:电源恢复后,systemd 网络依赖没就绪,模块启动时尝试连接数据库失败直接退出,触发 Restart=always 后又重试,但因为网络栈没完全初始化,反复失败。更隐蔽的是 unit 文件没写全After=network-online.target,systemd 以为网络服务启动了,实际还没拿到 IP。
解决:检查systemctl status daemon@collector里的启动时间和重启次数,如果看到很多 Restart 记录,说明在启动早期循环失败。unit 文件里把After=network-online.target和Wants=network-online.target写上,同时模块内部增加连接重试逻辑,不要建连失败就直接退出。DB 连接一般要重试至少 5 次,每次间隔 2 秒。
5.3 PID 文件与真实进程错位,kill 错无辜进程
现象:stop.sh 时报进程不存在,但明明ps -ef | grep main.py能看到一堆相关进程。或者更严重,kill 掉一个 PID 后发现杀掉的是别人的进程。
原因:PID 文件里记录的是旧进程的 PID,而旧进程退出后 PID 被系统复用给了其他进程。kill -0发现 PID 有效就以为模块还在运行,实际那个 PID 已经不是你的进程。或者启动时用了 shell 管道,$!拿到的 PID 和真实 python 进程父进程不一致。
解决:判定进程是否存活时,不能只看 PID,还要核对进程名和命令行参数。使用pgrep -f "modules/collector/main.py"拿到真实进程列表再比对:
pgrep -f "modules/collector/main.py" || echo "没有匹配的模块进程"同时停止操作应该用 pgrep 结果覆盖 PID 文件里的值,而不是盲信文件内容。
5.4 systemd 启动失败次数被限制,服务被“拉黑”
现象:某天登录机器,发现服务是 inactive (dead) 状态,手动 systemctl start 报错类似start request repeated too quickly。
原因:systemd 默认对单位时间内服务启动失败次数有限制,超过后进入 failed 状态,不再自动重启。常驻服务如果 config.ini 写错或者依赖的数据库迟迟连不上,启动秒退、反复重启,就会触发这个策略。
解决:如 4.2 节所说,unit 里加StartLimitIntervalSec=0关闭限制。但如果确实想保留限制防止陷入崩溃循环,可以设置为StartLimitIntervalSec=30配合StartLimitBurst=10。另外遇到已经进入 failed 状态的情况,先执行systemctl reset-failed daemon@collector清掉失败计数,再 start。
5.5 日志里全是 traceback,但 journal 里一个字看不到
现象:模块代码里 print traceback 输出到了标准错误,但 journalctl 查不到这些内容,日志文件里也没有。
原因:python 的 stdout 和 stderr 默认走不同句柄,如果模块代码里自己调用了 logging.basicConfig 把日志指向了某个文件,那 systemd 的StandardOutput=journal就接不到任何输出。这是典型的“双写日志”冲突:python 内日志进了自己定义的 handler,系统层面的重定向失效了。
解决:代码里不要自行指定 log 文件路径,让模块默认输出到 stdout,由 systemd 或外层重定向统一接管。或者配置里把日志同时写到文件和 stdout,但要维护两处。推荐前者,因为 journal 可以按时间、服务名过滤,比翻文件舒服太多。实在要写文件,记得在 systemd unit 里删掉 StandardOutput 相关字段,让进程自己去写,别同时又指定又写入,造成重复。
6. 进阶:健康检查与自愈,让模块“假装活着”也无法蒙混过关
前面 5.1 节讲了心跳文件法,下面把它和 systemd 的定时器结合起来,做一个完整的自愈闭环。先写一个每分钟执行的检查脚本,思路是:心跳超时就把对应模块重启,重启后如果心跳在 30 秒内恢复,说明是临时卡顿;如果连续 3 次检查都不恢复,发一封报警邮件。
#!/bin/bash # healthcheck.sh:心跳异常自动重启模块 HEARTBEAT_FILE="/opt/daemon/run/${MODULE_NAME}.heartbeat" MAX_AGE=120 RESTART_LIMIT_FILE="/opt/daemon/run/${MODULE_NAME}.restart_count" if [ -f "$HEARTBEAT_FILE" ]; then age=$(($(date +%s) - $(stat -c %Y "$HEARTBEAT_FILE"))) if [ "$age" -le "$MAX_AGE" ]; then echo "[ok] $MODULE_NAME 心跳正常" rm -f "$RESTART_LIMIT_FILE" exit 0 fi fi echo "[warn] $MODULE_NAME 心跳异常,准备重启" systemctl restart "daemon@${MODULE_NAME}"配合 systemd timer 来激活,比 cron 的优势是 timer 可以设置持久化,机器关机错过执行窗口后开机还会补跑:
# /etc/systemd/system/daemon-healthcheck.service [Unit] Description=Daemon Healthcheck [Service] Type=oneshot ExecStart=/opt/daemon/bin/healthcheck.sh Environment=MODULE_NAME=collector # /etc/systemd/system/daemon-healthcheck.timer [Unit] Description=Run healthcheck every minute [Timer] OnCalendar=*:0/1 Persistent=true [Install] WantedBy=timers.target这套做下来,常驻模块不仅在崩溃时会自动拉起,业务卡死时也会被检测并重启。5.1 到 5.5 那些坑,大多都能被这个机制兜住。我自己以前吃过一次大亏:某采集模块在死循环里疯狂打日志,磁盘写满,系统症状表现成“所有服务都变慢”,最后查了整整一天才定位到是它在作祟。从那以后,我每次部署任何常驻任务都强制走一遍流程:心跳文件必须写、健康检查必须挂、日志切割必须验。三样齐了才算上线,不然只能算是“跑起来,没看住”。这份资源包里的模块结构和脚本正好把这套流程标准化了,照着搭一遍就成你自己的常驻骨架,希望帮到你。
本文还有配套的精品资源,点击获取