Hangfire 筆記2 - 執行定期排程
7 |
想用 ASP.NET Hangfire 跑定期排程,有一個前題是「需確保網站永遠處於執行狀態」,先推薦幾篇相關文章:
- [ASP.NET]使用 Hangfire 來處理非同步的工作 - 亂馬客 - 點部落
- [IIS]為什麼應用程式集區設定了 AlwaysRunning 沒有效果呢- - 亂馬客 - 點部落
- Making ASP.NET application always running — Hangfire 1.6 documentation
摸索過程我發現更簡單的新做法,實測可行,整理設定步驟如下:
- 確認已安裝「應用程式初始化」,最簡單的檢查方法是確認模組清單包含 ApplicationInitializationModule:
- IIS AppPool 進階設定
啟動模式設 AlwaysRunning (註: 記得)
- 在 IIS 管理員站台或應用程式的進階設定啟用「預先載入已啟用」(Preload Enabled)
註: 如想在預先載入時呼叫特定網址可使用 web.config 設定。參考: Use IIS Application Initialization for keeping ASP.NET Apps alive - Rick Strahl's Web Log
<system.webServer>
<applicationInitialization remapManagedRequestsTo="Startup.htm"
skipManagedModules="true">
<add initializationPage="ping.ashx" />
</applicationInitialization>
</system.webServer>
- 依據 MSDN 文件 啟用 AlwaysRunning 時會無視閒置逾時設定,但在 Stackoverflow 上有仍被閒置停用的案例,可能與 IIS 版本有關,如果遇到可將閒置時間改成 0。
- Hangfire 官方出了一個 Hangfire.AspNet 套件,可簡化 IIS 設定及自己實作 IRegisteredObject 跟 IProcessHostPreloadClient 介面的程序,依據 Github 上的說明,這個新做法未來將取代現有官網所建議的安裝步驟(This package aims to replace the documentation article Making ASP.NET application always running.) 相關文件會晚一點才釋出... (眼看兩年過去了文件還沒好,但身為開發者,我懂,呵)
沒有文件無妨,直接參考 Github 上的範例專案,我琢磨調整完的 Startup 類別如下:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Web;
using System.Web.Hosting;
using Hangfire;
using Hangfire.Logging;
using Microsoft.Owin;
using Owin;
using Hangfire.SQLite;
[assembly: OwinStartup(typeof(MyApp.Startup))]
namespace MyApp
{
//REF: https://github.com/HangfireIO/Hangfire.AspNet
public class Startup : IRegisteredObject
{
public Startup()
{
HostingEnvironment.RegisterObject(this);
}
private static readonly string SqliteDbPath =
HostingEnvironment.MapPath("~/App_Data/Hangfire.sqlite");
private static BackgroundJobServer backJobServer = null;
public static IEnumerable<IDisposable> GetHangfireConfiguration()
{
GlobalConfiguration.Configuration
.UseSQLiteStorage($"Data Source={SqliteDbPath};");
backJobServer = new BackgroundJobServer(
new BackgroundJobServerOptions
{
ServerName =
$"JobServer-{Process.GetCurrentProcess().Id}"
});
yield return backJobServer;
}
public void Configuration(IAppBuilder app)
{
//改用UseHangfireAspNet設定Hangfire服務
app.UseHangfireAspNet(GetHangfireConfiguration);
app.UseHangfireDashboard();
ScheduledTasks.Setup();
}
//ApplicationPool結束時會呼叫
public void Stop(bool immediate)
{
//Thread.Sleep(TimeSpan.FromSeconds(30));
//Github範例等待30秒,會影響AppPool停止及回收速度
//這裡改為直接呼叫backJobServer.Dispose()
if (backJobServer != null)
{
backJobServer.Dispose();
}
HostingEnvironment.UnregisterObject(this);
}
}
}
設定排程部分我寫成另一顆物件,範例如下。這段程式每次啟動網站都會執行,故 AddOrUpdate() 時要指定排程名稱,排程已存在就只更新不新增,才不會新增一堆重複排程。實務上如求彈性,也可採用資料庫或設定檔管理排程。
using Hangfire;
namespace MyApp
{
public class ScheduledTasks
{
private static NLog.ILogger logger =
NLog.LogManager.GetLogger("SchTasks");
public static void Setup()
{
//REF: https://en.wikipedia.org/wiki/Cron#CRON_expression
RecurringJob.AddOrUpdate("PerMinute", () => DumpLog(),
Cron.Minutely);
}
private static int Counter = 0;
public static void DumpLog()
{
logger.Debug(Counter++.ToString());
}
}
}
實測 Hangfire.SQLite 發現一個問題,原本一分鐘跑一次的排程莫名每一分鐘執行 20 次,經調查應為 Bug,Hangfire 預設會開 20 條 Worker Thread,時間一到每個 Worker 都跑了一次。將 Worker 數調為 5 就變成跑 5 次。這問題在 Github 上也被網友被提報為 Issue,作者建議先將 Worker 數設成 1 避開。
所幸,又到了見識 Open Source 奇蹟的時刻,既然是 Open Source,遇到 Bug 自己查自己修也是很合理滴。花了點時間查出原因試著修正,也送了 PR,希望這個問題在未來的版本會被修復。
另外,實測 Hangfire.SQLite 跑定時排程還有另一個問題,當設成每分鐘整點執行,啟動時間並非 100% 精準。例如以下每分鐘一次的排程,每分鐘執行時點卻在 01-15 秒區間移動,為什麼是 15 秒?推測與預設 SchedulePollingInterval = 15 秒有關。 查了 Source Code,跟QueuePollInterval 預設 15 秒有關。
試著改用 SQL Server 或 Memory Storage 則沒發現類似問題,我懷疑這與 SQLite 執行速度不夠快有關,在一篇國外文章也提到類似的觀察。總之,如果系統對執行時間精準度要求很高,使用 SQLiteStorage 前應審慎評估。要解決這個問題,可在 UseSQLiteStorage() 時將 QueuePollInterval 改成 1 秒,誤差將縮小在兩秒以內,代價這會提高 Hangfire 查詢資料庫的頻率較耗損效能,大家可依專案需求自行拿捏。
.UseSQLiteStorage($"Data Source={SqliteDbPath};",
new SQLiteStorageOptions()
{
QueuePollInterval = TimeSpan.FromSeconds(1)
});
Tutorial of IIS AlwaysRunning settings for Hangfire recurring tasks.
Comments
# by 余小章
補充一下: Application Pool Identity 要有權限瀏覽 Default Page 或是排除Default Page的授權
# by Jeffrey
to 余小章,好一個眉角,謝謝提醒,已補充於本文。
# by Ho.Chun
有點不太懂這句話 "簡化 IIS 設定及自己實作 IRegisteredObject 跟 IProcessHostPreloadClient 介面的程序" 換個問法,為什麼要實作這些介面呢 ?? 原本的範例,感覺就能用了也 public class Startup { public void Configuration(IAppBuilder app) { GlobalConfiguration.Configuration.UseMemoryStorage(); app.UseHangfireServer(); app.UseHangfireDashboard(); } }
# by Jeffrey
to Ho.Chun, 實作 IRegisteredObject 跟 IProcessHostPreloadClient 是為了確保排程引擎永遠處於運轉狀況,否則 IIS 隔一段時間自己休眠會錯過定時排程的執行時間。
# by Ho.Chun
to Jeffrey, "IIS 隔一段時間自己休眠會錯過定時排程的執行時間" 不過休眠這件事,不是已經在 IIS 那邊設定過了嗎 ? (如下) 1. 應用程式集區 => 啟動模式 => Always Running 2. 應用程式集區 => 回收 => 固定時間間隔(分鐘) => 0 3. 應用程式集區 => 處理序模型 => 閒置逾時(分) => 0 4. 應用程式 => 預先載入已啟用 => true 為何還要去實作 IRegisteredObject / IProcessHostPreloadClient 呢 抱歉,這邊有點想不通
# by Jeffrey
to Ho.Chun, 這方面的文獻不多,我的理解是 IIS AppPool 啟動,Hangfire 這類背景排程並不會被執行,實作 IProcessHostPreloadClient 可確保 AppPool 啟動後在沒有任何使用者連上網站前執行指定的動作。參考:https://tacticalnuclearstrike.com/2012/07/how-to-use-iprocesshostpreloadclient
# by Ho.Chun
to Jeffrey, 原來如此! IIS 這部分一直讓我覺得很神奇 XD 感謝您