1小时完成应用程序健康检查组件

一、一个故事(虽然没有事故)

某天运维的同学通知我,云服务集群要加一台机器,过程是从当前线上集群中克隆一份服务器镜像,启动并加入集群,由于应用依赖的数据库服务器设置了白名单,新加的服务器需要加入白名单,悲剧的是,运维同学并不知道应用依赖了哪些数据库。

运维同学只好登录服务器,检查每个应用的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} \r\nstacktrace:{1} \r\nInnerException:message:{2}\r\nstacktrace:{3}", ex.Message, ex.StackTrace, ex.InnerException.Message, ex.InnerException.StackTrace);

                }
                else
                {
                    error = string.Format("message:{0} \r\nstacktrace:{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:以上代码均由我们一位刚毕业的工程师编写。

posted @ 2017-03-20 15:08  老肖想当外语大佬  阅读(1046)  评论(1编辑  收藏  举报