1、这本书对于初学者没有太大用处
2、这本书对于眼中只有架构、自己不写程序的、鄙视代码的人没有用处
3、这本书对于非微软的人用处不算太大,你不知道ms内部的数据结构,你没有private symbols。
4、这本书对于微软的人用处不算太大,搞debug的就那么几号人
5、这本书对于在客户现场被骂的狗血喷头的、自己即使架了.NET IDE也不知道如何找出问题的人很有用处
如果你是第五种人,疯狂购买吧!
此文是偶的偶像和哥们的,转过来,替他做一下宣传。偶会买10本,9本送人,有要的,现在报名。
(原来公司大量的COM+和.NET的case,都是熊老大做的)
Windows 高效排错
《Windows 高效排错》 可以在CSDN读书频道预览了
读书频道的排版有些问题,看起来不是很舒服。如果想看PDF的,可以在这里下载
纸板书籍估计在11月中下旬面世
现在在China-pub, dearbook等网上书店已经有介绍,地址分别
您的看法非常重要。如果你看过这本书的PDF,了解本书的内容和潜在读者,请您在上面的链接中留下你的观点,便于其它不了解这本书的人选择。
如果您通过这本书介绍的方法解决过实际的问题,非常希望您能够分享您的经验。您可以发邮件到
eparg@msn.com。如果您的分享能够帮助其它读者,我非常希望能够送书给您作为答谢。
如果您对书中所述内容有疑惑,也欢迎您写信来讨论。我会尽快回复。如果我们之间的讨论对其他读者有帮助,我也会放到网上,同时送书给您作为答谢。
===
上面是官腔了。我个人想说的话其实是:
1) 书前言里面说,"2007年年初我不再做技术支持。希望这本书能帮我记录下这一段美好的经历",其中最让我难忘的就是跟xiao,elan和leo站在水房门口讨论case的日子
2) 常来我blog踩的同学们,你们要书的话把地址留给我。等我拿到了我给大家发。(估计要等一段时间。出版社就给我6本。剩下的我争取去批发)
3) 跟我一起做过case的,如果你需要的话也把地址留给我
4) 多卖一本书,我可以多拿3块钱不到。整本书的稿费还不够在上海买半个车牌,或者买个厕所。我一点也不关心字面上的销量。我之所以花那么多时间来做这个亏本生意,目的只有一个,就是希望跟喜欢调试的人分享其中的过程,让真正需要这方面文章的人有所参考。所以如果你身边有这本书的潜在读者,劳烦你推荐一下。
(不好意思,置顶几天)
微软招聘SQL专家,如果您认为在下述方面有专长,请积极报名:
1、工作地点:上海;
2、很强的微软技术背景和产品熟悉度;
3、很强的客户沟通能力;
4、熟悉 SQLServer,熟悉Reporting Service开发、维护
5、熟悉 BI理念、产品,如果有 SharePoint Portal Server 2003 或 Office SharePoint Server 2007 实战经验将优先考虑;
请把个人简历发给我:juqiang1975@msn.com
12月份的那个sqlserver死锁分析(连接在下面),我从blogs.msdn上翻译过来的。不过很郁闷,我没有加“转载”或者“翻译”,被一眼尖的哥们儿发现了。
本来想修改原文,增加上“翻译”,但是现在进不去(貌似edit posts很慢),还是放一个单独的post说明此事吧!
郁闷啊!老天可以保证,偶的文品应该没问题的…………
不过,还是应该严重鄙视自己一下。
那个评论在这里:
http://blog.joycode.com/juqiang/archive/2006/12/18/89218.aspx#93122
(我处理的不当,导致了一些争吵,实在对不起大家!此文我从首页撤出,vc和czb兄弟的评论我适当删除。两个当事人我都了解一些,偶异常sorry!!!同时,此文不允许再评论,请后来看得人见谅!
总之一句话,此事我错了,即使想帮我辩解的人,也不要做。错就是错,对就是对,这个blog毕竟给所有人看的。偶极力欢迎大家继续对我的posts批评指正,大家一起提高。3x!)
C#代码如下:
private void button2_Click(object sender, System.EventArgs e)
{
System.Threading.Thread thread = new System.Threading.Thread( new System.Threading.ThreadStart(this.ShowOCX2));
thread.SetApartmentState(System.Threading.ApartmentState.STA);
thread.Start();
}
private void ShowOCX2()
{
Form2 form = new Form2();
form.ShowDialog();
}
Form2中是放入了一个ocx(delphi写的一个空ocx,任何自己的代码都没有)。
现在症状是,点击button2之后,出现了form2,直接关闭form2;再次点击button2,程序立刻crash掉。
抓dump(启用了pageheap),发现下面代码:(注意红色的)
Thread 4
Current frame: OCX2Proj1+0x3402
ChildEBP RetAddr Caller,Callee
0513f808 7c9237bf ntdll!ExecuteHandler2+0x26
0513f82c 7c92378b ntdll!ExecuteHandler+0x24, calling ntdll!ExecuteHandler2
0513f850 7c957860 ntdll!RtlDispatchException+0xb1, calling ntdll!RtlpExecuteHandlerForException
0513f874 060e3407 OCX2Proj1+0x3407
0513f880 7c9211a7 ntdll!LdrpCallInitRoutine+0x14
0513f884 7c93e6f4 ntdll!LdrUnloadDll+0x41c, calling ntdll!LdrpCallInitRoutine
0513f888 7c80abf7 kernel32!FreeLibrary+0x3f, calling kernel32!LdrUnloadDll
0513f890 769c3456 ole32!CClassCache::CFinishComposite::Finish+0x1d
0513f894 7c988c37 ntdll!RtlpDphNeedToTrimDelayedFreeQueue+0x3f, calling ntdll!RtlLeaveCriticalSection
0513f8a8 7c98acda ntdll!RtlpDphNormalHeapFree+0xb1, calling ntdll!RtlpDphNeedToTrimDelayedFreeQueue
0513f8bc 7c98770b ntdll!RtlpDebugPageHeapLeaveCritSect+0x10, calling ntdll!RtlLeaveCriticalSection
0513f8c8 7c98af7d ntdll!RtlpDebugPageHeapFree+0x192, calling ntdll!RtlpDebugPageHeapLeaveCritSect
0513f8dc 7c92eafa ntdll!KiUserExceptionDispatcher+0xe, calling ntdll!RtlDispatchException
0513fbdc 060e3402 OCX2Proj1+0x3402 ====> Exception cxr@513f910
0513fb00 7c9306eb ntdll!RtlAllocateHeap+0xeac, calling ntdll!_SEH_epilog
0513fc70 7c9211a7 ntdll!LdrpCallInitRoutine+0x14
0513fc90 7c93e6f4 ntdll!LdrUnloadDll+0x41c, calling ntdll!LdrpCallInitRoutine
0513fcf4 7c939213 ntdll!LdrShutdownThread+0xd7, calling ntdll!LdrpCallInitRoutine
0513fcf8 7c80c096 kernel32!ExitThread+0x3e, calling kernel32!LdrShutdownThread
0513fd70 77d193e9 user32!NtUserPeekMessage+0xc
然后切换进去
0:004> .cxr 513f910
eax=f0f0f0f0 ebx=0000000f ecx=00000000 edx=f0f0f001 esi=06146590 edi=06149638
eip=060e3402 esp=0513fbdc ebp=0513fbf0 iopl=0 nv up ei ng nz na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010286
OCX2Proj1+0x3402:
060e3402 8b08 mov ecx,dword ptr [eax] ds:0023:f0f0f0f0=????????
0:004> kb
*** Stack trace for last set context - .thread/.cxr resets it
ChildEBP RetAddr Args to Child
WARNING: Stack unwind information not available. Following frames may be wrong.
0513fbf0 060e3f10 0513fc64 060e3cff 0513fc10 OCX2Proj1+0x3402
0513fc10 060e4246 0513fc70 0513fc60 06149664 OCX2Proj1+0x3f10
0513fc70 7c9211a7 060e0000 00000000 00000000 OCX2Proj1+0x4246
0513fc90 7c93e6f4 061467b0 060e0000 00000000 ntdll!LdrpCallInitRoutine+0x14
0513fd88 7c80abf7 060e0000 0513fdd0 0513fdf4 ntdll!LdrUnloadDll+0x41c
0513fd9c 769c3442 060e0000 0513fe18 769c3456 kernel32!FreeLibrary+0x3f
0513fda8 769c3456 0513fddc 76ab67e0 00000000 ole32!CClassCache::CDllPathEntry::CFinishObject::Finish+0x2f
0513fdbc 769b2557 76991ab0 00000000 00000000 ole32!CClassCache::CFinishComposite::Finish+0x1d
0513fe18 769b2243 0133d878 00000080 00000001 ole32!CClassCache::CleanUpDllsForApartment+0x1b5
0513fe44 769b2171 00000000 0513fe90 76ab67e8 ole32!FinishShutdown+0xcd
0513fe60 769af221 00000000 00000000 0133d878 ole32!ApartmentUninitialize+0x7e
0513fe78 769aee88 0513fe90 00000000 00000001 ole32!wCoUninitialize+0x41
0513fe94 769c31f0 76990000 769ad1a2 0513fec4 ole32!CoUninitialize+0x5b
0513fe9c 769ad1a2 0513fec4 769ad141 76990000 ole32!DoThreadSpecificCleanup+0x47
0513fea4 769ad141 76990000 00000003 00000000 ole32!ThreadNotification+0x37
0513fec4 769ad0e9 76990000 00000003 00000000 ole32!DllMain+0x147
0513fee4 7c9211a7 76990000 00000003 00000000 ole32!_DllMainCRTStartup+0x52
0513ff04 7c939213 769ad0a1 76990000 00000003 ntdll!LdrpCallInitRoutine+0x14
0513ff7c 7c80c096 00000000 00000000 01321d40 ntdll!LdrShutdownThread+0xd7
0513ffb4 7c80b688 00000000 00000000 00000000 kernel32!ExitThread+0x3e
最上面三行就是delphi写的那个空白ocx,下面三个是各自的代码:
0:004> u OCX2Proj1+0x4246
OCX2Proj1+0x4246:
060e4246 ?? ???
^ Memory access error in 'u OCX2Proj1+0x4246'
0:004> u OCX2Proj1+0x3f10
OCX2Proj1+0x3f10:
060e3f10 ?? ???
^ Memory access error in 'u OCX2Proj1+0x3f10'
0:004> u OCX2Proj1+0x3402
OCX2Proj1+0x3402:
060e3402 8b08 mov ecx,dword ptr [eax]
060e3404 ff51fc call dword ptr [ecx-4]
060e3407 c3 ret
060e3408 53 push ebx
060e3409 56 push esi
060e340a 57 push edi
060e340b 89c3 mov ebx,eax
060e340d 89d7 mov edi,edx
0:004> d [ecx-4]
fffffffc ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ?? ????????????????
0000000c ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ?? ????????????????
0000001c ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ?? ????????????????
0000002c ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ?? ????????????????
0000003c ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ?? ????????????????
0000004c ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ?? ????????????????
0000005c ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ?? ????????????????
0000006c ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ?? ????????????????
郁闷之极,好像用VB写一个空OCX放到form上,然后thread调用起来,也有这个问题。哪位老大见过这个问题?貌似和Appartment有关?
第一步,在业务高峰期抓取样本数据(2个小时左右)。采用的工具是sqlserver自带的profiler,也叫事件探查器,如下图:
进入后,点击最左面的按钮,建立一个新的跟踪:
登录需要用DBO权限,所以可以用sa登录,也可以用windows集成验证方式(如果当前登录的就是sqlserver的话)
新建跟踪,一共有4个tab页进行配置,首先看第一个。跟踪名称不用更改,默认的即可。保存一共有两种方式,一是文件,扩展名是.trc(这种方式方便你把客户那里的跟踪结果发给你),其二是数据库中的表。
为了分析方便,我们把它另存为表。此时sql提示你重新进行登录,这里我们把表保存到master中
假设表名字叫做jq(如果有重复的,系统会提示是否覆盖)
确定后回到了刚才的第一个tab页中:
然后切换到第二个选项卡中:
左面列出了各种事件类(Event Class),右面是当前已有的事件类。对于性能调优,我们不需要安全审核、会话信息,点击删除按钮即可:
继续切换到第三个tab页上,这里的数据列默认就够了,当然,如果你看着不顺眼,可以把Appname/NT username等都删除。
最后一个tab页上,我们需要把系统自己产生的事件ID屏蔽掉:
把那个排除系统ID进行check即可,如下图:
所有项目配置好后,点击“运行”按钮。持续运行两个小时左右即可(业务高峰期,能典型的反应客户最近一段时间内的业务模式)
好了,第一步的准备工作完成了,等待一段时间后,我们开始检查刚才自动保存到master中的表jq。
第二步,开始查找影响速度的地方。
打开查询分析器(sql analyzer),登录到master中,从 表jq里面按照I/O倒序,读取若干个sql。根据我的习惯,一般是读取1000条记录。为什么根据I/O来找呢,而不是根据时间来找呢?原因很简单,一句SQL执行,“稳定”的是I/O,而duration是一个不稳定的因素。我们进行sql调优的目的,就是降低I/O成本,从而提高效率。(一般而言,I/O降低了,duration自然就会降低)详细内容,参考我以前的post:http://blog.joycode.com/juqiang
执行完成后,我们仔细看下面的输出。
1、 XL_TALLY_Proc04这个sp的reads最大,将近100w,duration也达到了25秒多。
2、 Erp_IM_GMBill_GetBill这个sp的I/O不算大,才7w,duration平均都在1秒多点。但是这个sp执行的次数非常多。
经过询问客户,XL_TALLY_Proc04这个sp执行的频度很低,一天也就一两次,但是Erp_IM_GMBill_GetBill大概5分钟就要一次。这样整体I/O就占用的非常大。
所以这里我们要重点分析Erp_IM_GMBill_GetBill这个sp,而不是第一个!
总结一个原则就是:调整的重点是客户最关心的内容,是执行频度最高、看起来I/O又比较大的那种。I/O最大的,不一定是我们要优先解决的内容。
第三步,开始分析刚才看到的那个语句。既然我们要分析I/O,那么就要把I/O打开,这样每次调整sql,我们都能随时看到I/O的变化情况。这句很有用处地:set statistics io on
单纯看I/O变化,我们会晕倒的。因为我们不知道自己做的任何改动,对I/O是如何产生影响的。所以,还要看sql的执行计划是怎佯的。 在查询分析器中,我们按Ctrl+K,或者如下图的菜单,check上即可。
好了,准备工作都做好了,下面开始干活了。
我们首先看sql语句的调优,假设下面这条sql语句性能低下:
上面的sql一共读取了6636条数据,逻辑读是1126。那么这个I/O是否合理呢?大了还是小了?还有改进的余地吗?我们看执行计划:
哦,一共4个咚咚在里面。Index seek的成本占了2%, index scan的占了47%,hash match占了51%,select最终是0%。我们应该牢记第二个原则,所有的index,尽可能的都走index seek。
我们看一下billsoflading的索引信息:
当前索引为什么走scan,这里就不说了,感兴趣的可以随便找一本介绍数据库索引的书籍来看看即可。根据我以前那篇blog的描述,我们知道应该建立一个复合索引(也叫convered index):boldate+companyid+bolcode
然后我们重新执行sql,看看I/O变化情况:
Ooh,非常cool!logical reads降低到了50。为什么会这样呢?我们看一下执行计划:
原来是index scan变成了index seek,效率自然大大的提升!
Sql语句在index上调优的方法,基本就是这样。我们继续看sp的。
对于sp的调优,有一点是和sql调优不同的:sp内部的逻辑处理可能非常复杂。单纯从查询分析器中,我们无法得知哪一小块的sql执行的I/O最大,我们只能看到一个总体的描述。所以,我们要知道sp内部的信息。
首先,了解自己当前的spid是多少。一种方法是select @@spid,另一种方法是看查询分析器下面的status bar的信息。
Ooh,我的spid是101。(上图的最下面那个tips)
然后我重新打开profiler(事件探查器),重新建立一个跟踪,这里面要修改第二个tab页的信息,把左面事件列“存储过程”中的SmtpCompleted加上
增加后的样子如下:
然后修改第4个tab页,把刚才看到的spid=101的信息填上:
点击运行后,这样profiler只能抓到在查询分析器中,spid=101那个窗口发送的sql。我们切换回查询分析器,执行有问题的sp,执行完成后,我们再回到profiler,点停止按钮。一个sp内部所有执行的sql,都被分开了!
这次的结果假设保存在了jq2表中,我们把所有执行的小片sql都列出来:
第一个是sp执行后的总体结果,I/O为62328,就是这个sp自己的。第二个是向临时表中插入数据,I/O为61514,我们很容易看到,这一句占用了整个sp的大概95%以上的成本。如果我们把这句insert into #temptable搞定,整个sp的成本自然就下来了。所以我们需要把这句insert搞出来。
但是慢着!default情况下,sqlserver的results只显示很少的字符,第二行的sql,我们根本抓不全的,所以我们需要修改一下设置。在查询分析器的工具-选项菜单中,切换到“结果”这个tab页,修改每列最多字符个数为8192(这是最大的允许值),然后点击“确定”按钮,重新从jq2中读取信息。也许你会问,如果某个sql特别长,怎么办?其实很简单,在你的代码中把这句sql单独写到log中,或者直接修改sp,把这句print出来即可。
Ok,我们把这句insert sql抓下来后,放到查询分析器中。因为temptable我们没有它的结构,所以我们把insert部分注释掉,看后面的select语句。执行后,ooh,在goodsmovement表上的成本是57834。
老办法,我们继续看执行计划:
其实,现在又回归到了sql调优的步骤,下面的工作我就不写啦!
这个步骤,看起来很简单,希望大家对于sql调优(索引部分)心中都有这么一个概念,知道第一步作什么,第二步作什么。还是那句话,索引调优,基本上是最简单的。但是貌似简单的东西,我们越应该重视。你随便找一个应用跟踪一下,各种效率低下的索引,会让你实在#¥*#(**……¥
最后,看看偶的桌面,14集何时能够出来呢?期待中……

