☰
C#实现Windows服务与IIS应用程序池自动监测重启工具
2026/10/7 7:31:03 网站建设 项目流程

简介:这是一套面向运维人员与C# Winform开发者的Windows服务与IIS网站实时监测源码项目,针对业务网站或服务因不确定因素停止运行、影响业务却一时难以根治的痛点,提供自动重启的临时救场方案。项目基于.NET Framework 4与Visual Studio 2022开发,包含可直接运行的监测程序与完整工程源码,支持二次开发。压缩包共70个文件,约373KB,以23个cs源码文件为核心,辅以exe、dll、pdb等可执行与调试文件,以及config、xml、resx、txt等配置与资源文件,并附README说明。内容涵盖Winform窗体程序使用、IIS网站与应用程序池操作帮助类、Windows服务操作帮助类、文件操作类与日志操作类,既能用于日常项目运维,也可作为Winform入门学习范例。目前已有114人学习下载,适合需要快速搭建服务与网站守护机制、并希望理解其实现思路的开发者参考借鉴。

1. 凌晨三点被告警叫醒后,我决定拆一个 Windows 服务与 IIS 监测的源码

凌晨三点,手机告警响了,某台业务服务器上的 Windows 服务停了,网站 502。爬起来连上去手动net start一下,两分钟恢复,但人已经清醒了。这种场景做运维的都懂——根因一时半会儿查不出来,业务不能停,你需要一个「临时救场」的机制:服务或应用程序池一挂,自动拉起来,先保住业务,再慢慢排查。servicecheck.zip就是干这个的:一个基于 C# / .NET Framework 4 的 Winform 程序,能实时监测 Windows 服务和 IIS 网站(含应用程序池),停止后自动重启,同时附带了服务操作、IIS 操作、文件操作、日志操作等帮助类,源码工程完整,可以直接编译使用,也能二次开发。适合做运维工具、做 Winform 入门练手,或者需要一个轻量级守护进程但不想引入 Zabbix、Prometheus 那套重方案的场景。

2. 拆开 servicecheck.zip:工程结构、技术栈与监测对象

2.1 从目录树看这个工程的组织方式

拿到servicecheck.zip解压后,目录结构大致是这样:

servicecheck/ ├── ServiceCheck.sln ├── ServiceCheck/ │ ├── ServiceCheck.csproj │ ├── app.config │ ├── Program.cs │ ├── Model/ │ ├── Kernal/ │ ├── Properties/ │ ├── Image/ │ ├── AllForms/ │ └── Global/ ├── README.md └── .vs/

.vs是 Visual Studio 的本地缓存目录,可以忽略。核心在ServiceCheck/下:Program.cs是入口,AllForms/放窗体,Model/放数据模型,Kernal/(原文拼写如此,实际就是 Kernel)放核心逻辑,Global/放全局变量或公共方法。这种分层不算复杂,但胜在清晰——窗体归窗体,模型归模型,核心逻辑单独抽出来,二次开发时改哪块很清楚。

技术栈是 C# + .NET Framework 4 + Winform,用 Visual Studio 2022 打开ServiceCheck.sln就能编译。注意是 .NET Framework 4,不是 .NET Core / .NET 6+,所以别指望跨平台跑在 Linux 上,它就是给 Windows Server 用的。

2.2 监测对象:Windows 服务和 IIS 应用程序池的区别

这个项目监测两类东西,机制完全不同,得分开说。

Windows 服务:通过System.ServiceProcess.ServiceController类操作。判断服务是否运行看Status属性,等于ServiceControllerStatus.Running就是正常,等于Stopped就是挂了。重启就是先Stop()再Start(),或者直接Start()(如果已经是 Stopped 状态)。

IIS 网站和应用程序池:这个不能直接用 .NET 自带的类,得走Microsoft.Web.Administration这个库(IIS 7 及以上提供)。应用程序池的状态通过ApplicationPool.State判断,等于ObjectState.Started是正常。网站本身的状态则要看它绑定的应用程序池是否在跑,以及网站是否Started。

为什么要区分?因为很多人以为「网站停了」就是网站的问题,实际上大部分时候是它背后的应用程序池挂了,网站跟着 503。所以监测逻辑里,应用程序池的状态判断比网站本身更关键。

