分享Web應用運行的細節問題
在這個文章里,我將分享一下在iOpenWorks.com這個網站試運行中碰到的若干問題和解決方案,這些問題包含了:(1)如果通過ASP.NET MVC預編譯提高性能;(2)如果知道網站在運行中,用戶響應速度、網站異常信息、用戶操作習慣;(3)解決與DiscuzToolkit集成的線程同步問題。
1 ASP.NET MVC 3預編譯支持
提高網站性能,除了我們常見的壓縮、CDN、緩存之外,還有一個就是使用預編譯。不管是ASP.NET WebForm,或者是ASP.NET MVC,這些頁面在網站運行過程中,都是要先經過編譯處理的。因此,如果能在網站運行前對其進行編譯,那無疑能更好的提高網站的響應速度。因此,我們選擇了一個RazorGenerator來對所有的ASP.NET MVC 3的視圖進行編譯,這樣,在部署時僅需要將dll文件拷貝過去,而不再需要cshtml文件了。下面介紹如何使用它來實現預編譯。
1.1 下載安裝RazorGenerator
你可以在http://razorgenerator.codeplex.com/下載到RazorGenerator,這是一個VS 2010的擴展。下載完成后,就可以直接安裝了。接著你還需要下載源代碼,然后編譯一下,獲取編譯的RazorGenerator.Mvc.dll程序集。
1.2 改變視圖文件的生成方式
將所有的視圖的BuildAction改成None,并且將CustomTool改成RazorGenerator,這時候,你可以看到一個關聯的.generated.cs文件,這個文件就是預編譯的源碼文件了。
1.3 處理Helper
對于Helpr文件,處理方式有所不同。Helper文件一般放在App_Code文件夾里面。首先,你需要在Helper文件的第一行添加 @* Generator: MvcHelper *@ 來聲明一下,接著將BuildAction改成None,并且將CustomTool改成RazorGenerator
下面,還需要額外一個步驟,這個非常重要,否則編譯無法通過,那就是需要將.generated.cs文件的BuildAction由Content改為Compile。
1.4 注冊PrecompiledMvcEngine
下面我們在ASP.NET MVC 3項目中引用RazorGenerator.Mvc.dll這個程序集,然后定義一個PreApplicationStartCode,并在AssemblyInfo.cs文件中注冊這個PreApplicationStartCode。這樣,我們就注冊了PrecompiledMvcEngine了。
(1)在AssemblyInfo.cs注冊
- [assembly: PreApplicationStartMethod(
- typeof(UIShell.iOpenWorks.PreApplicationStartCode), "PreStart")]
(2)PreApplicationStartCode定義
- namespace UIShell.iOpenWorks
- {
- public class PreApplicationStartCode
- {
- private static bool _isStarting;
- public static void PreStart()
- {
- if (!_isStarting)
- {
- _isStarting = true;
- var engine = new PrecompiledMvcEngine(
- typeof(PreApplicationStartCode).Assembly);
- ViewEngines.Engines.Add(engine);
- VirtualPathFactoryManager.RegisterVirtualPathFactory(engine);
- }
- }
- }
- }
1.5 部署
這時候,部署網站就不再需要將視圖文件部署過去了,只需要拷貝dll文件和網站資源。注意,在Views下面已經沒有.cshtml文件了,也沒有App_Code文件,因為它們都被預編譯到了UIShell.iOpenWorks.dll這個程序集了。接下來,你就可以測試一下網站,享受一下預編譯帶來的性能提升了。
2 跟蹤網站運行情況
網站在內測期間,會碰到較多的問題。但是,這時候,用戶已經進來測試,你怎么能夠及時發現用戶響應速度、用戶訪問過程中網站異常信息以及用戶是如何來使用你的網站。這里,我們使用了log4net這個日志組件,它用于記錄:(1)用戶訪問了哪些頁面;(2)用戶在訪問頁面過程中,碰到了哪些異常;(3)每一個頁面的響應速度。下面,我來介紹如何記錄這些信息的。
2.1 在Global中,跟蹤每個用戶訪問的頁面,并且要記錄用戶響應的速度
- [ThreadStatic]
- private static Stopwatch _stopwatch;
- protected void Application_BeginRequest()
- {
- _stopwatch = Stopwatch.StartNew(); // 計時開始
- if (DiscuzHelper.IsLoggedIn()) // 記錄當前用戶
- {
- try
- {
- var user = DiscuzHelper.LoggedUser();
- if(user != null)
- {
- ThreadContext.Properties["user"] = user.UserName;
- return;
- }
- }
- catch (Exception ex)
- {
- _logger.Error("Failed to get the user name though the user is logged in.", ex);
- }
- }
- ThreadContext.Properties["user"] = string.Empty;
- if (Request != null) // 記錄當前用戶的IP
- {
- ThreadContext.Properties["ipaddress"] = Request.ServerVariables["REMOTE_ADDR"];
- }
- else
- {
- ThreadContext.Properties["ipaddress"] = string.Empty;
- }
- }
- protected void Application_EndRequest()
- {
- if (Request != null && _stopwatch != null && _logger != null) // 計時結束,就用戶響應時間和訪問頁面
- {
- _stopwatch.Stop();
- _logger.Debug(string.Format("Accessed page 'Response time: {0} ms, Url: {1}'.", _stopwatch.ElapsedMilliseconds, Request.Url));
- }
- }
2.2 在Global中,記錄系統的異常
- void Application_Error(Object sender, EventArgs ea)
- {
- if (Server != null)
- {
- Exception e;
- for (e = Server.GetLastError(); e != null; e = e.InnerException)
- {
- _logger.Error("Unhandled server exception thrown.", e);
- }
- }
- }
2.3 處理關鍵方法
下面,我還在關鍵方法記錄了用戶的操作異常信息、響應速度。比如我必須記錄了:(1)用戶注冊時響應速度、注冊時發生的異常、用戶登錄時響應速度、用戶登錄時發生的異常;(2)用戶在什么情況下嘗試下載iOpenWorksSDK這個免費插件框架;(3)嘗試下載時,會轉到注冊頁面,這時候用戶是否繼續注冊并下載,還是放棄。
對這些關鍵方法的記錄,有助于提高應用系統的易用性。通過日志,我們修復了與Discuz集成的很多問題,并且提高了用戶響應速度。
2.4 日志分析
下面,我們需要來看一下日志分析,這里我們在一個開源的LogViewer自定義了一下。通過對日志的分析,你就可以知道系統發生了什么異常、系統性能如何、用戶操作習慣、關鍵方法的信息。當然,你也可以打開日志文件直接查看,只是,那樣比較費勁。對了,在這里我們絕不記錄用戶的密碼,這太不職業道德了,此外,所有密碼都是加密的,避免“CSDN”!
(1)查看異常信息
(2)查看關鍵方法信息:用戶訪問習慣、響應性能等
3 解決DiscuzToolkit線程同步
網站的社區是與Discuz集成的,我們就用了DiscuzToolkit來集成。這是官方發布的類庫,但是依然問題一堆。最嚴重的2個問題就是線程同步引起的,可見Discuz這幫人都ASP.NET多線程模型壓根沒有當一回事,或者連線程安全都沒有注意到。下面就說一下碰到的2個線程安全問題。
(1)在注冊用戶時,碰到以下異常:當前會話所提交的call_id沒有大于前一次的call_id
Failed to get the user name though the user is logged in. Discuz.Toolkit.DiscuzException: Code: 103, Message: 當前會話所提交的call_id沒有大于前一次的call_id at Discuz.Toolkit.Util.GetResponse[T](String method_name, DiscuzParam[] parameters) in E:\Work\Design\Core\milestore 1\osgi\m10\uishell.iopenworks\DiscuzToolkit\Util.cs:line 97 at Discuz.Toolkit.DiscuzSession.GetUserInfo(Int64[] uids, String[] fields) in E:\Work\Design\Core\milestore 1\osgi\m10\uishell.iopenworks\DiscuzToolkit\DiscuzSession.cs:line 224 at Discuz.Toolkit.DiscuzSession.GetUserInfo(Int64 uid) in E:\Work\Design\Core\milestore 1\osgi\m10\uishell.iopenworks\DiscuzToolkit\DiscuzSession.cs:line 255 |
這個問題是由Discuz.Toolkit.Util的Sign方法引起的,在這里,它為每一個API請求生成一個call_id。
- list.Add(DiscuzParam.Create("call_id", DateTime.Now.Ticks));
如果你在當前線程API調用太勤快的話,DateTime.Now.Ticks會生成一樣的值,從而引發異常。因此,官方提議可以Sleep一下。因此,我們就需要改成如下:
- list.Add(DiscuzParam.Create("call_id", DateTime.Now.Ticks));
- // Avoid to generate same 'call_id' and throws an exception on '當前會話所提交的call_id沒有大于前一次的call_id'.
- Thread.Sleep(50);
但是這樣,依然是不過的,這個異常只是變得更加詭異了,讓你碰到機會少一點而已。你別忘了ASP.NET應用程序是多線程的,當兩個線程同時訪問時,依然可能獲得同一個call_id,于是,在碰到若干次這個問題后,我用以下方法來修復。
- lock (_syncRoot)
- {
- list.Add(DiscuzParam.Create("call_id", DateTime.Now.Ticks));
- // Avoid to generate same 'call_id' and throws an exception on '當前會話所提交的call_id沒有大于前一次的call_id'.
- Thread.Sleep(50);
- }
(2)注冊用戶時,碰到以下異常:An item with the same key has already been added.
[2012-04-07 17:11:30,818] [7] [ERROR] [AccountController] [49.72.46.135] []: System.ArgumentException: An item with the same key has already been added. at System.ThrowHelper.ThrowArgumentException(ExceptionResource resource) at System.Collections.Generic.Dictionary`2.Insert(TKey key, TValue value, Boolean add) at System.Collections.Generic.Dictionary`2.Add(TKey key, TValue value) at Discuz.Toolkit.Util.GetSerializer(Type t) in E:\Work\Design\Core\milestore 1\osgi\m10\uishell.iopenworks\DiscuzToolkit\Util.cs:line 157 at Discuz.Toolkit.Util.GetResponse[T](String method_name, DiscuzParam[] parameters) in E:\Work\Design\Core\milestore 1\osgi\m10\uishell.iopenworks\DiscuzToolkit\Util.cs:line 88 at Discuz.Toolkit.DiscuzSession.GetUserID(String username) in E:\Work\Design\Core\milestore 1\osgi\m10\uishell.iopenworks\DiscuzToolkit\DiscuzSession.cs:line 243 at UIShell.iOpenWorks.Controllers.AccountController.Register(DiscuzNewUser newUser, String returnUrl) in E:\Work\Design\Core\milestore 1\osgi\m10\uishell.iopenworks\UIShell.iOpenWorks\Controllers\AccountController.cs:line 53 |
你想想,要是用戶注冊時,動不動碰到注冊不成功,是多么窩火!!所以,我根據日志再次調查發現,DiscuzToolkit在使用靜態變量保存數據時,竟然不加鎖,太不拿Thread-Safe當回事了。這會異常也發生在Util類里,代碼如下,其中serializer_dict是靜態全局變量。
- serializer_dict.Add(type_hash, new XmlSerializer(t));
于是,我修改如下。這樣,徹底解決了和Discuz的集成了。
- private static Dictionary<int, XmlSerializer> serializer_dict = new Dictionary<int, XmlSerializer>();
- private static ReaderWriterLock _lock = new ReaderWriterLock();
- public static XmlSerializer GetSerializer(Type t)
- {
- int type_hash = t.GetHashCode();
- const int timeout = 5000;
- try
- {
- _lock.AcquireReaderLock(timeout);
- if (!serializer_dict.ContainsKey(type_hash))
- {
- _lock.UpgradeToWriterLock(timeout);
- if (!serializer_dict.ContainsKey(type_hash))
- {
- serializer_dict.Add(type_hash, new XmlSerializer(t));
- }
- }
- return serializer_dict[type_hash];
- }
- catch (ApplicationException ex)
- {
- throw new Exception("Accquire lock failed.", ex);
- }
- finally
- {
- if (_lock.IsReaderLockHeld)
- {
- _lock.ReleaseReaderLock();
- }
- else if (_lock.IsWriterLockHeld)
- {
- _lock.ReleaseWriterLock();
- }
- }
- }
OK,關于網站試運行中,最重要的幾點分享描述完了。順道介紹一下什么是iOpenWorks.com。iOpenWorks.com是一個免費工廠的開放倉庫,旨在向開發人員提供完全免費的標準化的OSGi.NET面向服務插件框架以及共享的插件倉庫,這樣,你既可以從插件倉庫使用別人插件,也可以共享自己的插件,互利共贏!
你也可以加入iOpenWorks交流群:121369588,Thanks。
原文鏈接:http://www.cnblogs.com/baihmpgy/archive/2012/04/09/2438720.html
【編輯推薦】