Hangfire项目实践分享

Hangfire项目实践分享

目录

项目中使用Hangfire已经快一年了,期间经历过很多次的试错及升级优化,才达到现在的稳定效果。趁最近不是太忙,自己在github上做了个案列,也是拿来跟大家分享下,案例是从项目里剥离出来的,有兴趣的可以访问 这里.

什么是Hangfire

Hangfire 是一个开源的.NET任务调度框架,目前1.6+版本已支持.NET Core。个人认为它最大特点在于内置提供集成化的控制台,方便后台查看及监控:

另外,Hangfire包含三大核心组件:客户端、持久化存储、服务端,官方的流程介绍图如下:

从图中可以看出,这三个核心组件是可以分离出来单独部署的,例如可以部署多台Hangfire服务,提高处理后台任务的吞吐量。关于任务持久化存储,支持Sqlserver,MongoDb,Mysql或是Redis等等。

Hangfire基础

基于队列的任务处理(Fire-and-forget jobs)

基于队列的任务处理是Hangfire中最常用的,客户端使用BackgroundJob类的静态方法Enqueue来调用,传入指定的方法(或是匿名函数),Job Queue等参数.

  1. var jobId = BackgroundJob.Enqueue(
  2. () => Console.WriteLine("Fire-and-forget!"));

在任务被持久化到数据库之后,Hangfire服务端立即从数据库获取相关任务并装载到相应的Job Queue下,在没有异常的情况下仅处理一次,若发生异常,提供重试机制,异常及重试信息都会被记录到数据库中,通过Hangfire控制面板可以查看到这些信息。

延迟任务执行(Delayed jobs)

延迟(计划)任务跟队列任务相似,客户端调用时需要指定在一定时间间隔后调用:

  1. var jobId = BackgroundJob.Schedule(
  2. () => Console.WriteLine("Delayed!"),
  3. TimeSpan.FromDays(7));

定时任务执行(Recurring jobs)

定时(循环)任务代表可以重复性执行多次,支持CRON表达式:

  1. RecurringJob.AddOrUpdate(
  2. () => Console.WriteLine("Recurring!"),
  3. Cron.Daily);

延续性任务执行(Continuations)

延续性任务类似于.NET中的Task,可以在第一个任务执行完之后紧接着再次执行另外的任务:

  1. BackgroundJob.ContinueWith(
  2. jobId,
  3. () => Console.WriteLine("Continuation!"));

其实还有批量任务处理,批量任务延续性处理(Batch Continuations),但这个需要商业授权及收费。在我看来,官方提供的开源版本已经基本够用。

与quartz.net对比

在项目没有引入Hangfire之前,一直使用的是Quartz.net。个人认为Quartz.net在定时任务处理方面优势如下:

  • 支持秒级单位的定时任务处理,但是Hangfire只能支持分钟及以上的定时任务处理

原因在于Hangfire用的是开源的NCrontab组件,跟linux上的crontab指令相似。

  • 更加复杂的触发器,日历以及任务调度处理

  • 可配置的定时任务

但是为什么要换Hangfire? 很大的原因在于项目需要一个后台可监控的应用,不用每次都要从服务器拉取日志查看,在没有ELK的时候相当不方便。Hangfire控制面板不仅提供监控,也可以手动的触发执行定时任务。如果在定时任务处理方面没有很高的要求,比如一定要5s定时执行,Hangfire值得拥有。抛开这些,Hangfire优势太明显了:

  • 持久化保存任务、队列、统计信息

  • 重试机制

  • 多语言支持

  • 支持任务取消

  • 支持按指定Job Queue处理任务

  • 服务器端工作线程可控,即job执行并发数控制

  • 分布式部署,支持高可用

  • 良好的扩展性,如支持IOC、Hangfire Dashboard授权控制、Asp.net Core、持久化存储等

说了这么多的优点,我们可以有个案例,例如秒杀场景:用户下单->订单生成->扣减库存,Hangfire对于这种分布式的应用处理也是适用的,最后会给出实现。

Hangfire扩展

重点说一下上面提到的第8点,Hangfire扩展性,大家可以参考 这里,有几个扩展是很实用的.

Hangfire Dashborad日志查看

Hangfire.Console提供类似于console-like的日志体验,与Hangfire dashboard集成:

用法如下:

  1. public void SimpleJob(PerformContext context)
  2. {
  3. context.WriteLine($"{DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")} SimpleJob Running ...");
  4. var progressBar = context.WriteProgressBar();
  5. foreach (var i in Enumerable.Range(1, 50).ToList().WithProgress(progressBar))
  6. {
  7. System.Threading.Thread.Sleep(1000);
  8. }
  9. }

不仅支持日志输入到控制面板,也支持在线进度条展示.

Hangfire Dashborad授权

Hangfire.Dashboard.Authorization这个扩展应该都能理解,给Hangfire Dashboard
提供授权机制,仅授权的用户才能访问。其中提供两种授权机制:

  • OWIN-based authentication
  • Basic authentication

可以参考提供案例 ,我实现的是基本认证授权:

  1. var options = new DashboardOptions
  2. {
  3. AppPath = HangfireSettings.Instance.AppWebSite,
  4. AuthorizationFilters = new[]
  5. {
  6. new BasicAuthAuthorizationFilter ( new BasicAuthAuthorizationFilterOptions
  7. {
  8. SslRedirect = false,
  9. RequireSsl = false,
  10. LoginCaseSensitive = true,
  11. Users = new[]
  12. {
  13. new BasicAuthAuthorizationUser
  14. {
  15. Login = HangfireSettings.Instance.LoginUser,
  16. // Password as plain text
  17. PasswordClear = HangfireSettings.Instance.LoginPwd
  18. }
  19. }
  20. } )
  21. }
  22. };
  23. app.UseHangfireDashboard("", options);

IOC容器之Autofac

Hangfire对于每一个任务(Job)假如都写在一个类里,然后使用BackgroundJob/RecurringJob对方法(实例或静态)进行调用,这样会导致模块间太多耦合。实际项目中,依赖倒置原则可以降低模块之间的耦合性,Hangfire也提供了IOC扩展,其本质是重写JobActivator类。

Hangfire.Autofac是官方提供的开源扩展,用法参考如下:

  1. GlobalConfiguration.Configuration.UseAutofacActivator(container);

RecurringJob扩展

关于RecurringJob定时任务,我写了一个扩展 RecurringJobExtensions,在使用上做了一下增强,具体有两点:

使用特性RecurringJobAttribute发现定时任务

  1. public class RecurringJobService
  2. {
  3. [RecurringJob("*/1 * * * *")]
  4. [DisplayName("InstanceTestJob")]
  5. [Queue("jobs")]
  6. public void InstanceTestJob(PerformContext context)
  7. {
  8. context.WriteLine($"{DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")} InstanceTestJob Running ...");
  9. }
  10. [RecurringJob("*/5 * * * *")]
  11. [DisplayName("JobStaticTest")]
  12. [Queue("jobs")]
  13. public static void StaticTestJob(PerformContext context)
  14. {
  15. context.WriteLine($"{DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")} StaticTestJob Running ...");
  16. }
  17. }

使用json配置文件注册定时任务

  1. [AutomaticRetry(Attempts = 0)]
  2. [DisableConcurrentExecution(90)]
  3. public class LongRunningJob : IRecurringJob
  4. {
  5. public void Execute(PerformContext context)
  6. {
  7. context.WriteLine($"{DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")} LongRunningJob Running ...");
  8. var runningTimes = context.GetJobData<int>("RunningTimes");
  9. context.WriteLine($"get job data parameter-> RunningTimes: {runningTimes}");
  10. var progressBar = context.WriteProgressBar();
  11. foreach (var i in Enumerable.Range(1, runningTimes).ToList().WithProgress(progressBar))
  12. {
  13. Thread.Sleep(1000);
  14. }
  15. }
  16. }

Json配置文件如下:

  1. [{
  2. "job-name": "Long Running Job",
  3. "job-type": "Hangfire.Samples.LongRunningJob, Hangfire.Samples",
  4. "cron-expression": "*/2 * * * *",
  5. "job-data": {
  6. "RunningTimes": 300
  7. }
  8. }]

