在系统运维中常常需要定期去跑一些计划任务,比如扫描服务器监控其性能、检查SQL Server作业是否正常、监控MQ队列是否存在堵塞现象等。如果使用Windows计划任务调度,一来管理起来就比较松散,二来如需更改计划任务的配置就必须登录到服务器上进行修改,造成很大的不便。因此笔者在实际工作中自行开发计划任务调度服务来处理这些任务,将调度周期、任务配置等经常需要修改的配置信息保存到数据库中,并开发一个前台界面进行维护和管理。
一、基本结构
计划任务调度服务使用插件的方式处理各类不同的计划任务,插件必须继承自服务框架提供的MonitorTask抽象类,并在数据库中注册任务名、调度周期等信息,这样就可以由该服务调度执行。为方便起见,要求任务名、插件的dll文件名、插件对象的类型名必须一致,各插件都放在同一个命名空间MonitorTask下。
在工作中,各类监控任务主要有3种配置:
1.调度周期,以xml形式保存在数据库中。
2.监控项,以键值对的方式保存在数据库中。
3.基本不会修改的一些个性化配置,以xml文件方式保存在插件同级目录下,名称与插件名一致。
二、插件的父类
所有插件都必须继承自MonitorTask抽象类,这里之所以使用抽象类而不使用接口是考虑到可以在这个抽象类中实现一些共性的东西,如参数配置的加载、错误日志的记录等。
1 public abstract class MonitorTask 2 { 3 protected XElement _configElement; 4 public List<Schedule> Schedules; 5 protected List<MonitorSetting> MonitorSettings; 6 private bool _isRunning = false; 7 8 protected MonitorTask() 9 { 10 LoadParameters(); 11 } 12 13 public void Execute() 14 { 15 try 16 { 17 if (CouldExecute()) 18 { 19 _isRunning = true; 20 ExecuteTask(); 21 ExecuteLog(); 22 } 23 } 24 catch (Exception e) 25 { 26 new LogText(AppDomain.CurrentDomain.BaseDirectory + @"log.log").Write(GetType().Name + "插件执行异常", e.ToString()); 27 } 28 finally 29 { 30 _isRunning = false; 31 } 32 } 33 34 private bool CouldExecute() 35 { 36 if (_isRunning || Schedules == null) 37 { 38 return false; 39 } 40 foreach (var schedule in Schedules) 41 { 42 if (schedule.TimeUp()) 43 { 44 return true; 45 } 46 } 47 return false; 48 } 49 50 protected abstract void ExecuteTask(); 51 52 private void ExecuteLog() 53 { 54 if (ConfigManagement.GetConfig().GetAppSetting("ExecuteLog") == "1") 55 { 56 new LogText(AppDomain.CurrentDomain.BaseDirectory + @"executelog.log").Write(GetType().Name + "插件执行完毕"); 57 } 58 } 59 60 protected void LoadParameters() 61 { 62 if (File.Exists(GetConfigFile())) 63 { 64 _configElement = XElement.Load(GetConfigFile()); 65 LoadXMLParameters(); 66 } 67 LoadMonitorSettings(); 68 } 69 70 private string GetConfigFile() 71 { 72 return AppDomain.CurrentDomain.BaseDirectory + GetType().Name + ".xml"; 73 } 74 75 protected virtual void LoadXMLParameters() 76 { 77 } 78 79 protected void LoadMonitorSettings() 80 { 81 MonitorSettings = new List<MonitorSetting>(); 82 string sql = "SELECT Name, Value FROM MonitorTaskSetting WHERE MonitorTaskID IN (SELECT ID FROM MonitorTask WHERE Name = '" + GetType().Name + "')"; 83 using (SqlDataReader reader = SqlAssist.Select("SOMP", sql)) 84 { 85 while (reader.Read()) 86 { 87 MonitorSettings.Add(new MonitorSetting(reader.GetString(0), reader.GetString(1))); 88 } 89 } 90 } 91 }
在开发计划任务插件时只需要继承该类,并实现ExecuteTask方法就行了。在这里ConfigManagement.GetConfig().GetAppSetting(string name)、new LogText(string filename).Write(string value)以及SqlAssist.Select(string dbname, string sql)这三个方法都是笔者在开发过程中自行封装的,为了避免重复劳动,一看名字就该知道这些方法的作用吧。
二、任务调度周期
在MonitorTask抽象类中还有一个List<Schedule>,用于保存任务调度的周期,Schedule也是一个抽象类,该抽象类主要由3个方法:
public static List<Schedule> CreateSchedule(XElement element)
该方法根据保存在数据库中的xml数据构建出Schedule。
public abstract bool TimeUp()
由子类实现该方法,判断是否已到达需要调度任务的时间。
public abstract XElement ToXML()
由子类实现该方法,用于将任务调度周期序列化成xml,便于保存到数据库中。
三、管理类
该类主要根据数据库中的配置利用反射构建出相应的计划任务对象,并将其缓存在Dictionary对象里避免多次构建,同时创建线程执行这些插件。
1 public static class MonitorTaskManagement 2 { 3 private static Dictionary<string, MonitorTask> _tasks = new Dictionary<string, MonitorTask>(); 4 5 private static MonitorTask GetMonitorTask(string fileName, string taskType, XElement element) 6 { 7 Assembly assembly = Assembly.LoadFrom(AppDomain.CurrentDomain.BaseDirectory + fileName); 8 Type type = assembly.GetType(taskType); 9 if (type.IsSubclassOf(typeof(MonitorTask))) 10 { 11 MonitorTask task = Activator.CreateInstance(type) as MonitorTask; 12 task.Schedules = Schedule.CreateSchedule(element); 13 return task; 14 } 15 else 16 { 17 throw new Exception("程序集" + taskType + "有误。"); 18 } 19 } 20 21 private static MonitorTask GetMonitorTask(string taskName, XElement element) 22 { 23 MonitorTask task; 24 if (_tasks.ContainsKey(taskName)) 25 { 26 task = _tasks[taskName]; 27 } 28 else 29 { 30 task = GetMonitorTask(taskName + ".dll", "MonitorTask." + taskName, element); 31 _tasks.Add(taskName, task); 32 } 33 return task; 34 } 35 36 public static void ExecuteTask() 37 { 38 string sql = "SELECT Name, Schedule FROM MonitorTask WHERE Enable = 1"; 39 using (SqlDataReader reader = SqlAssist.Select("SOMP", sql)) 40 { 41 while (reader.Read()) 42 { 43 try 44 { 45 MonitorTask task = GetMonitorTask(reader.GetString(0), XElement.Parse(reader.GetString(1))); 46 Thread childThread = new Thread(task.Execute); 47 childThread.IsBackground = true; 48 childThread.Start(); 49 } 50 catch (Exception e) 51 { 52 new LogText(AppDomain.CurrentDomain.BaseDirectory + @"log.log").Write(reader.GetString(0) + "插件异常", e.ToString()); 53 } 54 } 55 } 56 } 57 }