天天看點

Adhesive架構系列文章--應用程式資訊中心子產品使用

應用程式資訊中心 Application Infomcation Center 簡稱AIC其實是一套已經實作的程式,集應用程式資料收集、資料存儲以及背景查詢、報警為一體。主要的作用是實作網站特殊資訊(比如未處理異常)的監控和報警。在Adhesive中進行了重寫和升華,把功能分為日志、異常、性能和狀态幾個部分:

public class AppInfoCenterService
    {
        public static ILoggingService LoggingService
        {
            get
            {
                return LocalServiceLocator.GetService<ILoggingService>();
            }
        }

        public static IExceptionService ExceptionService
        {
            get
            {
                return LocalServiceLocator.GetService<IExceptionService>();
            }
        }

        public static IPerformanceService PerformanceService
        {
            get
            {
                return LocalServiceLocator.GetService<IPerformanceService>();
            }
        }

        public static IStateService StateService
        {
            get
            {
                return LocalServiceLocator.GetService<IStateService>();
            }
        }
    }      

我們可以直接通過AppInfoCenterService類通路到這些功能。

分别來看一下每一個接口,首先是日志:

public interface ILoggingService : IDisposable
    {
        void LogDebug(string message,
            ExtraInfo extraInfo = null, bool localOnly = false);

        void LogDebug(string message, string categoryName, string subcategoryName,
            ExtraInfo extraInfo = null, bool localOnly = false);

        void LogInfo(string message,
            ExtraInfo extraInfo = null, bool localOnly = false);

        void LogInfo(string message, string categoryName, string subcategoryName,
            ExtraInfo extraInfo = null, bool localOnly = false);

        void LogError(string message,
          ExtraInfo extraInfo = null, bool localOnly = false);

        void LogError(string message, string categoryName, string subcategoryName,
            ExtraInfo extraInfo = null, bool localOnly = false);

        void LogWarning(string message,
             ExtraInfo extraInfo = null, bool localOnly = false);

        void LogWarning(string message, string categoryName, string subcategoryName,
            ExtraInfo extraInfo = null, bool localOnly = false);
    }      

這裡要說明幾點:

1、之是以結合使用了重載和命名參數,是因為命名參數雖然靈活但是對于調用者來說代碼還是不夠簡潔,是以把不常用的開關作為命名參數。在這裡localOnly指的是是否隻是記錄本地日志,其實把這個參數設定為true的話相當于直接調用LocalLoggingService,隻不過這樣調用更統一一點。

2、對于日志,除了根據自己的需要限定不同的等級之外,還可以提供大類和小類,這是很常用的,比如我們可以在一個業務網站内部把大類寫為類名(頁面名),把小類寫為方法名,比如:

AppInfoCenterService.LoggingService.LogDebug(string.Format("記憶體隊列服務 '{0}' 調節最大項!", configuration.MemoryQueueName), categoryName: ServiceName, subcategoryName: configuration.MemoryQueueName);      

這是一條記憶體隊列服務中的日志,我們把大類設定為服務名也就是記憶體隊列服務,把小類設定為隊列的名字,這樣我們在背景可以很友善篩選記憶體隊列服務子產品中的日志。當然,如果不設定大類和小類的話,系統會自動使用General作為大類名,而小類名對于網站就自動設定為頁面名,對于普通應用程式就設定為調用方法所在類名。

3、對于ExtraInfo,這裡允許補充一些資訊,包括兩個單選過濾的欄位、兩個多選過濾的欄位、兩個文本搜尋的欄位、以及幾個字典用于儲存任意多隻是用于呈現的資料,比如:

var extraInfo = new ExtraInfo
            {
                DisplayItems = new Dictionary<string, string>()
                {
                    { "DisplayItem1", "DisplayItem1" },
                    { "DisplayItem2", "DisplayItem2" }
                },
                DropDownListFilterItem1 = stringfilterpool[rnd.Next(4)],
                DropDownListFilterItem2 = stringfilterpool[rnd.Next(4)],
                CheckBoxListFilterItem1 = stringfilterpool[rnd.Next(4)],
                CheckBoxListFilterItem2 = stringfilterpool[rnd.Next(4)],
            };

            AppInfoCenterService.LoggingService.LogError("測試日志", extraInfo: extraInfo);      