实现接口IRecurringJob来定义具体的定时任务,这样的写法与Quartz.net相似,可以很方便的实现Quartz.net到Hangfire的迁移。类似地,参考了quartz.net,
使用job-data-map这样的方式来定义整个任务执行期间的上下文有状态的job.

  1. var runningTimes = context.GetJobData<int>("RunningTimes");

详细用法可以直接参考项目文档

与MSMQ集成

Hangfire server在处理每个job时,会将job先装载到事先定义好的job queue中,比如一次性加载1000个job,在默认的sqlsever实现中是直接将这些job queue中的
job id储存到数据库中,然后再取出执行。大量的job会造成任务的延迟性执行,所以更有效的方式是将任务直接加载到MSMQ中。

实际应用中,MSMQ队列不存在时一定要手工创建,而且必须是事务性的队列,权限也要设置,用法如下:

  1. public static IGlobalConfiguration<SqlServerStorage> UseMsmq(this IGlobalConfiguration<SqlServerStorage> configuration, string pathPattern, params string[] queues)
  2. {
  3. if (string.IsNullOrEmpty(pathPattern)) throw new ArgumentNullException(nameof(pathPattern));
  4. if (queues == null) throw new ArgumentNullException(nameof(queues));
  5. foreach (var queueName in queues)
  6. {
  7. var path = string.Format(pathPattern, queueName);
  8. if (!MessageQueue.Exists(path))
  9. using (var queue = MessageQueue.Create(path, transactional: true))
  10. queue.SetPermissions("Everyone", MessageQueueAccessRights.FullControl);
  11. }
  12. return configuration.UseMsmqQueues(pathPattern, queues);
  13. }

持久化存储之Redis

Hangfire中定义的job存储到sqlserver不是性能最好的选择,使用Redis存储,性能将会是巨大提升(下图来源于Hangfire.Pro.Redis).

Hangfire.Pro提供了基于servicestack.redis的redis扩展组件,然而商业收费,不开源。

但是,有另外的基于StackExchange.Redis的开源实现 Hangfire.Redis.StackExchange
github上一直在维护,支持.NET Core,项目实测稳定可用. 该扩展相当简单:

  1. services.AddHangfire(x =>
  2. {
  3. var connectionString = Configuration.GetConnectionString("hangfire.redis");
  4. x.UseRedisStorage(connectionString);
  5. });

Hangfire最佳实践

配置最大job并发处理数

Hangfire server在启动时会初始化一个最大Job处理并发数量的阈值,系统默认为20,可以根据服务器配置设置并发处理数。最大阈值的定义除了考虑服务器配置以外,
也需要考虑数据库的最大连接数,定义太多的并发处理数量可能会在同一时间耗尽数据连接池。

  1. app.UseHangfireServer(new BackgroundJobServerOptions
  2. {
  3. //wait all jobs performed when BackgroundJobServer shutdown.
  4. ShutdownTimeout = TimeSpan.FromMinutes(30),
  5. Queues = queues,
  6. WorkerCount = Math.Max(Environment.ProcessorCount, 20)
  7. });

使用 DisplayNameAttribute特性构造缺省的JobName

  1. public interface IOrderService : IAppService
  2. {
  3. /// <summary>
  4. /// Creating order from product.
  5. /// </summary>
  6. /// <param name="productId"></param>
  7. [AutomaticRetry(Attempts = 3)]
  8. [DisplayName("Creating order from product, productId:{0}")]
  9. [Queue("apis")]
  10. void CreateOrder(int productId);
  11. }

目前netstandard暂不支持缺省的jobname,因为需要单独引用组件System.ComponentModel.Primitives,hangfire官方给出的答复是尽量保证少的Hangfire.Core组件的依赖。

Hangfire在调用Background/RecurringJob创建job时应尽量使传入的参数简单.

Hangfire job中参数(包括参数值)及方法名都序列化为json持久化到数据库中,所以参数应尽量简单,如传入单据ID,这样才不会使Job Storage呈爆炸性增长。

为Hangfire客户端调用定义统一的REST APIs

