
简介服务检测工具是一款基于.NET 4.0开发的Winform程序面向需要保障Windows服务持续运行的运维与开发人员用于解决服务意外停止后无法及时自动恢复的问题。该工具支持自定义检测周期默认3秒、可设置提示信息显示行数并允许同时添加多个服务进行监控一旦检测到服务停止会自动尝试重启直至状态转为运行中且等待过程中不阻塞界面避免程序无响应。资源包共5个文件包括exe主程序、xml服务列表配置、config运行参数、pdb调试符号以及txt日志示例压缩包仅35KB轻量易用。目前已有452人学习下载。工具还提供提示信息导出、每日日期日志文件自动生成、手动停止检测以及关闭时缩小至托盘等实用细节读者可直接运行exe并结合xml配置快速部署适用于Windows服务实时守护与故障快速恢复场景也可作为C# Winform服务监控工具的参考实现。1. 服务检测工具的第一层含义是让宕机可见第二层才是自动恢复一台主机上跑了十几个 RPA 机器人凌晨 3 点某个调度服务悄悄退出早上一看积压了上千条任务。这类问题不是缺监控而是缺一种“检测到即恢复”的执行者告警只能告诉你系统病了离治好还有一段人工操作的距离。把“检测服务停止”和“自动重启服务”合并成同一个动作闭环这也是服务检测工具区别于普通监控脚本的核心差异点。实现这个闭环有两条路线优先用 systemd 这类进程管理器内置的自愈能力再做 HTTP 探活和偏业务侧的健康检查最后才是自研脚本兜底。本文按这条路径展开覆盖探测间隔、重启次数、防抖设计、幂等启动这几个最容易埋坑的参数适合负责运维编排、CI 基础设施和应用发布的人参考。新手可以照命令操作熟手可以重点看自愈策略的边界在哪。2. 用 systemd 把“服务停了就重启”变成最小可落地配置2.1 为什么默认的 Restarton-failure 不够用systemd 是目前 Linux 发行版的事实标准服务管理器它本身就内置了完整的“检测—重启”循环只要服务的主进程退出systemd 就会根据 unit 文件里的 Restart 指令决定要不要把它拉起来。常见的配置写法是Restartalways但“服务退出必重启”并不是所有场景下都正确。先看触发条件。Restartalways表示无论服务以什么状态码退出都重启包括被systemctl stop主动停止的情况Restarton-failure只对非零退出码、被信号杀死、超时等情况生效。实际运维中很容易踩一个坑某个 Java 服务因为 OOM 被系统直接杀了退出码可能是 134on-failure确实能识别并拉起来但如果进程是被kill -9强制终止部分场景下 systemd 会把它判定为“外部干预”on-failure的重启行为就会变得不那么确定。推荐的稳妥做法是区分业务类别来选策略无状态 API 服务Restartalways反正数据不在本地多试几次没有副作用有状态队列消费者Restarton-failure避免手动停止时被 systemd 抢着重启依赖外部资源才能启动的服务Restarton-watchdog配合看门狗比粗暴重启更安全还有一类边界情况是配置错误导致服务起不来。systemd 有启动次数限制默认是 5 秒内超过 5 次重启就会放弃unit 进入 failed 状态。这个限制参数是StartLimitIntervalSec和StartLimitBurst它们决定了“一直重启”和“失败后放弃”的切换点后面会专门调整。2.2 最小配置一个带探活的自愈服务示例以 Node.js 调度服务为例先准备一个服务文件/etc/systemd/system/task-scheduler.service[Unit] DescriptionTask Scheduler Service Afternetwork.target postgresql.service Wantspostgresql.service [Service] Typesimple Userdeploy Groupdeploy WorkingDirectory/data/apps/task-scheduler ExecStart/usr/bin/node /data/apps/task-scheduler/index.js Restartalways RestartSec5 StartLimitIntervalSec60 StartLimitBurst3 WatchdogSec30 [Install] WantedBymulti-user.target写完后执行systemctl daemon-reload让配置生效。这里RestartSec5表示每次重启前等 5 秒目的是给下游数据库或依赖方一点恢复时间StartLimitIntervalSec60和StartLimitBurst3组合的意思是 60 秒内如果重启了 3 次仍然失败systemd 就放弃并标记为 failed。WatchdogSec30是另一个容易被忽略的自动检测开关systemd 会每 30 秒检查一次服务是否通过 sd_notify 报心跳如果服务卡死但进程没退出普通探活方式检测不到看门狗却能直接触发重启。服务代码里需要初始化看门狗接口const sdNotify require(sd-notify); const timer setInterval(() sdNotify.watchdog(), 25000); // 业务逻辑异常时调用 process.exit(1) 主动退出交给 systemd 重启参数含义要分清WatchdogSec配的是超时时间实际发送心跳要略小于这个值这里 25 秒是给系统调度留余量。这套机制的核心价值在于把“进程疑似存活”和“应用确认存活”区分开前者只看 PID 存不存在后者依赖业务心跳反馈。2.3 调参矩阵重启次数、冷却时间、失败退避怎么配RestartSec直接决定服务抖动时后端能扛住多大压力。设 0 秒是极端自信适合启动瞬时完成的编译型小工具设 3 到 5 秒适合数据库已就绪但连接池需要预热的中型服务超过 10 秒适合依赖外部 API 且容易触发限流的服务。这里列一个参考矩阵服务特征RestartSecStartLimitBurstStartLimitIntervalSec说明无状态短任务2s530s快速恢复不怕抖动重启动初始化较长8s3120s给足初始化时间依赖外部队列/DB10s3180s留出依赖方恢复窗口核心交易链路5s1060s高可用优先容忍频繁重启还有一个容易被忽略的组合当 unit 进入 failed 状态后很多自动化脚本会执行systemctl restart xxx强行拉活。但系统默认的 failed 状态不会自动复位外部重启有时会碰到StartLimitIntervalSec的限制。这时需要先systemctl reset-failed task-scheduler再触发重启或者直接把这个指令加进告警回调脚本里。sd_notify 写心跳是这套方案里最容易被漏掉的一环。如果服务代码完全不通知看门狗WatchdogSec配置会立即生效并杀掉服务——这不是 bug是设计预期不表态的服务被视为不健康。3. 进程活着只是及格线HTTP 探活才能检测到半死不活3.1 从进程态到服务态探活必须分层systemd 能解决的只是“进程退出”这类硬故障但实际生产环境里更常见的是“服务出不来”端口在监听请求却全部超时进程占用率 100%任务不再消费或者健康检查接口返回 500但主进程还稳稳地挂在树上。这些都属于“服务停止”的语义扩展——业务角度它已经不可用了进程层面却没有停止。要覆盖这类场景探活必须分四层做梯度进程层systemd 的状态检测判断 PID 是否存活端口层本机或远程探测 TCP 端口是否能建立连接协议层对 HTTP 端口发起请求看响应状态码是否正常业务层请求专门的健康检查接口验证核心依赖是否就绪分工上systemd 适合防线第一层而端口和协议的探测可以放在同一台主机上用脚本调度或者委托给外部监控系统。关键在于不要全堆在 systemd 里做也不要完全撤掉 systemd 全靠外部探活。如果外部监控挂了systemd 的本地兜底能保底如果进程卡死不退出外部探活能兜住 systemd 覆盖不到的部分。3.2 用健康检查脚本补全 systemd 的检测盲区在宿主机上部署一个轻量健康检查脚本放在/usr/local/bin/check_svc.sh#!/bin/bash SERVICE_URLhttp://127.0.0.1:8080/healthz EXPECTED_CODE200 TIMEOUT_SEC5 code$(curl -s -o /dev/null -w %{http_code} --connect-timeout $TIMEOUT_SEC $SERVICE_URL) if [ $code ! $EXPECTED_CODE ]; then systemctl restart task-scheduler logger -t healthcheck task-scheduler health check failed, code$code, restart triggered exit 1 fi exit 0脚本逻辑用 curl 拿到 HTTP 状态码和期望值比对不一致就触发 systemd 重启并往 syslog 写一条审计日志。难处理的场景不在正常情况而是在重启后健康检查仍然失败——这通常是服务存在启动死循环或流量被挡不能无限重试所以要配合 systemd 的 StartLimit 参数使用达到次数上限后交给人工。配套一个用于调试的探活方式直接看服务从启动到接口可用的耗时systemctl start task-scheduler for i in $(seq 1 20); do if curl -fsS http://127.0.0.1:8080/healthz; then echo service is ready after ${i} checks break fi sleep 1 done生产环境里建议把健康检查脚本注册进 cron*/2 * * * * /usr/local/bin/check_svc.sh每 2 分钟一次配合 systemd 的秒级重启形成互补。cron 间隔调太短没有实际意义服务从崩溃到被拉起再完成初始化需要时间连续高频探测只会堆出无效告警还会干扰 systemd 本身的重试节奏。3.3 健康检查端点本身怎么设计才不误报后端健康检查接口如果写不好探活工具要么天天误报要么该报的时候不报。先给一个可用的参考实现Node.js 版本app.get(/healthz, async (req, res) { const checks { db: await checkDatabaseConnection(), queue: await checkQueueConnection() }; const ok Object.values(checks).every(v v true); res.status(ok ? 200 : 503).json(checks); });这个接口的特点是必须真实检查依赖连接而不是直接返回一个写死的 200。后端数据库连接池耗尽、消息队列断开时健康检查返回 503外层脚本拿到非 200 就会触发重启流程。但要注意健康检查里别把非关键依赖放进去Redis 缓存挂了但业务还能降级运行这时返回 503 会误导系统重启整个服务。要区分“硬依赖”挂了就不能提供服务和“软依赖”挂了影响性能但不影响主功能检查项只纳入硬依赖。还有一个容易踩坑的是健康检查超时设置。接口本身可能要等待数据库响应如果探测脚本把超时设成 1 秒DB 正常慢响应就会触发虚假重启。一般建议检测脚本超时设置大于接口 P95 响应时间比如接口 99% 的请求都在 1 秒内返回探测超时设 3 秒反向异常也要看进程活着一看内存已经涨到接近 OOM 阈值这类检查放在脚本里加个内存阈值判断更实用。4. 自研守护进程要考虑的幂等、防抖和并发问题4.1 为什么不能简单写个 while 循环判断服务名绕开 systemd 和健康检查脚本自己用 shell 或 Python 写一个守护进程最常见的第一版代码长这样while true; do if ! pgrep -f task-scheduler /dev/null; then nohup node /data/apps/task-scheduler/index.js fi sleep 10 done这个写法有三个问题。第一pgrep -f匹配的是命令行字符串改过启动参数或者有同名进程时容易误判导致服务明明活着却重复拉起第二nohup启动的进程脱离了进程管理器的控制重启机器后恢复逻辑需要重新设计第三最关键的如果服务启动后十几秒才崩溃while 循环扫描间隔是 10 秒进程反复被拉起和杀掉会出现“抖动风暴”更麻烦的是两个副本同时运行时没有锁控处理同一批消息会造成重复消费或端口冲突。这类自研方案的核心缺陷是没有状态管理。systemd 之所以可靠是因为它对每个 unit 都有明确的 active、activating、deactivating 状态机而自研脚本里的“存活”只是一个瞬间快照中断时无法准确判断服务到底处于什么阶段。如果生产环境里选项充分优先考虑 systemd只有当业务确实需要一个单独的“进程守护者”时才需要带上幂等设计和防抖策略去实现。4.2 PID 文件配合 flock 保证只有一个实例被拉起保证同一时刻只有一个服务实例运行业界常见的做法是 PID 文件加文件锁。启动时把当前进程号写入/var/run/task-scheduler.pid脚本用文件锁来判断这个文件是否正被别的实例使用#!/bin/bash PID_FILE/var/run/task-scheduler.pid LOCK_FILE/var/run/task-scheduler.lock exec 9$LOCK_FILE if ! flock -n 9; then echo another instance is running exit 1 fi if [ -f $PID_FILE ] kill -0 $(cat $PID_FILE) 2/dev/null; then echo service already alive with pid $(cat $PID_FILE) exit 0 fi nohup node /data/apps/task-scheduler/index.js /var/log/task-scheduler/stdout.log 21 echo $! $PID_FILE这里flock -n 9是核心-n表示非阻塞模式拿不到锁就立即失败防止两个守护脚本同时执行。kill -0只检测进程是否存在不实际发信号。这段逻辑把“判断存活”和“防止并发启动”两个动作合并到了同一个流程里比单独pgrep可靠得多。还有一层需要考虑PID 文件中记录的进程号可能被系统回收再分配给别的进程导致误判。稳妥的做法是启动时记录进程的/proc/$PID/start_time判断时同时比对开始时间。不过实际生产中这属于锦上添花flock加 PID 双约束已经能覆盖九成以上场景。4.3 退避重试和防抖策略从“疯狂重启”到“优雅自愈”监控告警里最丑的曲线之一是服务每 5 秒重启一次像心跳一样稳定。这说明重启逻辑缺少防抖无论失败多快都固定按最短间隔重试。回退策略核心思想是重启间隔随失败次数指数增长把“快恢复”和“防风暴”组合起来import subprocess import time max_retries 5 base_delay 2 current_delay base_delay for attempt in range(max_retries): result subprocess.run([systemctl, restart, task-scheduler]) if result.returncode 0 and is_healthy(): print(frecovered at attempt {attempt 1}) break else: print(fattempt {attempt 1} failed, waiting {current_delay}s) time.sleep(current_delay) current_delay current_delay * 2 # 指数回退指数回退本质是给服务留出自我修复时间数据库可能正在恢复依赖服务可能刚被拉起连续快速重启反而让启动过程永远达不到成功条件。实践中把基础延迟设为 2 秒指数因子 2最多 5 次总等待最坏情况约 60 秒不会拖太久也不会太急。还需要一个额外判断连续重启 N 次后服务仍然失败就不要再触发重启改为只发告警并等待人工介入。这个上限一般设在 3 到 5 次之间低于这个数会放过瞬时抖动高于这个数可能让故障时间无意义拉长。systemd 的StartLimitBurst本质上就是这个上限的内置实现。策略参数适用场景固定间隔 无限重试不推荐无固定间隔 次数上限RestartSec5, StartLimitBurst3依赖简单、启动快的服务指数退避 告警升级base2s, factor2, max5大部分后端服务指数退避 熔断连续失败 10 次后停手 10 分钟外部依赖严重故障场景熔断策略适合核心链路。如果检到服务反复重启但依赖的服务一直没恢复可以设置熔断计时器10 分钟内不再自动拉起只记录日志。这个设计能防止重启风暴把所有下游连接池打满让故障影响面尽量缩小。5. 验证自愈效果的三个实操细节配置写完之后验证工作最容易流于形式手动停一次服务发现能自动恢复就以为万事大吉。实际要把可能发生的“停止”种类都过一遍按下表列出需要测试的故障类型和预期行为故障注入方式预期行为验证命令正常退出exit 0Restartalways 时重启systemctl kill -s TERM task-scheduler异常退出exit 1on-failure/always 都重启kill -9 PID服务卡死WatchdogSec 超时触发重启在代码里模拟死循环健康检查返回 500脚本触发 systemctl restart手动关闭 DB 后观察验证WatchdogSec时要小心把心跳计时器暂停后服务会被 systemd 判定为“未及时报告”而主动杀掉。如果出现频繁超时重启优先检查心跳线程是否被业务线程阻塞而不是简单调大 WatchdogSec 参数。最终上线前把日志收集串联起来。看系统状态时用systemctl status task-scheduler确认重启次数和最近一次退出的原因看业务日志时确认重启后数据没有重复消费。一个实用的检查点是/etc/systemd/system/task-scheduler.service的 timeout 设置服务优雅停止如果超过TimeoutStopSec设定值systemd 会强制 kill可能截断正在落盘的数据配合TimeoutStartSec在启动超时时自动失败重启才算把整个生命周期兜住。本文还有配套的精品资源点击获取