ARTICLE DETAIL

资讯详情

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

.NET 5 Worker Service构建Windows服务完整实践指南

.NET 5 Worker Service构建Windows服务完整实践指南 简介本资源是一套基于.NET 5构建Windows服务的完整实战示例面向C#开发者、企业级后台服务工程师及.NET跨平台迁移学习者解决传统Windows服务开发门槛高、配置复杂、日志与配置管理不统一等痛点。压缩包为7.1MB的ZIP文件包含项目源码、配置文件、log4net集成模块、HTTP监听服务实现及Ant Design Pro前端对接逻辑等核心内容涵盖服务注册、生命周期管理、配置读写、后台任务托管等关键环节。目前已有410人学习下载适合希望掌握现代.NET服务架构、快速落地生产级Windows服务的中高级开发者。读者可直接运行调试深入理解Worker Service模板在Windows服务场景下的工程化实践包括服务安装/卸载脚本、依赖注入扩展、结构化日志输出及前后端协同接口设计等实用能力。 写这篇东西的起因挺直接的最近要把公司里的一个老定时任务从控制台程序改成Windows服务原本用.NET Framework 4.8跑得好好的但因为要接新的内部组件只能往上走最后落在.NET 5上。改的过程比预期折腾主要是.NET 5时代做Windows服务的资料还比较散很多教程还停留在.NET Framework的ServiceBase写法。我把整个过程的踩坑记录、完整代码和部署脚本整理成了一个demo项目也就是dotnet5-winservice-demo这篇文章就当是配套的讲解。如果你是刚接触.NET 5/6/8的开发者或者正准备把一个常驻程序改成Windows服务这篇文章应该能帮你少走不少弯路。先说清楚这东西能干什么用.NET 5写的Windows服务本质上就是一个可被Windows服务控制管理器SCM托管的后台常驻进程适合做文件监听、队列消费、定时任务、设备通信这类不需要界面的程序。跟老式的ServiceBase项目相比.NET 5推荐的做法是基于Worker Service模板底层借用了Generic Host那一套依赖注入、配置系统、日志管道都是现成的相当于用一个Web应用的标准姿势去写后台服务舒服很多。全文我会按这样的顺序来先讲为什么选.NET 5 Worker Service这套组合再带你完整创建和改造一个服务项目然后把核心代码一段段拆开讲接着是部署和自启动操作最后集中解决我实际遇到过的高频问题包括服务名占用、服务无响应控制、调试困难等场景。每个环节都有可直接复制的代码和命令。1. 整体设计与思路拆解1.1 为什么用.NET 5写Windows服务先理清一点Windows服务不是一门编程语言它是一套Windows机制。你的程序写出来以后通过服务控制管理器注册进去就能做到开机自启、无人值守运行、崩溃自动重启需配置、以系统账户或指定账户运行。老一套做法是用.NET Framework的ServiceBase类手动处理OnStart、OnStop这些生命周期方法然后还要费劲地引入System.ServiceProcess程序集。.NET 5做了一个关键转变微软把长时运行的后台任务统一收编到了IHostedService模型里也就是Worker Service模板。这个模型最早是从ASP.NET Core的托管服务演变过来的后来独立成了dotnet new worker模板。它的好处非常明显天然支持依赖注入构造函数里直接注入ILoggerT、IConfiguration这些。配置文件用appsettings.json支持环境变量覆盖比老式的App.config舒服太多。日志系统是Microsoft.Extensions.Logging可以随意往控制台、文件、EventLog等方向输出。生命周期方法只有StartAsync、ExecuteAsync、StopAsync三个理解成本低。用表格看更直观维度.NET Framework ServiceBase.NET 5 Worker Service生命周期OnStart / OnStop / OnPause / OnShutdownStartAsync / ExecuteAsync / StopAsync依赖注入没有需要手写单例容器内置构造函数注入即可配置系统ConfigurationManager弱类型IConfiguration强类型支持多样化源日志需要引第三方库内置ILogger接口统一发布产物需要安装.NET Framework系统自带可以单文件发布携带运行时跨版本升级基本锁死在Windows可升级到.NET 6/8生命周期一致所以结论是如果你的目标平台就是Windows且想用新语言特性record、switch表达式、模式匹配那.NET 5的Worker Service就是当前写Windows服务最合适的姿势。1.2 项目结构怎么设计才不拧巴demo项目我命名为dotnet5-winservice-demo不是随便起的名这个文件夹本身就是给那些想整个跑一遍的人准备的。如果你下载解压后直接打开解决方案会发现里面有几个主要部分Worker.cs后台任务的核心逻辑就是继承BackgroundService的那个类。appsettings.json存放服务的运行配置比如任务间隔、开关等。Program.cs程序的入口和Host构建过程服务最终就是在这里注册的。dotnet5-winservice-demo.csproj项目文件里面声明了目标框架、依赖包。我在demo里故意让项目结构保持了最小化但每块都有具体意义。实际在公司项目里你完全可以拆出单独的Services、Jobs、Utils目录但这是后话。我刚接触这个技术栈时最大的误区是把所有逻辑都塞进Worker.cs一个类结果方法几十个文件上千行。写服务代码的思维方式应该是后台调度器Worker只负责触发具体业务放在独立的服务类里比如IFileProcessService、IQueueConsumerServiceWorker只是调用它们。这样的好处是便于单元测试。你可以直接new出FileProcessService来跑测试而不需要真的启动Windows服务去验证。这是老式ServiceBase项目很难做到的。1.3 哪些场景适合用这个方案根据我实际接触的项目下面这些场景都适合用.NET 5/6的Worker Service做Windows服务定时任务调度比如每天凌晨拉取数据、生成报表、清理过期文件。消息队列消费从RabbitMQ、Kafka、Redis List里取消息做异步处理。文件夹监听监控某个目录的新增文件触发后续处理流程。TCP/UDP长连接服务比如设备数据采集、上位机通信。中间件常驻进程等待HTTP请求处理内部API回调。如果只是简单的开机启动某个exe或者每隔5分钟跑一个脚本那确实不需要Windows服务直接把exe丢进启动文件夹或者用任务计划程序就行。但如果你需要这个程序稳定运行、无人值守、崩溃自动拉起、不因用户注销而退出那Windows服务就是不二之选。2. 环境准备与项目创建2.1 安装.NET 5 SDK写这篇文章时.NET 5早已不算版本但在当年的语境里.NET 5是第一个统一了.NET Core和.NET Framework后续路线的版本LTS版本是后面的.NET 6。如果你现在下载SDK版本一般至少是.NET 6/8完全可以用net6.0目标框架替代net5.0代码几乎不变。这里按标题走就写.NET 5但你要清楚思路和代码在这些版本上是通用的。去微软官网下载.NET 5 SDK装完以后在命令行输入dotnet --info验证。正常输出会包含SDK版本和运行时列表。如果之前机器上装过多个版本也可以看到各个runtime的具体信息。提示如果你开发机上还有Visual Studio建议VS版本不低于2019 16.8否则识别不了.NET 5目标框架。用VS Code 命令行也行本质上就是dotnetCLI的事情。2.2 创建Worker Service项目打开命令行进到你要放项目的目录执行dotnet new worker -n dotnet5-winservice-demo cd dotnet5-winservice-demo这样一个小型项目就出来了。此时目录里的文件比想象中少核心就两个Program.cs和Worker.cs外加一个appsettings.json。先别急着写代码先跑一下这个空项目确认环境没问题dotnet build没有红色报错的话说明环境OK。这时候Worker.cs里面默认写了一个每隔1秒打印一条日志的循环但它在本地跑是以控制台程序方式运行的发布后默认也不会作为服务被系统识别还需要改造这一步很重要。2.3 引入Windows服务支持包为了让这个Worker Service能被Windows服务管理器托管必须引入一个专门的NuGet包Microsoft.Extensions.Hosting.WindowsServices。这个包的作用是让Host在检测到服务环境时自动切换到服务模式并处理日志重定向、服务生命周期通知等细节。dotnet add package Microsoft.Extensions.Hosting.WindowsServices然后在Program.cs里把UseWindowsService()加上完整代码如下using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; namespace dotnet5_winservice_demo { public class Program { public static void Main(string[] args) { CreateHostBuilder(args).Build().Run(); } public static IHostBuilder CreateHostBuilder(string[] args) Host.CreateDefaultBuilder(args) .UseWindowsService(options { options.ServiceName Dotnet5WinServiceDemo; }) .ConfigureServices((hostContext, services) { services.AddHostedServiceWorker(); }); } }UseWindowsService()做的事情有两块一是把应用生命周期和SCM绑定接收SCM的停止、关闭命令二是设置了日志输出到事件源。ServiceName属性直接决定Windows服务列表里显示的名字建议提前想好别跟已有服务重名否则后面服务创建会报服务名已被占用这个问题我在常见问题里会专门讲。2.4 目标框架与平台设置的坑如果你创建项目时默认用的net5.0那么在Windows上做服务是没问题的。但有一个细节值得注意当你把目标框架改为net5.0-windows以后项目会自动引用Microsoft.Windows.SDK.NET.Ref这是为了支持Windows专有API。对于简单的服务程序其实用net5.0就够了不需要特意声明-windows后缀。发布时建议指定运行时这样生成的exe携带对应运行时不会因为目标机器缺少.NET环境而运行失败。发布命令一般是dotnet publish -c Release -r win-x64 --self-contained true这行命令的含义是以Release配置发布目标运行时是64位Windows自包含模式也就是把.NET运行时一起打包进输出目录。这样目标机器不需要预装.NET双击exe直接运行。代价是发布出来的文件体积较大一般来说50~70MB是正常的。如果是内网环境部署我强烈建议用自包含模式避免目标机器还要装SDK或运行时。3. 核心代码实现与生命周期管理3.1 理解BackgroundService的三大方法Worker.cs默认继承BackgroundService这个类来自Microsoft.Extensions.Hosting它内部实现了三个关键方法StartAsync服务启动时执行一次适合做初始化操作。ExecuteAsync核心业务循环必须是异步的并且要能响应取消请求。StopAsync服务停止时执行一次适合做资源释放。需要注意ExecuteAsync如果不执行到完成服务会一直处于正在运行状态。如果你在里面写了一个while (true)死循环那就得确保循环体会响应stoppingToken.IsCancellationRequested否则服务永远停不下来执行net stop的时候会卡很久最后SCM会提示服务无响应。我用一个实际例子解释假设服务要每隔5分钟扫描一次某个目录下的文件并处理那么Worker.cs可以这样写public class Worker : BackgroundService { private readonly ILoggerWorker _logger; private readonly IConfiguration _configuration; public Worker(ILoggerWorker logger, IConfiguration configuration) { _logger logger; _configuration configuration; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation(服务已启动开始监听目录); // 从配置读取扫描间隔默认5分钟 var intervalMinutes _configuration.GetValueint(ScanIntervalMinutes, 5); while (!stoppingToken.IsCancellationRequested) { try { // 这里放业务逻辑 _logger.LogInformation($开始扫描目录当前时间{DateTime.Now}); await Task.Delay(TimeSpan.FromMinutes(intervalMinutes), stoppingToken); } catch (OperationCanceledException) { // 取消操作时不抛异常直接退出循环 break; } } _logger.LogInformation(服务正在停止); } }Task.Delay里的stoppingToken非常关键。没有它就算系统执行了停止命令服务也要等到Delay结束才能响应相当于停机延迟不可控。加了token停止命令一到Delay会被立即取消从而抛OperationCanceledException我们捕获后跳出循环完成优雅停机。3.2 配置文件的正确打开方式appsettings.json是Worker Service里搞配置的主要工具。它既可以放基础配置服务名、端口号也可以放自定义业务参数。以前用.NET Framework的服务项目改配置要靠ConfigurationManager.AppSettings然后项目重新编译。现在用appsettings.json改完以后只要重启服务就会重新加载不必重新编译。举个例子在appsettings.json里加一段{ ScanIntervalMinutes: 5, FileProcess: { WatchDirectory: D:\\Data\\Inbox, ProcessedDirectory: D:\\Data\\Processed }, ConnectionStrings: { Default: Server.;Databasemydb;Trusted_ConnectionTrue; } }在代码里通过构造函数注入IConfiguration就能直接读取var watchDir _configuration.GetSection(FileProcess)[WatchDirectory]; var connStr _configuration.GetConnectionString(Default);这里有个容易踩的坑appsettings.json在发布时不一定被复制到输出目录。老程序员应该都有印象VS里经常需要右键文件属性把复制到输出目录改成如果较新则复制。在.NET 5的项目文件里默认SDK已经把appsettings.json自动包含到发布输出中只要你没手动改过csproj就行。3.3 依赖注入与多服务协作Worker Service的价值有一半来自依赖注入。假设你的服务需要同时跑两个独立任务一个扫文件一个清日志。正常做法不是创建两个Worker类而是创建一个调度类在内部注入多个业务服务。我见过很多同事上来就写两个BackgroundService子类然后services.AddHostedServiceWorker1()、services.AddHostedServiceWorker2()各来一遍。这没有错但两个服务彼此通信会变得别扭。更合理的做法是public interface IReportService { Task GenerateDailyReportAsync(CancellationToken token); } public class ReportService : IReportService { private readonly ILoggerReportService _logger; public ReportService(ILoggerReportService logger) { _logger logger; } public async Task GenerateDailyReportAsync(CancellationToken token) { // 模拟耗时操作 await Task.Delay(2000, token); _logger.LogInformation(日报生成完成); } }然后在Worker中注入这个接口public class Worker : BackgroundService { private readonly IReportService _reportService; public Worker(IReportService reportService) { _reportService reportService; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { await _reportService.GenerateDailyReportAsync(stoppingToken); await Task.Delay(TimeSpan.FromHours(24), stoppingToken); } } }最后在Program.cs里注册services.AddSingletonIReportService, ReportService(); services.AddHostedServiceWorker();这种结构的核心优势在于ReportService可以被单元测试直接调用不需要真的起服务也不需要跟Windows SCM纠缠。你new一个ReportService传入空的ILogger然后直接调方法就能验证业务逻辑是否正确。3.4 日志输出与事件源配置默认情况下Worker Service直接把日志打印到控制台。在Windows服务场景下控制台是没有界面的日志丢了就真的丢了。UseWindowsService()已经帮我们把日志接入到了Windows事件日志但代码里需要稍微处理一下。有两种常见的日志落地方案方案一只靠Windows事件日志。启动时检查事件源没有就创建。需要管理员权限if (!EventLog.SourceExists(.NET Runtime)) { EventLog.CreateEventSource(.NET Runtime, Application); }方案二使用第三方日志库比如Serilog把日志写到文件里。我个人更推荐方案二因为文件日志查询起来比事件日志方便也有成熟的切割策略。下面是一个简单的Serilog配置Log.Logger new LoggerConfiguration() .WriteTo.File(logs/service-.log, rollingInterval: RollingInterval.Day) .CreateLogger();这里有个很重要的细节如果是自包含发布日志目录建议写绝对路径或者基于AppContext.BaseDirectory动态拼接不要指望当前工作目录就是exe所在目录。服务启动时的工作目录往往是C:\Windows\System32如果你径写相对路径日志文件会跑到系统目录里去排查问题时找半天找不到。3.5 Windows服务与普通控制台程序的关键区别这是整个demo里最容易忽略的部分。dotnet run运行时它就是个控制台程序可以前台运行CtrlC可以终止。但一旦作为Windows服务注册运行它就有几个截然不同的行为服务登录会话与当前登录用户无关。即使某个用户没登录服务照样跑。服务不能直接弹对话框或显示桌面UI。服务的标准输出和标准错误不会显示在任何终端里必须重定向到日志。服务的停止由SCM发命令而不是CtrlC。这些差异意味着如果你在服务代码里写了Console.WriteLine那这行输出在服务模式下是无处可去的。调试的时候就要换思路要么用ILogger要么直接写文件。习惯了Web开发的同事第一次写Windows服务最容易在这个问题上栽跟头。4. 部署、发布与自启动实现4.1 发布成自包含单文件构建Windows服务的第一步不是注册而是先发布。在demo项目的根目录执行dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFiletruePublishSingleFiletrue会生成一个单独的exe比一大捆文件清爽很多。要注意的是单文件发布以后appsettings.json不会被塞进exe里它仍然作为独立文件输出。你可以把配置文件和exe放同一个目录服务也能正常读取。如果希望把配置也嵌入exe需要额外设置但一般不推荐因为运维要改参数就麻烦了。发布完成以后输出目录通常在bin\Release\net5.0\win-x64\publish\下。把这个目录整体拷贝到目标机器的某个固定位置比如D:\Services\Dotnet5WinService。4.2 使用sc.exe创建Windows服务在目标机器上用管理员权限打开命令行然后执行sc create Dotnet5WinServiceDemo binPath D:\Services\Dotnet5WinService\dotnet5-winservice-demo.exe start auto DisplayName .NET5 WinService Demo这行命令有几个细节要特别强调sc create后面的服务名是SCM内部名称不区分大小写但不能有空格。如果服务名已存在SCM会报服务名已被占用。binPath后面必须有空格否则命令会解析失败。路径包含空格时必须用双引号包住。start auto表示开机自启动。DisplayName是显示给用户看的名字可以包含空格。创建完以后启动服务sc start Dotnet5WinServiceDemo如果一切正常命令行会返回SERVICE_RUNNING。如果状态一直是START_PENDING说明服务启动卡住了这个时候检查事件查看器最靠谱。4.3 配置服务失败后的重启策略服务进程崩溃是早晚的事不是if的问题是when的问题。所以部署时必须配置恢复策略。sc命令支持这个sc failure Dotnet5WinServiceDemo reset 86400 actions restart/5000/restart/10000/restart/30000这段的意思服务异常退出后第一次5秒后重启第二次10秒后重启第三次30秒后重启计数器在86400秒一天后重置。配置好这个服务挂了会自动拉起来不需要人工干预这对无人值守服务器非常关键。如果你不想用命令行也可以在Windows服务管理器里右键服务 - 恢复选项卡把第一次失败、第二次失败、后续失败都改成重新启动服务效果一样。4.4 卸载Windows服务卸载就简单多了同样用scsc stop Dotnet5WinServiceDemo sc delete Dotnet5WinServiceDemo需要注意的是sc delete只会删除服务注册项不会删除exe文件。如果你想连文件一起清理得手动删除目录。另外如果服务还在运行状态直接sc delete会失败提示服务已标记为删除所以卸载前一定要先stop。5. 调试技巧与排错实录5.1 如何在代码里直接调试Windows服务Windows服务有个很真实的痛点你不能像普通控制台程序那样直接F5运行并断点调试。因为服务需要SCM来启动直接dotnet run确实能跑起来但它是以控制台模式跑的跟服务模式行为有差异。我最常用的调试方法是在Main里加一个参数判断public static void Main(string[] args) { if (args.Contains(--console)) { CreateHostBuilder(args).Build().Run(); } else { CreateHostBuilder(args).Build().RunAsServiceAsync().GetAwaiter().GetResult(); } }这样你在开发机上直接dotnet run -- --console就能前台运行想看日志看日志想打断点打断点部署成服务时不需要加--console参数正常运行。这种双模式其实不需要额外引包用UseWindowsService()会自动处理环境差异但显式区分的好处是你可以模拟服务模式下的行为。更暴力的调试方式是直接在代码里加#if DEBUG System.Diagnostics.Debugger.Launch(); #endif服务启动时会在Debugger.Launch处弹出一个选择调试器的对话框方便附加进程。但注意服务运行在Session 0UI不可见所以你大概率看不到对话框实际场景下用得不多真正用的多的还是事件日志和文件日志。5.2 服务状态卡在START_PENDING的排查思路这类问题我用一个真实案例来讲。之前有个同事部署nginx到Windows服务他直接把nginx.exe注册成服务结果服务状态一直是START_PENDINGSCM报服务没有响应控制功能。这是因为nginx.exe根本不是按照Windows服务协议写的它不往SCM报告状态SCM等了30秒就判定它无响应。.NET 5的Worker Service不会出现这个问题因为UseWindowsService()已经内置了服务状态通知。但如果你的服务在StartAsync里做了大量耗时操作比如连接数据库、加载大文件服务也会在START_PENDING阶段停留很久直到StartAsync返回。解决方法是StartAsync里只做轻量初始化重活放到ExecuteAsync线程里去做。5.3 事件查看器怎么用才高效Windows服务出问题时事件查看器是最直接的线索来源。打开eventvwr.msc展开Windows 日志 - 应用程序过滤来源为.NET Runtime或者你的服务名重点看红色的错误级别条目。常见的报错有两种Service cannot be started.后面跟着异常堆栈说明StartAsync或Host构建阶段抛了异常。.NET Runtime version 5.0.x - The application to execute does not exist or one of its dependencies is missing说明exe或依赖文件路径不对或者自包含发布不完整。找到事件后把异常堆栈复制到搜索引擎基本都能定位到问题。如果事件日志没有记录可以手动给服务配置EventLog源或者检查你的日志输出路径是不是能正常写入。5.4 服务账户与文件权限的组合问题这是所有Windows服务排错里最隐蔽的一类。默认sc create创建的服务以LocalSystem账户运行这个账户权限极大本地文件基本都能访问。但如果你手动把服务登录选项卡改成了Network Service或者指定用户权限立刻收窄访问网络共享路径或者某些磁盘目录就会报Access Denied。我自己犯过的错是给服务配置了一个普通域账户来运行结果服务能启动但没法读取E:\Data目录下的文件事件日志里毛都没有。后来发现普通账户的权限不够再给目录加权限就解决了。所以遇到服务能起但业务不执行的情况先检查账户权限再检查代码逻辑。6. 常见问题速查与避坑技巧6.1 服务名已占用很多人第一次用sc create时报服务名已被占用比如mysql80这类安装版数据库已经注册了同名的服务。解决办法是先查一下现有服务sc query mysql80 sc queryex mysql80如果确实已存在要么换一个服务名要么先删掉旧服务如果确定没用。注意Windows服务名是全局唯一的不像进程名可以重名。用net start查看当前所有服务时服务名不会显示显示的是DisplayName所以服务名冲突时第一反应往往是对不上号。6.2 服务没有响应控制功能出现net start vmcompute这种命令后SCM返回服务没有响应控制功能十有八九是这个exe本身不是Windows服务。处理方式不是给它加什么特殊配置而是要么换成支持服务协议的程序要么用通用服务包装器比如WinSW这类工具把任意exe包装成Windows服务。我用WinSW包过Python脚本、Node.js应用思路是写一个XML配置文件声明可执行文件、参数、日志输出等然后把WinSW的exe注册成服务。如果你手里有那种不能改造的老程序这就是最稳妥的路线。但这种包装方案有一个局限性外层服务管理进程只能感知到wrapper进程是否存活内部业务线程卡死时它不一定能检测到。6.3 服务正常运行但日志没输出如果你把日志写到文件结果文件里空空如也大概率是路径问题。我前面提过服务的工作目录默认为C:\Windows\System32不是exe所在目录。所以代码里获取路径别用Environment.CurrentDirectory要用AppContext.BaseDirectoryvar logDir Path.Combine(AppContext.BaseDirectory, logs);这样无论服务从哪里启动日志文件都会落在exe旁边的logs目录下排查时按图索骥。6.4 服务占用CPU过高或内存持续上涨服务长期运行跟开发机跑一会有本质区别内存泄漏、句柄泄漏问题迟早会暴露。我第一次把Worker Service部署到服务器上跑了两周内存从200MB涨到1.5GB后来发现是在循环里创建了HttpClient对象没走单例复用导致socket资源一直没释放。合理的做法是HttpClient以单例方式注册到容器里由DI统一管理生命周期。还有一个容易被忽视的点Task.Delay配合while循环如果没写CancellationToken在服务被停止时可能导致线程卡住SCM等待超时就会提示服务没有响应控制功能。所以代码里凡是Task.Delay、异步IO操作一律传入stoppingToken。7. 从demo到生产环境还要做的事7.1 安装脚本一键化手动敲sc create和sc failure容易出错我建议直接把部署命令写成一个bat脚本放到服务目录里。每次部署新环境时管理员双击运行即可。核心脚本如下echo off set SERVICE_NAMEDotnet5WinServiceDemo set SERVICE_PATHD:\Services\Dotnet5WinService\dotnet5-winservice-demo.exe sc stop %SERVICE_NAME% 2nul sc delete %SERVICE_NAME% 2nul sc create %SERVICE_NAME% binPath %SERVICE_PATH% start auto DisplayName .NET5 WinService Demo sc failure %SERVICE_NAME% reset 86400 actions restart/5000/restart/10000/restart/30000 echo Service installed. sc start %SERVICE_NAME%注意脚本里sc delete之后的2nul是为了忽略服务不存在错误避免脚本中断。7.2 升级与备份策略Windows服务的升级不像Web应用那样可以无缝切换。普通做法是停止服务 - 备份旧exe和配置文件 - 覆盖新文件 - 启动服务。因为服务在运行时exe文件被锁定直接复制会提示文件正在使用。我习惯在服务目录下保留一个backup文件夹每次升级前把旧的exe和appsettings.json复制进去命名带上日期。这样回滚的时候只需要停服务、拷回文件、启动服务全程几分钟。7.3 日志轮转与磁盘空间长期运行的服务日志文件会越来越大。如果用的是Serilog配置好rollingInterval就能按天自动切割但还要考虑保留天数。常用的做法Log.Logger new LoggerConfiguration() .WriteTo.File(logs/service-.log, rollingInterval: RollingInterval.Day, retainedFileCountLimit: 30) .CreateLogger();retainedFileCountLimit: 30表示只保留最近30天的日志到期自动清理。这样能避免日志把磁盘塞满。Windows事件日志本身也有大小上限超过后会按策略覆盖旧事件所以事件日志不用太担心但文件日志一定得管。8. 几个特殊场景的配置备忘8.1 让Worker Service访问网络共享路径如果服务需要访问\\server\share\data这类网络共享目录只有两种方案可行一是用LocalSystem账户并配置好共享目录的访问权限二是在服务属性里指定一个有权限的域账户。Network Service账户对网络资源的访问是按机器账户权限走的经常摸不到共享目录排错的时候优先排除这个因素。8.2 服务与防火墙的交互如果服务监听了端口比如实现了一个内部的TCP服务或HTTP API目标机器的防火墙必须放行对应端口。服务进程路径和端口规则要一起配好然后重启防火墙服务使规则生效。我碰到过几次服务正常但别的主机连不上最后都是防火墙规则没生效导致的。8.3 多实例运行假如你要在同一台机器上跑两个相同逻辑的服务但是实例名不同、配置不同做法有几种写两个服务名分别指向两个不同目录的exe或者做一个动态配置通过命令行参数传入实例名。后者更优雅Program.cs里可以通过args读取实例名再加载对应的配置文件段。9. 个人经验与收尾建议做.NET 5的Windows服务最怕的不是代码写不对而是把服务当成普通程序来理解。服务的生命周期、账户、日志、部署都跟控制台程序有本质区别。我第一次把dotnet5-winservice-demo部署到服务器时犯了两个低级的错一是忘记配置UseWindowsService结果服务注册是成功了但SCM完全管不住它停止操作超时二是日志路径用了相对路径死活找不到日志文件最后发现都写到C:\Windows\System32里去了。这些坑在文档里不会写跑一次真实部署才能体会到。最后再分享一个小技巧开发调试时尽量把服务做成既能控制台运行又能服务运行的双模式。具体做法就是前面提到的在Main里判断命令行参数。这样你平时可以直接用dotnet run -- --console在开发机上跑验证逻辑没有问题以后再发布成服务部署到服务器整个流程顺滑很多。这个demo项目里已经集成了这个能力你下载后直接跑一下就能感受到。如果你现在手头的项目还在用.NET Framework的ServiceBase写法趁迁移到.NET 5/6的机会重构到Worker Service绝对是值得的。后面从.NET 5升到.NET 6、.NET 8代码基本不用动只需要改目标框架重新发布即可。到目前为止.NET 5/6/8这套Worker Service的服务开发模型已经稳定了很多年学习投入产出比很高。本文还有配套的精品资源点击获取
返回列表