1. 程式人生 > >使用Metrics.NET 構建 ASP.NET MVC 應用程式的效能指標

使用Metrics.NET 構建 ASP.NET MVC 應用程式的效能指標

通常我們需要監測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:

Gauges

Gauge是最簡單的度量型別,只有一個簡單的返回值,例如,你的應用中有一個由第三方類庫中保持的一個度量值,你可以很容易的通過Gauge來度量他

        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,返回最新的一個請求的時間。

Counters

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開始,上述程式碼描述的當前的請求數。

Histograms-直方圖

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 Histograms

Uniform Histogram提供直方圖完整的生命週期內的有效的中位數,它會返回一箇中位值。例如:這個中位數是對所有值的直方圖進行了更新,它使用了一種叫做Vitter’s R的演算法,隨機選擇了一些線性遞增的樣本。

當你需要長期的測量,請使用Uniform Histograms。在你想要知道流資料的分佈中是否最近變化的話,那麼不要使用這種。

Biased 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的含義是一致的。

Timers

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);
        }

Timer需要的引數處理Name之外還需要,持續時間單位跟比率時間單位,持續時間單位是要度量的時間的期間的一個單位,在上面的例子中,就是MILLISECONDS,表示這段週期內的資料會按照毫秒來進行度量。比率時間單位跟Meters的一致。

Health Checks(健康檢查)

Meters提供一種一致的、統一的方法來對應用進行健康檢查,健康檢查是一個基礎的對應用是否正常執行的自我檢查。

Reporters報告

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)));
    image
 上面我們介紹了基於Metrics.NET構建的ASP.NET MVC 應用程式的效能指標,如下表所示:
計數器名稱 描述
Last Call Elapsed Time 已完成最後一次呼叫的所花費的時間。這是表示所有已完成請求的時間測量中的最新一個點。它不是平均值。
Request Timer 統計執行時間以及其分佈情況
POST & PUT Request Size histogram
POST/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

參考文章

http://www.cnblogs.com/yangecnu/p/Using-Metrics-to-Profiling-WebService-Performance.html