前言:


       CPU占用率低,内存还有许多空余,但网站无法响应,这就是网站挂死,通常也叫做hang。这种情况对于我这样既是CEO,又是CTO,还兼职扫地洗碗的个人站长来说根本就是家常便饭。以下是一次处理hang的经验及总结,前后用了一个月,不仅涉及程序排查,数据库优化,还有硬件升级的苦恼。其中辛酸苦辣只有经历过的站长才能体会,希望此文能对各位有所帮助!
 
       首先介绍一下网站基本情况,是一个在线小说阅读网站,每天有一定页面访问量,在优化开始前由两台服务器运行,均为Dell PowerEdge 2950,配置为一台Intel xeon E5410 2.33G*2 ,4GB ECC内存,另一台Intel xeon E5405 2.0G*2 ,2GB ECC内存。
网站程序采用asp.net 2.0,操作系统为 windows 2003 server 企业版,数据库为Ms sqlserver 2005。数据库放在配置较低的那台机器上,网站小说图片和章节内容用EMC Replistor同步。


 问题描述:


         大概在五月中旬,网站速度开始变慢,根据读者的描述每当中午以后每间隔一段时间,浏览器处于连接的状态,但是一直没有收到服务器的响应,打开下一个页面往往需要一两分钟。这种情况持续大概5分钟后打开速度恢复正常,然后隔相同时间又出现。
 登陆服务器观察CPU波动在10%-30%之间,w3wp.exe内存占用500M左右,应该不是内存溢出或者泄露。观察任务管理中“联网”一项,发现服务器流量有如下变化:
 
         

      显然在hang期间服务器没有对外发送任何数据。


 初步尝试:


        是什么造成服务器假死呢?根据读者的描述:“中午以后”可以知道hang出现在访问人数较大的时期,“间隔相同时间出现”提示我们有可能是服务器某些资源被耗尽造成死锁,然后服务器回收,接着再次耗尽。
        有了以上判断,我们就有路可循了,估计问题无非出在IIS6,ASP.NET 2.0 ,MSSQL SERVER 2005这三个服务的配置上:


            1. 检查IIS设置,主要检查应用程序池里的设置,看看核心请求队列有没有限制. 可以参考 《Windows Server 2003 性能调整指南
            2. 检查ASP.NET的http管道设置,具体是machine.config下 <system.web>的<processModel />节点,可以参考《ASP.net 2.0:我还有多少秘密你不知道?(1)》
            3 MS SQLSERVER暂时没有想到有什么需要修改的,保持默认设置。


       经过检查发现IIS设置没有问题,machine.config配置文件下processModel为autoConfig="true",于是把requestQueueLimit修改为15000,另外还修改了其他一些配置。上传后发现问题依旧,这下郁闷了,难道还有什么会造成服务器资源消耗后回收?


      答案是显然的,.NET环境下确实有这么一个东西: GC!
      会不会是由于GC在压缩和回收垃圾造成网站无法响应?如何监视GC运行的情况呢?


      答案也是显然的:性能监视器 ((转)Windows 性能监视器工具-perfmon
      性能监视器下的.Net CLR Memory Object有很多关于GC的计数器,能让我们了解GC的详细工作情况。但是经过观察发现在hang期间和hang之前都没有GC运行的明显变化,我的天啊,也不是GC的问题,那会是什么呢?


      相信很多和我一样的菜鸟小站长到这个地步已经垂头丧气了,难道我们现在应该到论坛或者博客园发帖然后祷告哪个高手好心帮帮忙花点小力气解决一下?我们是否还有一些遗忘的东西?或者说我们是不是忽略了问题的本质?这里的问题显然是由于IIS无法响应请求,确切的来说是ASP.NET无法正常的响应请求(为什么是ASP.NET而不是IIS?猜的!),那有没有办法知道ASP.NET在hang期间正在进行什么工作呢?有的!这就是.net爱好者人手一把的神器级工具:windbg


  神器windbg:


       Windbg是微软发布的一款用于调试和debug的免费工具,可以在http://www.microsoft.com/whdc/DevTools/Debugging/default.mspx下载。
      我们主要利用windbg抓取hang期间的dump用于分析,其他windbg的功能请参考园子里的资料。
      在服务器上下载并安装好windbg后,进入命令行,转到C:\Program Files\Debugging Tools for Windows (x86)\目录下,
      输入 adplus –hang –pn w3wp.exe – quiet,但是不要急着按回车,等到出现hang情况再按,这样我们就在目录下得到一个dump文件,其大小与w3wp.exe进程大小相同。(adplus及如何自动抓取参考园子里资料)。
      把dump文件下载到本地后,运行自己机器上的windbg打开dump文件(windbg初始配置请参考资料),输入:
      1 .load sos  
      此命令加载.net 调试器,具体设置请参考资料
      2 !threads   
      查看当前运行的进程,得到结果如下,省略了一部分:
0:000> !threads
ThreadCount: 245
UnstartedThread: 0
BackgroundThread: 245
PendingThread: 0
DeadThread: 0
Hosted Runtime: no
                                      PreEmptive   GC Alloc           Lock
       ID OSID ThreadOBJ    State     GC       Context       Domain   Count APT Exception
  11    1  9e4 000e96a0   1808220 Enabled  00000000:00000000 0010f680     1 MTA (Threadpool Worker)
  23    2  f48 000bd750      b220 Enabled  00000000:00000000 000eac88     0 MTA (Finalizer)
  25    3 1324 00104370   180b220 Enabled  00000000:00000000 0010f680     1 MTA (Threadpool Worker)
  26    4  4f8 00106980   180b220 Enabled  00000000:00000000 0010f680     1 MTA (Threadpool Worker)
  27    5  9ec 0010bd38    80a220 Enabled  00000000:00000000 000eac88     0 MTA (Threadpool Completion Port)
。。。。。。。


      我们可以看到当前一共有245个进程正在运行,根据熊力大师的《windows用户态高效排错》P164页上描述(刚好例子也是博客园),超过30个线程估计程序中有blocking发生,那我们245个线程就是由blokiiiiiiiiing发生了,仔细检查一下,没有线程处于GC状态,说明blocking不是因为GC,证实了我上面的推断。
      接着输入:
       ~* e!clrstack  看看这些线程都在执行什么,结果发现几乎所有的进程都在执行同一个过程,如下(阅读请从下往上看,因为是stack):

24aeeb60 7c9585ec [NDirectMethodFrameStandalone: 24aeeb60] Microsoft.Win32.Win32Native.CloseHandle(IntPtr)
24aeeb70 7927984d Microsoft.Win32.SafeHandles.SafeFileHandle.ReleaseHandle()
24aeed90 79e71b4c [GCFrame: 24aeed90]
24aeef88 79e71b4c [GCFrame: 24aeef88]
24aeeff4 79e71b4c [HelperMethodFrame_1OBJ: 24aeeff4] System.Runtime.InteropServices.SafeHandle.InternalDispose()
24aef04c 792e5e06 System.Runtime.InteropServices.SafeHandle.Dispose(Boolean)
24aef054 792e5ddd System.Runtime.InteropServices.SafeHandle.Dispose()
24aef05c 792eb580 System.IO.FileStream.Dispose(Boolean)
24aef090 792dfc82 System.IO.Stream.Close()
24aef09c 79271d90 System.IO.StreamReader.Dispose(Boolean)
24aef0c8 792d88ad System.IO.TextReader.Dispose()
24aef0d0 1d879f07 XXXXXX.BLL.Chapter.ChapterContent(Int32, Int32)
24aef110 1d879e14 XXXXX.BLL.Chapter.GetChapterContent(Int32, Int32)
24aef120 1d874a4e XXXXX.ReadContent.Page_Load(System.Object, System.EventArgs)

24aef164 66f2a7ff System.Web.Util.CalliHelper.EventArgFunctionCaller(IntPtr, System.Object, System.Object, System.EventArgs)
24aef174 660b2344 System.Web.Util.CalliEventHandlerDelegateProxy.Callback(System.Object, System.EventArgs)
24aef188 660ab864 System.Web.UI.Control.OnLoad(System.EventArgs)
24aef19c 660ab8a3 System.Web.UI.Control.LoadRecursive()
24aef1b4 660a7954 System.Web.UI.Page.ProcessRequestMain(Boolean, Boolean)
24aef30c 660a7584 System.Web.UI.Page.ProcessRequest(Boolean, Boolean)
24aef344 660a74b1 System.Web.UI.Page.ProcessRequest()
24aef37c 660a7446 System.Web.UI.Page.ProcessRequestWithNoAssert(System.Web.HttpContext)
24aef388 660a7422 System.Web.UI.Page.ProcessRequest(System.Web.HttpContext)
24aef39c 26bff7d5 ASP.readcontent_aspx.ProcessRequest(System.Web.HttpContext)
24aef3a0 660ad8f6 System.Web.HttpApplication+CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
24aef3d4 6608132c System.Web.HttpApplication.ExecuteStep(IExecutionStep, Boolean ByRef)
24aef414 6608c3a3 System.Web.HttpApplication+ApplicationStepManager.ResumeSteps(System.Exception)
24aef464 660808ac System.Web.HttpApplication.System.Web.IHttpAsyncHandler.BeginProcessRequest(System.Web.HttpContext, System.AsyncCallback, System.Object)
24aef480 66083e1c System.Web.HttpRuntime.ProcessRequestInternal(System.Web.HttpWorkerRequest)
24aef4b4 66083ac3 System.Web.HttpRuntime.ProcessRequestNoDemand(System.Web.HttpWorkerRequest)
24aef4c4 66082c5c System.Web.Hosting.ISAPIRuntime.ProcessRequest(IntPtr, Int32)
24aef6d8 79f68c4e [ContextTransitionFrame: 24aef6d8]
24aef70c 79f68c4e [GCFrame: 24aef70c]
24aef868 79f68c4e [ComMethodFrame: 24aef868]
OS Thread Id: 0x17d4 (109)


      这下问题很明显了,是由于XXXXXX.BLL.Chapter.ChapterContent(Int32, Int32)引发磁盘IO操作造成锁定,ChapterContent是读取小说章节内容的一个函数,小说内容保存在txt文件中,每一个章节对应一个txt文件,在显示章节内容时首先读取txt的文件然后打印到网页当中。具体代码如下:

        private static string ChapterContent(int bookId, int chapterId)
        {
            string filepath = siteRoot + string.Format(chapterPath, bookId.ToString(), chapterId.ToString());

            if (File.Exists(filepath))
            {
               return File.ReadAllText(filepath, Encoding.UTF8);
            }

            string bookPath = siteRoot + "/book/" + bookId.ToString();

            if (!Directory.Exists(bookPath))
            {
                Directory.CreateDirectory(bookPath);
            }
            return "此文章内容丢失,请复制网址通知管理员";
        }


      代码看起来没有问题,查看源码知道File.ReadAllText内部用了using读取文件,应该是及时释放了的,那么估计问题出在大量IO同时进行,导致非托管代码出现了blocking(SafeHandle是.net2.0增加的保证程序可靠性的东东,熊力大师的书上有描述),那么问题是,如何得到一个支持大量IO操作,具有线程安全的文件系统呢?等等,这话听起来好熟悉,这不就是数据库吗??!
OK,这下我们的信心又从火星飞回来了,赶快动手写程序把txt文件导入数据库中,经过3天时间的终于把200多万个TXT章节共计17GB导入到ms sqlserver2005中(夜间人少的时候进行)。本以为这下搞定了,却不知道是另一场噩梦的开始!
站长之路何其艰辛!


数据库噩梦:


      好不容易把txt章节导入到数据库里,运行一天下来速度的确有那么一点提升,可是读者的抱怨依然没有减少,每当高峰时期速度还是一如既往的慢。这次又是怎么回事呢?老办法运起神器windbg,按照上一节的操作后发现一共有101个线程,大部分线程都在执行:
2036ec90 7c9585ec [InlinedCallFrame: 2036ec90] <Module>.SNIReadSync(SNI_Conn*, SNI_Packet**, Int32)
2036ec8c 65226f0a SNINativeMethodWrapper.SNIReadSync(System.Runtime.InteropServices.SafeHandle, IntPtr ByRef, Int32)
2036ecfc 65226c14 System.Data.SqlClient.TdsParserStateObject.ReadSni(System.Data.Common.DbAsyncResult, System.Data.SqlClient.TdsParserStateObject)
2036ed34 65611041 System.Data.SqlClient.TdsParserStateObject.ReadNetworkPacket()
2036ed44 65228680 System.Data.SqlClient.TdsParserStateObject.ReadBuffer()
2036ed50 65228609 System.Data.SqlClient.TdsParserStateObject.ReadByte()
2036ed5c 65609b88 System.Data.SqlClient.TdsParser.Run(System.Data.SqlClient.RunBehavior, System.Data.SqlClient.SqlCommand, System.Data.SqlClient.SqlDataReader, System.Data.SqlClient.BulkCopySimpleResultSet, System.Data.SqlClient.TdsParserStateObject)
2036edc8 65220f12 System.Data.SqlClient.SqlDataReader.ConsumeMetaData()
2036eddc 65220a34 System.Data.SqlClient.SqlDataReader.get_MetaData()
2036ee08 6521f396 System.Data.SqlClient.SqlCommand.FinishExecuteReader(System.Data.SqlClient.SqlDataReader, System.Data.SqlClient.RunBehavior, System.String)
2036ee40 6521eff5 System.Data.SqlClient.SqlCommand.RunExecuteReaderTds(System.Data.CommandBehavior, System.Data.SqlClient.RunBehavior, Boolean, Boolean)
2036ee8c 6521edf3 System.Data.SqlClient.SqlCommand.RunExecuteReader(System.Data.CommandBehavior, System.Data.SqlClient.RunBehavior, Boolean, System.String, System.Data.Common.DbAsyncResult)
2036eed0 6521ed31 System.Data.SqlClient.SqlCommand.RunExecuteReader(System.Data.CommandBehavior, System.Data.SqlClient.RunBehavior, Boolean, System.String)
2036eeec 6521ec3e System.Data.SqlClient.SqlCommand.ExecuteReader(System.Data.CommandBehavior, System.String)
2036ef2c 6521ea5d System.Data.SqlClient.SqlCommand.ExecuteDbDataReader(System.Data.CommandBehavior)
2036ef30 6521fcab System.Data.Common.DbCommand.System.Data.IDbCommand.ExecuteReader(System.Data.CommandBehavior)
2036ef38 652300e3 System.Data.Common.DbDataAdapter.FillInternal(System.Data.DataSet, System.Data.DataTable[], Int32, Int32, System.String, System.Data.IDbCommand, System.Data.CommandBehavior)
2036ef90 65230010 System.Data.Common.DbDataAdapter.Fill(System.Data.DataSet, Int32, Int32, System.String, System.Data.IDbCommand, System.Data.CommandBehavior)
2036efd4 6522fe9f System.Data.Common.DbDataAdapter.Fill(System.Data.DataSet)
2036f004 1e5b9e73 xxxx.SQLServerDAL.SqlHelper.ExecuteDataset(System.Data.SqlClient.SqlConnection, System.Data.CommandType, System.String, System.Data.SqlClient.SqlParameter[])
2036f020 1e5b9dbf XXXX.SQLServerDAL.SqlHelper.ExecuteDataset(System.String, System.Data.SqlClient.SqlParameter[])
2036f050 1e5b9d48 XXXX.SQLServerDAL.Book.GetBookContent(Int32)
2036f064 1e5b879f XXXX.BLL.Book.GetBookContent(Int32)
2036f098 1e5b3315 XXXX.ReadBook.Page_Load(System.Object, System.EventArgs)

2036f0f8 66f2a7ff System.Web.Util.CalliHelper.EventArgFunctionCaller(IntPtr, System.Object, System.Object, System.EventArgs)
2036f108 660b2344 System.Web.Util.CalliEventHandlerDelegateProxy.Callback(System.Object, System.EventArgs)
2036f11c 660ab864 System.Web.UI.Control.OnLoad(System.EventArgs)
2036f130 660ab8a3 System.Web.UI.Control.LoadRecursive()
2036f148 660a7954 System.Web.UI.Page.ProcessRequestMain(Boolean, Boolean)
2036f2a0 660a7584 System.Web.UI.Page.ProcessRequest(Boolean, Boolean)
2036f2d8 660a74b1 System.Web.UI.Page.ProcessRequest()
2036f310 660a7446 System.Web.UI.Page.ProcessRequestWithNoAssert(System.Web.HttpContext)
2036f31c 660a7422 System.Web.UI.Page.ProcessRequest(System.Web.HttpContext)
2036f330 1b66aed5 ASP.readbook_aspx.ProcessRequest(System.Web.HttpContext)
2036f334 660ad8f6 System.Web.HttpApplication+CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
2036f368 6608132c System.Web.HttpApplication.ExecuteStep(IExecutionStep, Boolean ByRef)
2036f3a8 6608c3a3 System.Web.HttpApplication+ApplicationStepManager.ResumeSteps(System.Exception)
2036f3f8 660808ac System.Web.HttpApplication.System.Web.IHttpAsyncHandler.BeginProcessRequest(System.Web.HttpContext, System.AsyncCallback, System.Object)
2036f414 66083e1c System.Web.HttpRuntime.ProcessRequestInternal(System.Web.HttpWorkerRequest)
2036f448 66686c53 System.Web.RequestQueue.WorkItemCallback(System.Object)
2036f460 792c9dff System.Threading._ThreadPoolWaitCallback.WaitCallback_Context(System.Object)
2036f468 792f5611 System.Threading.ExecutionContext.runTryCode(System.Object)
2036f88c 79e71b4c [HelperMethodFrame_PROTECTOBJ: 2036f88c] System.Runtime.CompilerServices.RuntimeHelpers.ExecuteCodeWithGuaranteedCleanup(TryCode, CleanupCode, System.Object)
2036f8f4 792f5507 System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object)
2036f910 792e0175 System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object)
2036f928 792ca363 System.Threading._ThreadPoolWaitCallback.PerformWaitCallbackInternal(System.Threading._ThreadPoolWaitCallback)
2036f93c 792ca1f9 System.Threading._ThreadPoolWaitCallback.PerformWaitCallback(System.Object)
2036facc 79e71b4c [GCFrame: 2036facc]
2036fc18 79e71b4c [ContextTransitionFrame: 2036fc18]


      毫无疑问,是由于某些函数(如GetBookContent)操作数据库造成SQL Blocking,打开GetBookContent执行的SQL语句,如下(是一存储过程):


