【复杂系统迁移 .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函數中加入參數的方式進行啟動,即可解決上述問題,下面是示例代碼:
? ? 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還廣泛應用在熔斷等分布式場景,這里只是個引子,有興趣大家可以網上找找相關介紹。
五、遇到的問題
dll版本兼容問題:在老板本中,由于平臺未提供ApiExecutor,所以產品會寫很多InProcessTask隨調度任務一起發布,這樣就會導致如果調度服務和產品開發所引用的dll沖突不好處理,在Framework版本中采用的是獨立進程+應用程序域來解決的,而新版本中,我們規范了產品無法開發InProcessTask,這樣所有的dll版本都在平臺管控中;
git倉庫散亂:在本次改造過程中,還將所有自定義的任務全部合并到一個倉庫之中,并且配合腳本進行整體發布,這樣避免以前發布一個調度任務需要人工多次操作之后的方式,直接一鍵完成;
寫日志的問題:這里我們引入Serilog,在不同的任務寫日志的時候,根據目錄和任務標識,創建不同的日志對象來寫日志,保證各個任務的日志之間不會相互影響;
多進程的靜態變量:在之前多進程的執行方式中存在靜態的變量,因為是不同執行在不同進程所以不會出現變量值被覆蓋問題。這里全部做了改進,能使用Ioc就是用Ioc解決,不能通過Ioc也盡量通過單例解決;
多進程任務管理:之前多進程情況任務會難以關閉,并且如果結束調度服務進程之后還會有執行進程在運行,導致不可預期的結果,這一次采用Quartz之后,其本身提供了對應的api來管理作業,并且任務之間也是隔離的,所以這一次沒有采用多進程方式進行執行。
六、總結
? ? 在整個調度任務的改造過程中發現了很多類同行問題,這里做了一個總結:
不要自己造輪子:重試、定時執行日志,這些之前都是自己手寫的,但是一直出問題一直改,使用開源成熟組件,簡單省心;
軟件生命周期:一個需要長時間維護的項目,一定需要根據職責劃分一個清晰的層次結構,這樣維護起來才不會導致大量臃腫的代碼。
? ? 這一篇是這個系列的第六篇文章,加上前面幾篇文章,幾乎介紹了這次.Net Core改造的方方面面,最后一篇我們將介紹最后的發布部署。
------ END ------
作者簡介
熊同學:?研發工程師,目前負責ERP運行平臺的設計與開發工作。
也許您還想看
【復雜系統遷移 .NET Core平臺系列】之WebApi改造
【復雜系統遷移 .NET Core平臺系列】之認證和授權
【復雜系統遷移 .NET Core平臺系列】之遷移項目工程
【復雜系統遷移 .NET Core平臺系列】之界面層
【復雜系統遷移 .NET Core平臺系列】之靜態文件
總結
以上是生活随笔為你收集整理的【复杂系统迁移 .NET Core平台系列】之调度服务改造的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 遵守这些原则让你开发效率提高一倍
- 下一篇: java信息管理系统总结_java实现科