源宝导读:微软跨平台技术框架—.NET Core已经日趋成熟,已经具备了支撑大型系统稳定运行的条件。本文将介绍明源云ERP平台从.NET Framework向.NET Core迁移过程中的实践经验。

一、背景

随着ERP的产品线越来越多,业务关联也日益复杂,应用间依赖关系也变得错综复杂,单体架构的弱点日趋明显。19年初,由于平台底层支持了分应用部署模式,将ERP从应用子系统层面进行了切割分离,迈出了从单体架构向微服务架构转型的坚实一步。不久的将来,ERP会进一步将各业务拆分成众多的微服务,而微服务势必需要进行容器化部署和运行管理,这就要求ERP技术底层必须支持跨平台,所以将现有ERP系统从.NET Framework迁移到 .NET Core平台势在必行。

前面我介绍了ERP的迁移的过程,整个Erp除了主站点之外,还有若干周边服务,我们本篇将讲述调度服务的迁移,调度服务因为功能比较简单,我们将已有功能做了重新的开发。

二、Windows服务

由于IIS的定期回收机制,所以调度服务这类需要一直在后台运行的应用我们采用Windows服务的方式来运行。并且由于启用.Net Core的目的也是为了支持容器化,所以也支持控制台的方式运行。这里我们采用在Main函数中加入参数的方式进行启动,即可解决上述问题,下面是示例代码:

class Program
{public static void Main(string[] args){var isService = !(Debugger.IsAttached || args.Contains("--console"));var builder = CreateWebHostBuilder(args.Where(arg => arg != "--console").ToArray());var host = builder.Build();if (isService){//设置当前目录var processModule = Process.GetCurrentProcess().MainModule;if (processModule != null){var pathToExe = processModule.FileName;var pathToContentRoot = Path.GetDirectoryName(pathToExe);Directory.SetCurrentDirectory(pathToContentRoot);Console.WriteLine(pathToContentRoot);}var webHostService = new SchedulerWebHostService(host);ServiceBase.Run(webHostService);//以服务方式运行}else{host.Run();//以控制台方式运行}}private static IWebHostBuilder CreateWebHostBuilder(string[] args){return WebHost.CreateDefaultBuilder(args)//接入Serilog.UseSerilog((hostingContext, loggerConfiguration) => loggerConfiguration.ReadFrom.Configuration(hostingContext.Configuration)).UseStartup<Startup.Startup>();}
}internal class SchedulerWebHostService : WebHostService
{private ILogger _logger;public SchedulerWebHostService(IWebHost host) : base(host){_logger = host.Services.GetRequiredService<ILogger<SchedulerWebHostService>>();}protected override void OnStarting(string[] args){_logger.LogInformation("OnStarting method called.");base.OnStarting(args);}protected override void OnStarted(){_logger.LogInformation("OnStarted method called.");base.OnStarted();}protected override void OnStopping(){_logger.LogInformation("OnStopping method called.");base.OnStopping();}
}

Docker和Debug模式采用Console方式运行,只需要在启动的时候增加—console参数即可,Windows服务的话只需要使用系统的sc命令创建启动服务即可。

三、架构优化

原来的调度服务因为历史发展的原因,结构比较混乱,在Core的版本中重新做了梳理,采用了简单的分层结构,并且使用依赖注入,将接口和实现做了分离,便于以后进行扩展,下面是一个简单的架构图:

说明:

  • Host :启动工程,由于调度服务提供的功能比较简单(增,删,改,查,禁用,设置结果),所以这一层比较薄;

  • Manager :核心业务处理的工程,其中TaskFactory借鉴了DDD中领域工厂的概念,创建任务时候通过这个来解析数据创建任务对象,还需要负责加载Store中任务并放到ExecutorProvider中执行;

  • Store:即任务的配置文件存储,目前沿用原来的采用xml文件本地存储的方式。由于使用了接口定义所以很简单即可切换到数据库等其他存储引擎;

  • Common:一些通用帮助和功能的定义,本篇后续将重点介绍StrategyFactory部分;

  • Contract:定义了任务和执行引擎对外暴露的接口。

其中老版本调度任务没有Store的概念,直接使用xml文件存储。这种在服务器环境单机情况是没有问题的,但是当在docker环境中,由于docker的环境不同,如果在集群环境中,根据负载容灾的策略,可能会存在调度服务挂掉重新启动一个情况。这样无论你文件是存在docker中,或者映射到物理机中都会存在丢失情况,所以重新定义了接口是数据可以集中存储在数据库中,以免丢失。

这里将任务和调度引擎的对外接口定义到Contract,为了减少无论是调度任务还是执行引擎和调度服务宿主程序的耦合。针对调度引擎目前我们采用Quartz的方式,但是考虑到以后要支持集群模式,重新实现接口使用Hangfire实现即可实现集群的调度。而平台自定义的调度任务可能实现逻辑比较负责,单独定义一个接口作为执行入口也很有必要。

四、调度引擎

调度服务的核心逻辑就是任务的定时执行逻辑,我们使用Quartz来实现定时的任务调度,通过策略工厂来组织不同的任务来执行。

4.1、任务执行器

所有的任务都是通过TaskConfig这一个类来创建,TaskConfig是存储在Store的数据结构需要转换成不同类型的Task然后使用执行器进行执行,下面是执行器的类图:

说明:

  • IExecutor定义了两个方法 Init用来初始化,Run用来执行;

  • BaseExecutor类似模板方法定义了执行的逻辑;

  • ApiExecutor 用来执行Http请求;

  • AsyncExecutor用来执行异步任务的请求,也是通过Http方法执行,区别在于执行的是固定url,并且需要回调调度任务告诉执行结果;

  • SqlExecutor用来执行sql任务;

  • InProecessExecutor用来实现自定义的执行逻辑,例如数据分发,日志清理等等。

在整个体系中最重要就是BaseExecutor的逻辑,因为它定义了整个执行的逻辑,而其他任务只是不同的实现方式而已,下面我们稍微分析一下其实现接口的init和run方法:

public virtual void Init(TaskConfig taskConfig)
{_taskConfig = taskConfig;Logger = _builder.GetLogger(taskConfig.TaskName, Path.GetDirectoryName(_taskConfig.ConfigFilePath));Task = new TTask{TaskGuid = taskConfig.TaskGuid,TaskName = taskConfig.TaskName,CreateBy = taskConfig.CreateBy,CreateTime = taskConfig.CreateTime,ConfigFilePath = taskConfig.ConfigFilePath,Description = taskConfig.Description,Triggers = taskConfig.Triggers,Status = taskConfig.Status,};InnerInit(taskConfig);
}public void Run()
{DateTime startTime = Clock.Now;try{Begin();InnerRun();Finish(startTime);}catch (SchedulingException ex) /* 记录回调调度服务的错误,写入日志 */{var errorMsg = "执行任务发生异常,详情:" + ex.Message;Error(errorMsg, startTime, ex);}catch (Exception ex){var errorMsg = "执行任务发生异常,详情:" + ex.Message;Error(errorMsg, startTime, ex);}
}
  • 基于Init的方法主要目的是为了初始化Task类的通用属性,子类只需要实现InnerInit实现自己的数据进行赋值就好了;

  • Run方法只要实现了日志记录和执行时间的统计,而具体的执行放到InnerRun里面去实现;

  • 总体来说Init方法为了代码复用存在,Run为了逻辑复用存在。

4.2、策略工厂

上述执行器的层次结构其实很像策略者模式,一般我们可以基于简单工厂就可以进行创建并使用,但是如果需要扩展的话难免会对工厂的代码做修改,这里我们定义了一个策略工厂来实现无需修改代码的扩展,下面是类图 :

说明:

  • IStragegyFactory

    定义工厂的接口,TStrategy即工厂创建出来的策略;

  • StragegyFactory

    接口实现,用来使用TStrategyInitilizer获取策略类型并缓存,以及创建等逻辑;

  • TStrategyInitilizer

    策略初始化器,用来提供提供相关策略的类型;

  • IStrategy策略的接口契约定义,主要是用来做泛型类型的限制。

在调度服务中我们IExecutor就是具体的策略,然后通过在对应的IExecutor子类上标记上StrategyAttribute,在程序集启动的时候扫描所有的类型继承自IExecutor,在StrategyFactory中获取StrategyAttribute的Description,缓存成策略-类型字典,然后在使用的时候传入策略,获取到类型,创建出对应的策略实例进行执行即可。

由于使用了反射机制,所以我们只要启动时候扫描程序集类型就可以加载新增加的策略,而无需修改代码,真正做到了对扩展开放,对更改关闭的开放封闭原则。

我们这里集成了.Net Core,所以StrategyFactory注入成单例生命周期,然后使用Ioc进行创建。如果是其他情况也建议是将策略工厂手动实现成单例,至于创建就可以使用.Net自带的Activator.CreateInstance。

4.3、Quartz

我们使用Quartz作为定时执行的触发器,由于其相关内容也比较多,我们这里讲述下我这里的使用,在QuartZ中有三个重要元素,执行计划,执行的作业和执行的策略,首先来看看代码:

//执行的作业
public class Job: IJob
{System.Threading.Tasks.Task IJob.Execute(IJobExecutionContext context){var executor = context.JobDetail.JobDataMap.Get("JobExecutor") as JobExecutor;executor?.Action();return System.Threading.Tasks.Task.CompletedTask;}
}
//执行器
public class JobExecutor
{public Action Action { get; set; }
}//执行引擎
public class ExecutorProvider : IExecutorProvider
{//启动任务public void Start(TaskConfig taskConfig){//构造job执行器var jobExecutor = new JobExecutor{Action = () =>{var strategy = _factory.GetStrategy(taskConfig.Type);strategy.Init(taskConfig);AssemblyHelper.LoadAssemblies(Path.GetDirectoryName(taskConfig.ConfigFilePath), SearchOption.TopDirectoryOnly);strategy.Run();}};//将执行作业添加到执行计划IJobDetail job = new JobDetailImpl(taskConfig.TaskGuid.ToString(), taskConfig.Type, typeof(Job));job.JobDataMap.Put("JobExecutor", jobExecutor);_scheduler.ScheduleJob(job, CreateTrigger(taskConfig));}// 根据Cron表达式创建执行策略private ITrigger CreateTrigger(TaskConfig taskConfig){//cronExpression = "1/1 * * * * ? ";//1秒执行一次var triggerBuilder = TriggerBuilder.Create().WithCronSchedule(taskConfig.Triggers.First()).WithIdentity(taskConfig.TaskGuid.ToString()).StartAt(DateTime.Now);return triggerBuilder.Build();}
}//启动所有的任务
public static IServiceCollection StartAllTask(this IServiceCollection services)
{var provider = services.BuildServiceProvider();provider.GetService<ITaskService>().StartAll();StrategyInitializer<IExecutor>.SetServices(provider);var scheduler = StdSchedulerFactory.GetDefaultScheduler().Result;scheduler.Start();return services;
}

在上述代码中,整个逻辑其实分为两段在ExecutorProvider中我们定义了任务使用QuartzJob进行执行的逻辑, 在StartUp的ConfigureServices的最后调用服务获取store中的task进行执行。

在创建Job过程中,因为我们是进程内执行,所以直接使用委托进行传递参数,如果是后续考虑到分布式环境运行,则需要将任务参数传递然后再Job中创建执行策略进行执行即可。

4.4、Http请求重试

针对Http请求可能由于网络超时原因失败,我们引入Polly进行了重试,这个主要应用在ApiExecutor和AsyncExecutor中。这里通过下面代码有个简单的了解:

var policy= Policy.Handle<TimeoutException>().Retry(10);
policy.Execute(() =>
{// 执行http请求调用逻辑
}

我们针对http请求发送逻辑过程,如果产生超时异常,则进行重试10次。这里只是Polly的一个简单应用,Polly还广泛应用在熔断等分布式场景,这里只是个引子,有兴趣大家可以网上找找相关介绍。

五、遇到的问题

  1. dll版本兼容问题:在老板本中,由于平台未提供ApiExecutor,所以产品会写很多InProcessTask随调度任务一起发布,这样就会导致如果调度服务和产品开发所引用的dll冲突不好处理,在Framework版本中采用的是独立进程+应用程序域来解决的,而新版本中,我们规范了产品无法开发InProcessTask,这样所有的dll版本都在平台管控中;

  2. git仓库散乱:在本次改造过程中,还将所有自定义的任务全部合并到一个仓库之中,并且配合脚本进行整体发布,这样避免以前发布一个调度任务需要人工多次操作之后的方式,直接一键完成;

  3. 写日志的问题:这里我们引入Serilog,在不同的任务写日志的时候,根据目录和任务标识,创建不同的日志对象来写日志,保证各个任务的日志之间不会相互影响;

  4. 多进程的静态变量:在之前多进程的执行方式中存在静态的变量,因为是不同执行在不同进程所以不会出现变量值被覆盖问题。这里全部做了改进,能使用Ioc就是用Ioc解决,不能通过Ioc也尽量通过单例解决;

  5. 多进程任务管理:之前多进程情况任务会难以关闭,并且如果结束调度服务进程之后还会有执行进程在运行,导致不可预期的结果,这一次采用Quartz之后,其本身提供了对应的api来管理作业,并且任务之间也是隔离的,所以这一次没有采用多进程方式进行执行。

六、总结

在整个调度任务的改造过程中发现了很多类同行问题,这里做了一个总结:

  1. 不要自己造轮子:重试、定时执行日志,这些之前都是自己手写的,但是一直出问题一直改,使用开源成熟组件,简单省心;

  2. 软件生命周期:一个需要长时间维护的项目,一定需要根据职责划分一个清晰的层次结构,这样维护起来才不会导致大量臃肿的代码。

这一篇是这个系列的第六篇文章,加上前面几篇文章,几乎介绍了这次.Net Core改造的方方面面,最后一篇我们将介绍最后的发布部署。

------ END ------

作者简介

熊同学: 研发工程师,目前负责ERP运行平台的设计与开发工作。

也许您还想看

【复杂系统迁移 .NET Core平台系列】之WebApi改造

【复杂系统迁移 .NET Core平台系列】之认证和授权

【复杂系统迁移 .NET Core平台系列】之迁移项目工程

【复杂系统迁移 .NET Core平台系列】之界面层

【复杂系统迁移 .NET Core平台系列】之静态文件

【复杂系统迁移 .NET Core平台系列】之调度服务改造相关推荐

  1. 【复杂系统迁移 .NET Core平台系列】之认证和授权

    源宝导读:微软跨平台技术框架-.NET Core已经日趋成熟,已经具备了支撑大型系统稳定运行的条件.本文将介绍明源云ERP平台从.NET Framework向.NET Core迁移过程中的实践经验. ...

  2. 【复杂系统迁移 .NET Core平台系列】之静态文件

    源宝导读:微软跨平台技术框架-.NET Core已经日趋成熟,已经具备了支撑大型系统稳定运行的条件.本文将介绍明源云ERP平台从.NET Framework向.NET Core迁移过程中的实践经验. ...

  3. 【复杂系统迁移 .NET Core平台系列】之界面层

    源宝导读:微软跨平台技术框架-.NET Core已经日趋成熟,已经具备了支撑大型系统稳定运行的条件.本文将介绍明源云ERP平台从.NET Framework向.NET Core迁移过程中的实践经验. ...

  4. 【复杂系统迁移 .NET Core平台系列】之应用发布与部署

    源宝导读:微软跨平台技术框架-.NET Core已经日趋成熟,已经具备了支撑大型系统稳定运行的条件.本文将介绍明源云ERP平台从.NET Framework向.NET Core迁移过程中的实践经验. ...

  5. rsviwe32 7.6 授权_「复杂系统迁移 .NET Core平台系列」之认证和授权

    源宝导读:微软跨平台技术框架-.NET Core已经日趋成熟,已经具备了支撑大型系统稳定运行的条件.本文将介绍明源云ERP平台从.NET Framework向.NET Core迁移过程中的实践经验. ...

  6. 【复杂系统迁移 .NET Core平台系列】之迁移项目工程

    源宝导读:微软跨平台技术框架-.NET Core已经日趋成熟,已经具备了支撑大型系统稳定运行的条件.本文将介绍明源云ERP平台从.NET Framework向.NET Core迁移过程中的实践经验. ...

  7. 蝉知门户系统迁移到SAE平台-对蝉知2.5版本部分功能的限制

    蝉知2.5版本加入了部分新功能,使用起来更加方便.但在sae平台上受限于平台环境,其中的插件安装.模板安装功能由于没有写权限无法使用.需要在迁移至sae平台时做出限制,提示用户进行其他方式的安装. 1 ...

  8. 蝉知门户系统迁移到SAE平台-File模块扩展

    安装完成后虽然可以正常浏览网站了,但是由于upload目录没有写权限,还需要对文件管理模块进行修改以适应sae的环境,经过导师指点查看了禅道sae3.0版本的迁移方案,初步确定修改思路.也使用了部分原 ...

  9. 微信公众平台系列-5关键字服务

    部分封装: <?php /*** Created by PhpStorm.* User: wangyetao* Date: 18-1-18* Time: 上午10:01*/namespace W ...

最新文章

  1. Data Analysis: What are the skills needed to become a data analyst?
  2. 《数据分析实战 基于EXCEL和SPSS系列工具的实践》一3.2 用“逐步推进法”推测需要的数据...
  3. excel vlookup多个条件匹配多列_Excel教程第12课:VLOOKUP函数近似匹配到底怎么回事,原理+操作...
  4. python消费kafka逻辑处理导致cpu升高_用Apache Kafka 和 Python 搭建分布式流处理系统[翻译]...
  5. 卷积码 c语言编码,利用c语言实现卷积码编码器示例
  6. 联想小新电脑摄像头黑屏、检测不到设备、指示灯不亮解决方案
  7. 【实战】python 小型商品销售统计系统
  8. 【1月7日】议程正式公布!年度AIoT产业盛典重磅来袭!
  9. 18张图,揭开阿里巴巴开发手册强制使用SLF4J作为门面担当的秘密
  10. 网络基础之静态路由配置及网络问题排查思路
  11. Js一句话实现打开QQ和客服聊天
  12. python用双重循环输出菱形_Python 使用双重循环打印图形菱形操作
  13. 查询mysql数据库中各shema中的表数量【存储过程】
  14. HUD1873看病要排队
  15. 计算机毕业设计JAVA人民医院体检预约mybatis+源码+调试部署+系统+数据库+lw
  16. linux提交任务执行时间,Linux之任务计划
  17. excel工具栏隐藏了怎么办_你会用 Excel照相机吗?
  18. R7800评测 转帖自http://gric.pixnet.net/blog/post/113779838/3
  19. 织梦dedecms 幻灯片 自定义设置
  20. 爱情如水,宽容是杯~

热门文章

  1. 在Windows Live Writer中插入C# code
  2. ios 拍照 实现 连拍_如何在iOS设备上使用连拍模式拍照
  3. 在Outlook 2007中查看您的Google日历
  4. 三年级计算机击键要领教案,闽教版信息技术三上《下行键操作》教案
  5. 阿里云三维可视化使用初体验
  6. The import com.sun.tools cannot be resolved
  7. C# 数据结构--排序[下]
  8. Objective-C 学习记录6--dictionary
  9. oracle vm 安装虚拟机小bug
  10. mysql查看当前连接数