SELECT [ID], [Name] FROM dbo.Book_ChapterCategory  WHERE Book_ID = @bookid
SELECT dbo.Book_Chapter.Chapter_ID,dbo.Book_Chapter.ChapterName,dbo.Book_Chapter.Category_ID,dbo.Book_Chapter.IsVipChapter
FROM dbo.Book_Chapter  WHERE dbo.Book_Chapter.Book_ID = @bookid ORDER BY dbo.Book_Chapter.Chapter_ID


      两条很简单的查询语句,没有什么问题,操作数据库过程中也用了using确保连接关闭。那会是什么问题呢?再仔细讯问读者如何定义“服务器卡死了”的描述,发现问题和以前一样,也是无法响应一段时候后恢复正常再无法响应,那又是用尽某些资源然后回收?可是我们已经导入数据库了呀!数据库,数据库,噢!对了,ADO.NET处理数据库连接时候用到连接池技术,会不会是已经达到了默认上限(默认是100)了呢?运起另外一件神器性能监视器查看(参照上节文章),果然已经达到上限,这下好办,我们修改连接字符串为以下(请参考SqlConnection..::.ConnectionString 属性):


server=XXX;user id=XXX;password=XXX;database=XXX;Max Pool Size =500;Connection Lifetime=300;


      这下连接池上限改成500了,满怀信心的传上去,结果无比郁闷,单独web那台服务器运行比较正常,但是同时运行web和database那台服务器几乎是无法对外响应!OK,一台服务器不应该承担过多的职责,幸好家里还有一台淘汰下来的DELL PE 1850机器,赶紧让老妈跑到电信局去申请托管(我在上海读书,服务器放在家里的城市),被告知需要领导审批3天后才能上架,极其难受的度过3天,那台老服务器终于用上。赶紧把数据库转移到老服务器上,心想这下搞定了吧!


      现实和理想总是有一定差距,这就是人生。新上架的服务器虽然解决了“一段时间无法响应,过一会好了,然后再无法响应“的问题,但速度依然很慢,表现为性能监视器 Asp.net Application里Request in Applicatong Queue一直有许多请求未处理,但是不会像以前一样完全无法响应。还有新的问题出现,在涉及大数据量操作的时候会提示发生死锁!OMG,程序也没改变,访问量也没大变化,一直以来都没问题,怎么会发生死锁呢!那叫一个郁闷呀!


      由于之前没有发生过,那估计是新托管的服务器处理速度不够导致的死锁,经过搜索后得知打开MS SQL SERVER 2005 的READ_COMMITTED_SNAPSHOT选项(参看SQL Server 2005使用基于行版本控制的隔离级别初探)能够提供基于行版本控制的隔离级别,这意味着读取操作不会阻止更新操作。但是打开这个选项要求暂停数据库所有事务,所以我选择了另外一个方法,在查询语句添加 with(nolock)达到同样的效果.
      但是事与愿违,速度依然没有得到改善,看来只能升级数据库服务器的硬件,于是一个电话打到DELL订购一台PE 2950,花了1万7大洋,销售代表小姐甜甜地告诉我需要7个工作日内服务器才能寄到家里,这一天是2009-6-4号(值得纪念吗?)。
