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

源宝导读:微软跨平台技术框架—.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平台系列】之静态文件

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/308918.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C++实现AOE网中的关键路径算法及机动时间计算算法(邻接表存储)

代码如下: #include <iostream> #include <stack> using namespace std; const int N 100;typedef struct Node {int adj;int w;Node *next;}Node;typedef struct VNode {int in;int v;Node *first;VNode(){first nullptr;} }VNode;class AOE { private:VNode a…

.NET 开源项目 StreamJsonRpc 介绍[中篇]

阅读本文大概需要 11 分钟。上一篇介绍了一些预备知识&#xff0c;包括 JSON-RPC 介绍和实现了 JSON-RPC 的 StreamJsonRpc 介绍&#xff0c;讲到了 StreamJsonRpc 可以通过 .NET 的 Stream 类和 WebSocket 类实现 JSON-RPC 协议的通信。本篇就先选择其中的 Stream 类来讲解&am…

Istio 中的多集群部署与管理

本文节选自ServiceMesher 社区出品的开源电子书《Istio Handbook——Istio 服务网格进阶实战》&#xff0c;作者钟华&#xff0c;来自腾讯云。Istio 在 1.1 后提供了两类多集群的连通的部署模式&#xff1a;1. 多控制面2. 单控制面&#xff0c;也称为 “共享控制面” 模式多控制…

认证授权方案之授权初识

1.前言回顾&#xff1a;认证授权方案之JwtBearer认证在上一篇中&#xff0c;我们通过JwtBearer的方式认证&#xff0c;了解在认证时&#xff0c;都是基于Claim的&#xff0c;因此我们可以通过用户令牌获取到用户的Claims&#xff0c;在授权过程中对这些Claims进行验证&#xff…

[数据结构]树、森林与二叉树之间的相互转换方法

树、二叉树与森林的相互转换 本文只给出树、森林与二叉树之间的转换方法&#xff0c;而详细的证明过程不在本文讨论范围之内。 树 → 二叉树 在所有兄弟结点之间加一连线。 对每个结点&#xff0c;除了保留与其长子的连线外&#xff0c;去掉该结点与其它孩子的连线。 二叉树 …

Blazor.Server以正确的方式集成Ids4

&#xff08;一个真正的以后端形式来集成认证中心的方案&#xff09;❤本文导读首先特别感谢张善友老师提供技术指导&#xff0c;源于上周我发了一篇文章《[Mvp.Blazor] 集成Ids4&#xff0c;实现统一授权认证》&#xff0c;我本来是想通过像vue框架那样&#xff0c;通过引oidc…

作为一个有理想的程序员,必读的书都有哪些?

很多程序员朋友问我&#xff1a;“哪本最具影响力的书&#xff0c;是每个程序员都应该读的&#xff1f;” 笔者从事软件开发15年&#xff0c;看过的计算机相关的书籍不下百本了&#xff0c;如果非要推荐的话&#xff0c;给大家精选以下10本&#xff0c;希望对大家有所帮助&…

认证授权方案之JwtBearer认证

1.前言回顾&#xff1a;认证方案之初步认识JWT在现代Web应用程序中&#xff0c;即分为前端与后端两大部分。当前前后端的趋势日益剧增&#xff0c;前端设备&#xff08;手机、平板、电脑、及其他设备&#xff09;层出不穷。因此&#xff0c;为了方便满足前端设备与后端进行通讯…

使用过滤器模式,让客户关怀中的代码更加干净整洁

一&#xff1a;实际场景介绍我们在给用户做订单催付通知的时候&#xff0c;会有这样的一种场景&#xff0c;用户在系统后台设置一组可以催付的规则&#xff0c;比如说订单金额大于xx元&#xff0c;非黑名单用户&#xff0c;来自哪个地区&#xff0c;已购买过某个商品&#xff0…

C++实现各种排序以及复杂度,稳定性分析

代码如下: #include<iostream> using namespace std;void Bubble_Sort(int *a, int n) {bool flag;int tmp 0;for (int i n - 1; i > 0; i--){flag false;for (int j 0; j < i; j){if (a[j] > a[j 1]){swap(a[j], a[j 1]);flag true;}}if (!flag) break…

Webapi管理和性能测试工具WebBenchmark

WebBenchmark是一款基于开源通讯组件Beetlex扩展的Webapi管理和性能测试工具&#xff0c;在传统工具中一般管理工具缺乏性能压测能力或有性能压测的缺少管理功能&#xff1b;WebBenchmark的设计目标是就管理和性能测试能力同时具备。接下来介绍一下工具的功能和使用&#xff1a…

Abstract Factory(抽象工厂)--对象创建模式

Abstract Factory &#xff08;抽象工厂&#xff09;–对象创建模式 一、意图 提供一个创建一系列相关或者相互依赖的接口&#xff0c;而无需指定它们具体的类。 二、动机 1.在软件系统中&#xff0c;经常面临着“一系列相互依赖的对象”的创建工 作;同时&#xff0c;由于需求…

Builder(生成器)--对象创建型模式

Builder&#xff08;生成器&#xff09;–对象创建型模式 一、意图 将一个复杂的对象构建与它的表示分离&#xff0c;使得同样的构建过程可以创建不同的表示。 二、动机 1.在软件系统中&#xff0c;有时候面临着“一个复杂对象”的创建工作&#xff0c;其通常由各个部分的子对…

Gartner:缺乏技术人才将影响企业数字化转型

导语大多数公司在数字化转型的阶段对所需的技能方面都处于“盲目”状态。正文随着COVID-19响应加快了数字化转型的速度和规模&#xff0c;缺乏数字化技能可能会危害人才计划不统一的公司。甚至在冠状病毒大流行之前&#xff0c;董事会就将数字/技术中断列为2020年的头等大事&am…

DEBUG org.springframework.web.servlet.DispatcherServlet - Error rendering view [org.thymeleaf.spring

报错信息如下: 报错原因: thymeleaf有一些限制&#xff0c;使用th语言&#xff0c;内容为空就会报错 改成这样解决问题:

Factory Method(工厂方法)--对象创建型模式

Factory Method&#xff08;工厂方法&#xff09;–对象创建型模式 一、意图 定义一个用于创建对象的接口&#xff0c;让子类决定实例化哪一个类。Factory Method使一个类的实例化延迟到其子类。 二、动机 1.在软件系统中&#xff0c;经常面临着创建对象的工作;由于需求的变化…

全内存的redis用习惯了?那能突破内存限制类redis产品ssdb呢?

首先说一下背景&#xff0c;在双十一的时候&#xff0c;我们系统接受X宝的订单推送&#xff0c;原先的实现方式是使用 redis 的 List 作为推送数据的承载&#xff0c;在非大促的场景下&#xff0c;一切运行正常&#xff0c;内存占用大概3-4G&#xff0c;机器是16G内存。由于提前…