基於.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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM