5

基于.Net Core 5.0 Worker Service 的 Quart 服务

 3 years ago
source link: https://www.cnblogs.com/Aaxuan/p/14699231.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

基于.Net Core 5.0 Worker Service 的 Quart 服务

看过我之前博客的人应该都知道,我负责了相当久的部门数据同步相关的工作。其中的艰辛不赘述了。
随着需求的越来越复杂,最近windows的计划任务已经越发的不能满足我了,而且计划任务毕竟太弱智,总是会失败之类,强制结束之类的。

最近增加了一些复杂的参数,每天的任务对同步程序调用需要多次调用不同参数,我也终于打算不再忍受弱智的计划任务。最初测试了一下基于 IIS 的 Quart ,发现还是存在会被回收无法定时的情况,
在此之前我并未做过 Quart 相关的开发。我查了查相关资料,可以更改 IIS 设置修改定时回收的模式,可以通过访问站点来唤醒等,觉得不是很合适。而且综合业务的考虑,实在是没必要在内网客户机搭一个 Web 站点。
这样一来,干脆搞一个 WindowsService 得了,而且定时的场景还是比较常见的,写一份肯定不亏,以后还是用的上。而且也没尝试过基于 Core 写 WindowsService,正好借此机会学习一下。

Worker Service

使用 VS2019 ,安装了 .NET CORE 3.0 以上的 SDK ,安装SDK的时候最好也安装运行时,免得最后忘记。
项目模板自带的代码如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace WorkerServiceTest
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }
        public static IHostBuilder CreateHostBuilder(string[] args) =>
      Host.CreateDefaultBuilder(args)
      .ConfigureServices((hostContext, services) =>
      {
          services.AddHostedService<Worker>();
      });
        }
    }
}

Program.cs中,依旧是创建一个 IHost 并启动。为了方便进行依赖注入,可以创建一个 IServiceCollection 的扩展方法来进行相关服务的注册。
而 Worker 类已经提供了一个默认例子,其中有一个 ExecuteAsync 方法,可以直接执行后台任务。这个时候,直接F5就可以正常运行了实现了一个显示当前时间的程序。
Work 类继承 BackgroundService,并重写其 ExecuteAsync 方法。显而易见,ExecuteAsync 方法就是执行后台任务的入口。

Quartz.Net

Quartz.Net 是一个功能齐全的开源作业调度系统,可以在最小规模的应用程序到大型企业系统使用。
Quartz.Net有三个主要概念:

  • job         这是你想要运行的后台任务。
  • trigger     trigger 控制 job 何时运行,通常按某种调度规则触发。
  • scheduler     它负责协调 job 和 trigger,根据 trigger 的要求执行 job。

ASP.NET Core 很好地支持通过 hosted services(托管服务)运行“后台任务”。当你的 ASP.NET Core 应用程序启动,托管服务也启动,并在应用程序的生命周期中在后台运行。

现在有了一个官方包 Quartz.Extensions.Hosting 实现使用 Quartz.Net 运行后台任务,所以把 Quartz.Net 添加到 ASP.NET Core 或 Worker Service 要简单得多。

Quartz.Net 3.2.0 通过 Quartz.Extensions.Hosting 引入了对该模式的直接支持。
Quartz.Extensions.Hosting 即可以用在ASP.NET Core应用程序,也可以用在基于“通用主机”的Worker Service。
虽然可以创建一个“定时”后台服务(例如,每10分钟运行一个任务),但Quartz.NET提供了一个更加健壮的解决方案。
通过使用Cron trigger,你可以确保任务只在一天的特定时间(例如凌晨2:30)运行,或者只在特定的日子运行,或者这些时间的任意组合运行。
Quartz.Net还允许你以集群的方式运行应用程序的多个实例,以便在任何时候只有一个实例可以运行给定的任务。
Quartz.Net托管服务负责Quartz的调度。它将在应用程序的后台运行,检查正在执行的触发器,并在必要时运行相关的作业。
你需要配置调度程序,但不需要担心启动或停止它,IHostedService 会为你管理。

引用 Quartz.Net

你可以通过使用 dotnet add package Quartz.Extensions.Hosting 命令安装 Quartz.Net 包。
如果你查看项目的.csproj,它应该是这样的:

<Project Sdk="Microsoft.NET.Sdk.Worker">
  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
    <UserSecretsId>dotnet-QuartzWorkerService-9D4BFFBE-BE06-4490-AE8B-8AF1466778FD</UserSecretsId>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0" />
		<PackageReference Include="Quartz.Extensions.Hosting" Version="3.3.2" />
  </ItemGroup>
