wcf利用IDispatchMessageInspector實現接口監控日志記錄和並發限流


一般對於提供出來的接口,雖然知道在哪些業務場景下才會被調用,但是不知道什么時候被調用、調用的頻率、接口性能,當出現問題的時候也不容易重現請求;為了追蹤這些內容就需要把每次接口的調用信息給完整的記錄下來,也就是記錄日志。日志中可以把調用方ip、服務器ip、調用時間點、時長、輸入輸出都給完整的記錄下來,有了這些數據,排查問題、重現異常、性能瓶頸都能准確的找到切入點。

這種功能,當然沒人想要去在每個Operation里邊插入一段代碼,如果有類似AOP的玩意就再好不過了。

wcf中有IDispatchMessageInspector分發消息檢查器這么個玩意,

 1 namespace System.ServiceModel.Dispatcher
 2 {
 3     using System;
 4     using System.ServiceModel;
 5     using System.ServiceModel.Channels;
 6     
 7     public interface IDispatchMessageInspector
 8     {
      // 接收請求后觸發, 此方法的返回值將作為BeforeSendReply的第二個參數correlcationState傳入
9 object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext);
      // 在輸出相應觸發
10 void BeforeSendReply(ref Message reply, object correlationState); 11 } 12 }

下面是英文解釋:

         

亦即允許我們對進出服務的消息進行檢查和修改,這看起來有點像mvc的過濾器

執行過程:  AfterReceiveRequest  ->  wcf操作 ->  BeforeSendReply

 

切入點找到了,需要做的就是實現這個接口方法,記錄日志,還可以計算一段時間內訪問的次數並設置相應的閥值也就是可以實現並發限流,限流的目的主要是防止惡意調用維持己方服務器的穩定。

實現思路: 

 1  public class ThrottleDispatchMessageInspector : IDispatchMessageInspector
 2     {
 3        public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
 4         {
 5        // [並發限流]
 6        以ContractName+OperationName 作為MemoryCache的鍵,值為調用次數; 設置絕對過期時間
 7        if(次數 > 設定的閥值) 
 8           request.Close(); 直接關閉請求 
 9         
10         // [log] 記錄請求輸入消息、開始時間、服務器ip、客戶端ip、 訪問的wcf契約和方法... 
11        LogVO log
12        // [log] 將日志實體作為返回值
13        return log;
14 
15          }
16        public void BeforeSendReply(ref Message reply, object correlationState) {
17         //[log]補充完整log的屬性: 請求結束時間, 調用時長...  然后將log丟入隊列(不直接插數據庫,防止日志記錄影響接口性能) 慢慢落地rds中
18         var log = correlationState as LogVO;
19         log => Queue 
20      }    
21   }    

完整的代碼如下:

 1 // 自定義分發消息檢查器
 2     public class ThrottleDispatchMessageInspector : IDispatchMessageInspector
 3     {
 4         //TODO 這兩個參數根據系統的配置處理方式存儲,作為示例就直接寫了 
 5         public static int throttleNum = 10; // 限流個數
 6         public static int throttleUnit = 4; // s
 7 
 8         CacheItemPolicy policy = new CacheItemPolicy();  //! 過期策略,保證第一個set和之后set的絕對過期時間保持一致
 9 
10         #region implement IDispatchMessageInspector
11 
12         // 此方法的返回值 將作為方法BeforeSendReply的第二個參數 object correlationState傳入
13         public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
14         {
15 
16 
17             // 獲取ContractName和OperationName 用來作為緩存鍵
18             var context = OperationContext.Current;
19             string contractName = context.EndpointDispatcher.ContractName;
20             string operationName = string.Empty;
21             if (context.IncomingMessageHeaders.Action == null)
22             {
23                 operationName = request.Properties.Values.LastOrDefault().ToString();
24             }
25             else
26             {
27                 if (context.IncomingMessageHeaders.Action.Contains("/"))
28                 {
29                     operationName = context.IncomingMessageHeaders.Action.Split('/').LastOrDefault();
30                 }
31             }
32             string throttleCacheKey = contractName + "_" + operationName;
33             // 緩存當前請求頻率, 以內存緩存System.Runtime.Caching.MemoryCache為例(.net4.0+)
34             ObjectCache cache = MemoryCache.Default;
35             var requestCount = cache.Get(throttleCacheKey);
36             int currRequestCount = 1;
37             if (requestCount != null && int.TryParse(requestCount.ToString(), out currRequestCount))
38             {
39                 // 訪問次數+1
40                 currRequestCount++;
41                 cache.Set(throttleCacheKey, currRequestCount, policy);  //必須保證過期策略和第一次set的時候一致,不然過期時間會有問題
42             }
43             else
44             {
45                 policy.AbsoluteExpiration = DateTime.Now.AddSeconds(throttleUnit);
46                 cache.Set(throttleCacheKey, currRequestCount, policy);
47             }
48 
49             // 如果當前請求數大於閥值,直接關閉
50             if (currRequestCount > throttleNum)
51             {
52                 request.Close();
53             }
54 
55             //作為返回值 傳給BeforeSendReply 
56             LogVO log = new LogVO
57             {
58                 BeginTime = DateTime.Now,
59                 ContractName = contractName,
60                 OperationName = operationName,
61                 Request = this.MessageToString(ref request),
62                 Response = string.Empty
63             };
64             return log;
65         }
66 
67         public void BeforeSendReply(ref Message reply, object correlationState)
68         {
69 
70             // 補充AfterReceiveRequest 傳遞過來的日志實體的屬性, 記錄
71             LogVO log = correlationState as LogVO;
72             log.EndTime = DateTime.Now;
73             log.Response = this.MessageToString(ref reply);
74             log.Duration = (log.EndTime - log.BeginTime).TotalMilliseconds;
75 
76             //attention 為不影響接口性能,日志實體push進隊列(redis .etc),然后慢慢落地
77             //TODO 這里直接寫文本啦~
78             try
79             {
80                 string logPath = "D:\\WcfLog.txt";
81                 if (!File.Exists(logPath))
82                 {
83                     File.Create(logPath);
84                 }
85                 StreamWriter writer = new StreamWriter(logPath, true);
86                 writer.Write(string.Format("at {0} , {1} is called , duration: {2} \r\n", log.BeginTime, log.ContractName + "." + log.OperationName, log.Duration));
87                 writer.Close();
88             }
89             catch (Exception ex) { }
90         }
91         #endregion
92 }
View Code

