前言:
CPU占用率低,內(nèi)存還有許多空余,但網(wǎng)站無法響應(yīng),這就是網(wǎng)站掛死,通常也叫做hang。這種情況對(duì)于我這樣既是CEO,又是CTO,還兼職掃地洗碗的個(gè)人站長(zhǎng)來說根本就是家常便飯。以下是一次處理hang的經(jīng)驗(yàn)及總結(jié),前后用了一個(gè)月,不僅涉及程序排查,數(shù)據(jù)庫(kù)優(yōu)化,還有硬件升級(jí)的苦惱。其中辛酸苦辣只有經(jīng)歷過的站長(zhǎng)才能體會(huì),希望此文能對(duì)各位有所幫助!
首先介紹一下網(wǎng)站基本情況,是一個(gè)在線小說閱讀網(wǎng)站,每天有一定頁(yè)面訪問量,在優(yōu)化開始前由兩臺(tái)服務(wù)器運(yùn)行,均為Dell PowerEdge 2950,配置為一臺(tái)Intel xeon E5410 2.33G*2 ,4GB ECC內(nèi)存,另一臺(tái)Intel xeon E5405 2.0G*2 ,2GB ECC內(nèi)存。
網(wǎng)站程序采用ASP.NET 2.0,操作系統(tǒng)為 windows 2003 server 企業(yè)版,數(shù)據(jù)庫(kù)為Ms sqlserver 2005。數(shù)據(jù)庫(kù)放在配置較低的那臺(tái)機(jī)器上,網(wǎng)站小說圖片和章節(jié)內(nèi)容用EMC Replistor同步。
問題描述:
大概在五月中旬,網(wǎng)站速度開始變慢,根據(jù)讀者的描述每當(dāng)中午以后每間隔一段時(shí)間,瀏覽器處于連接的狀態(tài),但是一直沒有收到服務(wù)器的響應(yīng),打開下一個(gè)頁(yè)面往往需要一兩分鐘。這種情況持續(xù)大概5分鐘后打開速度恢復(fù)正常,然后隔相同時(shí)間又出現(xiàn)。
登陸服務(wù)器觀察CPU波動(dòng)在10%-30%之間,w3wp.exe內(nèi)存占用500M左右,應(yīng)該不是內(nèi)存溢出或者泄露。觀察任務(wù)管理中“聯(lián)網(wǎng)”一項(xiàng),發(fā)現(xiàn)服務(wù)器流量有如下變化:
顯然在hang期間服務(wù)器沒有對(duì)外發(fā)送任何數(shù)據(jù)。
初步嘗試:
是什么造成服務(wù)器假死呢?根據(jù)讀者的描述:“中午以后”可以知道hang出現(xiàn)在訪問人數(shù)較大的時(shí)期,“間隔相同時(shí)間出現(xiàn)”提示我們有可能是服務(wù)器某些資源被耗盡造成死鎖,然后服務(wù)器回收,接著再次耗盡。
有了以上判斷,我們就有路可循了,估計(jì)問題無非出在IIS6,ASP.NET 2.0 ,MSSQL SERVER 2005這三個(gè)服務(wù)的配置上:
1. 檢查IIS設(shè)置,主要檢查應(yīng)用程序池里的設(shè)置,看看核心請(qǐng)求隊(duì)列有沒有限制. 可以參考 《Windows Server 2003 性能調(diào)整指南》
2. 檢查ASP.NET的http管道設(shè)置,具體是machine.config下 的節(jié)點(diǎn),可以參考《ASP.NET 2.0:我還有多少秘密你不知道?(1)》
3 MS SQLSERVER暫時(shí)沒有想到有什么需要修改的,保持默認(rèn)設(shè)置。
經(jīng)過檢查發(fā)現(xiàn)IIS設(shè)置沒有問題,machine.config配置文件下processModel為autoConfig="true",于是把requestQueueLimit修改為15000,另外還修改了其他一些配置。上傳后發(fā)現(xiàn)問題依舊,這下郁悶了,難道還有什么會(huì)造成服務(wù)器資源消耗后回收?
答案是顯然的,.NET環(huán)境下確實(shí)有這么一個(gè)東西: GC!
會(huì)不會(huì)是由于GC在壓縮和回收垃圾造成網(wǎng)站無法響應(yīng)?如何監(jiān)視GC運(yùn)行的情況呢?
答案也是顯然的:性能監(jiān)視器 ((轉(zhuǎn))Windows 性能監(jiān)視器工具-perfmon)
性能監(jiān)視器下的.NET CLR Memory Object有很多關(guān)于GC的計(jì)數(shù)器,能讓我們了解GC的詳細(xì)工作情況。但是經(jīng)過觀察發(fā)現(xiàn)在hang期間和hang之前都沒有GC運(yùn)行的明顯變化,我的天啊,也不是GC的問題,那會(huì)是什么呢?
相信很多和我一樣的菜鳥小站長(zhǎng)到這個(gè)地步已經(jīng)垂頭喪氣了,難道我們現(xiàn)在應(yīng)該到論壇或者博客園發(fā)帖然后禱告哪個(gè)高手好心幫幫忙花點(diǎn)小力氣解決一下?我們是否還有一些遺忘的東西?或者說我們是不是忽略了問題的本質(zhì)?這里的問題顯然是由于IIS無法響應(yīng)請(qǐng)求,確切的來說是ASP.NET無法正常的響應(yīng)請(qǐng)求(為什么是ASP.NET而不是IIS?猜的!),那有沒有辦法知道ASP.NET在hang期間正在進(jìn)行什么工作呢?有的!這就是.NET愛好者人手一把的神器級(jí)工具:windbg
神器windbg:
Windbg是微軟發(fā)布的一款用于調(diào)試和debug的免費(fèi)工具,可以在http://www.microsoft.com/whdc/DevTools/Debugging/default.mspx下載。
我們主要利用windbg抓取hang期間的dump用于分析,其他windbg的功能請(qǐng)參考園子里的資料。
在服務(wù)器上下載并安裝好windbg后,進(jìn)入命令行,轉(zhuǎn)到C:/Program Files/Debugging Tools for Windows (x86)/目錄下,
輸入 adplus –hang –pn w3wp.exe – quiet,但是不要急著按回車,等到出現(xiàn)hang情況再按,這樣我們就在目錄下得到一個(gè)dump文件,其大小與w3wp.exe進(jìn)程大小相同。(adplus及如何自動(dòng)抓取參考園子里資料)。
把dump文件下載到本地后,運(yùn)行自己機(jī)器上的windbg打開dump文件(windbg初始配置請(qǐng)參考資料),輸入:
1 .load sos
此命令加載.NET 調(diào)試器,具體設(shè)置請(qǐng)參考資料
2 !threads
查看當(dāng)前運(yùn)行的進(jìn)程,得到結(jié)果如下,省略了一部分:
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)
。。。。。。。
我們可以看到當(dāng)前一共有245個(gè)進(jìn)程正在運(yùn)行,根據(jù)熊力大師的《windows用戶態(tài)高效排錯(cuò)》P164頁(yè)上描述(剛好例子也是博客園),超過30個(gè)線程估計(jì)程序中有blocking發(fā)生,那我們245個(gè)線程就是由blokiiiiiiiiing發(fā)生了,仔細(xì)檢查一下,沒有線程處于GC狀態(tài),說明blocking不是因?yàn)镚C,證實(shí)了我上面的推斷。
接著輸入:
~* e!clrstack 看看這些線程都在執(zhí)行什么,結(jié)果發(fā)現(xiàn)幾乎所有的進(jìn)程都在執(zhí)行同一個(gè)過程,如下(閱讀請(qǐng)從下往上看,因?yàn)槭莝tack):
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)引發(fā)磁盤IO操作造成鎖定,ChapterContent是讀取小說章節(jié)內(nèi)容的一個(gè)函數(shù),小說內(nèi)容保存在txt文件中,每一個(gè)章節(jié)對(duì)應(yīng)一個(gè)txt文件,在顯示章節(jié)內(nèi)容時(shí)首先讀取txt的文件然后打印到網(wǎng)頁(yè)當(dāng)中。具體代碼如下:
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 "此文章內(nèi)容丟失,請(qǐng)復(fù)制網(wǎng)址通知管理員";
}
代碼看起來沒有問題,查看源碼知道File.ReadAllText內(nèi)部用了using讀取文件,應(yīng)該是及時(shí)釋放了的,那么估計(jì)問題出在大量IO同時(shí)進(jìn)行,導(dǎo)致非托管代碼出現(xiàn)了blocking(SafeHandle是.NET2.0增加的保證程序可靠性的東東,熊力大師的書上有描述),那么問題是,如何得到一個(gè)支持大量IO操作,具有線程安全的文件系統(tǒng)呢?等等,這話聽起來好熟悉,這不就是數(shù)據(jù)庫(kù)嗎??!
OK,這下我們的信心又從火星飛回來了,趕快動(dòng)手寫程序把txt文件導(dǎo)入數(shù)據(jù)庫(kù)中,經(jīng)過3天時(shí)間的終于把200多萬(wàn)個(gè)TXT章節(jié)共計(jì)17GB導(dǎo)入到ms sqlserver2005中(夜間人少的時(shí)候進(jìn)行)。本以為這下搞定了,卻不知道是另一場(chǎng)噩夢(mèng)的開始!
站長(zhǎng)之路何其艱辛!
數(shù)據(jù)庫(kù)噩夢(mèng):
好不容易把txt章節(jié)導(dǎo)入到數(shù)據(jù)庫(kù)里,運(yùn)行一天下來速度的確有那么一點(diǎn)提升,可是讀者的抱怨依然沒有減少,每當(dāng)高峰時(shí)期速度還是一如既往的慢。這次又是怎么回事呢?老辦法運(yùn)起神器windbg,按照上一節(jié)的操作后發(fā)現(xiàn)一共有101個(gè)線程,大部分線程都在執(zhí)行:
2036ec90 7c9585ec [InlinedCallFrame: 2036ec90] .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]
毫無疑問,是由于某些函數(shù)(如GetBookContent)操作數(shù)據(jù)庫(kù)造成SQL Blocking,打開GetBookContent執(zhí)行的SQL語(yǔ)句,如下(是一存儲(chǔ)過程):
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
兩條很簡(jiǎn)單的查詢語(yǔ)句,沒有什么問題,操作數(shù)據(jù)庫(kù)過程中也用了using確保連接關(guān)閉。那會(huì)是什么問題呢?再仔細(xì)訊問讀者如何定義“服務(wù)器卡死了”的描述,發(fā)現(xiàn)問題和以前一樣,也是無法響應(yīng)一段時(shí)候后恢復(fù)正常再無法響應(yīng),那又是用盡某些資源然后回收?可是我們已經(jīng)導(dǎo)入數(shù)據(jù)庫(kù)了呀!數(shù)據(jù)庫(kù),數(shù)據(jù)庫(kù),噢!對(duì)了,ADO.NET處理數(shù)據(jù)庫(kù)連接時(shí)候用到連接池技術(shù),會(huì)不會(huì)是已經(jīng)達(dá)到了默認(rèn)上限(默認(rèn)是100)了呢?運(yùn)起另外一件神器性能監(jiān)視器查看(參照上節(jié)文章),果然已經(jīng)達(dá)到上限,這下好辦,我們修改連接字符串為以下(請(qǐng)參考SqlConnection..::.ConnectionString 屬性):
server=XXX;user id=XXX;password=XXX;database=XXX;Max Pool Size =500;Connection Lifetime=300;
這下連接池上限改成500了,滿懷信心的傳上去,結(jié)果無比郁悶,單獨(dú)web那臺(tái)服務(wù)器運(yùn)行比較正常,但是同時(shí)運(yùn)行web和database那臺(tái)服務(wù)器幾乎是無法對(duì)外響應(yīng)!OK,一臺(tái)服務(wù)器不應(yīng)該承擔(dān)過多的職責(zé),幸好家里還有一臺(tái)淘汰下來的DELL PE 1850機(jī)器,趕緊讓老媽跑到電信局去申請(qǐng)托管(我在上海讀書,服務(wù)器放在家里的城市),被告知需要領(lǐng)導(dǎo)審批3天后才能上架,極其難受的度過3天,那臺(tái)老服務(wù)器終于用上。趕緊把數(shù)據(jù)庫(kù)轉(zhuǎn)移到老服務(wù)器上,心想這下搞定了吧!
現(xiàn)實(shí)和理想總是有一定差距,這就是人生。新上架的服務(wù)器雖然解決了“一段時(shí)間無法響應(yīng),過一會(huì)好了,然后再無法響應(yīng)“的問題,但速度依然很慢,表現(xiàn)為性能監(jiān)視器 ASP.NET Application里Request in Applicatong Queue一直有許多請(qǐng)求未處理,但是不會(huì)像以前一樣完全無法響應(yīng)。還有新的問題出現(xiàn),在涉及大數(shù)據(jù)量操作的時(shí)候會(huì)提示發(fā)生死鎖!OMG,程序也沒改變,訪問量也沒大變化,一直以來都沒問題,怎么會(huì)發(fā)生死鎖呢!那叫一個(gè)郁悶呀!
由于之前沒有發(fā)生過,那估計(jì)是新托管的服務(wù)器處理速度不夠?qū)е碌乃梨i,經(jīng)過搜索后得知打開MS SQL SERVER 2005 的READ_COMMITTED_SNAPSHOT選項(xiàng)(參看SQL Server 2005使用基于行版本控制的隔離級(jí)別初探)能夠提供基于行版本控制的隔離級(jí)別,這意味著讀取操作不會(huì)阻止更新操作。但是打開這個(gè)選項(xiàng)要求暫停數(shù)據(jù)庫(kù)所有事務(wù),所以我選擇了另外一個(gè)方法,在查詢語(yǔ)句添加 with(nolock)達(dá)到同樣的效果.
但是事與愿違,速度依然沒有得到改善,看來只能升級(jí)數(shù)據(jù)庫(kù)服務(wù)器的硬件,于是一個(gè)電話打到DELL訂購(gòu)一臺(tái)PE 2950,花了1萬(wàn)7大洋,銷售代表小姐甜甜地告訴我需要7個(gè)工作日內(nèi)服務(wù)器才能寄到家里,這一天是2009-6-4號(hào)(值得紀(jì)念嗎?)。
服務(wù)器還沒到,那就不妨再檢查看看還有什么可以優(yōu)化的地方,既然是由于數(shù)據(jù)庫(kù)服務(wù)器運(yùn)行速度慢導(dǎo)致的阻塞,那能不能優(yōu)化SQL操作來提高速度呢?首先找出具體引起死鎖的語(yǔ)句,采用《檢測(cè)死鎖》一文中提供的儲(chǔ)存過程發(fā)現(xiàn)這個(gè)存儲(chǔ)過程有問題:
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
該過程檢索小說信息后,順便增加點(diǎn)擊數(shù),列Book_ID建立了聚集索引,注意到已經(jīng)在select語(yǔ)句中加上with(nolock),為什么這個(gè)語(yǔ)句會(huì)引起死鎖呢?關(guān)鍵在于后面一個(gè)update,大量的并發(fā)操作和較低的硬件配置使得服務(wù)器在執(zhí)行此update時(shí)速度緩慢,而執(zhí)行update會(huì)加上排它鎖,進(jìn)而造成死鎖(但是我用了行級(jí)鎖,為什么還會(huì)這樣呢?),把update刪除后大部分死鎖也沒有了,但是一個(gè)網(wǎng)站不能不統(tǒng)計(jì)點(diǎn)擊數(shù),咨詢DUDU,他建議把點(diǎn)擊數(shù)分出一個(gè)單獨(dú)的表。但是這樣做會(huì)是一個(gè)大的工程,所以只好在不繁忙的時(shí)段才統(tǒng)計(jì)點(diǎn)擊數(shù),等待新服務(wù)器的到來。
在不改變表的情況下,接著對(duì)其他存儲(chǔ)過程進(jìn)行優(yōu)化,借助 SQL SERVER2005中數(shù)據(jù)庫(kù)引擎優(yōu)化顧問建立索引,十分傻瓜式,但是這個(gè)東西也不能全信,有些讀者反映一個(gè)頁(yè)面打開特別慢,顯示執(zhí)行超時(shí),檢查后發(fā)現(xiàn)是如下SQL語(yǔ)句(簡(jiǎn)化過)
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
此存儲(chǔ)過程設(shè)計(jì)三個(gè)表,在我機(jī)器上的數(shù)據(jù)量分別是
Book_Book 15145行
Book_Chapter 1006603行
Book_ChapterCategory 45928行
在SQL Server Management Studio中執(zhí)行該過程,并選擇窗口欄上“包括執(zhí)行計(jì)劃”,得到結(jié)果集 11455行,執(zhí)行計(jì)劃如圖:
把鼠標(biāo)移動(dòng)到各個(gè)方框上會(huì)顯示詳細(xì)的執(zhí)行信息,重點(diǎn)關(guān)注開銷比較大的,移動(dòng)到那個(gè)開銷為53%的上面,顯示:
其中對(duì)象里顯示的_dta_index_Book_Chapter_5_308964227__K1_K6_K5_2是數(shù)據(jù)庫(kù)引擎優(yōu)化顧問建立的非聚集索引,在Book_Chapter上建立[Chapter_ID] ASC, [Category_ID] ASC, [Addtime] ASC。
我們注意到物理類型為索引掃描,實(shí)際行數(shù)是1006603,等等,這不就是整個(gè)Book_Chapter表的所有行數(shù)嗎?為什么要掃描整個(gè)表?按照我的思路應(yīng)該首先從Book_Book里根據(jù)Moderator_ID的索引找出符合Moderator_ID =@UserID的11455行記錄,然后根據(jù)Book_Book.LastChapterID = Book_Chapter.Chapter_ID從Book_Chapter找出11455行記錄,再?gòu)腂ook_ChapterCategory找出11455條記錄然后合并,整個(gè)過程因?yàn)橛兴饕恍枰M(jìn)行整表掃描才對(duì)。
于是刪除Book_Chapter上的_dta_index_Book_Chapter_5_308964227__K1_K6_K5_2索引,并建立[chapter_ID ASC],[Book_ID] ASC的索引。再次執(zhí)行,執(zhí)行計(jì)劃如圖:
其中開銷66%的詳細(xì)信息為:
這下物理運(yùn)算變成索引查找了,實(shí)際行數(shù)也變成了期望的11438行(有些書還沒有章節(jié),所以比11455少),我們?cè)賹?duì)SQL語(yǔ)句優(yōu)化一下,改成(紅色字體是改變過的地方):
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
執(zhí)行結(jié)果如下:
這次查詢簡(jiǎn)單多了,至少能在一個(gè)圖片完全顯示整個(gè)查詢結(jié)構(gòu)。而且發(fā)現(xiàn)我們?cè)谏弦徊絻?yōu)化中建立的索引IX_Book_Chapter沒用用上,而是用了Book_Chapter本來的主鍵聚集索引查找。
經(jīng)過測(cè)試優(yōu)化后這條語(yǔ)句執(zhí)行速度比優(yōu)化前快了45%
對(duì)所有操作頻繁的存儲(chǔ)過程進(jìn)行優(yōu)化后,速度沒有明顯的提高!依然很慢,看來真的是需要等待新的服務(wù)器了。
消費(fèi)者的無奈:
服務(wù)器是在6月4號(hào)購(gòu)買的,6月7號(hào)媽媽打電話告訴我服務(wù)器到了,速度還挺快。大家首先來看看服務(wù)器的報(bào)價(jià)單,這幾張圖片是我截圖下來的,實(shí)際的報(bào)價(jià)單被分成好幾塊,每一塊都有很多重復(fù)的信息(用來干擾視線的?):
各位有看出報(bào)價(jià)單里面有什么不對(duì)嗎?反正當(dāng)時(shí)我就看了CPU,內(nèi)存和硬盤,檢查沒錯(cuò)后確認(rèn)了,6月8日幫我裝機(jī)器的yang13老師告訴我寄過來的電源是48V DC,他在學(xué)校里不能用,然后我打電話到電信局IDC,技術(shù)工程師告訴我他們主要用220V交流電,讓我換了220V的再托管。我回頭翻出報(bào)價(jià)單一看,在最后的倒數(shù)第二欄果然有一個(gè)小小的-48VDC,當(dāng)時(shí)沒在意,想著打個(gè)電話給DELL讓他們換一換就行了。
第二天一大早我就打電話給DELL的銷售代表,這位小姐說她不懂技術(shù),又讓我打給技術(shù)支持,技術(shù)說這個(gè)的確錯(cuò)了,而且只能更換,讓我和銷售調(diào)換,然后我再次打電話給銷售,她說這個(gè)要CC(customer care客戶關(guān)懷部)給我處理,她已經(jīng)提交上去了。這時(shí)我就郁悶,我好好的買一個(gè)服務(wù)器怎么就需要被關(guān)懷了呢?不過算了,還是等等吧。
一直等到星期五下午4點(diǎn)多(6月12號(hào)),一個(gè)自稱是客戶關(guān)懷部的小姐打電話給我說出了什么問題,電源壞了嗎?我當(dāng)時(shí)氣憤的不行,這已經(jīng)過了3天了,她們還搞不清楚電源是壞了還是錯(cuò)了,有這樣的服務(wù)態(tài)度嗎?然后我打電話給銷售,沒人接,打電話給技術(shù),沒人接。周末DELL公司不上班,星期一我再打給銷售,發(fā)現(xiàn)換人了,原來賣給我的銷售因某某某原因休假,接替她的人對(duì)這件事情什么都不知道,難道我要從頭來一次?氣憤!然后打電話給技術(shù),技術(shù)又撥通了客戶關(guān)懷部進(jìn)行3方通話,這次客戶關(guān)懷部說他們是按照訂單上寫的生產(chǎn),沒有出錯(cuò),不給退換,我當(dāng)時(shí)就郁悶,對(duì)她說你是在中國(guó)境內(nèi)賣服務(wù)器為什么默認(rèn)配48V 直流電源而不是中國(guó)標(biāo)準(zhǔn)220V交流電源?然后這位客戶關(guān)懷部的小姐就不說按照訂單生產(chǎn),反而一再問我是不是提了什么特殊要求,我當(dāng)時(shí)根本就沒提什么要求,完全是按照正常流程,居然把責(zé)任推給我,這實(shí)在讓人失望。然后我說你們不是有為了保證服務(wù)質(zhì)量有電話錄音的嗎?找出來看看不就知道了,現(xiàn)在那名銷售也知道去哪里了,你們想說什么就是什么了吧?
電話那頭沉默了幾秒,然后說:我們是按照訂單生產(chǎn),不能換。
我馬上掛掉電話。到網(wǎng)上買了兩個(gè)2950的220V電源,6月18號(hào)終于把新服務(wù)器裝上,一切問題都解決了!
后記:
這次hang排查過程前后花了一個(gè)月的時(shí)間,真可謂一波三折。
總結(jié)出來的經(jīng)驗(yàn):
1. 用性能監(jiān)視器查看系統(tǒng)運(yùn)行情況。
2. 用windbg抓取dump后用~*e!clrstack看看hang期間在執(zhí)行什么
3. 對(duì)癥下藥
通過這3步一般就能解決問題。但僅僅解決了技術(shù)上的問題,現(xiàn)實(shí)中有許多困難往往來自非技術(shù)上的,這對(duì)于像我這樣單槍匹馬的個(gè)人站長(zhǎng)+技術(shù)愛好者來說是經(jīng)常有的情況,但是一個(gè)好的站長(zhǎng)必須不怕困難,堅(jiān)持自己的信念才能走到最后。
感謝:
感謝DUDU,小力,V.c Fan (范維肖),Raymond對(duì)我的幫助和支持,感謝yang13老師一直以來在生活,學(xué)習(xí)和技術(shù)上對(duì)我的幫助
NET技術(shù):一次掛死(hang)的處理過程及經(jīng)驗(yàn),轉(zhuǎn)載需保留來源!
鄭重聲明:本文版權(quán)歸原作者所有,轉(zhuǎn)載文章僅為傳播更多信息之目的,如作者信息標(biāo)記有誤,請(qǐng)第一時(shí)間聯(lián)系我們修改或刪除,多謝。