</Project>

添加 Quartz.Net 托管服务

注册Quartz.Net需要做两件事:

  1. 注册Quartz.Net需要的DI容器服务。
  2. 注册托管服务。

在 ASP.NET Core 中,通常会在 Startup.ConfigureServices() 方法中完成这两项操作。
但 Worker Services 不使用 Startup 类,所以我们在 Program.cs 中的 IHostBuilder 的 ConfigureServices 方法中注册它们:

    public class Program
    {
        public static void Main(string[] args)
        {
            //...
        }

        public static IHostBuilder CreateHostBuilder(string[] args)
        {
            IHostBuilder hostBuilder = Host.CreateDefaultBuilder(args);
            hostBuilder.ConfigureServices((hostContext, services) =>
            {
                services.AddQuartz(q =>
                {
                    q.UseMicrosoftDependencyInjectionScopedJobFactory();
                    //q.InitJobAndTriggerFromJobsettings(hostContext.Configuration);
                });

                services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true);
                //services.AddHostedService<Worker>();
            });

            //Windows
            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            { hostBuilder.UseWindowsService(); }

            //Linux
            if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
            { hostBuilder.UseSystemd(); }

            return hostBuilder;
        }
    }
}

UseMicrosoftDependencyInjectionScopedJobFactory 告诉 Quartz.NET 注册一个 IJobFactory,该 IJobFactory 通过从DI容器中创建 job。
方法中的 Scoped 部分意味着你的作业可以使用 scoped 服务,而不仅仅是 single 或 transient 服务。

WaitForJobsToComplete 用来设置确保当请求关闭时,Quartz.NET在退出之前优雅地等待作业结束。
如果你现在运行应用程序,将看到Quartz服务启动,并将大量日志转储到控制台,因为篇幅原因,此处不再贴出。

另外 可能有读者注意到了其中的 hostBuilder.UseWindowsService();/hostBuilder.UseSystemd();
这是关于跨平台的一段设置,稍后会有简单讲解。

此时,你已经让 Quartz 作为托管服务在你的应用程序中运行,但是没有任何job让它运行。在下一节中,我们将创建并注册一个简单的job。

创建 Job

因为我的场景是定时运行一个EXE,最常见的通用定时任务场景应该是调用一个接口。
这里举例一个打印日志的Job,我的相关源代码会在结尾处放出。

using Quartz;
using Serilog;
using System;
using System.Threading.Tasks;

namespace AX.QuartzServer.Core.Jobs
{
    [DisallowConcurrentExecution]
    public class TestJob : AXQuartzJob
    {
        public string Name { get { return "测试Job"; } }
        public string Note { get { return "会打印日志"; } }

        public Task Execute(IJobExecutionContext context)
        {
            Log.Logger.Information($"{Newtonsoft.Json.JsonConvert.SerializeObject(context.JobDetail.JobDataMap)}");
            Log.Logger.Information($"Hello world! {DateTime.Now.ToLongTimeString()}");
            return Task.CompletedTask;
        }
    }
}

这里我使用了全局的 Serilog 来记录日志。所以和一般的日志不太一样。
我还用 [DisallowConcurrentExecution] 属性装饰了 job 。此属性防止Quartz.NET试图同时运行相同的作业。
它将定时的在日志或控制台中打印 Hello world! 和当前时间。
现在我们已经有了作业,我们需要将它与 trigger 一起注册到 DI 容器中。

启动时自动配置Job

