Hangfire 是一個開源的.NET任務調度框架,目前1.6+版本已支持.NET Core。我的認爲它最大特色在於內置提供集成化的控制檯,方便後臺查看及監控:javascript
另外,Hangfire包含三大核心組件:客戶端、持久化存儲、服務端,官方的流程介紹圖以下:html
從圖中能夠看出,這三個核心組件是能夠分離出來單獨部署的,例如能夠部署多臺Hangfire服務,提升處理後臺任務的吞吐量。關於任務持久化存儲,支持Sqlserver,MongoDb,Mysql或是Redis等等。java
基於隊列的任務處理是Hangfire中最經常使用的,客戶端使用BackgroundJob
類的靜態方法Enqueue
來調用,傳入指定的方法(或是匿名函數),Job Queue等參數.node
var jobId = BackgroundJob.Enqueue( () => Console.WriteLine("Fire-and-forget!"));
在任務被持久化到數據庫以後,Hangfire服務端當即從數據庫獲取相關任務並裝載到相應的Job Queue下,在沒有異常的狀況下僅處理一次,若發生異常,提供重試機制,異常及重試信息都會被記錄到數據庫中,經過Hangfire控制面板能夠查看到這些信息。linux
延遲(計劃)任務跟隊列任務類似,客戶端調用時須要指定在必定時間間隔後調用:git
var jobId = BackgroundJob.Schedule( () => Console.WriteLine("Delayed!"), TimeSpan.FromDays(7));
定時(循環)任務表明能夠重複性執行屢次,支持CRON
表達式:github
RecurringJob.AddOrUpdate(
() => Console.WriteLine("Recurring!"), Cron.Daily);
延續性任務相似於.NET中的Task
,能夠在第一個任務執行完以後緊接着再次執行另外的任務:web
BackgroundJob.ContinueWith(
jobId,
() => Console.WriteLine("Continuation!"));
其實還有批量任務處理,批量任務延續性處理(Batch Continuations),但這個須要商業受權及收費。在我看來,官方提供的開源版本已經基本夠用。redis
在項目沒有引入Hangfire以前,一直使用的是Quartz.net。我的認爲Quartz.net在定時任務處理方面優點以下:sql
緣由在於Hangfire用的是開源的NCrontab組件,跟linux上的crontab指令類似。
更加複雜的觸發器,日曆以及任務調度處理
可配置的定時任務
可是爲何要換Hangfire? 很大的緣由在於項目須要一個後臺可監控的應用,不用每次都要從服務器拉取日誌查看,在沒有ELK的時候至關不方便。Hangfire控制面板不只提供監控,也能夠手動的觸發執行定時任務。若是在定時任務處理方面沒有很高的要求,好比必定要5s定時執行,Hangfire值得擁有。拋開這些,Hangfire優點太明顯了:
持久化保存任務、隊列、統計信息
重試機制
多語言支持
支持任務取消
支持按指定Job Queue
處理任務
服務器端工做線程可控,即job執行併發數控制
分佈式部署,支持高可用
良好的擴展性,如支持IOC、Hangfire Dashboard受權控制、Asp.net Core、持久化存儲等
說了這麼多的優勢,咱們能夠有個案例,例如秒殺場景:用戶下單->訂單生成->扣減庫存,Hangfire對於這種分佈式的應用處理也是適用的,最後會給出實現。
重點說一下上面提到的第8點,Hangfire擴展性
,你們能夠參考 這裏,有幾個擴展是很實用的.
Hangfire.Console提供相似於console-like的日誌體驗,與Hangfire dashboard集成:
用法以下:
public void SimpleJob(PerformContext context) { context.WriteLine($"{DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")} SimpleJob Running ..."); var progressBar = context.WriteProgressBar(); foreach (var i in Enumerable.Range(1, 50).ToList().WithProgress(progressBar)) { System.Threading.Thread.Sleep(1000); } }
不只支持日誌輸入到控制面板,也支持在線進度條展現.
Hangfire.Dashboard.Authorization這個擴展應該都能理解,給Hangfire Dashboard
提供受權機制,僅受權的用戶才能訪問。其中提供兩種受權機制:
能夠參考提供案例 ,我實現的是基本認證受權:
var options = new DashboardOptions { AppPath = HangfireSettings.Instance.AppWebSite, AuthorizationFilters = new[] { new BasicAuthAuthorizationFilter ( new BasicAuthAuthorizationFilterOptions { SslRedirect = false, RequireSsl = false, LoginCaseSensitive = true, Users = new[] { new BasicAuthAuthorizationUser { Login = HangfireSettings.Instance.LoginUser, // Password as plain text PasswordClear = HangfireSettings.Instance.LoginPwd } } } ) } }; app.UseHangfireDashboard("", options);
Hangfire對於每個任務(Job)假如都寫在一個類裏,而後使用BackgroundJob
/RecurringJob
對方法(實例或靜態)進行調用,這樣會致使模塊間太多耦合。實際項目中,依賴倒置原則能夠下降模塊之間的耦合性,Hangfire也提供了IOC擴展,其本質是重寫JobActivator
類。
Hangfire.Autofac是官方提供的開源擴展,用法參考以下:
GlobalConfiguration.Configuration.UseAutofacActivator(container);
關於RecurringJob
定時任務,我寫了一個擴展 RecurringJobExtensions,在使用上作了一下加強,具體有兩點:
RecurringJobAttribute
發現定時任務public class RecurringJobService { [RecurringJob("*/1 * * * *")] [DisplayName("InstanceTestJob")] [Queue("jobs")] public void InstanceTestJob(PerformContext context) { context.WriteLine($"{DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")} InstanceTestJob Running ..."); } [RecurringJob("*/5 * * * *")] [DisplayName("JobStaticTest")] [Queue("jobs")] public static void StaticTestJob(PerformContext context) { context.WriteLine($"{DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")} StaticTestJob Running ..."); } }
[AutomaticRetry(Attempts = 0)] [DisableConcurrentExecution(90)] public class LongRunningJob : IRecurringJob { public void Execute(PerformContext context) { context.WriteLine($"{DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")} LongRunningJob Running ..."); var runningTimes = context.GetJobData<int>("RunningTimes"); context.WriteLine($"get job data parameter-> RunningTimes: {runningTimes}"); var progressBar = context.WriteProgressBar(); foreach (var i in Enumerable.Range(1, runningTimes).ToList().WithProgress(progressBar)) { Thread.Sleep(1000); } } }
Json配置文件以下:
[{
"job-name": "Long Running Job", "job-type": "Hangfire.Samples.LongRunningJob, Hangfire.Samples", "cron-expression": "*/2 * * * *", "job-data": { "RunningTimes": 300 } }]
實現接口IRecurringJob
來定義具體的定時任務,這樣的寫法與Quartz.net類似,能夠很方便的實現Quartz.net到Hangfire的遷移。相似地,參考了quartz.net,
使用job-data-map
這樣的方式來定義整個任務執行期間的上下文有狀態的job.
var runningTimes = context.GetJobData<int>("RunningTimes");
詳細用法能夠直接參考項目文檔。
Hangfire server在處理每一個job時,會將job先裝載到事先定義好的job queue中,好比一次性加載1000個job,在默認的sqlsever實現中是直接將這些job queue中的
job id儲存到數據庫中,而後再取出執行。大量的job會形成任務的延遲性執行,因此更有效的方式是將任務直接加載到MSMQ中。
實際應用中,MSMQ隊列不存在時必定要手工建立,並且必須是事務性的隊列,權限也要設置,用法以下:
public static IGlobalConfiguration<SqlServerStorage> UseMsmq(this IGlobalConfiguration<SqlServerStorage> configuration, string pathPattern, params string[] queues) { if (string.IsNullOrEmpty(pathPattern)) throw new ArgumentNullException(nameof(pathPattern)); if (queues == null) throw new ArgumentNullException(nameof(queues)); foreach (var queueName in queues) { var path = string.Format(pathPattern, queueName); if (!MessageQueue.Exists(path)) using (var queue = MessageQueue.Create(path, transactional: true)) queue.SetPermissions("Everyone", MessageQueueAccessRights.FullControl); } return configuration.UseMsmqQueues(pathPattern, queues); }
Hangfire中定義的job存儲到sqlserver不是性能最好的選擇,使用Redis存儲,性能將會是巨大提高(下圖來源於Hangfire.Pro.Redis).
Hangfire.Pro
提供了基於servicestack.redis
的redis擴展組件,然而商業收費,不開源。
可是,有另外的基於StackExchange.Redis
的開源實現 Hangfire.Redis.StackExchange,
github上一直在維護,支持.NET Core,項目實測穩定可用. 該擴展至關簡單:
services.AddHangfire(x => { var connectionString = Configuration.GetConnectionString("hangfire.redis"); x.UseRedisStorage(connectionString); });
Hangfire server在啓動時會初始化一個最大Job處理併發數量的閾值,系統默認爲20,能夠根據服務器配置設置併發處理數。最大閾值的定義除了考慮服務器配置之外,
也須要考慮數據庫的最大鏈接數,定義太多的併發處理數量可能會在同一時間耗盡數據鏈接池。
app.UseHangfireServer(new BackgroundJobServerOptions { //wait all jobs performed when BackgroundJobServer shutdown. ShutdownTimeout = TimeSpan.FromMinutes(30), Queues = queues, WorkerCount = Math.Max(Environment.ProcessorCount, 20) });
DisplayNameAttribute
特性構造缺省的JobNamepublic interface IOrderService : IAppService { /// <summary> /// Creating order from product. /// </summary> /// <param name="productId"></param> [AutomaticRetry(Attempts = 3)] [DisplayName("Creating order from product, productId:{0}")] [Queue("apis")] void CreateOrder(int productId); }
目前netstandard暫不支持缺省的jobname,由於須要單獨引用組件System.ComponentModel.Primitives
,hangfire官方給出的答覆是儘可能保證少的Hangfire.Core
組件的依賴。
Hangfire job中參數(包括參數值)及方法名都序列化爲json持久化到數據庫中,因此參數應儘可能簡單,如傳入單據ID,這樣纔不會使Job Storage呈爆炸性增加。
定義統一的REST APIs能夠規範並集中管理整個項目的hangfire客戶端調用,同時避免處處引用hangfire組件。使用例如Swagger這樣的組件來給不一樣的應用方(Consumer)提供文檔幫助,應用方能夠是App,Webservice,Microservices等。
/// <summary> /// Creating order from product. /// </summary> /// <param name="productId"></param> /// <returns></returns> [Route("create")] [HttpPost] public IActionResult Create([FromBody]string productId) { if (string.IsNullOrEmpty(productId)) return BadRequest(); var jobId = BackgroundJob.Enqueue<IOrderService>(x => x.CreateOrder(productId)); BackgroundJob.ContinueWith<IInventoryService>(jobId, x => x.Reduce(productId)); return Ok(new { Status = 1, Message = $"Enqueued successfully, ProductId->{productId}" }); }
不推薦將hangfire server 宿主到如ASP.NET application 中,須要有一堆配置。我的喜愛問題,推薦將hangfire server 單獨部署到windows service, 利用Topshelf+Owin Host:
/// <summary> /// OWIN host /// </summary> public class Bootstrap : ServiceControl { private static readonly ILog _logger = LogProvider.For<Bootstrap>(); private IDisposable webApp; public string Address { get; set; } public bool Start(HostControl hostControl) { try { webApp = WebApp.Start<Startup>(Address); return true; } catch (Exception ex) { _logger.ErrorException("Topshelf starting occured errors.", ex); return false; } } public bool Stop(HostControl hostControl) { try { webApp?.Dispose(); return true; } catch (Exception ex) { _logger.ErrorException($"Topshelf stopping occured errors.", ex); return false; } } }
從Hangfire 1.3.0
開始,Hangfire引入了日誌組件LibLog,因此應用不須要作任何改動就能夠兼容以下日誌組件:
Serilog
NLog
Log4Net
EntLib Logging
Loupe
Elmah
例如,配置 serilog以下,LibLog組件會自動發現並使用serilog
Log.Logger = new LoggerConfiguration() .MinimumLevel.Verbose() .WriteTo.LiterateConsole() .WriteTo.RollingFile("logs\\log-{Date}.txt") .CreateLogger();
下圖是一個多實例Hangfire服務部署:
其中,關於Hangfire Server Node 節點能夠根據實際須要水平擴展.
上述提到過一個秒殺場景:用戶下單->訂單生成->扣減庫存,實現參考github項目Hangfire.Topshelf.
服務應用消費方(App/Webservice/Microservices等。)
統一的REST APIs管理
Hangfire 控制面板
Hangfire server node cli 工具,使用以下:
@echo off set dir="cluster" dotnet run -p %dir%\HF.Samples.ServerNode nodeA -q order -w 100 dotnet run -p %dir%\HF.Samples.ServerNode nodeB -q storage -w 100
上述腳本爲建立兩個Hangfire server nodeA, nodeB分別用來處理訂單、倉儲服務。
-q 指定hangfire server 須要處理的隊列,-w表示Hangfire server 併發處理job數量。
能夠爲每一個job queue建立一個hangfire實例來處理更多的job.