在有的時候這是很有用的,比如我們僅僅依靠大類和小類不能很友善篩選到需要的資料,還可以使用單選或多選過濾進一步定位日志,并且額外的的DisplayItems可以以KeyValue的形式存放任意多需要檢視的資料,這比手動拼接需要的資訊放入Message中友善很多了。

此外,還針對string做了日志服務的各個重載的擴充方法:

public static void LogDebug(this string message, ExtraInfo extraInfo = null, bool localOnly = false)
        {
            AppInfoCenterService.LoggingService.LogDebug(message, string.Empty, string.Empty, extraInfo, localOnly);
        }      

那麼就可以這樣調用了:

"清除曆史資料-删除表".LogInfo(databaseName, collectionName);      

再來看看異常服務:

public interface IExceptionService : IDisposable
    {
        void Handle(Exception exception,
            ExtraInfo extraInfo = null, bool localOnly = false);

        void Handle(Exception exception, string categoryName, string subcategoryName,
           ExtraInfo extraInfo = null, bool localOnly = false);

        void Handle(Exception exception, string description,
            ExtraInfo extraInfo = null, bool localOnly = false);

        void Handle(Exception exception, string categoryName, string subcategoryName, string description,
         ExtraInfo extraInfo = null, bool localOnly = false);
    }      

1、在這裡隻有一個Handle方法,具體是怎麼Handle的取決于配置,在稍候我們會統一來看看AIC的配置。同樣,可以為異常提供大類和小類。

2、在這裡提供了一個描述字元串,這麼做的目的是為了友善補充異常的一些資訊,比如位置資訊:

AppInfoCenterService.ExceptionService.Handle(ex, categoryName: ServiceName, subcategoryName: typeFullName, description: "更新中繼資料到資料庫出現錯誤", extraInfo: new ExtraInfo
                {
                    DisplayItems = new Dictionary<string, string>()
                    {
                        {"DatabaseName" , MongodbServerConfiguration.MetaDataDbName}, 
                        {"TableName", databaseDescription.DatabasePrefix}
                    }
                });      

這樣會比重新包裝一個異常然後設定它的Message并把原來的異常設定為内部異常來的好。

3、原先在設計的時候考慮增加一個bool類型的rethrow參數,表明是否要重新抛出異常,後來思考後覺得不需要也沒必要。因為一旦我們在架構内部再抛出這個異常的話就會丢失原來的堆棧,如果真的要重新抛出還是推薦在原來的catch{}處直接使用throw關鍵字。

當然,異常接口也做了擴充方法,這樣調用是不是友善很多了:

catch (Exception ex)
            {
                ex.Handle(WcfLogProvider.LogCategoryName, "ClientMessageInspector.BeforeSendRequest");
            }      

說到這裡想談談日志和異常,為什麼我們需要自己開發一套而不是使用企業庫或類似Elmah開源架構呢?因為很多地方不能滿足我們的需求,如果使用的話需要改或擴充相當多的東西:

1、我們會為所有資料附加大量的資訊,并且這些配置是可以根據每一個應用程式需要自己配置的(依賴于配置服務)。

2、我們需要把資料存儲的到Mongodb中去,并且使用統一的背景檢視資料(依賴于Mongodb資料服務)。

3、我們需要整合各種應用程式的監控和錯誤頁跳轉等。

其實說白了,把日志和異常資料集中儲存,這會比使用本地日志檔案方式儲存在各個Web伺服器上好很多:

1、首先是檢視友善,可以搜尋可以過濾,對于檔案日志的話要搜尋和檢視是很不友善的,并且根本不可能他保持大量結構化資料

2、其次是可以及時發現問題,比如可以根據資料量進行報警,一般保持在Web伺服器上的話,隻有發現問題才會去看而不是主動去看來發現問題

3、運維友善,可以及時删除過期的資料,可以對資料進行統計

第三個接口是性能服務:

public interface IPerformanceService : IDisposable
    {
        void StartPerformanceMeasure(string name, ExtraInfo extraInfo = null);

        void StartPerformanceMeasure(string name, string categoryName, string subcategoryName, ExtraInfo extraInfo = null);

        void SetPerformanceMeasurePoint(string name, string pointName);
    }      