服务器还没到,那就不妨再检查看看还有什么可以优化的地方,既然是由于数据库服务器运行速度慢导致的阻塞,那能不能优化SQL操作来提高速度呢?首先找出具体引起死锁的语句,采用《检测死锁》一文中提供的储存过程发现这个存储过程有问题:


SELECT Book_Book.*, Book_Category.Description AS CategoryName
FROM Book_Book WITH(NOLOCK)  LEFT  JOIN
Book_Category WITH(NOLOCK)  ON  Book_Book.Category_ID = Book_Category.Category_ID
 WHERE Book_Book.Book_ID = @Book_ID
 UPDATE Book_Book WITH (ROWLOCK)  SET VisitedCount=VisitedCount+1,MonthHits=MonthHits+1,DayHits=DayHits+1,WeekVisitedCount=WeekVisitedCount+1 WHERE [Book_ID] = @Book_ID


      该过程检索小说信息后,顺便增加点击数,列Book_ID建立了聚集索引,注意到已经在select语句中加上with(nolock),为什么这个语句会引起死锁呢?关键在于后面一个update,大量的并发操作和较低的硬件配置使得服务器在执行此update时速度缓慢,而执行update会加上排它锁,进而造成死锁(但是我用了行级锁,为什么还会这样呢?),把update删除后大部分死锁也没有了,但是一个网站不能不统计点击数,咨询DUDU,他建议把点击数分出一个单独的表。但是这样做会是一个大的工程,所以只好在不繁忙的时段才统计点击数,等待新服务器的到来。
      在不改变表的情况下,接着对其他存储过程进行优化,借助 SQL SERVER2005中数据库引擎优化顾问建立索引,十分傻瓜式,但是这个东西也不能全信,有些读者反映一个页面打开特别慢,显示执行超时,检查后发现是如下SQL语句(简化过)


