ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Windows服务、IIS网站与应用程序池实时监测:C#源码实战与避坑指南

Windows服务、IIS网站与应用程序池实时监测:C#源码实战与避坑指南 简介这是一套面向运维人员与C#开发者的Windows服务及IIS网站实时监测源码项目基于.NET Framework 4与Winform开发可在服务或应用程序池意外停止后自动重启为排查根本问题争取缓冲时间也适合作为Winform入门与二次开发的学习范例。资源包共70个文件以23个cs源码、4个exe可执行程序、3个config配置、3个dll及若干resx、xml、txt等为主涵盖窗体逻辑、IIS与Windows服务操作帮助类、文件与日志处理类压缩包约373KB结构紧凑便于阅读。目前已有114人学习下载。读者可直接部署监测程序用于日常运维救场也能借助完整工程与帮助类快速搭建自己的监控工具理解服务重启、网站检测与日志记录的实现思路。1. 从一次凌晨三点的告警说起Windows服务、IIS网站和应用程序池到底该怎么实时监测凌晨三点手机响了客户说网站 502。远程上去一看IIS 应用程序池已经停了Windows 服务里那个负责后台任务的OrderSyncService也挂了。事件查看器里翻半天只看到一句「应用程序池 xxx 已自动禁用」。这种场景做运维的都懂——Windows 服务、IIS 网站和应用程序池这三样东西任何一个出问题业务就断。但 Windows 自带的能力很尴尬服务管理器要手动刷新IIS 管理器不会主动告诉你应用程序池挂了性能监视器配置繁琐且不告警。所以「实时监测」这四个字落到实操上就是用代码定时采集这三类对象的状态发现异常立刻记录并触发通知。这篇笔记讲的就是这套监测源码项目该怎么搭、参数怎么设、坑在哪。适合手里管着几台 Windows Server、想自己写一套轻量监测而不是上重型平台的运维和开发。2. 监测对象拆解Windows服务、IIS网站、应用程序池各自看什么指标2.1 Windows服务不只是看 Running 还是 Stopped很多人以为监测 Windows 服务就是查Status Running这太浅了。实际生产里服务状态是 Running 但内部线程卡死的情况非常常见比如一个消息队列消费服务进程活着但不消费了。所以监测要分两层第一层是服务控制管理器里的状态第二层是服务对应的进程是否还在、CPU 和内存是否异常。用 .NET 的ServiceController类可以拿到服务状态这是最直接的方式。但要注意ServiceController.Status返回的是ServiceControllerStatus枚举包含 Running、Stopped、Paused、StartPending、StopPending 等。监测逻辑里只有 Running 算健康其他都算异常包括 Paused——很多脚本只判断 Stopped结果服务被暂停了也不告警。using System.ServiceProcess; public class ServiceStatusChecker { // 服务名是服务在系统中的唯一标识不是显示名 public ServiceHealth Check(string serviceName) { try { using (var sc new ServiceController(serviceName)) { // 刷新一次避免拿到缓存状态 sc.Refresh(); var status sc.Status; // 只有 Running 算健康Paused 也要告警 bool healthy status ServiceControllerStatus.Running; return new ServiceHealth { Name serviceName, Status status.ToString(), Healthy healthy, CheckedAt DateTime.Now }; } } catch (InvalidOperationException ex) { // 服务不存在时会抛这个异常要单独处理 return new ServiceHealth { Name serviceName, Status NotFound, Healthy false, Error ex.Message }; } } }这段代码的关键点sc.Refresh()必须调用否则ServiceController可能返回缓存的状态导致你监测了个寂寞。InvalidOperationException要单独捕获因为服务被卸载或服务名写错时就是这个异常不能让它把整个监测循环搞崩。参数上serviceName用的是服务的「服务名称」而不是「显示名称」在服务属性里能看到别搞混。2.2 IIS网站和应用程序池状态、进程、请求队列三件套IIS 网站和应用程序池的监测核心是三个东西网站状态Started/Stopped、应用程序池状态、以及应用程序池对应的工作进程w3wp.exe情况。网站状态和应用程序池状态可以通过Microsoft.Web.Administration这个程序集拿到它是对 IIS 配置的托管封装。using Microsoft.Web.Administration; public class IisStatusChecker { public IisHealth Check() { var result new IisHealth(); using (var manager new ServerManager()) { // 遍历所有网站 foreach (var site in manager.Sites) { result.Sites.Add(new SiteHealth { Name site.Name, State site.State.ToString(), // Started / Stopped // 网站绑定的应用程序池名 AppPool site.Applications[0].ApplicationPoolName }); } // 遍历所有应用程序池 foreach (var pool in manager.ApplicationPools) { result.Pools.Add(new PoolHealth { Name pool.Name, State pool.State.ToString(), // Started / Stopped // 队列长度超过阈值说明处理不过来 QueueLength pool.QueueLength, // 当前工作进程数 WorkerProcesses pool.WorkerProcesses.Count }); } } return result; } }ServerManager需要引用Microsoft.Web.Administration.dll这个 DLL 在 IIS 安装目录下一般在%windir%\system32\inetsrv\里。pool.QueueLength是个容易被忽略但很有用的指标它表示应用程序池请求队列里排队的请求数如果持续大于 0说明工作进程处理不过来哪怕状态是 Started 也该告警。WorkerProcesses.Count为 0 但状态是 Started说明应用程序池启动了但工作进程没起来这也是异常。提示ServerManager在非管理员权限下可能读不到完整信息监测程序建议以管理员身份运行或者至少把运行账户加入 IIS_IUSRS 组并给足权限。2.3 实时监测的「实时」到底怎么定义轮询间隔与性能开销的平衡「实时」这个词很容易被误解。真正的实时推送在 Windows 服务监测里不现实常见做法是轮询。轮询间隔设多少我一般会分两档核心服务 10 秒一次非核心服务 30 秒到 60 秒一次。IIS 网站和应用程序池因为ServerManager创建开销较大不建议太频繁15 到 30 秒比较合适。这里有个血泪经验ServerManager每次new都会读取 IIS 配置如果每秒都创建一次CPU 会明显上升尤其是在 IIS 上跑了几十个站点的时候。所以要么复用ServerManager实例但要注意它不线程安全要么把间隔拉长。另一个坑是ServiceController的Refresh()在服务数量多的时候也有开销几百个服务的场景下全量刷新一次可能要几百毫秒所以监测服务本身要跑在独立线程里不能阻塞主业务。3. 用C#写一个可落地的监测服务从采集到告警的完整链路3.1 项目结构一个Windows服务承载采集、判断、通知三件事这套监测源码项目我一般会做成一个 Windows 服务内部拆成三个模块采集器Collector、判断器Evaluator、通知器Notifier。采集器负责定时拉取服务、网站、应用程序池的状态判断器根据配置的规则判断是否异常通知器负责写日志、发邮件或调用 Webhook。三个模块之间用内存队列解耦采集器只管采集判断器从队列里取数据判断通知器从另一个队列取告警发出去。// 采集器核心循环用 Timer 驱动 public class Collector { private readonly System.Threading.Timer _timer; private readonly BlockingCollectionMonitorSnapshot _queue; public Collector(BlockingCollectionMonitorSnapshot queue, int intervalSeconds) { _queue queue; // dueTime 设为 0 表示立即开始period 是间隔毫秒数 _timer new System.Threading.Timer(Collect, null, TimeSpan.Zero, TimeSpan.FromSeconds(intervalSeconds)); } private void Collect(object state) { var snapshot new MonitorSnapshot { CollectedAt DateTime.Now, Services _serviceChecker.CheckAll(), Iis _iisChecker.Check() }; // 加入队列如果队列满了会阻塞这里设了上限防止内存爆掉 _queue.Add(snapshot); } }BlockingCollection的容量要设上限比如 100防止判断器处理不过来时内存无限增长。Timer的period参数是毫秒别写成秒。采集器里不要做任何耗时操作比如发邮件、写数据库那些都交给通知器。3.2 判断规则怎么配阈值、连续次数、白名单判断器不能只看单次快照否则网络抖动或瞬时状态变化会误报。我一般用「连续 N 次异常才告警」的策略。比如某个服务连续 3 次采集都是 Stopped才判定为真异常。这个 N 可配置核心服务设 2非核心设 3 到 5。public class Evaluator { // 记录每个对象的连续异常次数 private readonly Dictionarystring, int _failCounts new(); private readonly int _threshold; public Evaluator(int threshold) { _threshold threshold; } public ListAlert Evaluate(MonitorSnapshot snapshot) { var alerts new ListAlert(); foreach (var svc in snapshot.Services) { var key $svc:{svc.Name}; if (!svc.Healthy) { _failCounts[key] _failCounts.GetValueOrDefault(key) 1; // 达到阈值才产生告警 if (_failCounts[key] _threshold) { alerts.Add(new Alert { Target svc.Name, Type Service, Message $服务 {svc.Name} 状态异常{svc.Status}, Level Critical }); } } else { // 恢复正常清零计数 _failCounts[key] 0; } } // IIS 网站和应用程序池的判断逻辑类似略 return alerts; } }白名单也很重要。有些服务本来就是手动启动的比如Windows Update在某些环境下是禁用状态你把它当异常告警就是自找麻烦。所以配置里要有一个IgnoreList把不需要监测的服务名放进去。应用程序池也一样有些池是给内部工具用的停了不影响业务可以排除。3.3 通知落地写日志、发邮件、调Webhook三种方式怎么选通知方式没有银弹我一般三种都留着按告警级别走不同通道。Critical 级别走邮件加 WebhookWarning 级别只写日志。写日志用NLog或Serilog都行关键是日志要分文件监测日志和告警日志分开不然排查时翻起来很痛苦。public class Notifier { private readonly ILogger _logger; private readonly SmtpClient _smtp; private readonly HttpClient _http; public async Task NotifyAsync(Alert alert) { // 所有告警都写日志 _logger.Warn($[{alert.Level}] {alert.Type} - {alert.Target}: {alert.Message}); if (alert.Level Critical) { // 发邮件注意 SmtpClient 要设超时不然会卡住 _smtp.Timeout 10000; await _smtp.SendMailAsync(BuildMail(alert)); // 调 Webhook比如钉钉或企业微信的机器人地址 var payload new { msgtype text, text new { content alert.Message } }; await _http.PostAsJsonAsync(_webhookUrl, payload); } } }SmtpClient的Timeout必须设默认值很大SMTP 服务器不响应时会把通知线程卡死。Webhook 调用要加try-catch外部接口不可用不能影响监测主流程。还有一点邮件和 Webhook 不要同步发用async或者丢到独立线程池否则告警一多通知器就成了瓶颈。4. 避坑与排查监测程序自己挂了才是最尴尬的4.1 现象监测服务运行几天后自动停止日志无异常原因监测服务本身是个 Windows 服务如果它内部有未捕获的异常或者内存泄漏进程会退出。最常见的是Timer回调里抛异常没接住导致线程池线程崩溃最终服务停止。另一个原因是ServerManager没释放长时间运行后内存涨到上限。解决在Collect方法外层包try-catch任何异常都写日志而不是抛出。ServerManager用using包住或者定期重建。另外给监测服务本身加一个「看门狗」——可以用另一个计划任务每 5 分钟检查监测服务是否在运行不在就启动它。4.2 现象应用程序池状态显示 Started但网站就是 502原因应用程序池的State是 Started但工作进程w3wp.exe可能已经挂了或者正在回收。pool.State反映的是应用程序池的配置状态不是工作进程的实际健康状态。IIS 在快速失败保护触发后应用程序池会被禁用但有时候状态更新有延迟。解决监测逻辑里要同时判断pool.State Started和pool.WorkerProcesses.Count 0。如果状态是 Started 但工作进程数为 0直接告警。另外可以加一个 HTTP 探测对网站发一个 HEAD 请求看返回码是不是 200这比只看 IIS 状态更可靠。4.3 现象服务状态频繁在 Running 和 Stopped 之间跳动告警风暴原因有些服务本身就有自动重启机制或者依赖的服务启动顺序不对导致它反复重启。监测程序如果每次状态变化都告警就会产生大量重复告警。解决用「连续 N 次异常」策略过滤瞬时抖动同时加告警抑制——同一个对象在 M 分钟内只告警一次。M 一般设 10 到 15 分钟。配置里还可以给每个对象单独设阈值核心服务严格非核心服务宽松。4.4 现象ServerManager报「拒绝访问」或「配置系统初始化失败」原因监测程序运行账户权限不够或者 IIS 的配置系统applicationHost.config被其他进程锁定。在 Windows Server 2012 等老系统上还可能是 .NET 版本和 IIS 版本不匹配。解决确保监测服务以管理员账户运行或者把账户加入IIS_IUSRS组。如果是配置文件锁定检查是不是有别的程序在频繁改 IIS 配置。老系统上Microsoft.Web.Administration的版本要和 IIS 版本对应别拿高版本的 DLL 往低版本 IIS 上怼。4.5 现象监测数据写入数据库越来越慢查询超时原因监测数据是时序数据如果每次都往关系型数据库里插数据量一大就扛不住。尤其是每秒都在采集的场景一天下来几百万条记录。解决监测数据不要全量存数据库。我一般只存状态变化和告警记录正常状态的快照存最近 N 条在内存里就行。如果非要存用轻量级的时序方案或者按天分表定期清理历史数据。写数据库的操作也要异步不能阻塞采集循环。5. 进阶技巧让监测数据真正能用来做容量规划和故障回溯监测做完了如果只是告警那价值只发挥了一半。我后来养成的习惯是把每次采集的快照都留一份轻量记录不存全量只存关键指标服务状态、应用程序池队列长度、工作进程数、内存占用。这些数据攒上一个月就能看出趋势。比如某个应用程序池的队列长度每周一上午都会涨那说明周一早上有定时任务在抢资源可以提前扩容或者调整任务时间。验证监测是否靠谱有个简单办法手动把某个测试应用程序池停掉看监测程序多久能发现、告警内容对不对、恢复后能不能自动消除告警。这个「演练」我一般每季度做一次比看代码管用。另外监测程序的日志要保留至少 30 天故障回溯时全靠它。我踩过最大的坑就是日志只留了 3 天结果一个偶发问题复现不了只能干瞪眼。// 轻量快照记录只存关键指标按天写文件 public class SnapshotRecorder { private readonly string _baseDir; public void Record(MonitorSnapshot snapshot) { var date snapshot.CollectedAt.ToString(yyyyMMdd); var path Path.Combine(_baseDir, ${date}.csv); var line string.Join(,, snapshot.CollectedAt.ToString(HH:mm:ss), string.Join(|, snapshot.Services.Select(s ${s.Name}:{s.Status})), string.Join(|, snapshot.Iis.Pools.Select(p ${p.Name}:{p.State}:{p.QueueLength}:{p.WorkerProcesses})) ); // 追加写入文件按天滚动 File.AppendAllText(path, line Environment.NewLine); } }这个 CSV 文件不大一天下来也就几 MB但排查问题时非常有用。比如客户说「昨天下午网站慢」你翻出昨天的 CSV看那个时间段的应用程序池队列长度一目了然。监测这件事采集和告警只是及格线能把数据留下来做分析才算真正把「实时监测」这四个字吃透了。希望帮到你。本文还有配套的精品资源点击获取
返回列表