性能服務的API(性能服務除了API部分還有架構内置的部分,這裡的AppInfoCenterService中的接口隻是AIC元件對于代碼方式提供的功能,還有很多功能是AIC自己實作的,不需要任何代碼)主要是通過代碼加點方式對代碼進行性能的衡量。比如:

AppInfoCenterService.PerformanceService.StartPerformanceMeasure("性能測試1");
            aa();
            AppInfoCenterService.PerformanceService.SetPerformanceMeasurePoint("性能測試1", "性能測試1aa");
            cc();
            AppInfoCenterService.PerformanceService.SetPerformanceMeasurePoint("性能測試1", "性能測試1cc");
            dd();
            AppInfoCenterService.PerformanceService.SetPerformanceMeasurePoint("性能測試1", "性能測試1dd");      

我們這樣就建立了一個名為性能測試1的測試項目,并且加了3個測試點,在這裡每一個測試點統計的是SetPerformanceMeasurePoint方法到之前那個SetPerformanceMeasurePoint或StartPerformanceMeasure之間的代碼消耗的CPU時間、逝去的時間等資訊,比如這些方法定義如下:

public void aa()
        {
            for (int i = 0; i < 2; i++)
            {
                bb();
                AppInfoCenterService.PerformanceService.SetPerformanceMeasurePoint("性能測試1", "性能測試1bb");
            }
        }

        public void bb()
        {
            Thread.Sleep(100);
        }

        public void cc()
        {
            Thread.Sleep(200);
        }

        public void dd()
        {
            for (long i = 0; i < 100000; i++)
            {
                Random r = new Random();
                double j = r.NextDouble();
            }
        }      

那麼我們就可以看到性能測試1這個測試項目中有一個性能測試1aa,總共消耗200毫秒的時間,其中有兩個性能測試1bb每一個分别消耗100毫秒的時間,但是它們都沒有消耗CPU時間等等。。在這裡CPU時間指占用CPU的時間,消耗的時間指的是程式執行需要的時間,是兩個不同的概念,往往消耗時間比CPU時間高很多。在這裡,我們不給出背景呈現的樣子,那是因為所有的這些資料記錄和呈現都是使用通用的Mongodb資料服務,在之後我們介紹通用Mongodb資料服務的時候會介紹。需要說明的是,我們往往是在遇到瓶頸的時候會采用代碼加點方式到線上排查性能問題,但在問題找到之後會去除相關的代碼(或者通過配置來限定一個收集資料的時間段,之後會介紹),畢竟進行相關資料的收集是本身也是消耗性能的。

第四個接口是狀态服務:

public interface IStateService : IDisposable
    {
        void Init(StateServiceConfiguration configuration);
    }      

狀态服務用于定期彙報一些程式内部的狀态,在這裡隻有一個初始化的接口。因為,狀态服務會根據配置定期調用回調方法去拉資料,然後送出,而不是手動定時調用狀态服務去記錄資料(那樣的話就和日志服務沒差別了)。其實,狀态服務也就是在内部使用了背景線程定時調用回調方法收集資料,然後使用Mongodb資料服務把資料進行入庫罷了。使用方式如下:

applicationStateService = AppInfoCenterService.StateService;
                applicationStateService.Init(new StateServiceConfiguration(typeof(ApplicationStateInfo).FullName, ApplicationStateService.GetState));      

這是一段AIC内部的代碼,之前也說過了,AIC内部本身就使用了一些狀态服務來定期彙報程式的狀态,這裡我們的applicationStateService字段是靜态字段,那是因為狀态服務作為一個長期駐留背景的服務内部需要有一個根來確定不被回收。現在來看一下配置類StateServiceConfiguration:

public class StateServiceConfiguration
    {
        public Func<IEnumerable<BaseInfo>> ReportStateFunc { get; private set; }

        public string TypeFullName { get; private set; }

        public StateServiceConfiguration(string typeFullName, Func<IEnumerable<BaseInfo>> reportStateFunc)
        {
            this.ReportStateFunc = reportStateFunc;
            this.TypeFullName = typeFullName;
        }
    }      

