写程序,难免会遇到需要做成系统服务的需求。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 中的配置内容如下,看注释就能理解。

<service>
    <!-- 配置服务名称 nginx-service,显示名称 Nginx Service,以及服务描述 -->
    <id>nginx-service</id>
    <name>Nginx Service</name>
    <description>Nginx Service</description>

    <!-- 服务运行的工作目录,给绝对路径 -->
    <workingdirectory>C:\Local\Nginx</workingdirectory>

    <!-- 服务可执行文件,给绝对路径 -->
    <executable>C:\Local\Nginx\nginx.exe</executable>

    <!-- 停止服务的可执行文件 -->
    <stopexecutable>C:\Local\Nginx\nginx.exe</stopexecutable>
    <!-- 停止服务的参数 -->
    <stoparguments>-s stop</stoparguments>

    <priority>Normal</priority>
    <stoptimeout>15 sec</stoptimeout>
    <stopparentprocessfirst>false</stopparentprocessfirst>

    <!-- 配置服务类型是「自动」启动 -->
    <startmode>Automatic</startmode>
    <waithint>15 sec</waithint>
    <sleeptime>1 sec</sleeptime>

    <!-- 将服务的控制台输出(标准输出/错误输出)写入日志 -->
    <!-- 其中 %BASE% 是指 winsw.exe 所在目录 -->
    <!-- 参考:https://github.com/winsw/winsw/blob/master/doc/loggingAndErrorReporting.md -->
    <logpath>%BASE%\logs</logpath>
    <log mode="roll-by-time">
        <pattern>yyyyMMdd</pattern>
    </log>
</service>

这个配置创建了名为 nginx-service 的 Windows 服务,它在 Windows 的「服务 (services.msc)」显示名称为 Nginx Service。启动服务的时候直接运行 nginx.exe 来启动,这是一个会执行占用控制台的程序;而停止服务则是运行 nginx.exe -s stop,可执行程序和参数分别配置在 <stopexecutable><stoparguments> 中 —— 由此不难推断,如果启动服务需要参数,是配置在 <arguments> 中的。

详细的配置可以在 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,官方出品。它至少需要依赖两个包:

在引入组件之后,只需要少量代码就可以让当前 .NET 的 Console Application 成为一个支持 Windows 服务接口的服务程序。

// Program.cs

class Program {
    static async Task<int> Main(string[] args) {
        await CreateHostBuilder(args).Build().RunAsync();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) {
        return Host.CreateDefaultBuilder(args)
            .ConfigureServices((hostContext, services) => {
                services.AddHostedService<DaemonService>();
            })
            .UseWindowsService();
    }
}

注意到 AddHostedService<DaemonServce>,这里的 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

接下来是写 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 等)
  • ……

既然做服务不难,那就不要太纠结如何“做”(提供)服务,还是多纠结纠结如何做好(设计)服务吧。

03-05 23:29