SELECT
一堆列名
FROM
Book_Book  LEFT JOIN Book_Chapter
 ON
Book_Book.LastChapterID = Book_Chapter.Chapter_ID
LEFT JOIN dbo.Book_ChapterCategory
 ON
Book_Chapter.Category_ID = Book_ChapterCategory.ID
WHERE
Book_Book.Moderator_ID =@UserID
ORDER BY
Book_Chapter.Addtime desc


      此存储过程设计三个表,在我机器上的数据量分别是
Book_Book                     15145行
Book_Chapter                 1006603行
Book_ChapterCategory        45928行


      在SQL Server Management Studio中执行该过程,并选择窗口栏上“包括执行计划”,得到结果集 11455行,执行计划如图:


      把鼠标移动到各个方框上会显示详细的执行信息,重点关注开销比较大的,移动到那个开销为53%的上面,显示:

 
     

       其中对象里显示的_dta_index_Book_Chapter_5_308964227__K1_K6_K5_2是数据库引擎优化顾问建立的非聚集索引,在Book_Chapter上建立[Chapter_ID] ASC, [Category_ID] ASC, [Addtime] ASC。
我们注意到物理类型为索引扫描,实际行数是1006603,等等,这不就是整个Book_Chapter表的所有行数吗?为什么要扫描整个表?按照我的思路应该首先从Book_Book里根据Moderator_ID的索引找出符合Moderator_ID =@UserID的11455行记录,然后根据Book_Book.LastChapterID = Book_Chapter.Chapter_ID从Book_Chapter找出11455行记录,再从Book_ChapterCategory找出11455条记录然后合并,整个过程因为有索引不需要进行整表扫描才对。
      于是删除Book_Chapter上的_dta_index_Book_Chapter_5_308964227__K1_K6_K5_2索引,并建立[chapter_ID ASC],[Book_ID] ASC的索引。再次执行,执行计划如图:
 
      

      其中开销66%的详细信息为:


 
      这下物理运算变成索引查找了,实际行数也变成了期望的11438行(有些书还没有章节,所以比11455少),我们再对SQL语句优化一下,改成(红色字体是改变过的地方):