定义统一的REST APIs可以规范并集中管理整个项目的hangfire客户端调用,同时避免到处引用hangfire组件。使用例如Swagger这样的组件来给不同的应用方(Consumer)提供文档帮助,应用方可以是App,Webservice,Microservices等。

  1. /// <summary>
  2. /// Creating order from product.
  3. /// </summary>
  4. /// <param name="productId"></param>
  5. /// <returns></returns>
  6. [Route("create")]
  7. [HttpPost]
  8. public IActionResult Create([FromBody]string productId)
  9. {
  10. if (string.IsNullOrEmpty(productId))
  11. return BadRequest();
  12. var jobId = BackgroundJob.Enqueue<IOrderService>(x => x.CreateOrder(productId));
  13. BackgroundJob.ContinueWith<IInventoryService>(jobId, x => x.Reduce(productId));
  14. return Ok(new { Status = 1, Message = $"Enqueued successfully, ProductId->{productId}" });
  15. }

利用Topshelf + Owin Host将hangfire server 宿主到Windows Service.

不推荐将hangfire server 宿主到如ASP.NET application 中,需要有一堆配置。个人喜好问题,推荐将hangfire server 单独部署到windows service, 利用Topshelf+Owin Host:

  1. /// <summary>
  2. /// OWIN host
  3. /// </summary>
  4. public class Bootstrap : ServiceControl
  5. {
  6. private static readonly ILog _logger = LogProvider.For<Bootstrap>();
  7. private IDisposable webApp;
  8. public string Address { get; set; }
  9. public bool Start(HostControl hostControl)
  10. {
  11. try
  12. {
  13. webApp = WebApp.Start<Startup>(Address);
  14. return true;
  15. }
  16. catch (Exception ex)
  17. {
  18. _logger.ErrorException("Topshelf starting occured errors.", ex);
  19. return false;
  20. }
  21. }
  22. public bool Stop(HostControl hostControl)
  23. {
  24. try
  25. {
  26. webApp?.Dispose();
  27. return true;
  28. }
  29. catch (Exception ex)
  30. {
  31. _logger.ErrorException($"Topshelf stopping occured errors.", ex);
  32. return false;
  33. }
  34. }
  35. }

日志配置

Hangfire 1.3.0开始,Hangfire引入了日志组件LibLog,所以应用不需要做任何改动就可以兼容如下日志组件:

  • Serilog

  • NLog

  • Log4Net

  • EntLib Logging

  • Loupe

  • Elmah

例如,配置 serilog如下,LibLog组件会自动发现并使用serilog

  1. Log.Logger = new LoggerConfiguration()
  2. .MinimumLevel.Verbose()
  3. .WriteTo.LiterateConsole()
  4. .WriteTo.RollingFile("logs\\log-{Date}.txt")
  5. .CreateLogger();

Hangfire多实例部署(高可用)

下图是一个多实例Hangfire服务部署:

其中,关于Hangfire Server Node 节点可以根据实际需要水平扩展.

上述提到过一个秒杀场景:用户下单->订单生成->扣减库存,实现参考github项目Hangfire.Topshelf.

HF.Samples.Consumer

服务应用消费方(App/Webservice/Microservices等。)

HF.Samples.APIs

统一的REST APIs管理

HF.Samples.Console

Hangfire 控制面板

HF.Samples.ServerNode

Hangfire server node cli 工具,使用如下:

  1. @echo off
  2. set dir="cluster"
  3. dotnet run -p %dir%\HF.Samples.ServerNode nodeA -q order -w 100
  4. 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.

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