很好的日子,2007年1月1日收到了确认通知。这是偶第二次通过Solution Architect的MVP,很是高兴。

上文中,我们解决了那个场景的死锁问题。这次,我们分析一下,为什么会死锁呢?再回顾一下两个sp的写法:
CREATE PROC p1 @p1 int AS
SELECT c2, c3 FROM t1 WHERE c2 BETWEEN @p1 AND @p1+1
GO
CREATE PROC p2 @p1 int AS
UPDATE t1 SET c2 = c2+1 WHERE c1 = @p1
UPDATE t1 SET c2 = c2-1 WHERE c1 = @p1
GO
很奇怪吧!p1没有insert,没有delete,没有update,只是一个select,p2才是update。这个和我们前面说过的,trans1里面updata A,update B;trans2里面upate B,update A,根本不贴边啊!
那么,什么导致了死锁?
需要从事件日志中,看sql的死锁信息:
Spid X is running this query (line 2 of proc [p1], inputbuffer “… EXEC p1 4 …”):
SELECT c2, c3 FROM t1 WHERE c2 BETWEEN @p1 AND @p1+1
Spid Y is running this query (line 2 of proc [p2], inputbuffer “EXEC p2 4”):
UPDATE t1 SET c2 = c2+1 WHERE c1 = @p1
The SELECT is waiting for a Shared KEY lock on index t1.cidx. The UPDATE holds a conflicting X lock.
The UPDATE is waiting for an eXclusive KEY lock on index t1.idx1. The SELECT holds a conflicting S lock.
首先,我们看看p1的执行计划。怎么看呢?可以执行set statistics profile on,这句就可以了。下面是p1的执行计划
SELECT c2, c3 FROM t1 WHERE c2 BETWEEN @p1 AND @p1+1
|--Nested Loops(Inner Join, OUTER REFERENCES:([Uniq1002], [t1].[c1]))
|--Index Seek(OBJECT:([t1].[idx1]), SEEK:([t1].[c2] >= [@p1] AND [t1].[c2] <= [@p1]+(1)) ORDERED FORWARD)
|--Clustered Index Seek(OBJECT:([t1].[cidx]), SEEK:([t1].[c1]=[t1].[c1] AND [Uniq1002]=[Uniq1002]) LOOKUP ORDERED FORWARD)
我们看到了一个nested loops,第一行,利用索引t1.c2来进行seek,seek出来的那个rowid,在第二行中,用来通过聚集索引来查找整行的数据。这是什么?就是bookmark lookup啊!为什么?因为我们需要的c2、c3不能完全的被索引t1.c1带出来,所以需要书签查找。
好,我们接着看p2的执行计划。
UPDATE t1 SET c2 = c2+1 WHERE c1 = @p1
|--Clustered Index Update(OBJECT:([t1].[cidx]), OBJECT:([t1].[idx1]), SET:([t1].[c2] = [Expr1004]))
|--Compute Scalar(DEFINE:([Expr1013]=[Expr1013]))
|--Compute Scalar(DEFINE:([Expr1004]=[t1].[c2]+(1), [Expr1013]=CASE WHEN CASE WHEN ...
|--Top(ROWCOUNT est 0)
|--Clustered Index Seek(OBJECT:([t1].[cidx]), SEEK:([t1].[c1]=[@p1]) ORDERED FORWARD)
通过聚集索引的seek找到了一行,然后开始更新。这里注意的是,update的时候,它会申请一个针对clustered index的X锁的。
实际上到这里,我们就明白了为什么update会对select产生死锁。update的时候,会申请一个针对clustered index的X锁,这样就阻塞住了(注意,不是死锁!)select里面最后的那个clustered index seek。死锁的另一半在哪里呢?注意我们的select语句,c2存在于索引idx1中,c1是一个聚集索引cidx。问题就在这里!我们在p2中更新了c2这个值,所以sqlserver会自动更新包含c2列的非聚集索引:idx1。而idx1在哪里?就在我们刚才的select语句中。而对这个索引列的更改,意味着索引集合的某个行或者某些行,需要重新排列,而重新排列,需要一个X锁。
SO………,问题就这样被发现了。
总结一下,就是说,某个query使用非聚集索引来select数据,那么它会在非聚集索引上持有一个S锁。当有一些select的列不在该索引上,它需要根据rowid找到对应的聚集索引的那行,然后找到其他数据。而此时,第二个的查询中,update正在聚集索引上忙乎:定位、加锁、修改等。但因为正在修改的某个列,是另外一个非聚集索引的某个列,所以此时,它需要同时更改那个非聚集索引的信息,这就需要在那个非聚集索引上,加第二个X锁。select开始等待update的X锁,update开始等待select的S锁,死锁,就这样发生鸟。
那么,为什么我们增加了一个非聚集索引,死锁就消失鸟?我们看一下,按照上文中自动增加的索引之后的执行计划:
SELECT c2, c3 FROM t1 WHERE c2 BETWEEN @p1 AND @p1+1
|--Index Seek(OBJECT:([deadlocktest].[dbo].[t1].[_dta_index_t1_7_2073058421__K2_K1_3]), SEEK:([deadlocktest].[dbo].[t1].[c2] >= [@p1] AND [deadlocktest].[dbo].[t1].[c2] <= [@p1]+(1)) ORDERED FORWARD)
哦,对于clustered index的需求没有了,因为增加的覆盖索引已经足够把所有的信息都select出来。就这么简单。
实际上,在sqlserver 2005中,如果用profiler来抓eventid:1222,那么会出现一个死锁的图,很直观的说。
下面的方法,有助于将死锁减至最少(详细情况,请看SQLServer联机帮助,搜索:将死锁减至最少即可。
- 按同一顺序访问对象。
- 避免事务中的用户交互。
- 保持事务简短并处于一个批处理中。
- 使用较低的隔离级别。
- 使用基于行版本控制的隔离级别。
- 将 READ_COMMITTED_SNAPSHOT 数据库选项设置为 ON,使得已提交读事务使用行版本控制。
- 使用快照隔离。
- 使用绑定连接。
死锁,简而言之,两个或者多个trans,同时请求对方正在请求的某个对象,导致双方互相等待。简单的例子如下:
trans1 trans2
------------------------------------------------------------------------
1.IDBConnection.BeginTransaction 1.IDBConnection.BeginTransaction
2.update table A 2.update table B
3.update table B 3.update table A
4.IDBConnection.Commit 4.IDBConnection.Commit
那么,很容易看到,如果trans1和trans2,分别到达了step3,那么trans1会请求对于B的X锁,trans2会请求对于A的X锁,而二者的锁在step2上已经被对方分别持有了。由于得不到锁,后面的Commit无法执行,这样双方开始死锁。
好,我们看一个简单的例子,来解释一下,应该如何解决死锁问题。
-- Batch #1
CREATE DATABASE deadlocktest
GO
USE deadlocktest
SET NOCOUNT ON
DBCC TRACEON (1222, -1)
-- 在SQL2005中,增加了一个新的dbcc参数,就是1222,原来在2000下,我们知道,可以执行dbcc
--traceon(1204,3605,-1)看到所有的死锁信息。SqlServer 2005中,对于1204进行了增强,这就是1222。
GO
IF OBJECT_ID ('t1') IS NOT NULL DROP TABLE t1
IF OBJECT_ID ('p1') IS NOT NULL DROP PROC p1
IF OBJECT_ID ('p2') IS NOT NULL DROP PROC p2
GO
CREATE TABLE t1 (c1 int, c2 int, c3 int, c4 char(5000))
GO
DECLARE @x int
SET @x = 1
WHILE (@x <= 1000) BEGIN
INSERT INTO t1 VALUES (@x*2, @x*2, @x*2, @x*2)
SET @x = @x + 1
END
GO
CREATE CLUSTERED INDEX cidx ON t1 (c1)
CREATE NONCLUSTERED INDEX idx1 ON t1 (c2)
GO
CREATE PROC p1 @p1 int AS SELECT c2, c3 FROM t1 WHERE c2 BETWEEN @p1 AND @p1+1
GO
CREATE PROC p2 @p1 int AS
UPDATE t1 SET c2 = c2+1 WHERE c1 = @p1
UPDATE t1 SET c2 = c2-1 WHERE c1 = @p1
GO
上述sql创建一个deadlock的示范数据库,插入了1000条数据,并在表t1上建立了c1列的聚集索引,和c2列的非聚集索引。另外创建了两个sp,分别是从t1中select数据和update数据。
好,打开一个新的查询窗口,我们开始执行下面的query:
-- Batch #2
USE deadlocktest
SET NOCOUNT ON
WHILE (1=1) EXEC p2 4
GO
开始执行后,然后我们打开第三个查询窗口,执行下面的query:
-- Batch #3
USE deadlocktest
SET NOCOUNT ON
CREATE TABLE #t1 (c2 int, c3 int)
GO
WHILE (1=1) BEGIN
INSERT INTO #t1 EXEC p1 4
TRUNCATE TABLE #t1
END
GO
开始执行,哈哈,很快,我们看到了这样的错误信息:
Msg 1205, Level 13, State 51, Procedure p1, Line 4
Transaction (Process ID 54) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Rerun the transaction.
spid54发现了死锁。
那么,我们该如何解决它?
在SqlServer 2005中,我们可以这么做:
1.在trans3的窗口中,选择EXEC p1 4,然后right click,看到了菜单了吗?选择Analyse Query in Database Engine Tuning Advisor。
2.注意右面的窗口中,wordload有三个选择:负载文件、表、查询语句,因为我们选择了查询语句的方式,所以就不需要修改这个radio option了。
3.点左上角的Start Analysis按钮
4.抽根烟,回来后看结果吧!出现了一个分析结果窗口,其中,在Index Recommendations中,我们发现了一条信息:大意是,在表t1上增加一个非聚集索引索引:t2+t1。
5.在当前窗口的上方菜单上,选择Action菜单,选择Apply Recommendations,系统会自动创建这个索引。
重新运行batch #3,呵呵,死锁没有了。
这种方式,我们可以解决大部分的Sql Server死锁问题。那么,发生这个死锁的根本原因是什么呢?为什么增加一个non clustered index,问题就解决了呢?且听下文分解。
龟速查询
阻塞和索引问题,是常见的导致sql以龟速执行的罪魁。
阻塞
阻塞主要等待逻辑锁,如请求一个X锁。关于锁的信息,遍地都是,msdn或者google都可以。SQL Server 2005提供了125中等待类型(2000是76种)。
假设我们sp_who看到了一个block在56号上,那么通过这个可以看到详细信息
select * from sys.dm_os_waiting_tasks where session_id=56
(在2000下,你可以通过dbcc inputbuffer(56)来看当前执行的文本)
0x022A8898 56 0 1103500 LCK_M_S 0x03696820 0x022A8D48 53 NULL ridlock fileid=1 pageid=143 dbid=9 id=lock3667d00 mode=X associatedObjectId=72057594038321152
这里显示,56被53阻塞,并且等待了1103500毫秒了。
通过使用sys.dm_tran_locks,我们可以看到56被53以X模式锁住了,53持有1:143:3这个资源。
select request_session_id as spid, resource_type as rt, resource_database_id as rdb, (case resource_type WHEN 'OBJECT' then object_name(resource_associated_entity_id) WHEN 'DATABASE' then ' ' ELSE (select object_name(object_id) from sys.partitions where obt_id=resource_associated_entity_id) END) as objname, resource_description as rd, request_mode as rm, request_status as rs from sys.dm_tran_locks
输出如下
spid rt rdb objname rd rm rs
-----------------------------------------------------------------------------
56 DATABASE 9 S GRANT
53 DATABASE 9 S GRANT
56 PAGE 9 t_lock 1:143 IS GRANT
53 PAGE 9 t_lock 1:143 IX GRANT
53 PAGE 9 t_lock 1:153 IX GRANT
56 OBJECT 9 t_lock IS GRANT
53 OBJECT 9 t_lock IX GRANT
53 KEY 9 t_lock (a400c34cb X GRANT
53 RID 9 t_lock 1:143:3 X GRANT
56 RID 9 t_lock 1:143:3 S WAIT
而在SQL Server 2000里面,可以从sysprocesses看到。
select * from master..sysprocesses where blocked <> 0.
更详细的阻塞信息
2005中提供了一个新的DMV:Sys.dm_db_index_operational_stats,它提供了针对每个表、索引、分区的详细阻塞情况,如:访问历史、锁、阻塞、waits等。详细信息如下:
· 页/行等持有锁的个数
· 页/行等锁或waits的个数
· 页/行等锁或waits的时间
· 页闩的waits个数(闩与hotspot,就是所谓的热点有关,下同)
· 页闩的waits时间
· 页I/O闩的waits时间
除了阻塞的信息,还有一些索引的信息。
· 访问方式,如某个range,或者lookup
· 在叶子层的插入/更新/修改
· 叶子层之上的插入/更新/修改,就是索引维护。每个叶子页面的第一行,指向了该层的上一层。假如说在叶子上分配了一个新页面,那么上面那层的页面就包含一个指向该层第一行的指针信息。
在原文的附录B中,包含了一系列的索引信息相关的存储过程。下面是使用该sp的步骤
1. 使用init_index_operational_stats来初始化indexstats表
2. 使用insert_indexstats建立一个基线
3. 运行你的负载
4. 到合时的实际,使用insert_indexstats捕获索引状态的快照
5. 跑get_indexstats来分析索引状态。诸如很高的阻塞或者很高的waits,基本可以表明索引有问题。
这里列出了一些使用上述sp的例子。
· 所有数据库中,使用最多的前5个索引
exec get_indexstats @dbid=-1, @top='top 5', @columns='index, usage', @order='index usage'
· 锁增长最多的前5个索引
exec get_indexstats @dbid=-1, @top='top 5', @order='index lock promotions', @threshold='[index lock promotion attempts] > 0'
· 递减模式,前5个最大的行锁waits时间的索引状态
exec get_indexstats @dbid=-1, @top='top 5', @order='avg row lock wait ms'
· 前10个,在dbid=5的数据库中,所有阻塞率大于10%的索引状态
exec get_indexstats @dbid=-1, @top='top 10', @order='block %', @threshold='[block %] > 0.1'
注:sql2000没有提供任何关于对象或者索引使用状态
监视索引使用情况
2005中提供了一个非常有用的DMV:sys.dm_db_index_usage_stats,通过它我们可以找到哪些索引正在被当前query使用,或者没被使用。注意的是,这些数据仅保留在内存中,并没有被持久化存储。所以,如果sql发生down机了,这些数据就都丢掉了。当然,我们可以把这些save到表中,供日后分析。
索引上的操作分为两种:用户方式和系统方式。一个索引,通过dbid、对象id和索引id三列信息唯一标示。索引id为0的时候,代表一个heap table;1的时候,聚集索引;大于1的时候,非聚集索引。
2005下,seek/scan/lookup的规则与定义如下:
· SEEK: 使用B-tree结构访问数据的次数。
· SCAN: 不使用B-tree结构访问数据的次数。
· LOOKUP: 使用不合适的非聚集索引配合聚集索引来寻找数据,如2000中的书签查找。
下面这个DMV可以得到“当前”所有数据库所有对象的索引状态。
select object_id, index_id, user_seeks, user_scans, user_lookups from sys.dm_db_index_usage_stats order by object_id, index_id
假设是下面的结果
object_id index_id user_seeks user_scans user_lookups
------------ ------------- -------------- -------------- ----------- ------
521690298 1 0 251 123
521690298 2 123 0 0
该结果表明,有251次的聚集索引scan,123次的书签查找,123次的非聚集索引seek。
如果想知道从上次sql启动之后,到现在为止,某个表中没有被使用过的索引状况,执行下面的sql。
select i.name from sys.indexes i where i.object_id=object_id('<table_name>') and i.index_id NOT IN (select s.index_id from sys.dm_db_index_usage_stats s where s.object_id=i.object_id and i.index_id=s.index_id and database_id = <dbid> )
所有未被使用的索引:
select object_name(object_id), i.name from sys.indexes i where i.index_id NOT IN (select s.index_id from sys.dm_db_index_usage_stats s where s.object_id=i.object_id and i.index_id=s.index_id and database_id = <dbid> ) order by object_name(object_id) asc
结束了
原文后面有N多的sp,大家可以参考原文,在你的sql2005上跑一次。当然,能够在客户的生产环境中,用这些sp进行实际测试,会得到更好的体会。
我个人认为,更好的利用这些DMVs和sp的前提是,对于sql的基本概念要有所了解。索引、锁、阻塞、死锁等,为什么会产生,他们在SqlServer这种数据库下面是如何处理的,等等。否则,看着那些DMVs,很容易发懵。
TempDB
每个实例只有一个tempdb,所以这里很可能成为性能或者磁盘空间的瓶颈。
常见的tempdb问题如下:
· 把磁盘空间用光了
· 因为tempdb的瓶颈,导致I/O很差。参见第一部分。
· DDL带来的对系统表的瓶颈
· 内容分配
诊断问题之前,先看看tempdb是如何利用空间的。
用户对象
· 表和索引
· 全局临时表 (##t1)和索引
· 局部临时表和索引(#t1) and index.
· 当前连接的
· 存储过程内的
· 表变量(同上)
内部对象
· Work file (hash join)
· Sort run
· Work table (cursor, spool和临时大对象)
版本存储
2005新增的
空闲空间
tempdb暂时没有用到的磁盘剩余空间.
整个tempdb就是上述4个东西的和。
监视tempdb剩余空间很简单,监测这个指标即可。Free Space in tempdb (KB)。下面这个DMVs很强大的说,上面四个都能看到。
Select SUM (user_object_reserved_page_count)*8 as user_objects_kb, SUM (internal_object_reserved_page_count)*8 as internal_objects_kb, SUM (version_store_reserved_page_count)*8 as version_store_kb, SUM (unallocated_extent_page_count)*8 as freespace_kb From sys.dm_db_file_space_usage Where database_id = 2
这是一个输出结果(kb表示的)
user_objets_kb internal_objects_kb version_store_kb freespace_kb
---------------- -------------------- ------------------ ------------
8736 128 64 448
分析空间使用问题
用户对象
跑这个,能看出来到底谁干的。
DECLARE userobj_cursor CURSOR FOR
select sys.schemas.name + '.' + sys.objects.name from sys.objects, sys.schemas where object_id > 100 and type_desc = 'USER_TABLE'and sys.objects.schema_id = sys.schemas.schema_id
go
open userobj_cursor
go
declare @name varchar(256)
fetch userobj_cursor into @name
while (@@FETCH_STATUS = 0)
begin
exec sp_spaceused @objname = @name
fetch userobj_cursor into @name
end
close userobj_cursor
版本存储
select top 2 transaction_id, transaction_sequence_num, elapsed_time_seconds from sys.dm_tran_active_snapshot_database_transactions order by elapsed_time_seconds DESC
这里显示一个带有XSN3的事务(ID是8609),已经激活了6523秒。
transaction_id transaction_sequence_num elapsed_time_seconds
-------------------- ------------------------ --------------------
8609 3 6523
20156 25 783
Kill掉第一个trans,我们可以释放iding数量的version store。但是,没有办法能够估计出来,kill掉之后,究竟能释放多少。
内部对象
SQL Server 2005提供了两个DMVs: sys.dm_db_session_space_usage和 asys.dm_db_task_space_usage,用以跟踪sessions和tasks在tempdb中的空间变化。
select session_id, internal_objects_alloc_page_count, internal_objects_dealloc_page_count from sys.dm_db_session_space_usage order by internal_objects_alloc_page_count DESC
再如这个
SELECT t1.session_id, (t1.internal_objects_alloc_page_count + task_alloc) as allocated, (t1.internal_objects_dealloc_page_count + task_dealloc) as deallocated from sys.dm_db_session_space_usage as t1, (select session_id, sum(internal_objects_alloc_page_count) as task_alloc, sum (internal_objects_dealloc_page_count) as task_dealloc from sys.dm_db_task_space_usage group by session_id) as t2 where t1.session_id = t2.session_id and t1.session_id >50 order by allocated DESC
这里一个示例输出
session_id allocated deallocated
---------- -------------------- --------------------
52 5120 5136
51 16 0
对于tasks,可以执行下面的sql
select t1.session_id, t1.request_id, t1.task_alloc, t1.task_dealloc, t2.sql_handle, t2.statement_start_offset, t2.statement_end_offset, t2.plan_handle from (Select session_id, request_id, sum(internal_objects_alloc_page_count) as task_alloc, sum (internal_objects_dealloc_page_count) as task_dealloc from sys.dm_db_task_space_usage group by session_id, request_id) as t1, sys.dm_exec_requests as t2 where t1.session_id = t2.session_id and (t1.request_id = t2.request_id) order by t1.task_alloc DESC
session_id request_id task_alloc task_dealloc
---------------------------------------------------------
52 0 1024 1024
sql_handle statement_start_offset
----------------------------------------------------------------------- 0x02000000D490961BDD2A8BE3B0FB81ED67655EFEEB360172 356
statement_end_offset plan_handle
---------------------------------
-1 0x06000500D490961BA8C19503000000000000000000000000
然后通过sql_handle和plan_handle,就可以得到sql的语句
select text from sys.dm_exec_sql_text(@sql_handle)
select * from sys.dm_exec_query_plan(@plan_handle)
(第三部分,介绍大家最关心的sql调优)
名次解释
DMVs:dynamic management views
三个点
· 资源瓶颈: CPU、内存、I/O(这里面不考虑网络问题)
· Tempdb瓶颈:
· User query瓶颈,可能是统计信息的变化、不恰当的索引、阻塞或者死锁等
上述三点,可能是相互影响的。
资源瓶颈
工具
1. System Monitor (PerfMon):windows自带
2. SQL Server Profiler: 2005继续有
3. DBCC commands: 参考联机文档
4. DMVs: 见上名次解释
CPU瓶颈
CPU瓶颈,是突然并且不可预料的。一般来讲,没有优化的查询计划、系统低配置、设计不合理等,很容易导致这些问题。
在perfmon中,我们一般需要监视Processor:% Processor Time,如果每个CPU持续高于80%,CPU就是瓶颈了。当然,在强大的2005下我们也可监视sys.dm_os_schedulers ,如果有内容,表明有任务等待CPU来分配给它。如下面这个DMVs的查询:
select scheduler_id,current_tasks_count,runnable_tasks_count from sys.dm_os_schedulers where scheduler_id < 255
下面的查询,更高级点。分析方法是,看结果的number_of_statements,如果该值大于1,说明可能有问题,要进一步分析。
select top 50 sum(qs.total_worker_time) as total_cpu_time, sum(qs.execution_count) as total_execution_count,count(*) as number_of_statements, qs.plan_handle from sys.dm_exec_query_stats qs group by qs.plan_handle order by sum(qs.total_worker_time) desc
执行计划的编译与重新编译
在sql2005中的一个改进,就是对于某个sp,进行recompile的时候,只需要针对改变的部分进行编译,sql2000只能是全部都搞一遍。
Recompile的原因很多,如:
· Schema的变更 changed
· Statistics变更
· 延迟编译
· SET option的执行
· 临时表的变化
· Sp使用了RECOMPILE提示或者使用了OPTION (RECOMPILE)
诊断方法,老朋友了,继续使用perfmon或者sql profiler。
对于perfmon,监视下面的 计数器
· SQL Server: SQL Statistics: Batch Requests/sec
· SQL Server: SQL Statistics: SQL Compilations/sec
· SQL Server: SQL Statistics: SQL Recompilations/sec
对于profiler抓到的trace,分析这几个event:SP:Recompile / SQL:StmtRecompile / Showplan XML For Query Compile。如果我们抓到了trace,对于文件,可以这么做:
select spid,StartTime,Textdata,EventSubclass,ObjectID,SQLHandle from fn_trace_gettable ( 'e:\recompiletrace.trc' , 1) where EventClass in(37,75,166)
这里面,EventClass 37 = Sp:Recompile, 75 = CursorRecompile, 166=SQL:StmtRecompile
如果你事先保存到了 table jq中,那么把上面的from修改为from jq即可。
或者使用这个DMVs: sys.dm_exec_query_optimizer_info(注意一个技巧!多执行几次,看中间的差异)
select * from sys.dm_exec_query_optimizer_info
另外一个DMVs是:sys.dm_exec_query_stats,如执行这个sql:
select * from sys.dm_exec_query_stats CROSS APPLY sys.dm_exec_sql_text(sql_handle) AS s2
另外, plan_generation_num标示出了被recompile的所有query。如下面这个
select top 25 sql_text.text,sql_handle,plan_generation_num, execution_count,dbid,objectid from sys.dm_exec_query_stats a cross apply sys.dm_exec_sql_text(sql_handle) as sql_text where plan_generation_num >1 order by plan_generation_num desc
解决办法
· 如果因为使用了Set option,那么通过profiler来观察是什么引起的变化。
· 如果因为临时表,那么尽量使用表变量,而不是临时表。(对于表变量的限制,请查看联机文档)另一个解决办法,使用KEEP PLAN查询提示,这会把临时表当作普通表一样对待,会进行statistics的跟踪
· 关闭对于索引或者索引视图上的状态自动更新(偶个人不建议)
· 对象名称写全了,如dbo.Table1
· 尽量少用延迟编译。如你的SP或者query里面,有N多的if/else之类的。
· 运行索引调优向导(sql2000里面就有)
· 看看sp是不是使用了WITH RECOMPILE来建立的,或者RECOMPILE查询提示。
弱智的查询计划
每个查询执行之前,sqlserver都会“试图”优化一个最快的查询计划出来。注意的是,这里的最快的,不代表I/O最小,也不代表CPU占用最小。它是一个权衡后的值。
对于Hash join或者sort等,它们都是与CPU密切相关的。对于nested loop,很可能会因为大量的index lookups,导致I/O迅速上涨。如果search的数据散落在各个pages里面,很可能会导致缓冲命中率下降。
诊断方法
使用这个DMVs: sys.dm_exec_query_stats,它可以有效地监视CPU的使用情况。
select highest_cpu_queries.plan_handle, highest_cpu_queries.total_worker_time,q.dbid,q.objectid,q.number,q.encrypted,q.[text] from (select top 50 qs.plan_handle, qs.total_worker_time from sys.dm_exec_query_stats qs order by qs.total_worker_time desc) as highest_cpu_queries cross apply sys.dm_exec_sql_text(plan_handle) as q order by highest_cpu_queries.total_worker_time desc
解决办法
· 使用优化向导调优
· 检查一下,WHERE条件是不是限制的足够好?
游标问题
强烈建议,尽量减少使用游标。可以使用perfmon监视SQL Server:Cursor Manager By Type – Cursor Requests/Sec。或者使用DMVs:
select cur.* from sys.dm_exec_connections con cross apply sys.dm_exec_cursors(con.session_id) as cur where cur.fetch_buffer_size = 1 and cur.properties LIKE 'API%'
如果使用profiler,可以监视sp_cursorfetch(前提是包含了RPC:Completed这个event class)
内存瓶颈
对于VAS和AWE概念,请自行查找MSDN。我印象中,M$的人强烈建议不要在32bit windows上面使用AWE或者3BG之类的东西。
检测内存问题
打开taskmgr看物理内存中的Avaiable,如果持续低于10M,恭喜你,系统内存压力太大!通过perfmon,监视Memory: Available Mbytes,一样的效果。
对于AWE使用,可以用这个DMVs来看:
select sum(awe_allocated_kb) / 1024 as [AWE allocated, Mb] from sys.dm_os_memory_clerks
对于虚拟内存等,可以观察Commit Charge中的Total,与limit的对比。如果两者很接近,虚拟内存可能不够了。如果你好奇,想看看sqlserver自己的内存分配,可以执行DBCC MEMORYSTATUS。具体内容,见联机文档。
对于内存问题,偶认为采用sql的默认设置,一般情况下足够了。
I/O瓶颈
除非操作系统能够,并且内存足够大,把你的db放到物理内存里,否则,I/O我们永远回避不过去。使用perfmon的话,可以监视
· PhysicalDisk Object: Avg. Disk Queue Length,如果经常性的大于2*磁盘个数,磁盘有性能问题。
· Avg. Disk Sec/Read,如果<10ms,很好。20以下,一般。50以下,密切观察。50以上,换硬盘吧!
· Avg. Disk Sec/Write,这个和上面的两个值,如果持续大于物理磁盘的指标的85%,说明磁盘已经到极限了。
· Physical Disk: %Disk Time,一般如果超过50%,I/O有瓶颈。
如果用了raid,采用下面这个公式来计算:
Raid 0 -- I/Os per disk = (reads + writes) / number of disks
Raid 1 -- I/Os per disk = [reads + (2 * writes)] / 2
Raid 5 -- I/Os per disk = [reads + (4 * writes)] / number of disks
Raid 10 -- I/Os per disk = [reads + (2 * writes)] / number of disks
如下面这个例子,2个磁盘,raid1,监测到的结果:
Disk Reads/sec 80
Disk Writes/sec 70
Avg. Disk Queue Length 5
那么I/O平均是80/2+70=110,队列长度上限是2*2=4
解决办法
· 检查sqlserver的内存配置
· 增加或者替换更快的硬盘,读写缓存越高越好
· 检查执行计划,找到I/O大的地方。如这个DMVs
select top 50 (total_logical_reads/execution_count) as avg_logical_reads, (total_logical_writes/execution_count) as avg_logical_writes, (total_physical_reads/execution_count) as avg_phys_reads, Execution_count, statement_start_offset as stmt_start_offset, sql_handle, s2.text plan_handle from sys.dm_exec_query_stats cross APPLY sys.dm_exec_sql_text(sql_handle) AS s2 order by (total_logical_reads + total_logical_writes) Desc
小TIP,如果要清除缓存的作用,执行这个:
checkpoint
dbcc freeproccache
dbcc dropcleanbuffers
(第二部分,介绍tempdb的优化)
(原文地址,点这里)
vista ultimate版本,sql2005,没有任何sp。安装后,连接本地实例,无论windows Authentication还是sqlserver authentication,都提示18456错误。
解决办法如下:
- 用admin或者run as administrator方式,运行sql mgr studio
- 展开security结点,到login,new login,在general里面起一个你自己的账户名字,假如叫juqiang,密码设置上。(注意!同时要把password的policy和expiration这两个东西uncheck掉)。然后到server rols里面,把sysadmin钩上。
- 没有了。
环境是Vistal ultimate+IE7,最近上csdn或者news.sina.com.cn,打开N个窗口后,IE突然就崩溃了,然后自动重新启动,甚是郁闷。
于是打开adplus,抓之:
adplus -crash -pn iexplore.exe -o d:\dumps
到了CSDN社区,看了两个帖子,哈哈,adplus开始create dump了。一会功夫,一共抓到了三个,分别是1st Chance AV mini,1st Chance Proc Shutdown,2nd Chance AV。
打开三个dump,1st Proc Shudown里面啥都没有,直接ret了,打开另外两个AV的。哦,看到东西了。
This dump file has an exception of interest stored in it.
The stored exception information can be accessed via .ecxr.
(ff0.158): Access violation - code c0000005 (first/second chance not available)
eax=6ee500c2 ebx=00000000 ecx=0a10ef08 edx=0a10ef14 esi=1000ea7c edi=0782d1c0
eip=8bffdb10 esp=0a10eeec ebp=0a10ef0c iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010246
8bffdb10 ?? ???
kb一下之后,看到这些:
0:019> kb 2000
ChildEBP RetAddr Args to Child
WARNING: Frame IP not in any known module. Following frames may be wrong.
0a10eee8 10003e1f 0782d1c0 1000ea7c 0a10ef08 0x8bffdb10
0a10ef0c 10003d46 0a94edc8 0a10f108 00dabd80 Jccatch!DllGetClassObject+0x28fc
0a10ef2c 100034c0 0a880d20 00dabd88 0a10f014 Jccatch!DllGetClassObject+0x2823
0a10ef60 705fecc2 0a880d20 00000103 70622ff4 Jccatch!DllGetClassObject+0x1f9d
0a10ef94 70624095 00dabd88 0a10f014 0a10f014 ieframe!EnumInvokeCallback+0x3c
0a10efb8 7062401a 00dabd88 0a10eff0 70623004 ieframe!EnumConnectionPointSinks+0x6c
0a10f004 70624af7 00122a84 0a10f014 00000003 ieframe!IConnectionPoint_InvokeIndirect+0x80
0a10f044 70624b7f 00122a84 00000103 0a10f108 ieframe!IConnectionPoint_InvokeWithCancel+0x3a
0a10f11c 70625069 0a905af0 00122a84 00000000 ieframe!DoInvokeParamHelper+0x8b
0a10f174 705f43d3 0a905af0 0a905af0 039cf310 ieframe!FireEvent_DocumentComplete+0xc2
0a10f194 705f4348 059c2620 039cf310 0a850bc0 ieframe!CBaseBrowser2::_FireDocumentComplete+0x46
0a10f1bc 6ebb8381 059c267c 0a850bc0 00000002 ieframe!CBaseBrowser2::FireDocumentComplete+0x52
0a10f1f0 6ebb3e49 00000000 0a9558ec 6ec05988 mshtml!CWebOCEvents::DocumentComplete+0x102
0a10f260 6eba036d 0a94edc8 0a94edd8 07800168 mshtml!CMarkup::OnLoadStatusDone+0x248
0a10f274 6ebb82ea 00000004 0a10f6f4 0000009d mshtml!CMarkup::OnLoadStatus+0x4c
0a10f6bc 6ebefdd7 0a62b500 00000000 0a10f700 mshtml!CProgSink::DoUpdate+0x533
0a10f6cc 6ec07a4a 0a62b500 0a62b500 00000000 mshtml!CProgSink::OnMethodCall+0xf
0a10f700 6ec014f2 0a10f79c 00008002 00000000 mshtml!GlobalWndOnMethodCall+0x101
0a10f720 75e81a10 0009066c 00000e5e 00000000 mshtml!GlobalWndProc+0x181
0a10f74c 75e81ae8 6ec01441 0009066c 00008002 user32!InternalCallWinProc+0x23
0a10f7c4 75e82a47 000b02c4 6ec01441 0009066c user32!UserCallWinProcCheckWow+0x14b
0a10f828 75e82a98 6ec01441 00000000 0a10f8a0 user32!DispatchMessageWorker+0x322
0a10f838 705fe5db 0a10f850 00000000 00000000 user32!DispatchMessageW+0xf
0a10f8a0 77303833 07753a58 0a10f8ec 775da9bd ieframe!CTabWindow::_TabWindowThreadProc+0x189
0a10f8ac 775da9bd 07618580 0a10680a 00000000 kernel32!BaseThreadInitThunk+0xe
0a10f8ec 00000000 705fe3a4 07618580 00000000 ntdll!_RtlUserThreadStart+0x23
ooh,jccatch!于是打开IE,tools-Managed Addin,看到了jccatch正在enabled。disable之后,貌似没有问题了。继续观察之………………(jccatch是jetcar就是网际快车的东西)
某个牛人说过,即使啥也不懂,用windbg也能看到系统的哪个dll出错了,:)
SqlServer的性能问题,也是窗户纸,让偶道来!
先考虑一个问题,怎么判断SQL的执行效率是好是坏?也许,95%的人会回答,看执行时间。
错!
为什么?因为在一个稳定的DB中(稳定这个词,我是这样定义的:某个时间段内,如1周,大部分被SQL缓存的数据是几乎不变的,这意味着客户这段时间内的操作模式、数据变化,是平稳的),同样的sql执行,可能会因为缓存的变化,导致时间变化无常,或者因为一些诸如hot spot,也会导致这个问题。
既然要tuning,就要有一个调优的标准。标准是什么呢?最主要的,看I/O。
在一个稳定的DB中,执行同样的SQL,I/O基本是不变化的。同样的内存配置,你的台式机,客户的高级server,产生的I/O相差不大的。I/O分为两种,逻辑的,从内存读写;物理的,从磁盘读写。
SQL调优的最终目的,就是大幅度的降低I/O大小,减少阻塞,避免死锁。
有了这个目标之后,就可以开始干活了!
首先打开sql analysis(查询分析器),用sa连接到你的数据库,执行
dbcc traceon(1204,3605,-1),这一句保证任何的死锁信息都会被记录在sql log中。
然后打开sql profiler(事件探查器),在业务高峰期开始,抓里面的sql completed和sp completed,持续2-4个小时(看客户的实际情况而定)。
然后把profiler里面的数据save as到一个table中,加入叫做:jq(偶名字的缩写)。好,到此,成功1/3了!
再次打开查询分析器,执行类似的这条sql:
Select textdata, reads, duration from jq order by reads desc
这会把所有抓到的sql按照I/O从大到小的顺序排列,睁大你的眼睛,找出来那些I/O最大的,执行最频繁的sql,copy出来,在查询分析器的另一个窗口中,粘贴上。
哦,先不要急着Ctrl+E,要先执行这个语句!
Set statistics io on
然后按一下Ctrl+K(打开执行计划)
好了,执行你从jq里面抓到的那个最大的sql吧!仔细分析下面的执行计划,仔细看output中每个表的I/O。分析为什么index的走向不是你所期望的,分析为什么这么多nested loops,分析为什么有大量的worktable?等等,等等。
上面是对于普通sql调优的办法。而对于阻塞,可以参考msdn的文章,kbid是271509。
对于死锁,刚才说过了,只要dbcc traceon(1204,3605,-1)执行后,所有的deadlock都会记录在sql log中。这个日志,纯文本文件,一般会列举出,至少两个对象的当前状态。常见的,如:
KEY: 8:1653632984:2 (da00ce043a9e) CleanCnt:1 Mode: U Fl ags: 0x0
KEY: 8:1653632984:1 (2d018af70d80) CleanCnt:1 Mode: X Flags: 0x0
通过察看sysobjects中ID,和index,我们可以找到对应的deadlock的table,通过分析执行计划,我们可以看出死锁发生的原因。具体内容,参考msdn文章,KBID是832524。
补充一下,GTEC也作SQL的case,虽然收费不菲,哈哈!
(注,连续三篇随笔介绍的情况和方法,同样适用于Vista/SqlServer 2005等最新MS产品)
要想解决上文提到的几种常见情况,首先,代码开发人员都要提供相应的dll的symbols。什么叫symbols?就是符号表!有了它,我们可以根据dump,确切的看到问题代码的所处位置:源文件名、方法名、行号等。对于VC++和.NET程序,symbols就是后缀为.PDB的那个文件,对于Borland系列的,需要build出来一个.map文件,然后通过Map2Dbg来生成.DBG文件。
这是我们自己的symbols,其实,微软各种产品也有symbols。一部分叫做public的,一部分叫做private的。对于后者,我们拿不到。
symbols准备好了之后,还要下载一个工具,叫做windbg。下载地址,你可以到google.cn上search:debugging tools for windows,安装就可以了。
有了这两样,我们就可以开始干活了!
对于Memory Leak,发现现象之后,分析哪里产生了泄漏,这是一个很难的任务。这里推荐几款工具和文章:
l XiongLi的blog,http://www.cnblogs.com/lixiong。 这是GTEC的大牛,偶的哥们+偶像。
l UMDH工具,安装好上面的windbg,你会在同一个目录下发现这个文件。具体用法,看上面的blog。
l Debug Diag工具,在google中搜索:IIS Diagnostics,到msdn上下载即可。
尤其是debug diag,使用非常简单。你用它抓到dump之后,可以自动进行分析,产生格式友好的html文件。UMDH也可以分析,不过使用起来有些罗嗦。
对于CPU使用占100%的情况,解决办法是,restart出问题的进程,打开taskmgr,选择该进程,眼睛盯着该进程的CPU变化(注意!眼睛不要眨!!!)。如果CPU持续达到85%以上(经常是100%居多),好,这时候利用上面提到的windbg,打开命令行,运行
adplus –hang –p 1234 –o c:\dumps
具体参数,可以运行adplus来看帮助。
抓好dump之后,可以用debugdiag来分析,也可以用windbg来分析。如果你想锻炼一下windbg的使用,可以按照如下操作来做:
.load clr10\sos.dll
!runaway
敲完这两个命令后,看输出的前几行,记录下来thread id。然后分别用kb来察看,把其中的代码都复制到你的notepad里面。
Ok,等一段时间(你自己决定长短),如果该进程CPU还是持续100%,按照上面步骤,继续抓。这样抓好3组后,通过!runaway命令,看占用CPU最高的thread的call stack。如果内容都类似,说明我们抓到了导致hang的代码。下面的工作就是分析这些call stack了。
对于hang,还有一种情况,就是CPU和Memory都很低,但是客户端执行就是没有反映。这里举一个我们自己项目的例子:
0x4bc8f458 0x77f88f03 [FRAME: ECallMethodFrame] [DEFAULT] I4 System.Threading.WaitHandle.WaitMultiple(SZArray Class System.Threading.WaitHandle,I4,Boolean,Boolean)
0x4bc8f470 0x26547659 [DEFAULT] I4 System.Threading.WaitHandle.WaitAny(SZArray Class System.Threading.WaitHandle,I4,Boolean)
0x4bc8f484 0x26385470 [DEFAULT] [hasThis] Class System.Data.SqlClient.SqlInternalConnection System.Data.SqlClient.ConnectionPool.GetConnection(ByRef Boolean)
0x4bc8f4b8 0x2758fbad [DEFAULT] Class System.Data.SqlClient.SqlInternalConnection System.Data.SqlClient.SqlConnectionPoolManager.GetPooledConnection(Class System.Data.SqlClient.SqlConnectionString,ByRef Boolean)
0x4bc8f4f8 0x2758f6b9 [DEFAULT] [hasThis] Void System.Data.SqlClient.SqlConnection.Open()
0x4bc8f534 0x2fdd26bc [DEFAULT] [hasThis] Void Genersoft.Focus.Db.DataBase.Open()
at [+0x7c] [+0x37]
从最下面看,那个Database.Open是我们自己的db helper,然后看最上面的红色部分,由一个WaitAny。这个call stack就是我们实际项目中发生的。
通过看中间那个红色加粗部分,我们看到了.net自己的代码,ConnPool.GetConn。通过reflector看这段代码实现,我们知道了.NET的db conn是从pool里面得到的,而pool的大小是有限的。所以,我们猜测:程序中存在着db conn没有Close或者DataReader没有Close的问题。searchi一番,果然如此。修改后,问题解决。
对于Access Violation问题(简称AV,J),我们一般也使用adplus进行抓取。命令如下:
adplus –crash –p 1234 –o c:\dumps
AV或者Crash分析起来也比较麻烦。如果想偷懒,可以用debug diag自动分析。如果自己手工作,参考上面xiong li的blog,哈哈!
这里也举一个我以前做培训时候的例子。
char a[] = "hello";
a[0] = 'X';
printf("%s",a);
char *p = "world"; // 注意p指向常量字符串
p[0] = 'X'; // 编译器不能发现该错误
printf("%s",p);
此段代码取自林锐博士的“高质量C/C++编程”,运行后会crash,我们抓了dump之后,按alt+7切换到汇编代码中,发现如下语句:
0040dcac c60058 mov byte ptr [eax],0x58 ds:0023:0042201c=77
在windbg下面运行!address 0042201c,得到如下结果:
0:000> !address 0042201c
00400000 : 00422000 - 00002000
Type 01000000 MEM_IMAGE
Protect 00000002 PAGE_READONLY
State 00001000 MEM_COMMIT
Usage RegionUsageImage
FullPath
注意上面的protect属性,该页面是只读的,所以导致了AV错误,这是真正的原因。当然,表面上的解释是,char * p = “world”,这个world存放在.data segment,不能修改。
好,讲完了,窗户纸好捅破吧?哈哈!
慢着,如果你没用这种思路来考虑过问题,你一定会看晕的。因为我写的太粗略了,太简单了!这里面涉及到的概念如此之多,涉及到的工具如此之多,经常让我们回忆起操作系统课程的N多东西。
想摆脱这种恶梦吗?找GTEC吧,上CASE,那些牛人们会解决的。
根据我的经验,上述问题几乎每个项目都会有,但是没有一个人会重视。用C++的,也许对于AV/Crash,彻底麻木了。用.NET的,对于session state、recycled等,也彻底麻木了。“很简单,重新启动机器就好了嘛!”。
是啊!这也是一个解决之道………………
(下面接着来做SQL Server的Performance Tuning,待续)
每个人都有这种经历,我们N多人辛苦作出来的软件,放到客户那里,过了一段时间,随着业务数据的增加和在线用户的增加,就开始“衰老”了。症状,典型的有几种:
1. 内存由100M疯涨到了1700M,最终要频繁重启进程或者服务器。
2. CPU狂涨到了100%,你用taskmgr眼睁睁的看着你的某个w3wp.exe站在那里居高不下。
3. 你的Button点下去之后,服务器内存很平稳,CPU一直低于5%,但是你的程序就是没有响应。
4. 客户无意中发现,event logs里面有大量的诸如aspnet_wp被recycled、某个dllhost突然访问了某个不该访问的地址。
5. ==………………………………
当我们还是少年的时候,碰到这种事情,第一件事就是问候BillG的所有女性亲属,然后手足无措的低头挨训 - 老板的抑或客户的。
这些问题,从良心上讲,基本上和BillG没关系,99.9999%的可能性,和我们自己相关。对于这种问题,我们需要的是解决思路。而解决问题的通则之一,就是对现象就行分类(能否看到本质,还说不上,呵呵)。对于上面的4种情况,我们一般归纳为几种情况:
l Memory Leak,就是内存泄漏
l Hang,某个东西挂起了
l Access Violation,访问了你不该访问的东西
那么,第一个疑问,如何把上面的N个问题,对号到这三种情况?
答案是,工具!每台Windows Server上,都有下面这几个工具:task manager、performance monitor、Event Viewer,对应的命令行分别是:taskmgr、perfmon、eventvwr。
对于上面的问题1,我们可以先用taskmgr找出有问题的进程,然后restart该进程。然后打开perfmon,建立一个log,抓取的对象是:Process。等内存涨到你忍受不了的地步的时候,stop那个log。
OK!我们再次打开perfmon,打开刚才那个log(它的尺寸应该和你的虚拟内存+物理内存大小相当),添加Process里面我们自己的那个进程,添加Private Bytes这个计数器。如果我们发现,这个值一直增长,直到最后,也没有掉下来,那么就基本可以确认为是Memory Leak的问题。
对于Performance Monitor,里面的Process/Processor/Memory/Physical Disks/Networks/System,都是我们经常要观察的。具体的含义,可以看perfmon自己的帮助。
对于上面的问题2,用taskmgr也能判断,但是taskmgr的缺点是,它是实时的,我们看不到CPU的趋势。所以,用perfmon我们可以检测Process,分析里面的几个cpu计数器。
问题3,稍微棘手一些,通过taskmgr或者performance monitor,看到的CPU和内存都很低。解决办法,后面再说。
问题4,系统的事件察看器中,有一些Dllhost发生Crash的记录,也有一些aspnet_wp被回收的记录。对于前者,我们叫做Crash,这可能是Access Violation引起的;对于后者,大部分情况是Memory Leak,或者某个杀毒软件干的事。
通过几个系统自带的工具,我们能够把客户眼中的问题,归纳到我们画的圈子中,下一步,就是要通过另外的手段,来解决它了。
(待续)