您的位置:首页 > 其它

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

2017-03-20 15:08 225 查看
一、一个故事(虽然没有事故)

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

运维同学只好登录服务器,检查每个应用的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:以上代码均由我们一位刚毕业的工程师编写。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