Quartz.NET 为运行 job 提供了一些简单的 schedule,但最常见的方法之一是使用 Quartz.NET Cron 表达式,这里不再赘述。
因为我的场景是Windows服务,暂不考虑一些高级的,可以实时停止,注册Job,运行Job之类的封装。
所以决定是在启动时直接通过读取配置文件注册 Job。
下面是注册的关键代码:

    public static class AXQuartzConfigExtensions
    {
        public static void InitJobAndTriggerFromJobsettings(this IServiceCollectionQuartzConfigurator quartz, IConfiguration configuration)
        {
            var allJobs = configuration.GetSection("Jobs").Get<List<BaseJobConfig>>();

            Log.Logger.Information($"开始注册 Job");
            Log.Logger.Information($"共获取到 {allJobs.Count} 个 Job");

            foreach (var item in allJobs)
            {
                Log.Logger.Information($"{JsonConvert.SerializeObject(item)}");

                var jobName = $"{item.JobType}_{item.Name}";
                var jobKey = new JobKey(jobName);
                Log.Logger.Information($"{nameof(jobKey)}_{jobKey}");

                var jobData = new JobDataMap();
                jobData.PutAll(ToIDictionary(item));

                if (item.JobType.ToLower().Contains("testjob"))
                { quartz.AddJob<Jobs.TestJob>(opts => { opts.WithIdentity(jobKey); opts.SetJobData(jobData); }); }
                if (item.JobType.ToLower().Contains("windowscmdjob"))
                { quartz.AddJob<Jobs.WindowsCMDJob>(opts => { opts.WithIdentity(jobKey); opts.SetJobData(jobData); }); }

                quartz.AddTrigger(opts => opts
                    .ForJob(jobKey)
                    .WithIdentity($"{jobName}_Trigger")
                    .WithCronSchedule(item.Cron));
            }

            Log.Logger.Information($"结束注册 Job");
        }
        
        //...
    }

从配置文件中读取了配置之后,为每个 job 创建唯一的 JobKey 。这用于将job与其trggier连接在一起。
用 AddJob 注册我们的 TestJob。它将 TestJob 添加到了 DI 容器中,这样就可以创建它。它还在内部向 Quartz 注册了job。
然后用 AddTrigger 添加触发器,
使用 JobKey 将 trigger 与一个 job 关联起来,并为 trigger 提供唯一的名称(在本例中不是必需的,但如果你在集群模式下运行quartz,这很重要)。
最后,为trigger 设置了 Cron 表达式, Cron 表达式来自配置文件,测试时我用的是每五秒一次。
其中有一些快速实现时未优化的弱智代码之类的,各位读者不用在意。

配置文件配置节:

{
  "Logging": {
      //...
    }
  },

  //任务配置 DEMO
  "JobDemo": {
    "Name": "唯一任务名称",
    "JobType": "任务类型 windowscmdjob/testjob",
    "Cron": "运行时间表达式"
  },

  "Jobs": [
    {
      "Name": "LogHelloWorldTest",
      "JobType": "testjob",
      "Cron": "0 0 */1 * * ?" //这是每小时一次
      //"Cron": "0/5 * * * * ?" 这是每五秒一次
    }
  ]
}

如果你现在运行你的应用程序,你会看到和以前一样的启动消息,然后每5秒你会看到HelloWorldJob写入控制台:
这就是搭建一个定时服务的全部关键内容了。

在 Host.CreateDefaultBuilder(args) 增加相关环境的调用。
可以使用判断平台的一个函数: IsOSPlatform ,可以判断是否在Windows平台运行,并进行分别调用。

虽然程序可以正常执行,但是还不能正常部署为服务,需要依据平台添加对应的nuget包:
windows服务,需要添加:
Install-Package Microsoft.Extensions.Hosting.WindowsServices
Linux服务,需要添加:
Install-Package Microsoft.Extensions.Hosting.Systemd

.UseWindowsService();
.UseSystemd();

Windows下部署

管理员下运行cmd/powershell,执行
sc.exe create WorkerServiceTest binPath=【你编译后的exe路径,不需要带双引号】
提示 CreateService 成功 即安装成功了,可以输入下面的命令运行服务。
sc.exe start WorkerServiceTest
sc.exe负责管理服务,具体配置启动方式和删除,可以查看命令的帮助。另外,友情提醒,如果是在powershell中,不要省略这个.exe,sc有别的用处...

https://github.com/aaxuan/AX.QuartzServer(请选择性的忽略其他仓库的垃圾代码 :)

本文将同步发布到个人的语雀博客,欢迎使用语雀的小伙伴相互关注。
https://www.yuque.com/cuxuan

https://devblogs.microsoft.com/aspnet/net-core-workers-as-windows-services/
https://devblogs.microsoft.com/dotnet/net-core-and-systemd/
https://docs.microsoft.com/en-us/dotnet/core/extensions/generic-host
https://dejanstojanovic.net/aspnet/2018/june/setting-up-net-core-servicedaemon-on-linux-os/
https://dotnetcoretutorials.com/2019/12/07/creating-windows-services-in-net-core-part-3-the-net-core-worker-way/
http://www.cnblogs.com/podolski/p/13890572.html
https://www.cnblogs.com/xhy0826/p/Net_Core_Windows_Service_Quartz.html
https://segmentfault.com/a/1190000038753018


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK