分享Web应用运行的细节问题

开发 前端
在这个文章里,我将分享一下在iOpenWorks.com这个网站试运行中碰到的若干问题和解决方案,这些问题包含了:(1)如果通过ASP.NET MVC预编译提高性能;(2)如果知道网站在运行中,用户响应速度、网站异常信息、用户操作习惯;(3)解决与DiscuzToolkit集成的线程同步问题。

在这个文章里,我将分享一下在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文件,这个文件就是预编译的源码文件了。

image

1.3 处理Helper

对于Helpr文件,处理方式有所不同。Helper文件一般放在App_Code文件夹里面。首先,你需要在Helper文件的第一行添加 @* Generator: MvcHelper *@ 来声明一下,接着将BuildAction改成None,并且将CustomTool改成RazorGenerator

image

下面,还需要额外一个步骤,这个非常重要,否则编译无法通过,那就是需要将.generated.cs文件的BuildAction由Content改为Compile。

image

1.4 注册PrecompiledMvcEngine

下面我们在ASP.NET MVC 3项目中引用RazorGenerator.Mvc.dll这个程序集,然后定义一个PreApplicationStartCode,并在AssemblyInfo.cs文件中注册这个PreApplicationStartCode。这样,我们就注册了PrecompiledMvcEngine了。

(1)在AssemblyInfo.cs注册

  1. [assembly: PreApplicationStartMethod(   
  2.     typeof(UIShell.iOpenWorks.PreApplicationStartCode), "PreStart")] 

(2)PreApplicationStartCode定义

  1. namespace UIShell.iOpenWorks   
  2. {   
  3.     public class PreApplicationStartCode   
  4.     {   
  5.         private static bool _isStarting;  
  6.         public static void PreStart()   
  7.         {   
  8.             if (!_isStarting)   
  9.             {   
  10.                 _isStarting = true;  
  11.                 var engine = new PrecompiledMvcEngine(   
  12.                     typeof(PreApplicationStartCode).Assembly);  
  13.                 ViewEngines.Engines.Add(engine);  
  14.                 VirtualPathFactoryManager.RegisterVirtualPathFactory(engine);   
  15.             }   
  16.         }   
  17.     }   

1.5 部署

这时候,部署网站就不再需要将视图文件部署过去了,只需要拷贝dll文件和网站资源。注意,在Views下面已经没有.cshtml文件了,也没有App_Code文件,因为它们都被预编译到了UIShell.iOpenWorks.dll这个程序集了。接下来,你就可以测试一下网站,享受一下预编译带来的性能提升了。

imageimage

2 跟踪网站运行情况

网站在内测期间,会碰到较多的问题。但是,这时候,用户已经进来测试,你怎么能够及时发现用户响应速度、用户访问过程中网站异常信息以及用户是如何来使用你的网站。这里,我们使用了log4net这个日志组件,它用于记录:(1)用户访问了哪些页面;(2)用户在访问页面过程中,碰到了哪些异常;(3)每一个页面的响应速度。下面,我来介绍如何记录这些信息的。

2.1 在Global中,跟踪每个用户访问的页面,并且要记录用户响应的速度

  1. [ThreadStatic]   
  2. private static Stopwatch _stopwatch;  
  3. protected void Application_BeginRequest()   
  4. {   
  5.     _stopwatch = Stopwatch.StartNew();  // 计时开始   
  6.     if (DiscuzHelper.IsLoggedIn()) // 记录当前用户   
  7.     {   
  8.         try   
  9.         {   
  10.             var user = DiscuzHelper.LoggedUser();   
  11.             if(user != null)   
  12.             {   
  13.                 ThreadContext.Properties["user"] = user.UserName;   
  14.                 return;   
  15.             }   
  16.         }   
  17.         catch (Exception ex)   
  18.         {   
  19.             _logger.Error("Failed to get the user name though the user is logged in.", ex);   
  20.         }   
  21.     }   
  22.               
  23.     ThreadContext.Properties["user"] = string.Empty;  
  24.     if (Request != null) // 记录当前用户的IP   
  25.     {   
  26.         ThreadContext.Properties["ipaddress"] = Request.ServerVariables["REMOTE_ADDR"];   
  27.     }   
  28.     else   
  29.     {   
  30.         ThreadContext.Properties["ipaddress"] = string.Empty;   
  31.     }   
  32. }  
  33. protected void Application_EndRequest()   
  34. {   
  35.     if (Request != null && _stopwatch != null && _logger != null)  // 计时结束,就用户响应时间和访问页面   
  36.     {   
  37.         _stopwatch.Stop();   
  38.         _logger.Debug(string.Format("Accessed page 'Response time: {0} ms, Url: {1}'.", _stopwatch.ElapsedMilliseconds, Request.Url));   
  39.     }   

2.2 在Global中,记录系统的异常

  1. void Application_Error(Object sender, EventArgs ea)   
  2. {   
  3.     if (Server != null)   
  4.     {   
  5.         Exception e;   
  6.         for (e = Server.GetLastError(); e != null; e = e.InnerException)   
  7.         {   
  8.             _logger.Error("Unhandled server exception thrown.", e);   
  9.         }   
  10.     }   

2.3 处理关键方法

下面,我还在关键方法记录了用户的操作异常信息、响应速度。比如我必须记录了:(1)用户注册时响应速度、注册时发生的异常、用户登录时响应速度、用户登录时发生的异常;(2)用户在什么情况下尝试下载iOpenWorksSDK这个免费插件框架;(3)尝试下载时,会转到注册页面,这时候用户是否继续注册并下载,还是放弃。

对这些关键方法的记录,有助于提高应用系统的易用性。通过日志,我们修复了与Discuz集成的很多问题,并且提高了用户响应速度。

2.4 日志分析

下面,我们需要来看一下日志分析,这里我们在一个开源的LogViewer自定义了一下。通过对日志的分析,你就可以知道系统发生了什么异常、系统性能如何、用户操作习惯、关键方法的信息。当然,你也可以打开日志文件直接查看,只是,那样比较费劲。对了,在这里我们绝不记录用户的密码,这太不职业道德了,此外,所有密码都是加密的,避免“CSDN”!

(1)查看异常信息

image

(2)查看关键方法信息:用户访问习惯、响应性能等

image

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。

  1. list.Add(DiscuzParam.Create("call_id", DateTime.Now.Ticks)); 

如果你在当前线程API调用太勤快的话,DateTime.Now.Ticks会生成一样的值,从而引发异常。因此,官方提议可以Sleep一下。因此,我们就需要改成如下:

  1. list.Add(DiscuzParam.Create("call_id", DateTime.Now.Ticks));   
  2. // Avoid to generate same 'call_id' and throws an exception on '当前会话所提交的call_id没有大于前一次的call_id'.   
  3. Thread.Sleep(50); 

但是这样,依然是不过的,这个异常只是变得更加诡异了,让你碰到机会少一点而已。你别忘了ASP.NET应用程序是多线程的,当两个线程同时访问时,依然可能获得同一个call_id,于是,在碰到若干次这个问题后,我用以下方法来修复。

  1. lock (_syncRoot)   
  2. {   
  3.     list.Add(DiscuzParam.Create("call_id", DateTime.Now.Ticks));   
  4.     // Avoid to generate same 'call_id' and throws an exception on '当前会话所提交的call_id没有大于前一次的call_id'.   
  5.     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是静态全局变量。

  1. serializer_dict.Add(type_hash, new XmlSerializer(t)); 

于是,我修改如下。这样,彻底解决了和Discuz的集成了。

  1. private static Dictionary<int, XmlSerializer> serializer_dict = new Dictionary<int, XmlSerializer>();   
  2. private static ReaderWriterLock _lock = new ReaderWriterLock();   
  3. public static XmlSerializer GetSerializer(Type t)   
  4. {   
  5.     int type_hash = t.GetHashCode();   
  6.     const int timeout = 5000;  
  7.     try   
  8.     {   
  9.         _lock.AcquireReaderLock(timeout);   
  10.         if (!serializer_dict.ContainsKey(type_hash))   
  11.         {   
  12.             _lock.UpgradeToWriterLock(timeout);   
  13.             if (!serializer_dict.ContainsKey(type_hash))   
  14.             {   
  15.                 serializer_dict.Add(type_hash, new XmlSerializer(t));   
  16.             }   
  17.         }  
  18.         return serializer_dict[type_hash];   
  19.     }   
  20.     catch (ApplicationException ex)   
  21.     {   
  22.         throw new Exception("Accquire lock failed.", ex);   
  23.     }   
  24.     finally   
  25.     {   
  26.         if (_lock.IsReaderLockHeld)   
  27.         {   
  28.             _lock.ReleaseReaderLock();   
  29.         }   
  30.         else if (_lock.IsWriterLockHeld)   
  31.         {   
  32.             _lock.ReleaseWriterLock();   
  33.         }   
  34.     }   

OK,关于网站试运行中,最重要的几点分享描述完了。顺道介绍一下什么是iOpenWorks.com。iOpenWorks.com是一个免费工厂的开放仓库,旨在向开发人员提供完全免费的标准化的OSGi.NET面向服务插件框架以及共享的插件仓库,这样,你既可以从插件仓库使用别人插件,也可以共享自己的插件,互利共赢!

你也可以加入iOpenWorks交流群:121369588,Thanks。

原文链接:http://www.cnblogs.com/baihmpgy/archive/2012/04/09/2438720.html

【编辑推荐】

  1.  10个基于Web的HTML5音乐播放器
  2. 浅谈Web自动化测试原理
  3. 分享21个最新的超酷web设计特效
  4. 开发者应该关注的五项Web新兴技术
  5. 手机WEBKIT引擎HTML元素定位和事例
责任编辑:林师授 来源: 陈贞宝的博客
相关推荐

2011-03-28 10:05:20

细节PlayBookAndroid

2009-12-29 16:08:41

Silverlight

2009-10-22 11:03:20

OSGi Web应用程

2014-05-26 09:13:46

DockerPython

2020-11-04 13:18:34

WebAPIWeb Share A

2010-04-28 17:01:30

Apusic负载均衡器

2023-11-20 08:02:49

2015-10-14 10:03:28

Web应用云服务PaaS

2010-05-28 10:23:59

JavaScriptWeb

2023-01-03 10:30:00

Java工具

2010-07-26 08:46:21

PHP负载均衡

2010-06-23 08:56:58

ASP.NET MVC

2011-03-25 11:06:46

2011-10-28 13:34:10

iPhone应用设计

2011-05-27 17:28:27

nofollowSEO

2022-08-09 09:55:23

Web方案

2009-01-15 09:43:51

Web架构设计缓存

2014-08-26 10:29:31

2012-03-24 20:40:16

Windows 8

2013-02-26 09:51:31

Windows 8应用异常问题
点赞
收藏

51CTO技术栈公众号