ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

C#实现Windows服务与IIS自动监测重启:源码结构与应用解析

C#实现Windows服务与IIS自动监测重启:源码结构与应用解析 简介这是一套面向Windows运维场景的实时监测源码项目基于C#与.NET Framework 4开发使用Visual Studio 2022即可打开并编译运行。核心功能是持续监测Windows服务、IIS网站及应用程序池的运行状态一旦发现异常停止便会自动执行重启操作适合在业务故障根因尚未明确时作为临时救场工具能显著降低服务中断影响并为后续排查留出时间。资源压缩包约373KB共70个文件其中23个cs格式的C#源码构成主体4个exe为可直接运行的监测程序3个dll为运行时库另有config、xml、resx等配置与资源文件以及sln、csproj、README等工程说明文件目录包含Model、Kernal、AllForms、Global等模块项目结构完整清晰。程序内既提供开箱即用的监测工具也封装了Windows服务与IIS操作帮助类、文件操作类、日志操作类方便在项目基础上进行二次开发。目前已有114人学习下载适合运维人员应急使用也适合C# WinForm开发者参考学习。1. 服务停止后的临时救场Windows服务与IIS站点监测的选型思考业务网站凌晨挂着Windows 服务不知什么时候变成了 Stopped等发现时业务已经停了一两个小时。这类故障往往不是根因导向的——重启就能恢复但没人能 7x24 小时盯着服务列表。这个基于 C# .NET Framework 4 的 Winform 源码项目用一套轮询机制实时监测 Windows 服务和 IIS 网站及应用程序池的运行状态一旦检测到停止就自动执行重启操作。它解决的是运维中暂时查不出根因、但业务不能停的中间阶段适合现场运维快速部署也适合想学习服务操作、IIS 管理、日志与文件帮助类写法的 .NET 开发者。下文直接从源码工程结构拆起把它的监测逻辑、配置方式和二次开发路径逐一讲透。2. ServiceCheck源码工程的目录拆解与启动调用链解压 servicecheck.zip 后解决方案里包含ServiceCheck.sln、.vs目录和ServiceCheck项目文件夹还有 README.md。真正要关注的源码都在ServiceCheck项目下其中Kernal注意拼写是 Kernal不是 Kernel存放核心监测与操作逻辑AllForms存放 Winform 窗体Model存放实体类Image是 UI 用到的图标资源。这个目录划分对小项目来说有一个明确的好处界面、实体、操作逻辑三层分开二次开发时改监测逻辑不用碰窗体代码。2.1 解决方案结构从 sln 到 Kernal哪些目录需要改从项目正文可以看到完整的目录清单下面按职责分组ServiceCheck.slnVisual Studio 2022 解决方案文件双击打开即可还原整个工程ServiceCheck.csproj/ServiceCheck.csproj.user项目文件与用户级设置打包发布时主要操作前者Program.csWinform 入口负责启动主窗体app.config配置监测项与轮询参数编译后复制为ServiceCheck.exe.configKernal核心业务逻辑包含对 Windows 服务和 IIS 操作的帮助类Model数据实体例如监测目标的状态模型AllForms主窗体和配置窗体Image窗体中的状态图标、按钮图片Properties程序集信息与资源文件obj/bin编译中间目录和输出目录发布时只取 bin 下的 exe 与 config项目采用 .NET Framework 4意味着在没有安装更高运行时版本的 Windows Server 2008 R2 / 2012 上也能直接运行这点对存量服务器环境比较友好。如果你用 Visual Studio 2022 打开需要确认已安装 .NET 桌面开发 工作负载否则加载 csproj 时会提示目标框架不受支持。2.2 Program.cs 入口与 Winform 主循环Program.cs 是标准 Winform 入口核心代码通常是这样using System; using System.Windows.Forms; namespace ServiceCheck { internal static class Program { [STAThread] static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new MainForm()); } } }这段代码有两个关键点。[STAThread]指定线程模型为单线程单元Winform 拖拽控件、剪贴板操作以及通过 Com 组件访问 IIS 元数据库时都依赖这个模型如果缺失部分环境会抛出ThreadStateException。Application.Run启动了消息循环程序的 UI 事件和System.Windows.Forms.Timer都会在这个循环里执行。2.3 核心轮询调度如何串起服务、站点与应用池监测调度的入口在Kernal目录中。常见做法是定义一个监测管理类持有监测目标列表和一个定时器每次定时器触发时遍历列表先查 Windows 服务状态再查 IIS 站点状态和应用程序池状态发现异常后调用对应的重启方法。这种把所有检查放在一个类里的方式对小型项目很实用切换监测项时只需要改 app.config不需要改编译后的逻辑。需要注意的是System.Windows.Forms.Timer的回调发生在 UI 线程如果监测项多或者 ServerManager 实例化较慢界面会卡顿工程里更多是用System.Threading.Timer或后台线程做轮询然后通过Invoke更新界面状态。后续第 4 章会给出主循环的具体写法。3. Windows服务与IIS操作帮助类的实现细节这一章回答项目中最核心的问题如何用代码判断服务停了、如何拉起服务、如何检测 IIS 站点和应用池的状态。两类操作走的是完全不同的 APIWindows 服务用System.ServiceProcess命名空间下的ServiceController即可而 IIS 站点和应用池在 IIS 7 环境推荐用Microsoft.Web.Administration.ServerManager它既支持站点启停也支持应用池回收。3.1 ServiceController 封装状态读取、启动与超时控制ServiceController是 .NET Framework 自带的类不需要额外安装 SDK。封装时至少要考虑三个方法查询状态、启动服务、停止服务。下面是在工程里最常见的封装方式using System; using System.ServiceProcess; namespace ServiceCheck.Kernal { public class ServiceHelper { public static bool IsRunning(string serviceName) { using (var controller new ServiceController(serviceName)) { return controller.Status ServiceControllerStatus.Running; } } public static bool Restart(string serviceName, int timeoutSeconds) { try { using (var controller new ServiceController(serviceName)) { if (controller.Status ServiceControllerStatus.Running) { controller.Stop(); controller.WaitForStatus( ServiceControllerStatus.Stopped, TimeSpan.FromSeconds(timeoutSeconds)); } controller.Start(); controller.WaitForStatus( ServiceControllerStatus.Running, TimeSpan.FromSeconds(timeoutSeconds)); return controller.Status ServiceControllerStatus.Running; } } catch (Exception ex) { // 记录日志后返回失败由上层决定是否重试 return false; } } } }这里有一个容易被忽略的细节new ServiceController(serviceName)在服务名不存在时不会立即抛异常而是延迟到第一次访问Status时才抛Win32Exception。所以Restart中把整段逻辑包在 try 里是必要的不能只保护Start那一行。TimeoutException是另一个高频异常当服务在指定时间内没有进入目标状态时发生timeoutSeconds 设为 30 比较常见但如果被监测的服务启动本身就需要 40 秒那就应该调大这个值否则会出现启停超时被误判为失败的case。Stop时如果服务当前处于Stopped状态直接调用会抛异常所以上面的代码先判断Running再 Stop。Start时同理如果服务已经在启动过程中Start会抛InvalidOperationException此时等待状态确认比直接抛错更合适。更健壮的写法是把Start后等待状态变更放在一个循环里这里不再展开。3.2 ServerManager 封装IIS站点与应用池的重启接口IIS 操作是这个项目里技术含量最高的部分。Microsoft.Web.Administration.ServerManager需要引用Microsoft.Web.Administration.dll该 DLL 在安装 IIS 管理工具后位于%windir%\system32\inetsrv\下也可以从 NuGet 获取。每次new ServerManager()会读取 IIS 配置并建立对象模型实例化开销比普通类大所以不建议在循环体里频繁创建。下面是站点和应用池的封装using System; using System.Linq; using Microsoft.Web.Administration; namespace ServiceCheck.Kernal { public class IisHelper { public static bool SiteExists(string siteName) { using (var sm new ServerManager()) { return sm.Sites.Any(s s.Name siteName); } } public static bool RestartSite(string siteName) { using (var sm new ServerManager()) { var site sm.Sites.FirstOrDefault(s s.Name siteName); if (site null) { return false; } site.Stop(); site.Start(); return site.State ObjectState.Started; } } public static bool RestartAppPool(string poolName) { using (var sm new ServerManager()) { var pool sm.ApplicationPools.FirstOrDefault(p p.Name poolName); if (pool null) { return false; } pool.Stop(); pool.Start(); return pool.State ObjectState.Started; } } } }站点启动和应用程序池启动是两回事站点停掉不一定导致应用池停止应用池停止则站点必然无法响应。实际部署中有些业务是站点本身 unstable有些是应用池崩溃所以配置里要区分两个监测项。监控应用池时不需要把站点对应关系硬编码在代码里可以直接通过site.Applications[0].ApplicationPoolName拿到站点关联的池名称做一次自动映射。ObjectState.Started是ServerManager对已启动状态的枚举值这里对比的是site.State。需要注意site.Stop()后紧接着site.Start()是同步方法正常情况下立即返回但某些老 IIS 版本上会遇到状态未及时刷新的情况返回的State可能还是 Stopped。遇到这种问题可以在 Stop/Start 之间加一个短暂延时或者用轮询方式等待状态稳定。源码项目里如果直接复用了这个方法在 IIS 8/10 上通常没问题但部署到 IIS 7 时要关注这个差异。3.3 连续失败阈值避免服务启动过程中的误重启如果每次轮询发现服务不是 Running 状态就立刻重启会踩到一个典型的坑服务本身正在启动过程中Status返回的是StartPending这时候去执行Start()会抛InvalidOperationException。更稳妥的办法是引入连续失败次数机制第一次监测到异常只计数、不处理连续 N 次仍异常才触发重启同时成功一次就清零计数。private readonly Dictionarystring, int _failureCount new Dictionarystring, int(); private bool ShouldRestart(string key, int threshold) { if (!_failureCount.ContainsKey(key)) { _failureCount[key] 1; return false; } _failureCount[key]; return _failureCount[key] threshold; }这个方法的原理很简单每次检查不通过时调用ShouldRestart返回 false 就只写入日志并等待下一轮返回 true 才执行重启并把计数清零。阈值设置为 2 到 3 比较常见因为一次轮询失败可能只是网络瞬间抖动或 ServerManager 实例化失败。另一个相关问题是监测间隔和阈值要配合间隔 30 秒、阈值 3意味着服务停止后最多 90 秒内会被拉起这个时间窗口要在部署时告知业务方。某些数据库类型的 Windows 服务启动耗时较长重启后第一次轮询大概率还是非 Running 状态阈值 1 会导致反复二次重启这是这个项目二次开发时最值得调整的参数。4. app.config 配置驱动的监测系统参数表、主循环与部署验证把监测目标写在代码里是运维的噩梦所以这个工程使用 app.config 作为配置中心。编译部署后配置文件被复制为ServiceCheck.exe.config运维人员可以直接编辑 XML 来调整监测项无需重新编译。下面先给出完整的配置项设计再看主循环怎么写。4.1 配置项结构与参数说明一个常见的app.config结构如下?xml version1.0 encodingutf-8 ? configuration appSettings add keyCheckInterval value30 / add keyTimeoutSeconds value30 / add keyRestartThreshold value3 / add keyWatchServices valueW3SVC,MSSQLSERVER / add keyWatchSites valueDefault Web Site / add keyWatchAppPools valueDefaultAppPool / /appSettings /configuration各配置项说明如下配置键类型示例值说明CheckIntervalint30轮询间隔单位秒决定服务停止后多久被发现TimeoutSecondsint30单次停止/启动操作的超时时间单位秒RestartThresholdint3连续失败达到该次数才执行重启防止误判WatchServicesstringW3SVC,MSSQLSERVER需要监测的 Windows 服务名逗号分隔WatchSitesstringDefault Web Site需要监测的 IIS 站点名逗号分隔WatchAppPoolsstringDefaultAppPool需要监测的应用程序池逗号分隔这里的WatchServices使用的是服务名Service Name不是显示名Display Name。两者通常一致但有些服务如Windows Update服务名是wuauserv显示名却是Windows Update填错会导致找不到服务。拿到一台新服务器时建议先在 PowerShell 里执行Get-Service确认服务名到底是什么。4.2 Timer 轮询主循环与线程安全读取配置后主循环使用System.Threading.Timer而不是System.Windows.Forms.Timer目的是把监测工作放在后台线程避免 UI 卡顿。同时要加一个防重入标记因为CheckInterval如果设置得很短上一次检查还没结束下一次回调就可能启动private System.Threading.Timer _timer; private int _isChecking; public void Start() { var interval int.Parse(ConfigurationManager.AppSettings[CheckInterval] ?? 30); _timer new System.Threading.Timer(MonitorTick, null, TimeSpan.Zero, TimeSpan.FromSeconds(interval)); } private void MonitorTick(object state) { if (Interlocked.Exchange(ref _isChecking, 1) 1) { return; } try { CheckServices(); CheckSites(); CheckAppPools(); } finally { Interlocked.Exchange(ref _isChecking, 0); } }Interlocked.Exchange是原子操作前提是字段类型为 int。这段代码的作用是当线程 A 还在执行检查、线程 B 又触发回调时B 发现_isChecking已经被置为 1直接返回避免并发检查带来的重复重启。CheckServices内部遍历WatchServices对每个服务调用ServiceHelper.IsRunning失败时累加计数并决定是否重启CheckSites和CheckAppPools同理。如果被监测的服务器是单核低配机器且直接复用了 ServerManager每次new ServerManager()读取配置文件的耗时可能达到几百毫秒监测项一多压力会明显上升。这时可以把ServerManager实例复用到单次检查周期中即一个MonitorTick里只创建一次实例然后同时查站点和应用池。要注意的是ServerManager在长时间持有后可能读到缓存配置所以不建议跨多个轮询周期复用同一个实例。4.3 部署验证与异常排查部署时不可直接双击 exe。IIS 操作和ServiceController都有权限要求必须以管理员身份运行。工程输出目录下的ServiceCheck.exe右键以管理员身份运行或通过批处理配合runas启动。下面是验证顺序打开窗体确认界面上的监测项列表与 app.config 中的配置一致手动停止一个测试服务等待CheckInterval * RestartThreshold秒观察服务是否自动拉起在 IIS 管理器中停止一个站点确认站点状态恢复为 Started停止一个应用程序池观察应用池是否被自动启动如果在验证过程中没有生效优先查看日志文件。日志输出到Logs目录下具体以工程中 LogHelper 的实现为准里面会记录每次状态检查的结果、异常信息以及重启动作。常见的两种异常是拒绝访问和找不到服务名。拒绝访问说明当前进程没有管理员权限或者用户不属于 Administrators 组找不到服务名则说明配置里填的是显示名而非服务名。提示从 Visual Studio 按 F5 调试时默认不启用管理员权限需要在项目中添加 app.manifest 并将requestedExecutionLevel设为requireAdministrator或直接以管理员身份打开 VS 再调试。否则访问部分服务时会抛出Win32Exception。5. 二次开发切入点日志按天轮转、异常通知与监测目标扩展这个项目能直接用于生产但真正提升运维效率的是把日志、通知和配置管理补完整。下面分享几个常见的改造方向每个都是独立的小功能不需要大改核心结构。5.1 日志帮助类的按天轮转改造原工程的 LogHelper 如果只往单个 Log.txt 里写运行几个月后文件会膨胀排查问题时按时间定位也很麻烦。常见做法是改造为按日期生成文件同时加线程锁private static readonly object LockObj new object(); public static void WriteInfo(string message) { lock (LockObj) { var logDir Path.Combine(AppDomain.CurrentDomain.BaseDirectory, Logs); var logFile Path.Combine(logDir, DateTime.Now.ToString(yyyy-MM-dd) .log); Directory.CreateDirectory(logDir); File.AppendAllText(logFile, ${DateTime.Now:yyyy-MM-dd HH:mm:ss} [INFO] {message}{Environment.NewLine}); } }按天切分后运维可以按日期归档旧日志也方便和 IIS 日志分析工具配合使用——当应用池重启记录与站点访问日志的停止时间对得上判断根因就更有依据。文件锁是必须的吗如果只有 Timer 的一个线程在写不加锁也能跑但监测逻辑一旦扩展出邮件通知、手动刷新按钮等入口多线程写日志的风险就会出现提前加锁成本最低。5.2 告警通知自动重启是兜底但不通知人就等于没有监控。改造方式是在Restart方法执行完成后追加一个通知方法按优先级选择邮件、企业微信或钉钉机器人 Webhook。Webhook 通知只需要一个 HTTP POST不需要短信网关接入成本最低。注意把 Webhook 地址放在 app.config 里与现有配置风格保持一致。5.3 监测目标从配置到服务端下发管理 5 台机器时手动改配置文件还能接受数量上来后就需要把监测清单从配置文件中解放出来。常见的做法是让程序启动时先请求运维平台获取监测目标列表请求失败时回落到本地配置。改造时把WatchServices字符串拆成Liststring通过 JSON 序列化与外部系统交互。这与源码中 Model 目录的定位一致新增一个MonitorTarget实体类保存服务名、类型、重启阈值等字段监测调度逻辑不需要跟着改。5.4 验证改造后逻辑的正确性改造完不是编译通过就结束。建议在测试服务器上注册两个测试 Windows 服务推荐用 NSSM 把普通 exe 包成服务一个设置为自动启动但运行 5 秒后退出另一个保持正常运行。同时建一个测试站点通过应用程序池的特定时间回收功能制造一次状态变更观察监测程序是否正确识别、重启并核对日志中记录的重启时间和服务实际恢复时间是否吻合。观察 30 分钟后根据日志中的失败记录调整RestartThreshold和CheckInterval的配合值。本文还有配套的精品资源点击获取
返回列表