非常簡單,隻有狀态服務所彙報資料對象的類型完整名以及回調委托,委托傳回的是BaseInfo的集合,也就是所有需要通過狀态服務儲存的狀态至少都是繼承BaseInfo類的。

看到這裡,大家可能覺得這些API的使用都很簡單,但不知如何根據自己的需要對異常、日志、狀态以及性能進行配置。其實架構使用了統一的配置服務儲存所有配置,并且設定為每一個應用程式有自己獨立的配置(而不是全局配置),現在就來看一下有哪些資訊可以配置(在這裡會給出配置背景的一些截圖,需要注意配置背景是通用的,并不是根據每一個配置單獨制作的,Mongodb資料的背景也是這樣):

1、首先打開配置服務的背景找到需要配置的應用程式:

Adhesive架構系列文章--應用程式資訊中心子產品使用

2、可以看到裡面有一個非全局配置:

Adhesive架構系列文章--應用程式資訊中心子產品使用

3、裡面針對每一個子產品都有配置:

Adhesive架構系列文章--應用程式資訊中心子產品使用

4、首先要介紹的是包含資訊政策。我們知道很多時候我們希望記錄的日志和異常資訊能自動附加諸如時間、調用堆棧、頁面位址等資訊,那麼我們可以通過不同的包含資訊政策來靈活配置每一種日志或異常資訊需要自動附加哪些詳細的資訊。在這裡,我們預設提供了四種(也可以根據需要增加任意多種配置是非常靈活的):

Adhesive架構系列文章--應用程式資訊中心子產品使用

這裡看一個Simple的配置:

Adhesive架構系列文章--應用程式資訊中心子產品使用

這裡可以看到對于Simple這種政策,我們隻會自動包含Get資訊以及請求和響應的Cookie資訊。

5、那麼每一種日志或異常記錄對應那種IncludeInfoStrategy呢,如下圖:

Adhesive架構系列文章--應用程式資訊中心子產品使用

在這裡我們配置了每一種類型對應的包含資訊詳細程度的政策,我們進入LogLevel=Error的LogInfo進去看看:

Adhesive架構系列文章--應用程式資訊中心子產品使用

這裡表明了對于LogInfo這個完整類型,我們使用Full這種包含資訊的政策(錯誤日志當然要詳細點),但是我們怎麼限定條件呢?在Conditions裡面:

Adhesive架構系列文章--應用程式資訊中心子產品使用

在這裡增加了一個過濾條件,并且這個值是枚舉類型的(配置服務支援枚舉):

Adhesive架構系列文章--應用程式資訊中心子產品使用

之是以不能是字元串的是因為代碼裡面我們通過反射來根據目前資料對象中的值和配置的條件來判斷應該應用哪種包含資訊政策,一旦資料類型不一緻的話無法比對。看到這裡,是不是驚歎配置服務的靈活了,也就是說如果我們以後在代碼裡增加了什麼XXXInfo的話,其包含資訊的政策配置隻需要在配置背景直接完成,不需要添加一行代碼,當然如果沒有配置的話,就會使用預設值(全部為false)了。

6、介紹完了包含資訊政策,再來往下看:

Adhesive架構系列文章--應用程式資訊中心子產品使用

公共配置裡面暫時隻有一個開關,配置了是否嵌入基本資訊到頁面(這些資訊會以HTML注釋呈現,使用者不能看到,是以這個功能也隻是針對網站有用)。所謂基本資訊包括目前頁面、機器名、頁面執行時間。别小看了這些資料,如果網站采用負載均衡的話,那麼内嵌一個機器名會很有用,有助于排查問題;如果頁面采用緩存的話,那麼輸出一個時間則會很有用;而如果希望關注頁面性能的話,輸出一個頁面執行時間也是不錯的。這個功能不需要進行任何的編碼,通過HttpModule進行,是以,如果要使用應用程式資訊中心子產品的話,第一就是引用Adhesive.AppInfoCenter.Imp程式集,第二如果是網站的話需要配置HttpModule:

