使用IronPython檢測ASP.NET程序狀況(下)
在上一篇文章中,我們在一個請求中執行了IronPython代碼,通過這個方法我們可以輕松地的檢查系統運行的狀態,或對系統進行一些簡單修改。但是這種做法只能檢查系統在當前時刻的狀態,在很多情況下,我們需要對系統的請求進行一段時間的采樣。對于簡單的數據(例如每秒執行的請求數量,請求時間),我們可以通過查看Performance Monitor中相關的計數器來獲得一些概要的數據。但是,如果我們需要獲取一些系統的詳細狀態,甚至是需要根據需要進行動態改變的自定義需求,則勢必要深入到系統內部進行數據采集。因此,我們可能需要讓代碼執行“一段時間”,并將直接結果進行匯總輸出。
讓代碼執行一段時間不難,簡單地使用Thread.Sleep便可,也不會造成什么性能或吞吐量上的損失。關鍵就在于,在代碼停留的這“一段時間”內,我們使用什么樣的做法來采集數據。這時候,老趙腦海中立即浮現出的便是HttpModule般監聽請求管道(Pipeline)中的各式事件。于是立馬寫出以下的代碼(在前文的示例基礎上進行修改):
protected void btnExecute_Click(object sender, EventArgs e) { ScriptEngine engine = Python.CreateEngine(); var scope = engine.CreateScope(); var script = engine.CreateScriptSourceFromString( this.txtCode.Text, SourceCodeKind.Statements); script.Execute(scope); TextWriter writer = new StringWriter(); scope.SetVariable("logger", writer); Action<HttpContext> traceRequest; if (scope.TryGetVariable<Action<HttpContext>>("traceRequest", out traceRequest)) { Action endTraceRequests; scope.TryGetVariable<Action>("endTraceRequests", out endTraceRequests); int waitTime; if (!scope.TryGetVariable<int>("waitTime", out waitTime)) { waitTime = 10000; } this.TraceRequests(traceRequest, waitTime, endTraceRequests); } this.txtOutput.Text = writer.ToString(); } private void TraceRequests(Action<HttpContext> traceRequest, int waitTime, Action endTraceRequests) { EventHandler handler = (sender, e) => { try { traceRequest((sender as HttpApplication).Context); } catch { } }; this.Context.ApplicationInstance.BeginRequest += handler; Thread.Sleep(waitTime); this.Context.ApplicationInstance.BeginRequest -= handler; if (endTraceRequests != null) endTraceRequests(); }
再編譯了IronPython代碼之后,我們會設法獲取其中的traceRequest和endTraceRequests函數,前者用于“記錄每個請求”,而后者用于采樣最后的“聚合”。此外,還會設法從代碼中獲取等待時間waitTime。然后,使用TraceRequest方法開始對當前請求進行采樣。具體做法為監聽當前Application的BeginRequest事件,并在每次獲得請求時調用traceRequest委托進行“記錄”。在等待時間過后,自然將委托從BeginRequest事件中剝離。最后,再通過endTraceRequests函數進行聚合輸出。
代碼邏輯很清晰,但可惜的是,上面這段代碼不能生效。具體原因不明,可能是ASP.NET對這方面進行了限制,使得我們無法在HttpModule之外為請求管道動態添加事件處理函數(存疑,求證)。對此我們只能進行讓步。不過,既然ASP.NET允許HttpModule監聽管道事件,那么我們不如事先準備一個HttpModule監聽各種事件,并且在合適的時候把這一事件轉發給IronPython函數。我們這里還是以BeginRequest事件為例:
public class IronPythonTraceModule : IHttpModule { private class TraceRequestEventArgs : EventArgs { public TraceRequestEventArgs(HttpContext context) { this.HttpContext = context; } public HttpContext HttpContext { get; private set; } } public void Dispose() { } public void Init(HttpApplication context) { context.BeginRequest += new EventHandler(OnBeginRequest); } static void OnBeginRequest(object sender, EventArgs e) { var traceRequest = IronPythonTraceModule.TraceRequest; if (traceRequest != null) { var context = (sender as HttpApplication).Context; traceRequest(null, new TraceRequestEventArgs(context)); } } private static event EventHandler<TraceRequestEventArgs> TraceRequest; public static void TraceRequests( Action<HttpContext> traceRequest, Action endTraceRequests, int milliseconds) { EventHandler<TraceRequestEventArgs> handler = (sender, e) => { try { traceRequest(e.HttpContext); } catch { } }; IronPythonTraceModule.TraceRequest += handler; Thread.Sleep(milliseconds); IronPythonTraceModule.TraceRequest -= handler; if (endTraceRequests != null) endTraceRequests(); } }
IronPythonTraceModule需要放入應用程序中,它時刻對請求管道的BeginRequest進行監聽,只是在合適的時候才會發起TraceRequest靜態事件(顯然這并不會對系統性能造成什么影響)。Module包函一個靜態的TraceRequests方法,這便是給外部調用的接口。可以發現這段代碼和之前的TraceRequests方法非常接近,唯一不同的只是動態添加/刪除處理函數的事件是IronPythonTraceModule.TraceRequest,而不是HttpApplication.BeginRequest。于是,原來的代碼也需要做一定修改:
protected void btnExecute_Click(object sender, EventArgs e) { ... Action<HttpContext> traceRequest; if (scope.TryGetVariable<Action<HttpContext>>("traceRequest", out traceRequest)) { Action endTraceRequests; scope.TryGetVariable<Action>("endTraceRequests", out endTraceRequests); int waitTime; if (!scope.TryGetVariable<int>("waitTime", out waitTime)) { waitTime = 10000; } // 以下代碼有所修改 IronPythonTraceModule.TraceRequests(traceRequest, endTraceRequests, waitTime); } this.txtOutput.Text = writer.ToString(); }
現在我們進行一番測試,簡單地檢測一下5秒鐘內收到 了多少請求:
使用自己的Module進行處理還有其他一些好處,比如可以提供更好的控制。事實上,目前文章里的解決方案有一些缺陷,并且肯定無法完全滿足真實需求。不過我們完全可以對目前的做法進行改進,例如:
- 保證代碼執行的線程安全,包括事件添加時和traceRequest函數執行時。
- 提供“采樣特性”,而不是對每個請求都使用traceRequest函數進行處理。
- 將Logger放入公用的空間,并在程序代碼中植入檢查功能,這樣便可以得知當前系統中每個功能的執行時間(這就是Profiler的功能,不是嗎?)。
不過,除了應對前一篇文章中所提到的負載均衡環境下的問題之外,這個解決方案還有另一個較為重要的情況需要特殊對待。如果使用目前的做法,每次采樣都是通過一個請求進行的,所以它并不會在在請求隊列阻塞時立即執行,但是采樣的常見場景便是在隊列阻塞時間檢查狀況,這顯然形成了一個矛盾。不過要解決這個問題并非難事,只要采樣不要通過IIS即可。例如您使用普通Socket,或“偷偷懶”使用WCF的TcpBinding進行采樣便可——只要和被檢查的應用程序在同一進程(即w3wp.exe,還有人用IIS5 嗎?)中,便可以使用任何方式進行通信。
改變、探索的過程,其實都是在追求一種編程的美感。老趙喜歡“不拘一格”,只要是有價值的都會去設法嘗試一番,探索的過程會遇到大量問題,解決問題后又能帶來成就感以及新的感受。因為美感和成就感,所以產生樂趣,因為樂趣才有源源不斷地動力。因為有這樣的切身體會,所以在這里也建議大家,不妨放開思路,海納百川。我們可以獲得和想到的東西,遠比預料中來的豐富。
相關文章:使用IronPython檢測ASP.NET程序狀況(上)