Hangfire项目实践分享
項目中使用Hangfire已經(jīng)快一年了,期間經(jīng)歷過很多次的試錯及升級優(yōu)化,才達到現(xiàn)在的穩(wěn)定效果。趁最近不是太忙,自己在github上做了個案列,也是拿來跟大家分享下,案例是從項目里剝離出來的,有興趣的可以訪問?這里.
什么是Hangfire
Hangfire?是一個開源的.NET任務(wù)調(diào)度框架,目前1.6+版本已支持.NET Core。個人認為它最大特點在于內(nèi)置提供集成化的控制臺,方便后臺查看及監(jiān)控:
另外,Hangfire包含三大核心組件:客戶端、持久化存儲、服務(wù)端,官方的流程介紹圖如下:
從圖中可以看出,這三個核心組件是可以分離出來單獨部署的,例如可以部署多臺Hangfire服務(wù),提高處理后臺任務(wù)的吞吐量。關(guān)于任務(wù)持久化存儲,支持Sqlserver,MongoDb,Mysql或是Redis等等。
Hangfire基礎(chǔ)
基于隊列的任務(wù)處理(Fire-and-forget jobs)
基于隊列的任務(wù)處理是Hangfire中最常用的,客戶端使用BackgroundJob類的靜態(tài)方法Enqueue來調(diào)用,傳入指定的方法(或是匿名函數(shù)),Job Queue等參數(shù).
var jobId = BackgroundJob.Enqueue(() => Console.WriteLine("Fire-and-forget!"));在任務(wù)被持久化到數(shù)據(jù)庫之后,Hangfire服務(wù)端立即從數(shù)據(jù)庫獲取相關(guān)任務(wù)并裝載到相應(yīng)的Job Queue下,在沒有異常的情況下僅處理一次,若發(fā)生異常,提供重試機制,異常及重試信息都會被記錄到數(shù)據(jù)庫中,通過Hangfire控制面板可以查看到這些信息。
延遲任務(wù)執(zhí)行(Delayed jobs)
延遲(計劃)任務(wù)跟隊列任務(wù)相似,客戶端調(diào)用時需要指定在一定時間間隔后調(diào)用:
var jobId = BackgroundJob.Schedule(() => Console.WriteLine("Delayed!"),TimeSpan.FromDays(7));定時任務(wù)執(zhí)行(Recurring jobs)
定時(循環(huán))任務(wù)代表可以重復(fù)性執(zhí)行多次,支持CRON表達式:
RecurringJob.AddOrUpdate(() => Console.WriteLine("Recurring!"),Cron.Daily);延續(xù)性任務(wù)執(zhí)行(Continuations)
延續(xù)性任務(wù)類似于.NET中的Task,可以在第一個任務(wù)執(zhí)行完之后緊接著再次執(zhí)行另外的任務(wù):
BackgroundJob.ContinueWith(jobId,() => Console.WriteLine("Continuation!"));其實還有批量任務(wù)處理,批量任務(wù)延續(xù)性處理(Batch Continuations),但這個需要商業(yè)授權(quán)及收費。在我看來,官方提供的開源版本已經(jīng)基本夠用。
與quartz.net對比
在項目沒有引入Hangfire之前,一直使用的是Quartz.net。個人認為Quartz.net在定時任務(wù)處理方面優(yōu)勢如下:
支持秒級單位的定時任務(wù)處理,但是Hangfire只能支持分鐘及以上的定時任務(wù)處理
原因在于Hangfire用的是開源的NCrontab組件,跟linux上的crontab指令相似。
更加復(fù)雜的觸發(fā)器,日歷以及任務(wù)調(diào)度處理
可配置的定時任務(wù)
但是為什么要換Hangfire? 很大的原因在于項目需要一個后臺可監(jiān)控的應(yīng)用,不用每次都要從服務(wù)器拉取日志查看,在沒有ELK的時候相當不方便。Hangfire控制面板不僅提供監(jiān)控,也可以手動的觸發(fā)執(zhí)行定時任務(wù)。如果在定時任務(wù)處理方面沒有很高的要求,比如一定要5s定時執(zhí)行,Hangfire值得擁有。拋開這些,Hangfire優(yōu)勢太明顯了:
持久化保存任務(wù)、隊列、統(tǒng)計信息
重試機制
多語言支持
支持任務(wù)取消
支持按指定Job Queue處理任務(wù)
服務(wù)器端工作線程可控,即job執(zhí)行并發(fā)數(shù)控制
分布式部署,支持高可用
良好的擴展性,如支持IOC、Hangfire Dashboard授權(quán)控制、Asp.net Core、持久化存儲等
說了這么多的優(yōu)點,我們可以有個案例,例如秒殺場景:用戶下單->訂單生成->扣減庫存,Hangfire對于這種分布式的應(yīng)用處理也是適用的,最后會給出實現(xiàn)。
Hangfire擴展
重點說一下上面提到的第8點,Hangfire擴展性,大家可以參考?這里,有幾個擴展是很實用的.
Hangfire Dashborad日志查看
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 Dashborad授權(quán)
Hangfire.Dashboard.Authorization這個擴展應(yīng)該都能理解,給Hangfire Dashboard
提供授權(quán)機制,僅授權(quán)的用戶才能訪問。其中提供兩種授權(quán)機制:
OWIN-based authentication
Basic authentication
可以參考提供案例?,我實現(xiàn)的是基本認證授權(quán):
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 textPasswordClear = HangfireSettings.Instance.LoginPwd}}} )} }; app.UseHangfireDashboard("", options);IOC容器之Autofac
Hangfire對于每一個任務(wù)(Job)假如都寫在一個類里,然后使用BackgroundJob/RecurringJob對方法(實例或靜態(tài))進行調(diào)用,這樣會導(dǎo)致模塊間太多耦合。實際項目中,依賴倒置原則可以降低模塊之間的耦合性,Hangfire也提供了IOC擴展,其本質(zhì)是重寫JobActivator類。
Hangfire.Autofac是官方提供的開源擴展,用法參考如下:
GlobalConfiguration.Configuration.UseAutofacActivator(container);RecurringJob擴展
關(guān)于RecurringJob定時任務(wù),我寫了一個擴展?RecurringJobExtensions,在使用上做了一下增強,具體有兩點:
使用特性RecurringJobAttribute發(fā)現(xiàn)定時任務(wù)
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 ...");} }使用json配置文件注冊定時任務(wù)
[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}}]實現(xiàn)接口IRecurringJob來定義具體的定時任務(wù),這樣的寫法與Quartz.net相似,可以很方便的實現(xiàn)Quartz.net到Hangfire的遷移。類似地,參考了quartz.net,
使用job-data-map這樣的方式來定義整個任務(wù)執(zhí)行期間的上下文有狀態(tài)的job.
詳細用法可以直接參考項目文檔。
與MSMQ集成
Hangfire server在處理每個job時,會將job先裝載到事先定義好的job queue中,比如一次性加載1000個job,在默認的sqlsever實現(xiàn)中是直接將這些job queue中的
job id儲存到數(shù)據(jù)庫中,然后再取出執(zhí)行。大量的job會造成任務(wù)的延遲性執(zhí)行,所以更有效的方式是將任務(wù)直接加載到MSMQ中。
實際應(yīng)用中,MSMQ隊列不存在時一定要手工創(chuàng)建,而且必須是事務(wù)性的隊列,權(quán)限也要設(shè)置,用法如下:
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); }
持久化存儲之Redis
Hangfire中定義的job存儲到sqlserver不是性能最好的選擇,使用Redis存儲,性能將會是巨大提升(下圖來源于Hangfire.Pro.Redis).
Hangfire.Pro提供了基于servicestack.redis的redis擴展組件,然而商業(yè)收費,不開源。
但是,有另外的基于StackExchange.Redis的開源實現(xiàn)?Hangfire.Redis.StackExchange,
github上一直在維護,支持.NET Core,項目實測穩(wěn)定可用. 該擴展相當簡單:
Hangfire最佳實踐
配置最大job并發(fā)處理數(shù)
Hangfire server在啟動時會初始化一個最大Job處理并發(fā)數(shù)量的閾值,系統(tǒng)默認為20,可以根據(jù)服務(wù)器配置設(shè)置并發(fā)處理數(shù)。最大閾值的定義除了考慮服務(wù)器配置以外,
也需要考慮數(shù)據(jù)庫的最大連接數(shù),定義太多的并發(fā)處理數(shù)量可能會在同一時間耗盡數(shù)據(jù)連接池。
使用?DisplayNameAttribute特性構(gòu)造缺省的JobName
public 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官方給出的答復(fù)是盡量保證少的Hangfire.Core組件的依賴。
Hangfire在調(diào)用Background/RecurringJob創(chuàng)建job時應(yīng)盡量使傳入的參數(shù)簡單.
Hangfire job中參數(shù)(包括參數(shù)值)及方法名都序列化為json持久化到數(shù)據(jù)庫中,所以參數(shù)應(yīng)盡量簡單,如傳入單據(jù)ID,這樣才不會使Job Storage呈爆炸性增長。
為Hangfire客戶端調(diào)用定義統(tǒng)一的REST APIs
定義統(tǒng)一的REST APIs可以規(guī)范并集中管理整個項目的hangfire客戶端調(diào)用,同時避免到處引用hangfire組件。使用例如Swagger這樣的組件來給不同的應(yīng)用方(Co
/// <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}" }); }
利用Topshelf + Owin Host將hangfire server 宿主到Windows Service.
不推薦將hangfire server 宿主到如ASP.NET appl署到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,所以應(yīng)用不需要做任何改動就可以兼容如下日志組件:
Serilog
NLog
Log4Net
EntLib Logging
Loupe
Elmah
例如,配置?serilog如下,LibLog組件會自動發(fā)現(xiàn)并使用serilog
Log.Logger = new LoggerConfiguration().MinimumLevel.Verbose().WriteTo.LiterateConsole().WriteTo.RollingFile("logs\\log-{Date}.txt").CreateLogger();Hangfire多實例部署(高可用)
下圖是一個多實例Hangfire服務(wù)部署:
其中,關(guān)于Hangfire Server Node 節(jié)點可以根據(jù)實際需要水平擴展.
上述提到過一個秒殺場景:用戶下單->訂單生成->扣減庫存,實現(xiàn)參考github項目Hangfire.Topshelf.
HF.Samples.Consumer
服務(wù)應(yīng)用消費方(App/Webservice/Microservices等。)
HF.Samples.APIs
統(tǒng)一的REST APIs管理
HF.Samples.Console
Hangfire 控制面板
HF.Samples.ServerNode
Hangfire server node cli 工具,使用如下:
@echo offset dir="cluster"dotnet run -p %dir%\HF.Samples.ServerNode nodeA -q order -w 100dotnet run -p %dir%\HF.Samples.ServerNode nodeB -q storage -w 100上述腳本為創(chuàng)建兩個Hangfire server nodeA, nodeB分別用來處理訂單、倉儲服務(wù)。
-q 指定hangfire server 需要處理的隊列,-w表示Hangfire server 并發(fā)處理job數(shù)量。
可以為每個job queue創(chuàng)建一個hangfire實例來處理更多的job.
原文地址: http://www.cnblogs.com/ecin/p/6201262.html
.NET社區(qū)新聞,深度好文,微信中搜索dotNET跨平臺或掃描二維碼關(guān)注
總結(jié)
以上是生活随笔為你收集整理的Hangfire项目实践分享的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 博客园官方 NuGet镜像上线试运行
- 下一篇: 中间件和微服务,Docker以及原生云架