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等参数.
var jobId = BackgroundJob.Enqueue(
() => Console.WriteLine("Fire-and-forget!"));
在任务被持久化到数据库之后,Hangfire服务端立即从数据库获取相关任务并装载到相应的Job Queue下,在没有异常的情况下仅处理一次,若发生异常,提供重试机制,异常及重试信息都会被记录到数据库中,通过Hangfire控制面板可以查看到这些信息。
延迟任务执行(Delayed jobs)
延迟(计划)任务跟队列任务相似,客户端调用时需要指定在一定时间间隔后调用:
var jobId = BackgroundJob.Schedule(
() => Console.WriteLine("Delayed!"),
TimeSpan.FromDays(7));
定时任务执行(Recurring jobs)
定时(循环)任务代表可以重复性执行多次,支持CRON
表达式:
RecurringJob.AddOrUpdate(
() => Console.WriteLine("Recurring!"),
Cron.Daily);
延续性任务执行(Continuations)
延续性任务类似于.NET中的Task
,可以在第一个任务执行完之后紧接着再次执行另外的任务:
BackgroundJob.ContinueWith(
jobId,
() => 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集成:
用法如下:
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授权
Hangfire.Dashboard.Authorization这个扩展应该都能理解,给Hangfire Dashboard
提供授权机制,仅授权的用户才能访问。其中提供两种授权机制:
- OWIN-based authentication
- Basic authentication
可以参考提供案例 ,我实现的是基本认证授权:
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);
IOC容器之Autofac
Hangfire对于每一个任务(Job)假如都写在一个类里,然后使用BackgroundJob
/RecurringJob
对方法(实例或静态)进行调用,这样会导致模块间太多耦合。实际项目中,依赖倒置原则可以降低模块之间的耦合性,Hangfire也提供了IOC扩展,其本质是重写JobActivator
类。
Hangfire.Autofac是官方提供的开源扩展,用法参考如下:
GlobalConfiguration.Configuration.UseAutofacActivator(container);
RecurringJob扩展
关于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 ...");
}
}
使用json配置文件注册定时任务
[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");
详细用法可以直接参考项目文档。
与MSMQ集成
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);
}
持久化存储之Redis
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最佳实践
配置最大job并发处理数
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
特性构造缺省的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官方给出的答复是尽量保证少的Hangfire.Core
组件的依赖。
Hangfire在调用Background/RecurringJob创建job时应尽量使传入的参数简单.
Hangfire job中参数(包括参数值)及方法名都序列化为json持久化到数据库中,所以参数应尽量简单,如传入单据ID,这样才不会使Job Storage呈爆炸性增长。
为Hangfire客户端调用定义统一的REST APIs
定义统一的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}" });
}
利用Topshelf + Owin Host将hangfire server 宿主到Windows Service.
不推荐将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服务部署:
其中,关于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 工具,使用如下:
@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.
Hangfire项目实践分享的更多相关文章
- Hangfire项目实践
Hangfire项目实践分享 Hangfire项目实践分享 目录 Hangfire项目实践分享 目录 什么是Hangfire Hangfire基础 基于队列的任务处理(Fire-and-forget ...
- Vue + webpack 项目实践
Vue.js 是一款极简的 mvvm 框架,如果让我用一个词来形容它,就是 “轻·巧” .如果用一句话来描述它,它能够集众多优秀逐流的前端框架之大成,但同时保持简单易用.废话不多说,来看几个例子: & ...
- 《Spring Boot 入门及前后端分离项目实践》系列介绍
课程计划 课程地址点这里 本课程是一个 Spring Boot 技术栈的实战类课程,课程共分为 3 个部分,前面两个部分为基础环境准备和相关概念介绍,第三个部分是 Spring Boot 项目实践开发 ...
- DEVOPS落地实践分享
DEVOPS落地实践分享 转载本文需注明出处:微信公众号EAWorld,违者必究. 引言: DevOps的理念已经说了很多年,其带来的价值逐渐被接受,很多企业也逐渐引入了DevOps.目前普元DevO ...
- BizTalk 2010/2013 EDI B2B项目实践(1)
BizTalk 2010/2013 EDI B2B项目实践(1) BizTalk开发标准EDI B2B是件非常容易的事情,但对于初学者可能有很多专业术语不太理解,不知道如何下手,我之前开始学的时候虽然 ...
- nodejs, vue, webpack 项目实践
vue 及 webpack,均不需要与nodejs一期使用,他们都可以单独使用到任何语言的框架中. http://jiongks.name/blog/just-vue/ https://cn.vuej ...
- 一套海量在线用户的移动端IM架构设计实践分享(含详细图文)(转)
1.写在前面 1.1.引言 如果在没有太多经验可借鉴的情况下,要设计一套完整可用的移动端IM架构,难度是相当大的.原因在于,IM系统(尤其是移动端IM系统)是多种技术和领域知识的横向应用综合体:网络编 ...
- WOT干货大放送:大数据架构发展趋势及探索实践分享
WOT大数据处理技术分会场,PingCAP CTO黄东旭.易观智库CTO郭炜.Mob开发者服务平台技术副总监林荣波.宜信技术研发中心高级架构师王东及商助科技(99Click)顾问总监郑泉五位讲师, ...
- lucene 简介和实践 分享
之前项目做了搜索的改造,使用lucene,公司内做了相关的技术分享,故先整理下ppt内容,后面会再把项目中的具体做法进行介绍 lucene 简介和实践 分享 搜索改造项目
随机推荐
- CSS3 border-radius边框圆角
在CSS3中提供了对边框进行圆角设定的支持,可对边框1~4个角进行圆角样式设置. 目录 1. 介绍 2. value值的格式和类型 3. border-radius 1~4个参数说明 4. 在线示例 ...
- html5 canvas常用api总结(三)--图像变换API
canvas的图像变换api,可以帮助我们更加方便的绘画出一些酷炫的效果,也可以用来制作动画.接下来将总结一下canvas的变换方法,文末有一个例子来更加深刻的了解和利用这几个api. 1.画布旋转a ...
- CENTOS 6.5 平台离线编译安装 PHP5.6.6
一.下载php源码包 http://cn2.php.net/get/php-5.6.6.tar.gz/from/this/mirror 二.编译 编译之前可能会缺少一些必要的依赖包,加载一个本地yum ...
- 如何优化coding
如何优化coding 前言 最近一直在做修改bug工作,修改bug花费时间最多的不是如何解决问题而是怎样快速读懂代码.如果代码写的好的,不用debug就可以一眼看出来哪里出了问题.实际上,我都要deb ...
- 调用微信退款接口或发红包接口时出现System.Security.Cryptography.CryptographicException: 出现了内部错误 解决办法
我总结了一下出现证书无法加载的原因有以下三个 1.证书密码不正确,微信证书密码就是商户号 解决办法:请检查证书密码是不是和商户号一致 2.IIS设置错误,未加载用户配置文件 解决办法:找到网站使用的应 ...
- Android中实现APP文本内容的分享发送与接收方法简述
谨记(指定选择器Intent.createChooser()) 开始今天的内容前,先闲聊一下: (1)突然有一天头脑风暴,对很多问题有了新的看法和见解,迫不及待的想要分享给大家,文档已经写好了,我需要 ...
- hbase协处理器编码实例
Observer协处理器通常在一个特定的事件(诸如Get或Put)之前或之后发生,相当于RDBMS中的触发器.Endpoint协处理器则类似于RDBMS中的存储过程,因为它可以让你在RegionSer ...
- jsp富文本图片和数据上传
好记性不如烂笔头,记录一下. 2016的最后一天,以一篇博客结尾迎接新的一年. 此处用的富文本编辑器是wangEditor,一款开源的轻量级的富文本编辑器,这里着重说一下里面的图片上传功能. 服务器端 ...
- C#移动跨平台开发(2)Xamarin移动跨平台解决方案是如何工作的?
概述 上一篇 C#移动跨平台开发(1)环境准备发布之后不久,无独有偶,微软宣布了开放.NET框架源代码并且会为Windows.Mac和Linux开发一个核心运行时(Core CLR),这也是开源的!I ...
- 《徐徐道来话Java》:PriorityQueue和最小堆
在讲解PriorityQueue之前,需要先熟悉一个有序数据结构:最小堆. 最小堆是一种经过排序的完全二叉树,其中任一非终端节点数值均不大于其左孩子和右孩子节点的值. 可以得出结论,如果一棵二叉树满足 ...