<httpModules>
      <add name="AppInfoCenterHttpModule" type="Adhesive.AppInfoCenter.Imp.AppInfoCenterHttpModule, Adhesive.AppInfoCenter.Imp"/>
    </httpModules>      

第三就是之前提到了需要在Global中添加架構的啟動和結束代碼。

7、然後是日志服務的配置:

Adhesive架構系列文章--應用程式資訊中心子產品使用

如果關閉開關的話,所有日志将不會記錄。具體日志的配置在政策清單配置中:

Adhesive架構系列文章--應用程式資訊中心子產品使用

在這裡的幾種級别的日志配置是固定的(如果新增也不會用到,如果删除那麼就隻會使用預設的了),可以點選進去看看每一個的配置:

Adhesive架構系列文章--應用程式資訊中心子產品使用

也就是說,對于Info這個級别的日志,我們需要同時記錄本地日志(基于本地日志服務)和遠端日志(基于Mongodb資料服務)。

8、然後是性能服務:

Adhesive架構系列文章--應用程式資訊中心子產品使用

在這裡有兩種架構内置的服務,一個是對于網站适用的,記錄執行慢的頁面的執行情況,一個是對于網站和普通程式都适用的用代碼加點方式記錄代碼性能(也就是之前提到的性能服務API)。首先看看前者:

Adhesive架構系列文章--應用程式資訊中心子產品使用

這裡的配置很簡單,隻是是否開啟的開關和閥值,也就是當頁面執行時間超過1秒時,這個頁面的執行情況就會記錄下來。那麼通過背景統計我們就可以看到哪些頁面執行比較慢,網站出現慢頁面的情況是增加了還是減少了等等。。再來看看性能測量的配置:

Adhesive架構系列文章--應用程式資訊中心子產品使用

這裡除了開關之外還可以指定一個閥值,當頁面慢到一定程式的時候才去彙報性能測試的結果。另外一個很有用的是我們可以指定測試的起始時間,比如一天,這樣可以又可以抓取到足夠的樣本又可以及時減少伺服器壓力。

9、然後是狀态服務的配置:

Adhesive架構系列文章--應用程式資訊中心子產品使用

很明顯,這是一個配置表,繼續往下看:

Adhesive架構系列文章--應用程式資訊中心子產品使用

在這裡可以看到有應用程式的狀态、網站請求狀态、wcf服務端和用戶端狀态以及Mongodb服務狀态。很明顯,系統内部自動會收集這些狀态資料,在這裡屬于AIC範疇的是前兩種狀态:

Adhesive架構系列文章--應用程式資訊中心子產品使用
Adhesive架構系列文章--應用程式資訊中心子產品使用

我們10秒一次彙報應用程式的狀态和網站的請求狀态(隻對網站适用)。應用程式狀态的資料結構如下:

[MongodbPersistenceEntity("State", DisplayName = "應用程式狀态", Name = "Application")]
    public class ApplicationStateInfo : BaseInfo
    {
        [MongodbPresentationItem(DisplayName = "程序名")]
        public string ProcessName { get; set; }

        [MongodbPresentationItem(DisplayName = "工作集記憶體")]
        public long WorkingSet64 { get; set; }

        [MongodbPresentationItem(DisplayName = "非分頁系統記憶體")]
        public long NonpagedSystemMemorySize64 { get; set; }

        [MongodbPresentationItem(DisplayName = "分頁記憶體")]
        public long PagedMemorySize64 { get; set; }

        [MongodbPresentationItem(DisplayName = "分頁的系統記憶體")]
        public long PagedSystemMemorySize64 { get; set; }

        [MongodbPresentationItem(DisplayName = "私有記憶體")]
        public long PrivateMemorySize64 { get; set; }

        [MongodbPresentationItem(DisplayName = "虛拟記憶體")]
        public long VirtualMemorySize64 { get; set; }

        [MongodbPresentationItem(DisplayName = "工作線程")]
        public int CurrentWorkThreadCount { get; set; }

        [MongodbPresentationItem(DisplayName = "完成端口線程")]
        public int CurrentCompletionPortThreadCount { get; set; }
    }      

而網站請求狀态的資料結構如下:

