把程序做成系统服务

写程序,难免会遇到需要做成系统服务的需求。Windows 下写系统服务需要实现一些特定的接口,做起来有一定难度,所以不少程序采用了 近似的备选方案 —— 做成带系统任务栏图标的桌面应用。但是,服务之所以是服务,就在于他有一个非常重要的特点:可以开机自启动,而且不需要用户登录。要不然每次重启还得人工去登录,是件多么辛苦的事情。Windows 当然是可以设置自动登录的,但如果是托管服务器,你真放心自动登录吗?
而 Linux 下面似乎就要方便得多,大概不需要 GUI 持续运行的程序都可以做成服务。
1. 在 Windows 中做服务 先说 Windows。如果你还在用 Windows XP,那我们就此别过 ……
1.1. Windows Service Wrapper
[Windows Service Wrapper] 是全称。其简称 WinSW 的知名度可能更高一些。
WinSW 基于 .NET Framework 4.6.1 和 .NET 5 实现,所以至少需要 Windows 7 SP1 / Windows Server 2008 R2 SP1 才可以使用。它可以把任意 Windows 程序封装成 Windows 服务,你所需要做的,只是写个配置文件,然后用 WinSW 注册一个 Windows 服务即可。WinSW 下载下来是个独立的可执行文件,使用前需要写一个与可执行文件名同名但扩展名是 .xml 的配置文件置于同一目录下。
举例来说,Nginx 本身并没有提供注册成 Windows 服务的能力,如果需要注册成 Windows 服务,就可以用 WinSW 来封装一下。把下载的 WinSW 可执行文件改名为 winsw.exe(随便改成什么名字都行,配置文件名按相同的名称创建即可),放在 nginx 的主目录下面,创建配置文件之后的目录结构大概是这样:

[-] nginx |-- conf |-- ...(其他 nginx 的目录或文件) |-- nginx.exe |-- winsw.exe `-- winsw.xml

winsw.xml 中的配置内容如下,看注释就能理解。
nginx-service Nginx Service Nginx ServiceC:\Local\NginxC:\Local\Nginx\nginx.exeC:\Local\Nginx\nginx.exe-s stopNormal 15 sec falseAutomatic 15 sec 1 sec%BASE%\logs yyyyMMdd

这个配置创建了名为 nginx-service 的 Windows 服务,它在 Windows 的「服务 (services.msc)」显示名称为 Nginx Service。启动服务的时候直接运行 nginx.exe 来启动,这是一个会执行占用控制台的程序;而停止服务则是运行 nginx.exe -s stop,可执行程序和参数分别配置在 中 —— 由此不难推断,如果启动服务需要参数,是配置在 中的。
详细的配置可以在 github 库里的 XML configuratoin file 中查到,也可以查到一些示例。
配置完成之后运行 winsw.exe install 即可安装为 Windows 服务。安装完成之后可以使用 winsw.exe start 命令启动服务,也可以去 Windows 的服务管理器启动,或者使用 net start 命令来启动。github 库首页的 Usage 部分有完整的命令说明。
1.2. 用 .NET Framework/Core/5 自己写一个
用 .NET 写个服务还是比较容易的,因为有现成的包(组件)可以用:NuGet Gallery | Microsoft.Extensions.Hosting.WindowsServices,官方出品。它至少需要依赖两个包:
  • NuGet Gallery | Microsoft.Extensions.Hosting
  • NuGet Gallery | Microsoft.Extensions.Hosting.Abstractions
在引入组件之后,只需要少量代码就可以让当前 .NET 的 Console Application 成为一个支持 Windows 服务接口的服务程序。
// Program.csclass Program { static async Task Main(string[] args) { await CreateHostBuilder(args).Build().RunAsync(); }public static IHostBuilder CreateHostBuilder(string[] args) { return Host.CreateDefaultBuilder(args) .ConfigureServices((hostContext, services) => { services.AddHostedService(); }) .UseWindowsService(); } }

注意到 AddHostedService,这里的 DaemonServce 是一个自己实现的服务业务类,命名自由,但需要从 Microsoft.Extensions.Hosting.BackgroundService 继承
class DaemonService : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { // TODO 提供服务内容的代码 } }

服务的业务代码通常都是持续运行,或者监听的代码。如果是计划性/周期性的任务,可以考虑使用 Quartz 来实现。
程序完成后可以使用 Windows 提供的 sc 命令来注册/注销服务。假设生成的程序是 MyService.exe,那么注册、配置和启动服务的命令如下:
sc create "my-service" binPath="C:\MyService\MyService.exe --service" sc config "my-service" start= auto sc start "my-service"

注意:binPath 中应该给绝对路径。
2. 在 Ubuntu 中做 Systemd 服务 Linux 下服务种类比较多,最近主要是用 Ubuntu,所以做 Ubuntu 下的 Systemd 服务。
假设我们写了一个 .NET 5 的 ASP.NET 应用,放在 /app/my-web/,主文件是 MyWeb.dll。如果用命令行启动这个 Web 应该应该是
cd /app/my-web dotnet MyWeb.dll

注意:需要提前准备好 .NET 5 的运行环境,可参考 在 Ubuntu 上安装 .NET - .NET | Microsoft Docs。
接下来是写 Systemd 服务配置。配置文件名起为 my-web.service,放在 /etc/systemd/system 目录下。内容(含注释)如下:
[Unit] # 服务说明 Description=My Web Application # 在启动网络服务之后启动 After=network.target[Service] # 总是重启(无论什么原因结束都会立即重启) Restart=always RestartSec=10 # 工作目录 WorkingDirectory=/app/my-web # 启动服务的命令 ExecStart=/usr/bin/dotnet MyWeb.dll # 通过杀主进程来结束服务 ExecStop=/bin/kill -HUP $MAINPID TimeoutStopSec=5 KillMode=mixed SyslogIdentifier=my-web # 指定运行此服务的用户,涉及到目录访问权限等问题 User=james[Install] WantedBy=multi-user.target

配置完之后还不能马上启动服务,需要 systemd 重新加载配置,然后才启动服务:
sudo systemctl daemon-reload sudo systemctl start my-web

顺便,再介绍一下,如果想在内容发布之后自动重启,需要加两个配置文件,一个 .path 监控变化,一个 .service 来重启 my-web
  • restart-my-web.path
[Path] # 监控主文件 MyWeb.dll 的变动,如果有变动会触发 restart-my-web.service 启动 PathModified=/app/my-web/MyWeb.dll[Install] WantedBy=multi-user.target

  • restart-my-web.service
[Unit] Description=My Web Restarter After=network.target[Service] Type=oneshot # 防抖,60 秒内只启动 1 次 ExecStartPre=/bin/sleep 60 # 重启 my-web.service ExecStart=/bin/systemctl restart my-web.service[Install] WantedBy=multi-user.target

3. 小小的总结一下 做服务并不难,上面唯一的一个需要写代码的方式,还是开箱即用的组件实现的。但话说回来,做服务不难,做服务的设计还是有不少事情需要考虑。比如
  • 如何监控服务的状态?—— 进程监控、心跳检查……
  • 如何分析服务中出现的错误?—— 系统日志
  • 如何提供 GUI 来对服务进行管理?—— Web 或其他 UI 跟服务进程进行交互(进程通信、管理 API 等)
  • ……
【把程序做成系统服务】既然做服务不难,那就不要太纠结如何“做”(提供)服务,还是多纠结纠结如何做好(设计)服务吧。

    推荐阅读