SELECT
一堆列名
FROM
Book_Book  LEFT JOIN (Book_Chapter LEFT JOIN dbo.Book_ChapterCategory
 ON
Book_Chapter.Category_ID = Book_ChapterCategory.ID

 ON
Book_Book.LastChapterID = Book_Chapter.Chapter_ID
WHERE
Book_Book.Moderator_ID =@UserID
ORDER BY
Book_Chapter.Addtime desc


      执行结果如下:
 

      这次查询简单多了,至少能在一个图片完全显示整个查询结构。而且发现我们在上一步优化中建立的索引IX_Book_Chapter没用用上,而是用了Book_Chapter本来的主键聚集索引查找。
      经过测试优化后这条语句执行速度比优化前快了45%
      对所有操作频繁的存储过程进行优化后,速度没有明显的提高!依然很慢,看来真的是需要等待新的服务器了。


消费者的无奈:


      服务器是在6月4号购买的,6月7号妈妈打电话告诉我服务器到了,速度还挺快。大家首先来看看服务器的报价单,这几张图片是我截图下来的,实际的报价单被分成好几块,每一块都有很多重复的信息(用来干扰视线的?):
   

 

    

  

      各位有看出报价单里面有什么不对吗?反正当时我就看了CPU,内存和硬盘,检查没错后确认了,6月8日帮我装机器的yang13老师告诉我寄过来的电源是48V DC,他在学校里不能用,然后我打电话到电信局IDC,技术工程师告诉我他们主要用220V交流电,让我换了220V的再托管。我回头翻出报价单一看,在最后的倒数第二栏果然有一个小小的-48VDC,当时没在意,想着打个电话给DELL让他们换一换就行了。
第二天一大早我就打电话给DELL的销售代表,这位小姐说她不懂技术,又让我打给技术支持,技术说这个的确错了,而且只能更换,让我和销售调换,然后我再次打电话给销售,她说这个要CC(customer care客户关怀部)给我处理,她已经提交上去了。这时我就郁闷,我好好的买一个服务器怎么就需要被关怀了呢?不过算了,还是等等吧。
      一直等到星期五下午4点多(6月12号),一个自称是客户关怀部的小姐打电话给我说出了什么问题,电源坏了吗?我当时气愤的不行,这已经过了3天了,她们还搞不清楚电源是坏了还是错了,有这样的服务态度吗?然后我打电话给销售,没人接,打电话给技术,没人接。周末DELL公司不上班,星期一我再打给销售,发现换人了,原来卖给我的销售因某某某原因休假,接替她的人对这件事情什么都不知道,难道我要从头来一次?气愤!然后打电话给技术,技术又拨通了客户关怀部进行3方通话,这次客户关怀部说他们是按照订单上写的生产,没有出错,不给退换,我当时就郁闷,对她说你是在中国境内卖服务器为什么默认配48V 直流电源而不是中国标准220V交流电源?然后这位客户关怀部的小姐就不说按照订单生产,反而一再问我是不是提了什么特殊要求,我当时根本就没提什么要求,完全是按照正常流程,居然把责任推给我,这实在让人失望。然后我说你们不是有为了保证服务质量有电话录音的吗?找出来看看不就知道了,现在那名销售也知道去哪里了,你们想说什么就是什么了吧?


      电话那头沉默了几秒,然后说:我们是按照订单生产,不能换。


      我马上挂掉电话。到网上买了两个2950的220V电源,6月18号终于把新服务器装上,一切问题都解决了!


后记:


      这次hang排查过程前后花了一个月的时间,真可谓一波三折。
      总结出来的经验:
      1. 用性能监视器查看系统运行情况。
      2. 用windbg抓取dump后用~*e!clrstack看看hang期间在执行什么
      3. 对症下药
      通过这3步一般就能解决问题。但仅仅解决了技术上的问题,现实中有许多困难往往来自非技术上的,这对于像我这样单枪匹马的个人站长+技术爱好者来说是经常有的情况,但是一个好的站长必须不怕困难,坚持自己的信念才能走到最后。


感谢:


      感谢DUDU,小力,V.c Fan (范维肖),Raymond对我的帮助和支持,感谢yang13老师一直以来在生活,学习和技术上对我的帮助

 

posted on 2009-06-21 17:46  黑山小妖  阅读(6915)  评论(56编辑  收藏  举报