[MongodbPersistenceEntity("State", DisplayName = "網站請求狀态", Name = "WebsiteRequest")]
    public class WebsiteRequestStateInfo : BaseInfo
    {
        public Dictionary<string, WebsiteRequestStateItem> WebsiteRequestStateItems { get; set; }
    }      
public class WebsiteRequestStateItem
    {
        [MongodbPresentationItem(DisplayName = "目前請求數量")]
        public long CurrentRequestCount { get; set; }

        [MongodbPresentationItem(DisplayName = "最大請求數量")]
        public long MaxRequestCount { get; set; }

        [MongodbPresentationItem(DisplayName = "最大請求數量發生在")]
        public DateTime MaxRequestCountOccur { get; set; }

        [MongodbPresentationItem(DisplayName = "總共請求數量")]
        public long TotalRequestCount { get; set; }

        [MongodbPresentationItem(DisplayName = "總共請求執行時間")]
        public long TotalRequestExecutionTime { get; set; }

        [MongodbPresentationItem(DisplayName = "平均請求執行時間")]
        public long AverageRequestExecutionTime { get; set; }

        [MongodbPresentationItem(DisplayName = "最大請求執行時間")]
        public long MaxRequestExecutionTime { get; set; }

        [MongodbPresentationItem(DisplayName = "最大請求執行發生在")]
        public DateTime MaxRequestExecutionTimeOccur { get; set; }

        [MongodbPresentationItem(DisplayName = "上一次請求執行時間")]
        public long LastRequestExecutionTime { get; set; }

        [MongodbPresentationItem(DisplayName = "上一次請求執行發生在")]
        public DateTime LastRequestExecutionTimeOccur { get; set; }

        [MongodbPresentationItem(DisplayName = "頁面位址")]
        public string Url { get; set; }
    }      

每10秒都可以看到網站中所有頁面執行情況的最新資料。對于這兩種狀态資料的收集不需要編寫任何代碼,是由架構内部實作的。在這裡,我們也看到了其實所有要記錄到Mongodb中的資料都是通過特性的形式進行标注中繼資料的,我們要做的隻是定義資料結構和加上特性,之後的事情都交給Mongodb資料服務了。在之後的文章中我們會詳細介紹這些特性。

10、最後來看一下異常服務的配置:

Adhesive架構系列文章--應用程式資訊中心子產品使用

在這裡需要說明的是未處理異常的處理方式:

1)如果是本地請求則不會捕獲異常,還是可以看到黃頁。

2)如果不是本地請求則會看是否配置了跳轉(比如轉到統一的出錯站點),如果沒配置的話直接顯示配置的UnhandledExceptionMessage。

跳轉位址配置在處理政策中:

Adhesive架構系列文章--應用程式資訊中心子產品使用

這裡我們根據的是異常類的類名來比對政策,可以看到配置了網站未處理異常、Mvc網站處理異常、Wcf用戶端未處理異常、Wcf服務端未處理異常、處理異常以及應用程式域未處理異常的政策。所謂處理異常就是手動調用異常服務Handle()方法的異常,未處理異常就是非程式捕獲的,架構捕獲的會導緻黃頁的異常。我們來看看網站未處理異常的配置:

Adhesive架構系列文章--應用程式資訊中心子產品使用

在這裡要區分一下異常類型名和異常分類名,前者是諸如NullReferenceException、ArgumentException,後者是諸如WebSiteUnhandledExceptionInfo、HandledExceptionInfo。這麼做的目的是增加靈活性,比如我們可以定義一種BusinessException的異常,然後指定對于這種異常類型WebSiteUnhandledExceptionInfo的我們不需要跳轉到出錯頁面,也不需要記錄本地日志,而對于其它類型異常的話則還是使用預設的(在這裡我們對雖有異常類型都采取相同的政策,是以異常類型名字段留白)。這裡的政策是記錄本地日志、記錄遠端日志,不進行跳轉輸出錯誤提示語句,Http響應代碼為200。

至此配置介紹完了,本文一開始從代碼使用角度介紹了應用程式資訊中心子產品,然後從背景配置角度介紹了子產品的配置。之後的文章會從實作角度介紹其中的一些關鍵實作。

作者:

lovecindywang

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接,否則保留追究法律責任的權利。

繼續閱讀