這邊需要注意 : 1. 類似於Web上的HttpContext.Current.Cache 這邊使用的是相應的內存緩存 MemoryCache,在更新緩存值的時候 過期策略要保持和初次設置的時候一致,如果沒傳入則緩存將不過期;

         2. 並發限制的配置根據自身系統框架來設定,不要寫死

         3. 日志記錄不要直接落rds,不然並發高了rds的連接數很容易爆,而且影響api的處理速度(可以push to redis, job/service land data)

         4. 讀取wcf的消息載體Message的數據后,要重新寫入(這個方法是直接用老外的)

接着,只要自定義服務行為在ApplyDispatchBehavior方法里將自定義的分發消息檢查器給注入到分發運行時就可以了,直接貼代碼:

 1     // 應用自定義服務行為的2中方式:
 2     // 1. 繼承Attribute作為特性 服務上打上標示
 3     // 2. 繼承BehaviorExtensionElement, 然后修改配置文件
 4     public class ThrottleServiceBehaviorAttribute : Attribute, IServiceBehavior
 5     {
 6         #region implement IServiceBehavior
 7         public void AddBindingParameters(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase, System.Collections.ObjectModel.Collection<ServiceEndpoint> endpoints, System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
 8         {
 9 
10         }
11 
12         public void ApplyDispatchBehavior(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase)
13         {
14             foreach (ChannelDispatcher channelDispather in serviceHostBase.ChannelDispatchers)
15             {
16                 foreach (var endpoint in channelDispather.Endpoints)
17                 {
18                     // holyshit DispatchRuntime 
19                     endpoint.DispatchRuntime.MessageInspectors.Add(new ThrottleDispatchMessageInspector());
20                 }
21             }
22         }
23 
24         public void Validate(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase)
25         {
26 
27         }
28         #endregion
29 
30         #region override BehaviorExtensionElement
31         //public override Type BehaviorType
32         //{
33         //    get { return typeof(ThrottleServiceBehavior); }
34         //}
35 
36         //protected override object CreateBehavior()
37         //{
38         //    return new ThrottleServiceBehavior();
39         //}
40         #endregion
41     }

這邊,由於本人比較懶,直接就繼承Attribute后 將服務行為貼在服務上;更好的做法是 繼承 BehaviorExtensionElement , 然后在配置文件里邊注冊自定義行為讓所有的接口走自定義檢查器的邏輯。

 

試驗: 閥值 10次每4秒

隨便弄個Service

 1  [ThrottleServiceBehavior]
 2     public class Service1 : IService1
 3     {
 4         public string GetData()
 5         {
 6             object num = MemoryCache.Default.Get("IService1_GetData") ?? "0";
 7 
 8             return string.Format("already request {0} times, Throttle is : {1} per {2} seconds", num, WcfDispatchMessageInspector.ThrottleDispatchMessageInspector.throttleNum, WcfDispatchMessageInspector.ThrottleDispatchMessageInspector.throttleUnit);
 9         }
10     }

 

1. 四秒內刷5次:    

2. 超過10次的時候: 

直接就斷開了~

 

完整的代碼

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM