26

Hangfire 筆記2 - 執行定期排程

 3 years ago
source link: https://blog.darkthread.net/blog/hangfire-recurringjob-notes/
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.

Hangfire 筆記2 - 執行定期排程

2018-01-25 06:48 AM 7 13,687

想用 ASP.NET Hangfire 跑定期排程,有一個前題是「需確保網站永遠處於執行狀態」,先推薦幾篇相關文章:

摸索過程我發現更簡單的新做法,實測可行,整理設定步驟如下:

  1. 確認已安裝「應用程式初始化」,最簡單的檢查方法是確認模組清單包含 ApplicationInitializationModule:
    4565-6b17-o.gif
  2. IIS AppPool 進階設定
    啟動模式設 AlwaysRunning (註: 記得)
    4410-c7f6-o.gif
  3. 在 IIS 管理員站台或應用程式的進階設定啟用「預先載入已啟用」(Preload Enabled)
    4411-f84f-o.gif
    註: 如想在預先載入時呼叫特定網址可使用 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>
    再註:initializaionPage 指向的網頁應開放匿名存取或允許 AppPool 執行身分有權存取,否則會自動啟動失效。參考 感謝余小章補充。
  4. 依據 MSDN 文件 啟用 AlwaysRunning 時會無視閒置逾時設定,但在 Stackoverflow 上有仍被閒置停用的案例,可能與 IIS 版本有關,如果遇到可將閒置時間改成 0。
    4564-0f5d-o.gif
  5. 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 秒有關。

4412-c57b-o.gif

試著改用 SQL Server 或 Memory Storage 則沒發現類似問題,我懷疑這與 SQLite 執行速度不夠快有關,在一篇國外文章也提到類似的觀察。總之,如果系統對執行時間精準度要求很高,使用 SQLiteStorage 前應審慎評估。要解決這個問題,可在 UseSQLiteStorage() 時將 QueuePollInterval 改成 1 秒,誤差將縮小在兩秒以內,代價這會提高 Hangfire 查詢資料庫的頻率較耗損效能,大家可依專案需求自行拿捏。

排版顯示純文字
                .UseSQLiteStorage($"Data Source={SqliteDbPath};", 
                new SQLiteStorageOptions()
                {
                    QueuePollInterval = TimeSpan.FromSeconds(1)
                });

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK