一、一个故事(虽然没有事故)
某天运维的同学通知我,云服务集群要加一台机器,过程是从当前线上集群中克隆一份服务器镜像,启动并加入集群,由于应用依赖的数据库服务器设置了白名单,新加的服务器需要加入白名单,悲剧的是,运维同学并不知道应用依赖了哪些数据库。
运维同学只好登录服务器,检查每个应用的web.config文件查看数据库配置,并在对应的数据库服务器添加白名单。
一个小时后,运维同学告诉我,白名单添加完成,请协助验证应用是否正常工作。
此时我内心是纠结的,由于云服务集群服务器部署的都是无界面的WEBAPI,要验证它是否正常需要模拟HTTP请求,幸运的是,我对这些API对于数据库的依赖还算熟悉,一番操作后,终于验证完毕,耗时1小时。
最终,服务器上线了,过程还算顺利,并未发生意外。
二、懒鬼的思考
作为一个资深懒鬼,我觉得做这样的工作即费力又没有收益,而且还有发生意外导致背锅的风险,实在恶心至极,为了避免再做这样的事情,我决定做点什么。
首先,分析这次维护过程,不足之处:
1.缺失应用程序依赖管理及服务器依赖管理,导致集群新增服务器时,对应的白名单添加操作需要人工确认和验证;
2.应用程序(尤其时无UI应用)没有很好的依赖自检功能,无法很方便检查应用程序的依赖项健康状况;
基于以上两点,可以我可以做什么事情:
1.建议运维团队建立应用、服务器等依赖管理
2.做一个健康检查组件供各个应用使用,方便检查应用程序的健康状况
三、健康检查组件
1.检查器
由于我们并不知道每个应用程序需要检查哪些依赖以及怎么检查,因此我们将检查器设计成接口:
/// <summary> /// 健康检查器接口 /// </summary> public interface IHealthChecker { /// <summary> /// 名称 /// </summary> string Name { get; } /// <summary> /// 描述 /// </summary> string Description { get; } /// <summary> /// 检查方法 /// </summary> void Check(); }
2.检查结果对象
检查结果包括名称、描述、耗时、是否成功、错误信息
/// <summary> /// 检查结果 /// </summary> public class CheckResult { /// <summary> /// 名字 /// </summary> public string Name { get; set; } /// <summary> /// 描述 /// </summary> public string Description { get; set; } /// <summary> /// 状态-success,-failed /// </summary> public bool Success { get; set; } /// <summary> /// 错误信息 /// </summary> public string Error { get; set; } /// <summary> /// 耗时 /// </summary> public long Elapsed { get; set; } }
3.检查器管理
一般场景下,我们的应用程序不仅仅有一个依赖项,因此会创建多个检查器,就需要一个类来管理这些检查器,集中地调用检查器的Check方法来得到检查结果,并且处理各种异常。
我们将这个类命名为HealthCheckManger,这里我们设计为单例模式:
/// <summary> /// 健康检查管理器 /// </summary> public class HealthCheckManger { static HealthCheckManger manager = new HealthCheckManger(); private static HealthCheckManger Instance { get { return manager; } } /// <summary> /// 注册checker,通过注册checker /// </summary> /// <param name="checker"></param> public static void RegisterChecker(IHealthChecker checker) { manager.checkerList.Add(checker); } /// <summary> /// 注册checker,通过注册名字,描述,函数 /// </summary> /// <param name="name"></param> /// <param name="description"></param> /// <param name="action"></param> public static void RegisterChecker(string name, string description, Action action) { manager.checkerList.Add(new AddHealthChecker(name, description, action)); } /// <summary> /// 公开检查方法 /// </summary> /// <returns></returns> public static List<CheckResult> CheckAll() { return manager.DoCheckAll().Result; } /// <summary> /// 异步检查 /// </summary> /// <returns></returns> public static async Task<List<CheckResult>> CheckAllAsync() { return await manager.DoCheckAll().ConfigureAwait(false); }/// <summary> /// 检查器列表 /// </summary> List<IHealthChecker> checkerList = new List<IHealthChecker>(); /// <summary> /// 多线程跑所有check方法 /// </summary> /// <returns></returns> private async Task<List<CheckResult>> DoCheckAll() { var tasks = new List<Task<CheckResult>>(); foreach (var checker in checkerList) { tasks.Add(Task.Run(() => { return DoCheck(checker); })); } var t = await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false); return t.ToList(); } /// <summary> /// 单个check方法 /// </summary> /// <param name="checker"></param> /// <returns></returns> private CheckResult DoCheck(IHealthChecker checker) { Stopwatch sw = new Stopwatch(); sw.Start(); try { checker.Check(); sw.Stop(); return new CheckResult { Name = checker.Name, Description = checker.Description, Success = true, Elapsed = sw.ElapsedMilliseconds }; } catch (Exception ex) { sw.Stop(); var error = ""; if (ex.InnerException != null) { error = string.Format("message:{0} stacktrace:{1} InnerException:message:{2} stacktrace:{3}", ex.Message, ex.StackTrace, ex.InnerException.Message, ex.InnerException.StackTrace); } else { error = string.Format("message:{0} stacktrace:{1}", ex.Message, ex.StackTrace); } return new CheckResult { Name = checker.Name, Description = checker.Description, Success = false, Elapsed = sw.ElapsedMilliseconds, Error = error }; } } }
4.内置检查器
通常情况,我们的应用都会依赖数据库,因此,我们设计一个内置的数据库连接检查器,此时不得不感慨ADO.NET设计的精妙,我们可以通过很少的代码就实现一个支持多种数据库的检查器:
/// <summary> /// 数据库健康检查器 /// </summary> public class DatabaseHealthChecker:IHealthChecker { private DbProviderFactory dbFactory = null; private string connectionString = null; /// <summary> /// 构造函数 /// </summary> /// <param name="connectionStringName"></param> public DatabaseHealthChecker(string connectionStringName) { this.Name = connectionStringName; this.Description = "数据库连接"; var providerName = ConfigurationManager.ConnectionStrings[Name].ProviderName; this.connectionString = ConfigurationManager.ConnectionStrings[Name].ConnectionString; if (!string.IsNullOrEmpty(providerName)) { this.dbFactory = DbProviderFactories.GetFactory(providerName); } else { throw new ArgumentNullException("ProviderName", "数据库提供名称参数不能为空"); } } /// <summary> /// 名称 /// </summary> public string Name { get; private set; } /// <summary> /// 描述 /// </summary> public string Description { get; private set; } /// <summary> /// 检查方法 /// </summary> public void Check() { using (var con = dbFactory.CreateConnection()) { con.ConnectionString = this.connectionString; con.Open(); } } }
有了内置的数据库连接检查器,我们还可以给HealthCheckManger添加一个方法,来帮助我们根据web.config的connectionStrings配置快速注册检查器实例:
public static void RegisterAllDatabaseHealthChecker() { foreach (ConnectionStringSettings con in ConfigurationManager.ConnectionStrings) { manager.RegisterChecker(new DatabaseHealthChecker(con.Name)); } }
至此,咱们的组件就完成了,根据各位读者的实力,大家肯定能在1个小时内完成这些代码(虽然我们花费了一天)。
四、健康检查组件for ASP.NET MVC
在我们的ASP.NET MVC项目Global.cs文件中添加如下代码,注册检查器,也可以注册自己的检查器:
HealthCheckManger.RegisterAllDatabaseHealthChecker(); //HealthCheckManger.RegisterChecker(new MyChecker()); //自己定义的checker
我们可以写一个controller用来输出检查结果,当然也可以输出一个更加漂亮的页面显示具体信息:
public class HealthCheckController : Controller { /// <summary> /// 首页 /// </summary> /// <returns></returns> public ActionResult Index() { var result = HealthCheckManger.CheckAll(); return Json(result, JsonRequestBehavior.AllowGet); } }
五、总结
1.我们用到了单例模式,HealthCheckManger类;
2.我们用到了多线程并行运算,HealthCheckManger在调用检查器的Check方法时,使用了Task.WhenAll方法,这样我们可以尽早拿到最终检查结果,而不是一个一个排队check
3.我们使用接口定义检查器,保证了组件的可扩展性
4.我们可以写更多的内置检查器,提高代码复用
5.我们将组件打包为NuGet包,就可以让全世界的同学使用啦
六、延伸思考
1.目前我们的组件只是被动地接收检查命令,可以考虑做一个Job定期检查并记录日志和报警
2.基于健康检查结果,我们可以对数据库、Redis等依赖对象进行熔断策略,当依赖项挂掉(超时)的时候,不至于应用整个由于处理连接响应过慢而雪崩;
PS:以上代码均由我们一位刚毕业的工程师编写。