分享Web應(yīng)用運(yùn)行的細(xì)節(jié)問題
在這個(gè)文章里,我將分享一下在iOpenWorks.com這個(gè)網(wǎng)站試運(yùn)行中碰到的若干問題和解決方案,這些問題包含了:(1)如果通過ASP.NET MVC預(yù)編譯提高性能;(2)如果知道網(wǎng)站在運(yùn)行中,用戶響應(yīng)速度、網(wǎng)站異常信息、用戶操作習(xí)慣;(3)解決與DiscuzToolkit集成的線程同步問題。
1 ASP.NET MVC 3預(yù)編譯支持
提高網(wǎng)站性能,除了我們常見的壓縮、CDN、緩存之外,還有一個(gè)就是使用預(yù)編譯。不管是ASP.NET WebForm,或者是ASP.NET MVC,這些頁面在網(wǎng)站運(yùn)行過程中,都是要先經(jīng)過編譯處理的。因此,如果能在網(wǎng)站運(yùn)行前對(duì)其進(jìn)行編譯,那無疑能更好的提高網(wǎng)站的響應(yīng)速度。因此,我們選擇了一個(gè)RazorGenerator來對(duì)所有的ASP.NET MVC 3的視圖進(jìn)行編譯,這樣,在部署時(shí)僅需要將dll文件拷貝過去,而不再需要cshtml文件了。下面介紹如何使用它來實(shí)現(xiàn)預(yù)編譯。
1.1 下載安裝RazorGenerator
你可以在http://razorgenerator.codeplex.com/下載到RazorGenerator,這是一個(gè)VS 2010的擴(kuò)展。下載完成后,就可以直接安裝了。接著你還需要下載源代碼,然后編譯一下,獲取編譯的RazorGenerator.Mvc.dll程序集。
1.2 改變視圖文件的生成方式
將所有的視圖的BuildAction改成None,并且將CustomTool改成RazorGenerator,這時(shí)候,你可以看到一個(gè)關(guān)聯(lián)的.generated.cs文件,這個(gè)文件就是預(yù)編譯的源碼文件了。
1.3 處理Helper
對(duì)于Helpr文件,處理方式有所不同。Helper文件一般放在App_Code文件夾里面。首先,你需要在Helper文件的第一行添加 @* Generator: MvcHelper *@ 來聲明一下,接著將BuildAction改成None,并且將CustomTool改成RazorGenerator
下面,還需要額外一個(gè)步驟,這個(gè)非常重要,否則編譯無法通過,那就是需要將.generated.cs文件的BuildAction由Content改為Compile。
1.4 注冊(cè)PrecompiledMvcEngine
下面我們?cè)贏SP.NET MVC 3項(xiàng)目中引用RazorGenerator.Mvc.dll這個(gè)程序集,然后定義一個(gè)PreApplicationStartCode,并在AssemblyInfo.cs文件中注冊(cè)這個(gè)PreApplicationStartCode。這樣,我們就注冊(cè)了PrecompiledMvcEngine了。
(1)在AssemblyInfo.cs注冊(cè)
- [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 部署
這時(shí)候,部署網(wǎng)站就不再需要將視圖文件部署過去了,只需要拷貝dll文件和網(wǎng)站資源。注意,在Views下面已經(jīng)沒有.cshtml文件了,也沒有App_Code文件,因?yàn)樗鼈兌急活A(yù)編譯到了UIShell.iOpenWorks.dll這個(gè)程序集了。接下來,你就可以測(cè)試一下網(wǎng)站,享受一下預(yù)編譯帶來的性能提升了。
2 跟蹤網(wǎng)站運(yùn)行情況
網(wǎng)站在內(nèi)測(cè)期間,會(huì)碰到較多的問題。但是,這時(shí)候,用戶已經(jīng)進(jìn)來測(cè)試,你怎么能夠及時(shí)發(fā)現(xiàn)用戶響應(yīng)速度、用戶訪問過程中網(wǎng)站異常信息以及用戶是如何來使用你的網(wǎng)站。這里,我們使用了log4net這個(gè)日志組件,它用于記錄:(1)用戶訪問了哪些頁面;(2)用戶在訪問頁面過程中,碰到了哪些異常;(3)每一個(gè)頁面的響應(yīng)速度。下面,我來介紹如何記錄這些信息的。
2.1 在Global中,跟蹤每個(gè)用戶訪問的頁面,并且要記錄用戶響應(yīng)的速度
- [ThreadStatic]
- private static Stopwatch _stopwatch;
- protected void Application_BeginRequest()
- {
- _stopwatch = Stopwatch.StartNew(); // 計(jì)時(shí)開始
- if (DiscuzHelper.IsLoggedIn()) // 記錄當(dāng)前用戶
- {
- 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) // 記錄當(dāng)前用戶的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) // 計(jì)時(shí)結(jié)束,就用戶響應(yīng)時(shí)間和訪問頁面
- {
- _stopwatch.Stop();
- _logger.Debug(string.Format("Accessed page 'Response time: {0} ms, Url: {1}'.", _stopwatch.ElapsedMilliseconds, Request.Url));
- }
- }
2.2 在Global中,記錄系統(tǒng)的異常
- 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 處理關(guān)鍵方法
下面,我還在關(guān)鍵方法記錄了用戶的操作異常信息、響應(yīng)速度。比如我必須記錄了:(1)用戶注冊(cè)時(shí)響應(yīng)速度、注冊(cè)時(shí)發(fā)生的異常、用戶登錄時(shí)響應(yīng)速度、用戶登錄時(shí)發(fā)生的異常;(2)用戶在什么情況下嘗試下載iOpenWorksSDK這個(gè)免費(fèi)插件框架;(3)嘗試下載時(shí),會(huì)轉(zhuǎn)到注冊(cè)頁面,這時(shí)候用戶是否繼續(xù)注冊(cè)并下載,還是放棄。
對(duì)這些關(guān)鍵方法的記錄,有助于提高應(yīng)用系統(tǒng)的易用性。通過日志,我們修復(fù)了與Discuz集成的很多問題,并且提高了用戶響應(yīng)速度。
2.4 日志分析
下面,我們需要來看一下日志分析,這里我們?cè)谝粋€(gè)開源的LogViewer自定義了一下。通過對(duì)日志的分析,你就可以知道系統(tǒng)發(fā)生了什么異常、系統(tǒng)性能如何、用戶操作習(xí)慣、關(guān)鍵方法的信息。當(dāng)然,你也可以打開日志文件直接查看,只是,那樣比較費(fèi)勁。對(duì)了,在這里我們絕不記錄用戶的密碼,這太不職業(yè)道德了,此外,所有密碼都是加密的,避免“CSDN”!
(1)查看異常信息
(2)查看關(guān)鍵方法信息:用戶訪問習(xí)慣、響應(yīng)性能等
3 解決DiscuzToolkit線程同步
網(wǎng)站的社區(qū)是與Discuz集成的,我們就用了DiscuzToolkit來集成。這是官方發(fā)布的類庫,但是依然問題一堆。最嚴(yán)重的2個(gè)問題就是線程同步引起的,可見Discuz這幫人都ASP.NET多線程模型壓根沒有當(dāng)一回事,或者連線程安全都沒有注意到。下面就說一下碰到的2個(gè)線程安全問題。
(1)在注冊(cè)用戶時(shí),碰到以下異常:當(dāng)前會(huì)話所提交的call_id沒有大于前一次的call_id
Failed to get the user name though the user is logged in. Discuz.Toolkit.DiscuzException: Code: 103, Message: 當(dāng)前會(huì)話所提交的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 |
這個(gè)問題是由Discuz.Toolkit.Util的Sign方法引起的,在這里,它為每一個(gè)API請(qǐng)求生成一個(gè)call_id。
- list.Add(DiscuzParam.Create("call_id", DateTime.Now.Ticks));
如果你在當(dāng)前線程API調(diào)用太勤快的話,DateTime.Now.Ticks會(huì)生成一樣的值,從而引發(fā)異常。因此,官方提議可以Sleep一下。因此,我們就需要改成如下:
- list.Add(DiscuzParam.Create("call_id", DateTime.Now.Ticks));
- // Avoid to generate same 'call_id' and throws an exception on '當(dāng)前會(huì)話所提交的call_id沒有大于前一次的call_id'.
- Thread.Sleep(50);
但是這樣,依然是不過的,這個(gè)異常只是變得更加詭異了,讓你碰到機(jī)會(huì)少一點(diǎn)而已。你別忘了ASP.NET應(yīng)用程序是多線程的,當(dāng)兩個(gè)線程同時(shí)訪問時(shí),依然可能獲得同一個(gè)call_id,于是,在碰到若干次這個(gè)問題后,我用以下方法來修復(fù)。
- lock (_syncRoot)
- {
- list.Add(DiscuzParam.Create("call_id", DateTime.Now.Ticks));
- // Avoid to generate same 'call_id' and throws an exception on '當(dāng)前會(huì)話所提交的call_id沒有大于前一次的call_id'.
- Thread.Sleep(50);
- }
(2)注冊(cè)用戶時(shí),碰到以下異常: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 |
你想想,要是用戶注冊(cè)時(shí),動(dòng)不動(dòng)碰到注冊(cè)不成功,是多么窩火?。∷?,我根據(jù)日志再次調(diào)查發(fā)現(xiàn),DiscuzToolkit在使用靜態(tài)變量保存數(shù)據(jù)時(shí),竟然不加鎖,太不拿Thread-Safe當(dāng)回事了。這會(huì)異常也發(fā)生在Util類里,代碼如下,其中serializer_dict是靜態(tài)全局變量。
- 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,關(guān)于網(wǎng)站試運(yùn)行中,最重要的幾點(diǎn)分享描述完了。順道介紹一下什么是iOpenWorks.com。iOpenWorks.com是一個(gè)免費(fèi)工廠的開放倉庫,旨在向開發(fā)人員提供完全免費(fèi)的標(biāo)準(zhǔn)化的OSGi.NET面向服務(wù)插件框架以及共享的插件倉庫,這樣,你既可以從插件倉庫使用別人插件,也可以共享自己的插件,互利共贏!
你也可以加入iOpenWorks交流群:121369588,Thanks。
原文鏈接:http://www.cnblogs.com/baihmpgy/archive/2012/04/09/2438720.html
【編輯推薦】