2.3 编译前必须确认的两件事

在 VS 2022 里打开工程后,别急着 F5,先确认两件事:

第一,目标框架。右键项目 → 属性 → 应用程序 → 目标框架,确认是.NET Framework 4或更高(4.5、4.6 都行,但别降到 3.5,Microsoft.Web.Administration用不了)。

第二,引用。检查ServiceCheck.csproj里的引用是否完整,尤其是System.ServiceProcess和Microsoft.Web.Administration。后者如果缺失,需要手动添加:

# 在 VS 的 Package Manager Console 里执行 Install-Package Microsoft.Web.Administration

或者直接在「引用」上右键 → 添加引用 → 程序集 → 扩展,找到Microsoft.Web.Administration勾上。

提示:Microsoft.Web.Administration需要目标机器安装了 IIS 管理工具(IIS Management Console)。如果服务器上只装了 IIS 但没装管理工具,这个库会报FileNotFoundException。装 IIS 时勾上「IIS 管理控制台」即可。

编译通过后,bin/Debug/或bin/Release/下会生成ServiceCheck.exe,可以直接拷到目标服务器上跑。但注意,操作 Windows 服务和 IIS 都需要管理员权限,所以运行时必须右键「以管理员身份运行」,否则会抛InvalidOperationException或UnauthorizedAccessException。

3. 核心监测逻辑:怎么判断服务挂了、怎么自动重启

3.1 服务状态轮询:Timer 还是后台线程

这个项目用的是 Winform,所以最自然的做法是System.Windows.Forms.Timer或者System.Timers.Timer。两者区别:Forms.Timer跑在 UI 线程上,Tick 事件里可以直接更新界面控件,但如果监测逻辑耗时长了会卡界面;Timers.Timer跑在线程池线程上,不卡 UI,但更新界面得Invoke。

我一般会选System.Timers.Timer,间隔设 10 到 30 秒。太短了没必要,服务不会秒挂秒起;太长了恢复不及时。10 秒是个比较稳的值。

核心逻辑大概长这样:

// 监测单个 Windows 服务 private void CheckService(string serviceName) { try { using (var sc = new ServiceController(serviceName)) { // 判断服务是否已停止 if (sc.Status == ServiceControllerStatus.Stopped) { // 尝试启动服务 sc.Start(); // 等待服务进入 Running 状态,最多等 30 秒 sc.WaitForStatus(ServiceControllerStatus.Running, TimeSpan.FromSeconds(30)); WriteLog($"服务 {serviceName} 已自动重启"); } } } catch (InvalidOperationException ex) { // 服务不存在或权限不足 WriteLog($"服务 {serviceName} 操作失败:{ex.Message}"); } catch (System.ServiceProcess.TimeoutException ex) { // WaitForStatus 超时 WriteLog($"服务 {serviceName} 启动超时:{ex.Message}"); } }

逻辑说明:ServiceController构造时传入服务名(不是显示名,是服务名,在服务属性里能看到)。Status属性是实时查询的,不是缓存值。Start()之后用WaitForStatus等待状态变更,避免刚 Start 完就判断还是 Stopped 导致重复启动。超时设 30 秒是因为有些服务启动确实慢,尤其是依赖数据库或网络的服务。

参数说明:serviceName从配置文件或界面上读取,建议做成可配置的列表,而不是硬编码。WaitForStatus的超时时间根据实际服务调整,数据库类服务可以放到 60 秒。

3.2 IIS 应用程序池的自动重启

应用程序池的重启比 Windows 服务简单,因为 IIS 提供了Recycle()方法:

// 监测并重启 IIS 应用程序池 private void CheckAppPool(string poolName) { try { using (var manager = new ServerManager()) { var pool = manager.ApplicationPools[poolName]; if (pool == null) { WriteLog($"应用程序池 {poolName} 不存在"); return; } // 判断应用程序池是否已停止 if (pool.State == ObjectState.Stopped) { pool.Start(); WriteLog($"应用程序池 {poolName} 已自动启动"); } else if (pool.State == ObjectState.Started) { // 可选:如果进程假死但状态还是 Started,可以强制回收 // pool.Recycle(); } } } catch (Exception ex) { WriteLog($"应用程序池 {poolName} 操作失败:{ex.Message}"); } }