Hangfire项目实践的更多相关文章

  1. Hangfire项目实践分享

    Hangfire项目实践分享 目录 Hangfire项目实践分享 目录 什么是Hangfire Hangfire基础 基于队列的任务处理(Fire-and-forget jobs) 延迟任务执行(De ...

  2. Hangfire项目

    什么是Hangfire Hangfire 是一个开源的.NET任务调度框架,目前1.6+版本已支持.NET Core.个人认为它最大特点在于内置提供集成化的控制台,方便后台查看及监控: 另外,Hang ...

  3. Windows on Device 项目实践 3 - 火焰报警器制作

    在前两篇<Windows on Device 项目实践 1 - PWM调光灯制作>和<Windows on Device 项目实践 2 - 感光灯制作>中,我们学习了如何利用I ...

  4. Windows on Device 项目实践 2 - 感光灯制作

    在上一篇<Windows on Device 项目实践 1 - PWM调光灯制作>中,我们学习了如何利用Intel Galileo开发板和Windows on Device来设计并完成一个 ...

  5. Windows on Device 项目实践 1 - PWM调光灯制作

    在前一篇文章<Wintel物联网平台-Windows IoT新手入门指南>中,我们讲解了Windows on Device硬件准备和软件开发环境的搭建,以及Hello Blinky项目的演 ...

  6. MVC项目实践,在三层架构下实现SportsStore,从类图看三层架构

    在"MVC项目实践,在三层架构下实现SportsStore-02,DbSession层.BLL层"一文的评论中,博友浪花一朵朵建议用类图来理解本项目的三层架构.于是就有了本篇: I ...

  7. MVC项目实践,在三层架构下实现SportsStore-02,DbSession层、BLL层

    SportsStore是<精通ASP.NET MVC3框架(第三版)>中演示的MVC项目,在该项目中涵盖了MVC的众多方面,包括:使用DI容器.URL优化.导航.分页.购物车.订单.产品管 ...

  8. MVC项目实践,在三层架构下实现SportsStore-01,EF Code First建模、DAL层等

    SportsStore是<精通ASP.NET MVC3框架(第三版)>中演示的MVC项目,在该项目中涵盖了MVC的众多方面,包括:使用DI容器.URL优化.导航.分页.购物车.订单.产品管 ...

  9. MVC项目实践,在三层架构下实现SportsStore-03,Ninject控制器工厂等

    SportsStore是<精通ASP.NET MVC3框架(第三版)>中演示的MVC项目,在该项目中涵盖了MVC的众多方面,包括:使用DI容器.URL优化.导航.分页.购物车.订单.产品管 ...

随机推荐

  1. WPF 使用Caliburn.Micro 多线程打开窗口

    我们都知道在WPF里面用多线程打开一个窗口很简单.如下 public void ClickMe(object sender) { Thread newWindowThread = new Thread ...

  2. 分页查询的SQL语句

    select * from(select row_number() over (ORDER BY Id DESC) cyqrownum,t.* from [Table_TY_Member] t ) v ...

  3. Java数组的12个常用方法

    以下是12个关于Java数组最常用的方法,它们是stackoverflow得票最高的问题. 声明一个数组 String[] aArray = new String[5]; String[] bArra ...

  4. android Activity生命周期(设备旋转、数据恢复等)与启动模式

    1.Activity生命周期     接下来将介绍 Android Activity(四大组件之一) 的生命周期, 包含运行.暂停和停止三种状态,onCreate.onStart.onResume.o ...

  5. [MySQL Reference Manual] 6 安全性

    6. 安全性 在Mysql安装配置时要考虑安全性的影响,以下几点: Ÿ   常规因素影响安全性 Ÿ   程序自身安全性 Ÿ   数据库内部的安全性,即,访问控制 Ÿ   网络安全性和系统安全性 Ÿ   ...

  6. T-SQL基础--TOP

    理解TOP子句 众所周知,TOP子句可以通过控制返回行的数量来影响查询. 我们知道TOP子句能很容易的满足返回指定行数的子集,接下来有一些例子来展示什么情况下使用TOP子句来返回一个结果集: 你打算返 ...

  7. DGbroker三种保护模式的切换

    1.三种保护模式 – Maximum protection 在Maximum protection下, 可以保证从库和主库数据完全一样,做到zero data loss.事务同时在主从两边提交完成,才 ...

  8. 从本地向 Github 上传项目步骤攻略(快速上手版)

    最近想把之前自己做的一些好玩的项目共享到Github,网上找了一圈上传教程,都感觉写的太深奥.复杂,云里雾里,特把自己的方法纪录如下: PS:这种方式一般适用于:开始做项目时,没有直接在github上 ...

  9. Mycat实现读写分离,主备热切换

    实验环境:ubutu server 14 Master IP:172.16.34.212 Slave IP:172.16.34.34.156 Mycat server IP:172.16.34.219 ...

  10. 编写NPAPI plugin的命名问题

    最近写了个NPAPI的插件,在chrome上用得好好的,结果在火狐上死活不认我的插件,找了N多资料最后在官方的说明里才找到说火狐浏览器的插件的文件名必须是以np开头的,立刻吐血三升,难怪被chrome ...