通常我们需要监测ASP.NET MVC 或 Web API 的应用程序的性能时,通常采用的是自定义性能计数器,性能计数器会引发无休止的运维问题(损坏的计数器、权限问题等)。这篇文章向你介绍一个新的替代性能计数器的工具Metrics.NET,因为是它是内部的,所以我们能够向系统中添加更多更有意义的度量标准。
Metrics.NET(https://github.com/etishor/Metrics.NET)是一个给CLR 提供度量工具的包,它是移植自Java的metrics,支持的平台 .NET 4.5.1, .NET 4.5, .NET 4.0 和 Mono 3.8.0,在c#代码中嵌入Metrics代码,可以方便的对业务代码的各个指标进行监控, 提供5种度量的类型:Gauges, Counters, Histograms, Meters,Timers:
Gauge是最简单的度量类型,只有一个简单的返回值,例如,你的应用中有一个由第三方类库中保持的一个度量值,你可以很容易的通过Gauge来度量他
class="csharpcode"> long milliseconds = this.ConvertTicksToMilliseconds(elapsedTicks); String controllerName = this.actionInfo.ControllerName; String actionName = this.actionInfo.ActionName; string counterName = string.Format("{0} {1} {2}", controllerName, actionName, COUNTER_NAME); Metric.Context(this.actionInfo.ActionType).Gauge(counterName, () => milliseconds, Unit.Custom("Milliseconds"));
那么Metrics会创建一个叫做[MVC] Account LogOn Last Call Elapsed Time.Gauge的Gauge,返回最新的一个请求的时间。
Counter是一个简单64位的计数器:
String categoryName = this.actionInfo.ControllerName; String instanceName = this.actionInfo.ActionName; string counterName = string.Format("{0} {1} {2}", categoryName, instanceName, COUNTER_NAME); this.callsInProgressCounter = Metric.Context(this.actionInfo.ActionType).Counter(counterName, Unit.Custom(COUNTER_NAME)); /// <summary> /// Constant defining the name of this counter /// </summary> public const String COUNTER_NAME = "ActiveRequests"; private Counter callsInProgressCounter; /// <summary> /// Method called by the custom action filter just prior to the action begining to execute /// </summary> /// <remarks> /// This method increments the Calls in Progress counter by 1 /// </remarks> public override void OnActionStart() { this.callsInProgressCounter.Increment(); } /// <summary> /// Method called by the custom action filter after the action completes /// </summary> /// <remarks> /// This method decrements the Calls in Progress counter by 1 /// </remarks> public override void OnActionComplete(long elapsedTicks, bool exceptionThrown) { this.callsInProgressCounter.Decrement(); } 所有的Counter都是从0开始,上述代码描述的当前的请求数。
Histrogram是用来度量流数据中Value的分布情况,例如,每一个POST/PUT请求中的内容大小:
public PostAndPutRequestSizeMetric(ActionInfo info) : base(info) { this.histogram = Metric.Context(this.actionInfo.ActionType).Histogram(COUNTER_NAME, Unit.Bytes, SamplingType.FavourRecent); } /// <summary> /// Constant defining the name of this counter /// </summary> public const String COUNTER_NAME = "Post & Put Request Size"; /// <summary> /// Reference to the performance counter /// </summary> private Histogram histogram; public override void OnActionStart() { var method = this.actionInfo.HttpMethod.ToUpper(); if (method == "POST" || method == "PUT") { histogram.Update(this.actionInfo.ContentLength); } }
Histrogram 的度量值不仅仅是计算最大/小值、平均值,方差,他还展现了分位数(如中位数,或者95th分位数),如75%,90%,98%,99%的数据在哪个范围内。
传统上,中位数(或者其他分位数)是在一个完整的数据集中进行计算的,通过对数据的排序,然后取出中间值(或者离结束1%的那个数字,来计算99th分位数)。这种做法是在小数据集,或者是批量计算的系统中,但是在一个高吞吐、低延时的系统中是不合适的。
一个解决方案就是从数据中进行抽样,保存一个少量、易管理的数据集,并且能够反应总体数据流的统计信息。使我们能够简单快速的计算给定分位数的近似值。这种技术称作reservoir sampling。
Metrics中提供两种类型的直方图:uniform跟biased。
Uniform Histogram提供直方图完整的生命周期内的有效的中位数,它会返回一个中位值。例如:这个中位数是对所有值的直方图进行了更新,它使用了一种叫做Vitter’s R的算法,随机选择了一些线性递增的样本。
当你需要长期的测量,请使用Uniform Histograms。在你想要知道流数据的分布中是否最近变化的话,那么不要使用这种。
Biased Histogram提供代表最近5分钟数据的分位数,他使用了一种forward-decayingpriority sample的算法,这个算法通过对最新的数据进行指数加权,不同于Uniform算法,Biased Histogram体现的是最新的数据,可以让你快速的指导最新的数据分布发生了什么变化。Timers中使用了Biased Histogram。
Meters
Meter度量一系列事件发生的比率:
public DeltaExceptionsThrownMetric(ActionInfo info) : base(info) { this.deltaExceptionsThrownCounter = Metric.Context(this.actionInfo.ActionType).Meter(COUNTER_NAME, Unit.Errors, TimeUnit.Seconds); } /// <summary> /// Constant defining the name of this counter /// </summary> public const String COUNTER_NAME = "Errors"; /// <summary> /// Reference to the performance counter /// </summary> private Meter deltaExceptionsThrownCounter; /// <summary> /// Method called by the custom action filter after the action completes /// </summary> /// <remarks> /// If exceptionThrown is true, then the Total Exceptions Thrown counter will be /// incremented by 1 /// </remarks> public override void OnActionComplete(long elapsedTicks, bool exceptionThrown) { if (exceptionThrown) this.deltaExceptionsThrownCounter.Mark(); }
Meter需要除了Name之外的两个额外的信息,事件类型(enent type)跟比率单位(rate unit)。事件类型简单的描述Meter需要度量的事件类型,在上面的例子中,Meter是度量失败的请求数,所以他的事件类型也叫做“Errors”。比率单位是命名这个比率的单位时间,在上面的例子中,这个Meter是度量每秒钟的失败请求次数,所以他的单位就是秒。这两个参数加起来就是表述这个Meter,描述每秒钟的失败请求数。
Meter从几个角度上度量事件的比率,平均值是时间的平均比率,它描述的是整个应用完整的生命周期的情况(例如,所有的处理的请求数除以运行的秒数),它并不描述最新的数据。幸好,Meters中还有其他3个不同的指数方式表现的平均值,1分钟,5分钟,15分钟内的滑动平均值。
Hint:这个平均值跟Unix中的uptime跟top中秒数的Load的含义是一致的。
Timer是Histogram跟Meter的一个组合
public TimerForEachRequestMetric(ActionInfo info) : base(info) { String controllerName = this.actionInfo.ControllerName; String actionName = this.actionInfo.ActionName; string counterName = string.Format("{0}{1}", controllerName, actionName); this.averageTimeCounter = Metric.Context(this.actionInfo.ActionType).Timer(counterName, Unit.Requests, SamplingType.FavourRecent, TimeUnit.Seconds, TimeUnit.Milliseconds); } #region Member Variables private Timer averageTimeCounter; #endregion /// <summary> /// Method called by the custom action filter after the action completes /// </summary> /// <remarks> /// This method increments the Average Time per Call counter by the number of ticks /// the action took to complete and the base counter is incremented by 1 (this is /// done in the PerfCounterUtil.IncrementTimer() method). /// </remarks> /// <param name="elapsedTicks">A long of the number of ticks it took to complete the action</param> public override void OnActionComplete(long elapsedTicks, bool exceptionThrown) { averageTimeCounter.Record(elapsedTicks, TimeUnit.Nanoseconds); }
Meters提供一种一致的、统一的方法来对应用进行健康检查,健康检查是一个基础的对应用是否正常运行的自我检查。
Reporters是将你的应用中所有的度量指标展现出来的一种方式,metrics.net中用了三种方法来导出你的度量指标,Http,Console跟CSV文件, Reporters是可定制的。例如可以使用Log4net进行输出,具体参见 https://github.com/nkot/Metrics.Log4Net 。
Metric.Config.WithHttpEndpoint("http://localhost:1234/") .WithAllCounters() .WithReporting(config => config.WithCSVReports(@"c:\temp\csv", TimeSpan.FromSeconds(10)) .WithTextFileReport(@"C:\temp\reports\metrics.txt", TimeSpan.FromSeconds(10)));
上面我们介绍了基于Metrics.NET构建的ASP.NET MVC 应用程序的性能指标,如下表所示:计数器名称 描述 Last Call Elapsed Time 已完成最后一次调用的所花费的时间。这是表示所有已完成请求的时间测量中的最新一个点。它不是平均值。 Request Timer 统计执行时间以及其分布情况
POST & PUT Request Size histogramPOST/PUT请求中的内容大小 Global Error Meter ASP.NET引发 未捕获的异常的比率。如果此计数器增加时,它会显示与该应用程序的健康问题
Delta Calls最后一个采样周期内被调用的次数
ActiveRequests当前的并发请求数
通过自定义Action Filter集成到ASP.NET MVC
定义一个MvcPerformanceAttribute,继承自ActionFilterAttribute:
/// <summary> /// Custom action filter to track the performance of MVC actions /// </summary> public class MvcPerformanceAttribute : ActionFilterAttribute { public MvcPerformanceAttribute() { } /// <summary> /// Constant to identify MVC Action Types (used in the instance name) /// </summary> public const String ACTION_TYPE = "MVC"; /// <summary> /// Method called before the action method starts processing /// </summary> /// <param name="filterContext">An ActionExecutingContext object</param> public override void OnActionExecuting(ActionExecutingContext filterContext) { // First thing is to check if performance is enabled globally. If not, return if ( ConfigInfo.Value.PerformanceEnabled == false) { return; } // Second thing, check if performance tracking has been turned off for this action // If the DoNotTrackAttribute is present, then return ActionDescriptor actionDescriptor = filterContext.ActionDescriptor; if (actionDescriptor.GetCustomAttributes(typeof(DoNotTrackPerformanceAttribute), true).Length > 0 || actionDescriptor.ControllerDescriptor.GetCustomAttributes(typeof(DoNotTrackPerformanceAttribute), true).Length > 0) { return; } // ActionInfo encapsulates all the info about the action being invoked ActionInfo info = this.CreateActionInfo(filterContext); // PerformanceTracker is the object that tracks performance and is attached to the request PerformanceTracker tracker = new PerformanceTracker(info); // Store this on the request String contextKey = this.GetUniqueContextKey(filterContext.ActionDescriptor.UniqueId); HttpContext.Current.Items.Add(contextKey, tracker); // Process the action start - this is what starts the timer and increments any // required counters before the action executes tracker.ProcessActionStart(); } /// <summary> /// Method called after the action method has completed executing /// </summary> /// <remarks> /// This method first checks to make sure we are indeed tracking performance. If so, it stops /// the stopwatch and then calls the OnActionComplete() method of all of the performance metric /// objects attached to this action filter /// </remarks> /// <param name="filterContext">An ActionExecutedConext object</param> public override void OnActionExecuted(ActionExecutedContext filterContext) { // This is the unique key the PerformanceTracker object would be stored under String contextKey = this.GetUniqueContextKey(filterContext.ActionDescriptor.UniqueId); // Check if there is an object on the request. If not, must not be tracking performance // for this action, so just go ahead and return if (HttpContext.Current.Items.Contains(contextKey) == false) { return; } // If we are here, we are tracking performance. Extract the object from the request and call // ProcessActionComplete. This will stop the stopwatch and update the performance metrics PerformanceTracker tracker = HttpContext.Current.Items[contextKey] as PerformanceTracker; if (tracker != null) { bool exceptionThrown = (filterContext.Exception != null); tracker.ProcessActionComplete(exceptionThrown); } } #region Helper Methdos /// <summary> /// Helper method to create the ActionInfo object containing the info about the action that is getting called /// </summary> /// <param name="actionContext">The ActionExecutingContext from the OnActionExecuting() method</param> /// <returns>An ActionInfo object that contains all the information pertaining to what action is being executed</returns> private ActionInfo CreateActionInfo(ActionExecutingContext actionContext) { var parameters = actionContext.ActionDescriptor.GetParameters().Select(p => p.ParameterName); String parameterString = String.Join(",", parameters); int processId = ConfigInfo.Value.ProcessId; String categoryName = ConfigInfo.Value.PerformanceCategoryName; String controllerName = actionContext.ActionDescriptor.ControllerDescriptor.ControllerName; String actionName = actionContext.ActionDescriptor.ActionName; String httpMethod = HttpContext.Current.Request.HttpMethod; int contentLength = HttpContext.Current.Request.ContentLength; ActionInfo info = new ActionInfo(processId, categoryName, ACTION_TYPE, controllerName, actionName, httpMethod, parameterString,contentLength); return info; } /// <summary> /// Helper method to form the key that will be used to store/retrieve the PerformanceTracker object /// off if the HttpContext /// </summary> /// <remarks> /// To minimize any chance of collisions, this method concatenates the full name of this class /// with the UniqueID of the MVC action to get a unique key to use /// </remarks> /// <param name="actionUniqueId">A String of the unique id assigned by ASP.NET to the MVC action</param> /// <returns>A Strin suitable to be used for the key</returns> private String GetUniqueContextKey(String actionUniqueId) { return this.GetType().FullName + ":" + actionUniqueId; } #endregion }
首要任务是确定是否正在跟踪此控制器操作性能。首先,它会检查一个名为 ConfigInfo,看看是否在整个应用程序范围的基础上启用性能的单例类。如果 ConfigInfo 类不是能够在 Web.Config 文件中查找的AspNetPerformance.EnablePerformanceMonitoring,此调用将返回 false。
然后应该跟踪此控制器操作性能。辅助方法用于创建一个 ActionInfo 对象,它是一个对象,封装有关控制器操作的所有信息。然后创建 PerformanceTracker 对象,它是具有主要负责跟踪性能的控制器操作的对象。度量性能的每个请求将相关联的 PerformanceTracker 对象和关联的 PerformanceTracker 对象将需要再次检索在 OnActionExecuted() 方法中控制器动作完成后。PerformanceTracker 对象存储在当前的 HttpContext 对象项目字典中。对 HttpContext 项目字典是用于当数据需要在请求过程中不同的 Http 处理程序和模块之间共享而设计的。使用的诀窍是基于属性类型的完整名称和 ASP.NET 生成的唯一 id 的方法。通过将这些因素结合在一起,我们应该与其他模块的使用项目字典任何关键碰撞安全。最后,调用 PerformanceTracker 对象的 ProcessActionStart() 方法。
internal void ProcessActionStart() { try { // Use the factory class to get all of the performance metrics that are being tracked // for MVC Actions this.performanceMetrics = PerformanceMetricFactory.GetPerformanceMetrics(actionInfo); // Iterate through each metric and call the OnActionStart() method // Start off a task to do this so it can it does not block and minimized impact to the user Task t = Task.Factory.StartNew(() => { foreach (PerformanceMetricBase m in this.performanceMetrics) { m.OnActionStart(); } }); this.stopwatch = Stopwatch.StartNew(); } catch (Exception ex) { String message = String.Format("Exception {0} occurred PerformanceTracker.ProcessActionStart(). Message {1}\nStackTrace {0}", ex.GetType().FullName, ex.Message, ex.StackTrace); Trace.WriteLine(message); } }
PerformanceMetricBase 对象和 PerformanceMetricFactory
更新实际性能计数器的任务是继承 PerformanceMetricBase 类的对象。这些对象作为PerformanceTracker 对象的中间人 ,并需要更新的任何性能计数器。代码分解为单独的一组对象允许要专注于管理全过程的测量性能的控制器操作和离开如何更新计数器对 PerformanceMetricBase 对象的详细信息的 PerformanceTracker 对象。如果我们想要添加额外的性能指标,可以通过简单地编写一个新的类,扩展了 PerformanceMetricBase 并不会受到 PerformanceTracker 的代码的干扰。
每个子类扩展 PerformanceMetricBase 负责更新对应的值到这篇文章前面定义的自定义性能计数器之一。因此,每个类将包含持有对 Metric.NET 的引用对象,他们是负责更新的成员变量。通常,这是一个单一的Metric.NET 对象。
PerformanceMetricBase 提供了两种虚拟方法,OnActionStart() 和 OnActionComplete() 子类别在哪里能够对性能计数器执行更新。需要覆盖至少一个方法实现或可重写这两种方法。 具体的实现代码放在Github: https://github.com/geffzhang/AspNetPerformanceMetrics
参考文章
Building Performance Metrics into ASP.NET MVC Applications