逻辑说明:ServerManager是Microsoft.Web.Administration的入口类,每次操作完要Dispose,所以用using。ApplicationPools[poolName]按名称索引,名称区分大小写。State是ObjectState枚举,常见值有Started、Stopped、Starting、Stopping。

参数说明:poolName要和 IIS 管理器里看到的应用程序池名称完全一致。如果应用程序池状态是Started但网站还是 503,那可能是工作进程假死,这时候可以考虑调Recycle()强制回收,但别频繁调,回收会导致当前请求中断。

3.3 日志记录:别等出事了才后悔没记日志

这个项目带了日志操作类,这是很关键的一环。自动重启这种机制,最怕的就是「默默重启了但没人知道」,结果问题被掩盖了。所以每次重启操作都必须记日志,包括:时间、对象名称、操作类型(启动/重启)、操作结果(成功/失败)、失败原因。

日志格式建议用简单的文本追加,别搞太复杂:

// 简单的日志写入方法 private static readonly object LogLock = new object(); public static void WriteLog(string message) { lock (LogLock) { string logPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "logs"); if (!Directory.Exists(logPath)) Directory.CreateDirectory(logPath); string fileName = $"servicecheck_{DateTime.Now:yyyyMMdd}.log"; string fullPath = Path.Combine(logPath, fileName); string line = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss} | {message}{Environment.NewLine}"; File.AppendAllText(fullPath, line, Encoding.UTF8); } }

逻辑说明:lock是为了防止多线程同时写文件导致内容错乱。按天分文件,方便排查。AppDomain.CurrentDomain.BaseDirectory取的是 exe 所在目录,这样不管从哪启动,日志都在程序目录下。

参数说明:日志路径可以做成可配置的,但默认放程序目录下最省事。编码用 UTF8,避免中文乱码。

4. 避坑与排查:那些我踩过的坑

4.1 服务名和显示名搞混,导致找不到服务

现象:代码里写的是「SQL Server (MSSQLSERVER)」,运行时报「服务不存在」。

原因:ServiceController构造函数接收的是服务名(Service Name),不是显示名(Display Name)。在服务管理器里看到的那一列是显示名,服务名要右键属性才能看到。

解决:用sc query命令或者 PowerShell 的Get-Service | Select-Object Name, DisplayName确认服务名。代码里统一用服务名。

4.2 权限不足,操作被拒绝

现象:程序在开发机上跑得好好的,拷到服务器上就报UnauthorizedAccessException或InvalidOperationException。

原因:操作 Windows 服务和 IIS 都需要管理员权限。开发机上 VS 是以管理员身份运行的,所以没感觉;服务器上双击运行就是普通权限。

解决:右键 exe → 以管理员身份运行。或者修改 exe 的兼容性设置,勾选「以管理员身份运行此程序」。更彻底的做法是在app.manifest里声明requireAdministrator。

4.3 应用程序池状态是 Started 但网站还是 503

现象:监测程序显示应用程序池正常,但网站访问返回 503。

原因:应用程序池的State是Started,但工作进程(w3wp.exe)可能已经假死或者被回收了但没起来。这种情况State不会变。

解决:这种情况光靠状态判断不够,需要加一层「健康检查」——比如定期发一个 HTTP 请求到网站的健康检查接口,如果连续失败 N 次,就强制Recycle()应用程序池。这个项目本身没带 HTTP 检查,但可以自己扩展。

4.4 服务启动超时,WaitForStatus 抛异常

现象:调用WaitForStatus时抛TimeoutException,但服务实际上后来起来了。

原因:有些服务启动确实慢,尤其是依赖数据库、网络或其它服务的。默认超时时间不够。

解决:把WaitForStatus的超时时间调大,比如 60 秒甚至 120 秒。同时捕获TimeoutException,记录日志但不要当成致命错误——服务可能只是慢,不是起不来。

4.5 日志文件被占用,写入失败

现象:日志写不进去,报IOException。

原因:多个线程同时写同一个文件,或者日志文件被其它程序(比如日志采集工具)打开了。

