前言
看過我之前部落格的人應該都知道,我負責了相當久的部門資料同步相關的工作。其中的艱辛不贅述了。
隨著需求的越來越複雜,最近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需要做兩件事:
- 註冊Quartz.Net需要的DI容器服務。
- 註冊託管服務。
在 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
然後用 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