基於.Net Core 5.0 Worker Service 的 Quart 服務

Aaxuan發表於2021-04-25

前言

看過我之前部落格的人應該都知道,我負責了相當久的部門資料同步相關的工作。其中的艱辛不贅述了。
隨著需求的越來越複雜,最近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

相關文章