解决:用lock保证单线程写入。如果还是被占用,可以考虑用FileStream以FileShare.ReadWrite方式打开,或者换用NLog、log4net这类成熟的日志库。

5. 进阶用法:把监测程序做成 Windows 服务,以及验证它真的在工作

5.1 让监测程序自己变成 Windows 服务

现在这个程序是个 Winform,得手动开着窗口才能跑。但运维场景下,你不可能一直开着远程桌面。更合理的做法是把它改成一个 Windows 服务,开机自启,后台运行。

改造方法:用sc create命令把 exe 注册成服务:

# 以管理员身份运行 cmd sc create ServiceCheck binPath= "C:\path\to\ServiceCheck.exe" start= auto sc description ServiceCheck "监测 Windows 服务和 IIS 应用程序池,停止后自动重启" sc start ServiceCheck

但注意,Winform 程序直接注册成服务会有问题——它没有实现ServiceBase,SCM(服务控制管理器)无法正确控制它。所以更稳妥的做法是新建一个 Windows Service 项目,把监测逻辑放到OnStart里,用Timer定期执行。或者用NSSM(Non-Sucking Service Manager)这类工具把普通 exe 包装成服务。

我一般会选后者,因为改动最小。NSSM 的用法:

# 下载 nssm.exe 后 nssm install ServiceCheck "C:\path\to\ServiceCheck.exe" nssm set ServiceCheck AppDirectory "C:\path\to" nssm set ServiceCheck Start SERVICE_AUTO_START nssm start ServiceCheck

这样程序就在后台跑了,开机自启,崩溃了 NSSM 还会帮你拉起来。

5.2 验证监测是否真的生效

部署完了别就不管了,得验证。验证方法很简单:手动把被监测的服务停掉,等一个监测周期(比如 10 秒),看它是不是自动起来了。

# 停掉服务 net stop "YourServiceName" # 等 15 秒 timeout /t 15 # 查看服务状态 sc query "YourServiceName"

如果STATE显示RUNNING,说明监测生效了。同时去看日志文件,应该有对应的重启记录。

对于 IIS 应用程序池,可以在 IIS 管理器里手动停止应用程序池,然后观察是否自动启动。或者用 PowerShell:

# 停止应用程序池 Stop-WebAppPool -Name "YourAppPoolName" # 等 15 秒 Start-Sleep -Seconds 15 # 查看状态 Get-WebAppPoolState -Name "YourAppPoolName"

如果返回Started,说明生效了。

5.3 一个容易忽略的细节:监测频率和重启次数的平衡

监测频率设太短(比如 1 秒),服务刚停就重启,可能服务本身还在停止过程中,导致Start()失败。设太长(比如 5 分钟),业务中断时间太久。10 到 30 秒是个比较稳的区间。

另外,如果某个服务反复挂,监测程序会反复重启它。这时候需要加一个「重启次数限制」——比如 10 分钟内重启超过 5 次,就停止自动重启并告警,因为这说明问题不是「临时救场」能解决的了,得人工介入。

这个逻辑可以在监测代码里加一个计数器:

// 简单的重启频率控制 private Dictionary<string, Queue<DateTime>> _restartHistory = new Dictionary<string, Queue<DateTime>>(); private bool CanRestart(string serviceName) { if (!_restartHistory.ContainsKey(serviceName)) _restartHistory[serviceName] = new Queue<DateTime>(); var history = _restartHistory[serviceName]; var now = DateTime.Now; // 移除 10 分钟前的记录 while (history.Count > 0 && (now - history.Peek()).TotalMinutes > 10) history.Dequeue(); // 10 分钟内重启超过 5 次,不再自动重启 if (history.Count >= 5) return false; history.Enqueue(now); return true; }

逻辑说明:用Queue记录每次重启的时间,每次判断前先清理超过 10 分钟的旧记录,然后看剩余数量是否达到阈值。达到阈值就返回false,调用方记录告警日志并跳过本次重启。

参数说明:10 分钟和 5 次是经验值,可以根据实际服务的稳定性调整。对于特别稳定的服务,可以放宽到 3 次;对于本身就不太稳定的服务,可以放到 10 次。

从那以后我每次部署这类监测程序,都会先手动停一次服务验证它真的能拉起来,再去看日志确认记录完整,最后才放心离开。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询