摘要:深入介绍了用于调试分布式 Web 应用程序的各种工具。目的是在一个基于 Web 的开发环境中,用 Internet Information Services (IIS) Active Server Pages (ASP) 来调试中间层的 Component Object Model (COM) 组件。
目录
本文重点
这篇文章将帮助您理解用来调试分布式 Web 应用程序的各种工具。在一个基于 Web 的开发环境中,用 Microsoft® Internet Information Services (IIS) Active Server Pages (ASP) 来调试中间层 Component Object Model (COM) 组件。阅读完本文后,用户应该能够:
面向的读者
在读完本文后,开发人员、技术支持人员以及从事网站开发和维护工作的 Web 管理人员将对 IIS 的体系结构和调试过程有一个更好的了解。
Internet Information Server 的体系结构
要了解调试组件的过程,必须首先对 IIS 的体系结构有一个很好的认识。下图将有助于您理解 IIS、ASP、Internet Server Application Programming Interface (ISAPI) 和组件之间的关系。
图 1. Internet Information Server 的体系结构
Internet 服务
inetinfo.exe 进程包括许多由 IIS 提供的 Internet 服务 (HTTP、FTP、SMTP、NNTP)。Inetinfo.exe 接收来自 TCP/IP 子系统的请求,并将这些请求送往各自的服务,这些服务又解释请求,执行请求,并把结果返回给客户机。因为所有这些服务都在同一个进程 (inetinfo.exe) 中运行,所以它们能够共享高速缓存的数据,如文件句柄、帐户信息和日志文件数据。
ISAPI
ISAPI DLL 有两种类型:ISAPI 过滤器和 ISAPI 扩展组件。
ISAPI 过滤器是在 inetinfo.exe 进程中运行的一种 DLL,用来过滤进出服务器的数据。过滤器注册事件的通知。当所选的事件发生时,控制权就会被转交给过滤器,您就可以监视和更改在服务器和客户机之间来回传送的数据。ISAPI 过滤器能用来提供 HTTP 请求的增强记录(例如,跟踪登陆到您服务器的用户)、自定义的加密、自定义的压缩或其它验证方法。一旦实施了 ISAPI 过滤器,所有的请求和响应都将通过它们,这将对站点的性能产生巨大的影响。
ISAPI 服务器扩展组件是一种能被 HTTP 服务器加载和调用的 DLL。ISAPI 服务器扩展组件,也称为 Internet 服务器应用程序 (ISA),用来增强符合 ISAPI 的服务器的功能。ISA 是从浏览响应程序中调用的,提供与公共网关接口 (CGI) 应用程序相似的功能,但它在性能上要大大优于 CGI。
Active Server Pages(ASP)
ASP 是作为 ISAPI 的扩展 DLL 实现的。IIS 和 ASP 有不同的线程池。IIS 使用一个称为异步线程队列 (ATQ) 的线程池,也叫共享池线程。IIS 用 ATQ 来接受套接字连接并向客户机发送静态内容。一旦确定请求是 ASP 页,IIS 就会将这个页请求发送到 ASP/MTS 线程池进行处理。
所有的 ISAPI 管理和 ASP 代码都被移植到一个称为 Web Application Manager (WAM) 的组件中。所有的信息都通过 WAM 进出于应用程序服务层。
异步线程队列(ATQ)
ATQ 提供了一个可重用线程池来处理每一个客户机请求。用操作系统的 Completion Port Logic 来执行调度。
Web 应用程序管理员(WAM)
WAM 是一个在 Web 服务之上的 COM 组件层,它存放 ISAPI DLL 的组件,并提供诸如支持多个 ISAPI DLL、故障恢复和进程隔离等服务。WAM 被注册为 MTS/COM+ 程序包/应用程序。
联机处理的 IIS 应用程序
如果将一个 IIS 应用程序配置为以联机处理方式(在 IIS 5.0 中为低级别的应用程序保护)运行,则它会运行在 Web 服务器 (inetinfo) 的进程空间。这提供了巨大的性能优势,因为避免了跨进程的高代价编组。但缺点是单个出现故障的应用程序就可能导致整个 Web 服务器的瘫痪。这将严重影响 Web 服务器的稳定性和可用性。默认情况下,在 IIS 4.0 中所有应用程序都是以联机处理方式运行,而在 IIS 5.0 中所有应用程序都是以共享的脱机处理方式(中等的应用程序保护)运行。
脱机处理的 IIS 应用程序
以脱机处理方式运行的应用程序阻止单个应用程序错误导致整个 inetinfo 进程瘫痪。这种服务器保护对于管理复杂的产品 Web 站点和托管多个应用程序是非常可贵的。如果一个应用程序出现故障,它的进程会自动终止,而不会影响 inetinfo 进程。
以脱机处理方式运行的应用程序的另外一个好处是,您可以卸载应用程序而无须重新启动 IIS。
以脱机处理方式运行的应用程序能获得自己的 ASP Worker 线程。这是一个非常大的优势,因为每向机器中添加一个处理器,脱机处理的应用程序就会获得另外 10 个 ASP Worker 线程。这只是默认情况,改变 ProcessorThreadMax 和 ASPProcessorThreadMax 的值将影响这个数值。本章稍后将详细讨论这些计数器。
脱机处理的应用程序的主要缺点是,每个应用程序都有自己的进程空间,因此存在影响性能的开销。
而且,如果在单个服务器上将大量的应用程序配置为以脱机处理方式运行,将会变得相当资源密集,并会影响服务器的性能。
共享脱机处理的 IIS 应用程序 (只出现在 IIS 5.0 中)
IIS 5.0 中介于脱机处理和联机处理之间的一个新选项。所有配置为共享脱机处理的 IIS 应用程序,都运行于在 MTS/COM+ 下创建的一个服务器程序包/应用程序之下。
这个选项享有脱机处理选项的优点,因为共享应用程序有它们自己的进程空间,一个应用程序的失败不会影响个 Web 服务器的可用性。另外,因为所有共享应用程序共享相同的进程空间,所以不会像脱机处理的应用程序那样资源密集。
共享脱机处理的应用程序的缺点是,由于共享相同的进程空间,所以一个应用程序的失败将导致共享池中的所有应用程序无法运行。
在 IIS 5.0 中,默认情况下应用程序以共享脱机处理方式运行。
图 2. 进程隔离:性能与坚固性
为什么调试很重要
调试是软件开发人员为了发现并解决代码中存在的问题所使用的方法。即便在单元测试和集成测试中非常努力地查找并改正了错误,但仍然有一些错误留在产品版本中。即使经过了高质量的软件生存期(其间进行了全面的单元测试和集成测试)的产品也难以幸免。
调试可能是 Web 应用程序开发人员的工作中最令人厌烦的部分。与独立的桌面应用程序不同,Web 应用程序能够分布在若干个系统上,并且经常结合不同的程序语言和技术。尽管有些应用程序错误可以通过周密计划予以避免,但组件间复杂交互作用的不可预料的副作用仍然会导致一些错误。跟踪和捕捉这些问题经常需要用到各种调试和开发工具。
调试中最重要的问题是,您必须紧记您是在调试数据,而不是在调试代码。询问正确的问题来获得正确的数据对于成功的调试是非常重要的。数据可以是全局的也可以是局部的。人们在调试中最常犯的错误是,认为他们所看见的症状就是问题的实际原因。
在分析调试过程以前,最好先熟悉本文中将会频繁使用的与调试有关的几个术语。
调试符号(符号调试信息)
符号提供了一个解析已加载的可执行文件中的全局变量和函数名的方法。符号就是被映射到相对于 DLL 的加载基地址的偏移量的函数名称或 API 名称。在组件的编译过程中,链接程序生成符号。将符号信息包含在组件本身内部(内部符号)使得它变得易于处理,因为这种情况下不会出现不匹配的符号。在产品中通常会去掉组件的符号,这样组件的尺寸就会显著变小,当组件被加载时消耗的内存也会减少,并会使组件更高效。
DBG 文件是通过使用 SPLITSYM 或 REBASE 将符号从进程映像分离到一个单独的文件中而创建的。DBG 文件一般不包括可识别代码形式的符号,因此它不能用于源代码级的调试。
Microsoft Visual C++® 和 Microsoft Visual Basic® 目前的版本都能创建一种称为 Program Database (PDB) 的符号格式。调试程序能够直接使用这种 PDB 文件。
除了安装自定义组件的符号之外,Microsoft 产品的符号也很容易获得。
符号所在的位置
以下站点指向这些符号所在的位置
Windows NT 4 Service Pack 6 symbols
http://www.microsoft.com/ntserver/nts/downloads/recommended/sp6/debug/default.asp
Windows NT 4 Service Pack 5 Symbols
http://www.microsoft.com/ntserver/nts/downloads/recommended/sp5/debug/default.asp
Windows NT 4 Service Pack 3 Symbols:
ftp://ftp.microsoft.com/bussys/winnt/winnt-public/fixes/usa/nt40/ussp3/i386/nt4sym3i.exe
http://support.microsoft.com/download/support/mslfiles/Nt4sym3i.exe
Windows NT Option Pack Symbols:
ftp://ftp.microsoft.com/bussys/IIS/iis-public/iis40/symbols/x86/symbols.cab
ftp://ftp.microsoft.com/bussys/IIS/iis-public/iis40/symbols/x86/install.inf
ftp://ftp.microsoft.com/bussys/IIS/iis-public/iis40/symbols/x86/install.exe
Windows NT Option QFE Update Symbols:
ftp://ftp.microsoft.com/bussys/IIS/iis-public/fixes/usa/IISUPD/SYMBOLS/x86/IISUpdis.exe
Site Server 3.0 Service Pack 1 Symbols:
ftp://ftp.microsoft.com/bussys/sitesrv/sitesrv-public/fixes/usa/siteserver3/sp1/x86/symbols/ss3sp1db.exe
Site Server 3.0 Service Pack 2 Symbols:
ftp://ftp.microsoft.com/bussys/sitesrv/sitesrv-public/fixes/usa/siteserver3/sp2/x86/Symbols/ss3sp2debug.exe
Microsoft Windows 2000® Server Symbols:
http://www.microsoft.com/WINDOWS2000/downloads/otherdownloads/symbols/default.asp(英文)
Windows 2000 Customer Support and Diagnostic Tools 光盘中也包含了这些符号
常用调试程序概述
CDB
CDB 是基于控制台的一种调试程序,它在一个命令窗口中启动并运行。因为它只占很小的内存空间,所以它的效率非常高。可以用它调试 Windows NT 上的 Win32 应用程序。
NTSD
NTSD 类似于 CDB,但它不是在启动它的窗口中运行,而是打开一个新窗口并在其中运行。
WinDBG
WinDBG 是一种流行的轻型 GUI 调试程序。它能调试 Windows NT、Windows 95 和 Windows 98 中的 Win32 应用程序。目前它包含在 Windows 2000 Customer Diagnostic and Support Tools CD 中。它能够执行源代码级的调试,能够读取转储文件并且对符号有极好的支持。由于它是一种 GUI 调试程序,所以比 CDB 和 NTSD 稍微慢一些。
IIS Exception Monitor 6.1
IIS Exception Monitor 是一种轻型且极易使用的调试程序。使用这种调试程序可以直接分离出引起问题的 DLL。它提供一个由内存中的进程映像组成的快照文本文件。它使用 CDB 收集信息。有时收集的信息可能不够充足。运行 IIS Exception Monitor 需要 Windows Scripting Host。您可以从借助 IIS 异常监视器进行调试(英文)下载这个工具。
Exception Monitor 7.x IIS Exception Monitor 的最新版是 DBGPLUS 套件的一部分,该套件包含了本文讨论的所有调试工具。工具名中不再包含 "IIS" 这个词,因为它已不再局限于调试 IIS 问题;可以用它来调试任何进程。该工具使用 WinDBG 收集有关它所连接的进程的信息。尽管功能与其以前的版本非常类似,但用户界面已彻底更新并加入了一些新的功能部件。该工具包含在 Windows 2000 Customer Diagnostic and Support Tools CD 中,您也可以从 Windows 2000 客户支持诊断(英文)下载它。
注: 本文中的示例是基于 IIS Exception Monitor 6.1 的。但是,稍加修改就能用于 Exception Monitor 7.x。
Developer Studio
Developer Studio 是一种集成开发环境,包括一个功能强大的调试程序。它主要用在开发阶段,其源代码级的调试功能非常出色,并且会留出很大的内存空间。将 Developer Studio 安装到计算机上会修改很多系统文件;因此,通常不会在产品环境中使用它。开发人员可以根据需要写一些自己的 add-in 来定制此调试程序。
检查符号
调试过程的第一步就是检查是否正确地安装了符号。可以使用 CHECKSYM 工具来完成这一任务。例如,为了检查为在某个进程中运行的所有 DLL 安装的符号是否正确,您可以使用下面的命令:
checksym -p processname.exe -r -v -y c:\symbols
可以使用通配符 (*.*) 指定某个目录中的全部文件。-v 开关指示工具检查组件的符号,而 -y 开关指定符号文件的路径。
要获得有关可以与 CHECKSYM 一起使用的开关和选项的详细信息,请键入以下命令:
checksym -??
选择调试程序
各种调试程序的功能各不相同,因此,在决定使用某种调试工具之前对任务有一个深入的了解是非常重要的。调试方案大致可分为三类:本地调试,远程调试和用户转储调试。
本地调试
当您能够在本地重现问题,最好使用本地调试;因为它提供了对环境的完全控制,这样您就有可能更快地解决问题。在这个方案中,调试程序的选择和安装也相对容易一些。本地调试的缺点是,有时很难准确地在本地重复或重现(用技术支持人员的话来说,叫做 "repro")。
远程调试
当调试程序和正在调试的进程不在同一台计算机上时,称为远程调试。远程调试可按两种方式进行,一种方式是,通过代理将一个调试程序连接到远程进程;另一种方式是,在远程计算机上运行调试程序,而在本地计算机上共享调试程序的命令行。在这种情况下,代理在远程的系统上运行,而调试程序在本地系统上运行;代理连接到进程,读取进程的内存,并将它传送到本地调试程序。使用代理的远程调试的优点是,安装简单而快速。其缺点是,容易出现网络问题,有时会导致远程计算机上的代理崩溃。因为符号只安装在本地计算机上,所以它们可能与远程计算机上的符号不匹配。因此,远程调试可能相当慢。
使用共享命令行的远程调试需要使用 remote.exe 工具,它是随 Windows 2000 Customer Diagnostic and Support Tools CD 分发的。remote.exe 工具既可以作为服务器运行,也可以作为客户机运行。使用命名管道连接服务器和客户机。WSRemote.exe 是 remote.exe 的一个套接字版本/变体。Remote.exe 允许共享 STDIN(标准输入)和 STDOUT(标准输出)。在远程计算机上,可以使用下面的命令运行该远程计算机的服务器实例:
remote /s application pipename
在本地计算机上,您可以使用下面的命令将 remote.exe 作为客户机运行:
remote /c machinename pipename
这种设置非常可靠,并且能够容忍网络故障。
用户转储调试
当出现异常时,用户转储会在硬盘上创建进程的内存的一个快照。Windows NT 有一种称为 DrWatson 的工具,当系统中出现异常时,它能够创建用户转储。还有一种称为 User Mode Process Dump 的调试工具。您能配置该用户转储工具,使它监视某个特定进程的异常。随后可以在调试程序(如 WinDbg)中查看这样创建的用户转储文件,很轻松地对它进行分析,而不必调试实时进程。
使用 NTSD
NTSD 调试程序在启动时要求用户指定一个要连接的进程。使用 TLIST 或 PVIEWER,您可以获得某个现有进程的进程 ID,然后键入 NTSD -p pid 来调试这个进程。NTSD 命令行使用如下的句法:
NTSD [options] imagefile
其中,imagefile 是要调试的映像名称,options 是下面选项之一:
表 1. NTSD 映像文件选项
选项 | 说明 |
-2 | 打开一个用于调试字符模式的应用程序的新窗口 |
-d | 将输出重定向到调试终端 |
-g | 使执行自动通过第一个断点 |
-G | 使 NTSD 在子程序终止时立即退出 |
o | 启用多个进程的调试,默认值为由调试程序衍生的一个进程 |
-p | 指定调试由进程 ID 标识的进程 |
-v | 产生详细的输出 |
例如,假设 inetinfo.exe 的进程 ID 为 104。键入以下命令将 NTSD 调试程序连接到 inetinfo 进程 (IIS)。
NTSD -p 104
也可使用 NTSD 启动一个新进程来进行调试。例如,NTSD notepad.exe 将启动一个新的 notepad.exe 进程,并与它建立连接。
一旦连接到某个进程,您就可以用各种命令来查看堆栈、设置断点、转储内存,等等。
表 2. 常用命令
命令 | 含义 |
~ | 显示所有线程的一个列表 |
KB | 显示当前线程的堆栈轨迹 |
~*KB | 显示所有线程的堆栈轨迹 |
R | 显示当前帧的寄存器输出 |
U | 反汇编代码并显示过程名和偏移量 |
D[type][<range>] | 转储内存 |
BP[#] <address> | 设置断点 |
BC[<bp>] | 清除一个或多个断点 |
BD[<bp>] | 禁用一个或多个断点 |
BE[<bp>] | 启用一个或多个断点 |
BL[<bp>] | 列出一个或多个断点 |
使用 WinDBG
WinDBG 在命令窗口中列出与 inetinfo 进程有关的 DLL,并显示是否为某个特定的 DLL 加载了符号。这时正在调试的 inetinfo.exe 应用程序 (IIS) 被停止。要再次启动它,请在命令窗口中键入 g。
这时列出此进程中的所有线程的堆栈。
查看当前线程的堆栈的另一种方法是,通过单击 View 菜单上的 Call Stack 来查看 Call Stack 窗口。
这时列出进程中的所有模块 (DLL),包括它们的名称和加载基地址。
使用 IIS Exception Monitor
在下面的示例中,用 IIS Exception Monitor 连接到 inetinfo (IIS) 进程,并调试该进程。
注: Verify Symbols 对话框使用 CHECKSYM.EXE 来获得它的符号信息。CHECKSYM.EXE 位于 C:\ixcptmon\bin 目录下。
对于我们的示例,选择 In-Process 选项,然后单击 next。
对于我们的示例,请选选 Automatic 选项,然后单击 Next。
您将看到标题栏中为 "IIS_<Process ID>" 的一个命令提示符窗口。该窗口用来监视 inetinfo.exe 进程。一旦有异常发生, IIS Exception monitor 就会创建日志文件 (.dbl),重新启动 IIS Service,并关闭此命令提示符窗口。
日志文件在 \IXCPTMON\Logs 目录下。
用日志文件鉴别问题
当 IIS Exception Monitor 安装时,日志读取程序 Readlogs.exe 也会随之安装。您可以用 Readlogs 来打开刚刚生成的 .dbl 日志文件。
图 3. Readlog 对话框
当 Readlogs 打开日志文件时,它显示引起异常的线程的堆栈。如果符号安装正确,则 Readlogs 就会解析函数名,并将它们显示在 Function 栏中,如上图所示。函数名与 DLL 名之间用 "!" 符号分隔。例如,上图中的函数名 OutputDebugStringA 就驻留在 KERNEL32 DLL 中。
在分析器中,始终可以通过单击 Fault Stack 按钮来查看有故障的线程的堆栈。可以使用 << 和 >> 按钮滚动堆栈列表。您也可以选择下拉列表 Choose a Command 中的任何一个命令来执行。如果您不想看 Microsoft 的文件,则您可以将它们隐藏。这有助于您快速确认是否有第三方 DLL 包含在进程中。除了列出堆栈以外,日志文件还包含有关以下各项的信息:加载的 DLL 或模块、任何锁定及其线程 ID、以及检测到的错误。
单击 DLLs 按钮,您可以列出在此进程中加载的全部 DLL。
您可以用 Errors 按钮列出在进程运行时出现所有错误,包括警告。您也可以定制 IIS Exception Monitor 来捕获某些错误。
单击 Locks 按钮可以显示锁定及其拥有的线程 ID 的一个列表。只要一个线程想以独占方式访问某个资源,这个线程就会要求锁定此资源。当两个或多个线程同时锁定一个特定资源并等待另一个线程放弃锁定时,就会出现称为“死锁”的现象。您可以检查锁定列表以及它们拥有的线程,以确定死锁问题的可能来源。
通过单击 P.T.C. (Prior to Crash) 按钮,可以列出进程失败前的最后 20 行日志。
通过单击 Report 按钮,也可以将全部信息的一份副本以报表格式粘贴到剪贴板中。
通过单击 Sys Info 按钮,可以显示有关可用于调试的系统的一些有用信息。
通过单击 Config 按钮,可以定制读取日志以满足您的需要。
使用 User Mode Process Dump Tool
User Mode Process Dump 工具连接到某个特定的进程,并在发生异常时将进程的内存转储到硬盘中。首先,您必须运行 C:\Program Files\Debuggers\bin\userdump\ 目录下的 setup.exe 来安装 User Mode Process Dump 服务。User Mode Process Dump 安装程序是作为 DBGPLUS 套件安装的一部分安装的。
通过创建规则,您可以定制该工具,使其只捕获某些异常。单击 Rules。
如果在该工具正在监视的进程中发生异常,它将把进程的内存转储到硬盘中。与 IIS Exception Monitor 不同,User Mode Process Dump 工具在转储进程之后并不重新启动 IIS 服务。
在 WinDBG 调试程序中能够打开并分析这样创建的用户转储,以便确定故障的原因:
调试哪个进程?
确定要连接和调试哪个进程并不容易。正像早些时候在 "Internet Information Server 的结构" 中讨论的那样,可以按几种不同的方案使用组件和 IIS 应用程序。下表根据组件类型和 IIS 配置说明应该连接哪个进程。
表 3. 组件类型和 IIS 配置
组件类型 | 联机处理的 IIS 应用程序 | 脱机处理的 IIS 应用程序 | 共享脱机处理的 IIS 应用程序 (IIS 5.0 only) |
库程序包/应用程序中的组件或不属于 MTS/COM+ 的组件 | 连接 inetinfo 进程 | 连接 IIS 应用程序驻留在其中的程序包/应用程序的 MTX/DLLHOST 实例 | |
服务器程序包/应用程序中的组件 | 连接 IIS 组件驻留在其中的程序包/应用程序的 MTX/DLLHOST 实例 |
当您试图调试 MTS/COM+ 下一个组件或 IIS 应用程序,而这个组件或 IIS 应用程序又是作为一个服务器程序包/应用程序安装的时,调试将变得异常复杂。在单个系统中可能运行着几个服务器程序包/应用程序,其中每一个都有自己的 MTX.exe 或 DLLHost.exe。哪一个是您需要的呢?首要任务是找出加载您要调试的组件的那个进程的 PID(进程标识符)。您可以使用 IIS Exception Monitor 来获取这个信息,在 Process Options 页中选择 Out-Of-Process 选项。
这时出现 Select Out-Of-Process Application 页,在此您可以查看按应用程序名/程序包名列出的进程,从而使确定要连接哪个 PID 变得容易了。
在分布式 Web 应用程序中最常见的问题有:
在这一节,我们将分析引起这些问题的可能原因,讨论如何调试并鉴别引起每个问题的代码行。
ASP 0115—访问冲突
访问冲突主要是由堆和堆栈故障引起的。
如果您进行这样一些操作,如分配一块内存并试图在该内存块之外执行读写操作,就会引起访问冲突。
例如:
char* pszTemp = (char*) malloc(strlen("BOMB THIS"));strcpy(pszTemp,"BOMB THIS");
在本例中,用户分配 9 个字节的内存,但没有包含 NULL 的结束符("\0")。当试图向此内存位置复制字符串时,因为函数试图访问所分配的内存段以外的部分,就会产生错误。
另一个方案试图访问一个非法的内存位置。
例如:
FunctionA(){ m_pAV = NULL; FunctionB(m_pAV); }FunctionB(CAccessViolate* pAV){ pAV->BombThis();}
在本例中,FunctionB 使用传递给它的 pAV 指针而未检查其合法性,从而导致访问冲突。
大多数访问冲突发生在当 ASP 页试图访问一个组件,而这个组件又正在试图做错误的事情时。为简单起见,我们假定您已将某个组件安装为库程序包。当您试图访问使用这个组件的一个 ASP 页时,ASP 页可能返回以下信息:
error 'ASP 0115'
Unexpected error
/<Web Server Name>/<ASP file name>.asp
A trappable error occurred in an external object. The script cannot continue running.
这个错误消息指出在某个特定的 ASP 页中有一个错误,它是由在某个外部对象中产生一个异常引起的。它表明您试图在 ASP 页中访问一个失败的组件。您可以这样来进一步隔离问题,在 ASP 页中删除对任何组件的所有引用,然后再逐个将它们添加进来,直到找到故障原因为止。
如果隔离的过程过于冗长乏味,或者您没有获得一个指明 ASP 页名的清晰错误信息,则可以将 WinDBG、User Mode Process Dump 或 IIS Exception Monitor 连接到 inetinfo,并开始调试。
找出引起访问冲突的代码行
假定您已使用 User Mode Process Dump 工具连接到 inetinfo 进程,并创建了一个转储文件。现在开始在 WinDBG 中检查转储文件,并尽力勾画出所发生的事情。
在 WinDBG 中打开故障转储文件,在 View 菜单中,单击 Call Stack。
图 4. 显示堆栈的 Windows 调试程序对话框
在上图中,即使堆栈中最顶部的函数是 KERNEL32 DLL 中的一个函数,但这并不一定意味着出错的代码就在这个函数中,而且大多数情况都不是这样。下一步是确定列表中是否有非标准的 Windows 系统组件,并确定在出现故障时它们正在做什么。如果您从下到上的读上图中的堆栈,您会发现第 8 行和第 7 行表明 ASP 使用了 Microsoft VBScript 引擎来启动组件,第 6 行和第 5 行表明 VBScript 引擎使用 IDispatch 接口方法调用组件 (CRASHATLAPP) 内部的方法。因为使用了 IDispatch 接口,图中出现 OLEAUT32.dll 便是很自然的了。第 4 行和第 3 行表明了这一点。堆栈中的第 2 行表明调用了 BlowOff 方法,BlowOff 又调用了 KERNEL32 DLL 中的 OutputDebugString API。假定标准 Windows 组件 KERNEL32 dll、OLEAUT32.dll 以及 VBScript 引擎没有问题(从问题隔离的角度看),这是一种安全的假定。接下来要处理的只剩下堆栈中列出的唯一的非 Windows 组件,CRASHATLAPP。
在 CRASHATLAPP 内部的 BlowOff 方法调用了 OutputDebugString API。通过在命令窗口输入 kb,我们可以检查 BlowOff 传递给 OutputDebugString 的参数的值。
在 KERNEL32 DLL 内部,BlowOff 方法将 NULL 作为参数传递给 OutputDebugString 方法。最好查看一下在 BlowOff 方法内部进行了什么操作,使得它传递一个 NULL 值。如果您手头有 CRASHATLAPP 组件的源代码,您可以双击堆栈中的第 2 行。
图 5. 显示堆栈的 Windows 调试程序对话框
这将调出 BlowOff 方法的源代码。
图 6. 显示 BlowOff 方法的源代码的 Windows 调试程序对话框
源代码清楚地显示,NULL 被作为参数传递给 OutputDebugString。
尽管不会在产品代码中遇到这种问题,提供这个示例的意图是,使您对调试和确定访问冲突错误的原因的过程中所涉及的步骤有一个概念。
ASP 服务器太忙的错误
ASP 服务器太忙错误通常是由 ASP 请求队列填满引起的。当 IIS Web 服务器高负荷运转或等待一个阻塞的线程时,就可能产生非常大的队列。当调用 IIS Web 服务器正在等待的某个组件的频率超过该组件所能承受的频率时,通常就会发生这种情况。所有进入的请求都被放在 ASP Request 对列中并按照它们进入的先后顺序进行处理。如果阻塞只持续很短的一段时间,则队列机制最终将解决问题并开始及时响应请求。但是如果阻塞持续很长的时间,队列中的请求就会不断增加,并最终达到 ASPRequestQueueMax 元数据设置中的峰值,默认值为 3000。如果队列大小在 3000 以下,当第 2999 个请求在队列中,并等待要填入的所有其它请求时,发出这个请求的用户就会看到一个沙漏标志。即使在这个时候,如果队列清除得足够快,这个请求仍然会被接收和处理。相反,如果队列大小达到 ASPRequestQueueMax 的值,而又有一个新的请求加进去,则 IIS 服务器返回 ASP 服务器太忙的错误。
ASPProcessorThreadMax 和 ASPRequestQueueMax 的设置
ASPProcessorThreadMax 和 ASPRequestQueueMax 元数据设置对站点的性能有很大的影响。ASPProcessorThreadMax 参数指定处理器能承受的最大线程数。ASPRequestQueueMax 参数指定请求队列的最大值。这两个设置可以通过以下命令更改:
adsutil.vbs set w3svc/AspProcessorThreadMax NewValue
adsutil.vbs set w3svc/AspRequestQueueMax NewValue
对于写得较好的脚本,ASPProcessorThreadMax 值应该低一点。调整 ASPProcessorThreadMax 的主要目的是,使处理器在最大负荷情况下的利用率达到 50% 以上。
由于每一个站点的物理配置有很大的不同,因此很难预测这些参数的最优值。应该使用性能监视器来监视关键的计数器,以作出有关这些参数的一个决策。使用性能监视器的统计数据应该包括:
处理器时间:%Processor time
ASP: Requests per second, Requests rejected, Requests queued
您可以在一个产品站点达到最大负荷时监视这些值。通常,您会在高容量的站点观察到队列中的请求数的起落。如果处理器的利用率很低,并且总的队列长度从不上升,那表明您的站点的容量远大于您的实际需要。如果总的队列长度上下起伏,而处理器利用率在 50% 以下,那就表明有些请求被阻塞,这时您将从增大线程数中获益。如果在增加线程后观察到队列中的请求的数量比以前更糟,而且处理器利用率也降低了,这就说明有严重的阻塞问题。典型的阻塞问题可能是由对数据库进行的、需要执行很长时间的 SQL 查询引起的。增大线程数允许对执行查询的页的更多请求进入;这样会给数据库造成更大的压力,从而使查询的响应时间变得更长。要获得 ASPRequestQueueMax 的一个最优值,请选择一个可接受的最长响应时间,计算在这段时间内可清空的队列的大小,然后将 ASPRequestQueueMax 的值设置为稍小于这个大小。
调试示例
我们已详细讨论了 ASPProcessorThreadMax 参数和 ASPRequestQueueMax 参数,接下来我们将模拟 ASP 服务器太忙的错误,并试着调试哪个组件的线程发生了阻塞,以及为什么会发生阻塞。假定有一个 ASP 页,并且该页正在调用一个发生阻塞的组件。
为了模拟这个问题,我们通过以下命令将 ASPRequestQueueMax 参数设置为 20:
cscript c:\inetpub\AdminScripts\adsutil.vbs SET w3svc/ASPRequestQueueMax 20
要启动性能监视器,请执行以下步骤:
假定在您的 ASP 代码中调用某个组件,而该组件正在阻塞请求。
添加这些计数器后,观察计数器的值。您会发现,当队列中的请求的计数器达到 ASPRequestQueueMax 的值(20)时,服务器返回 ASP 服务器太忙的错误。
图 7. 显示队列中的请求计数器值的性能监视器
为了调试 inetinfo 进程来确定哪个线程发生阻塞以及为什么发生阻塞,请启动 WinDBG 并连接到 inetinfo 进程。导致线程阻塞的最常见的原因是死锁。您应该试着识别锁定计数大于 0 的那个关键部分,以及该部分拥有的线程 ID。这个线程就是问题所在。一旦获得线程 ID,就可以调试该线程的堆栈,找出是哪个方法发生了阻塞。您可以在命令窗口输入以下命令来完成这一步:
!locks
这时应该列出锁定计数大于零的线程。输出将类似于以下内容:
CritSec ?g_cs@@3U_RTL_CRITICAL_SECTION@@A+0 at 100355A0LockCount 24RecursionCount 1OwningThread 6e8EntryCount 18ContentionCount 18*** Locked
找出锁定计数大于零的那个部分,并注意它拥有的线程 ID。
有时,!locks 命令不提供所需的信息,这通常是因为 ntdll.dll 的符号排列不正确。在这种情况下,您可以使用下面比较原始却行之有效的方法。
在命令窗口中,键入 ~*kb 来显示所有线程的堆栈。查找包含 NTDLL!RtlEnterCriticalSection 的线程堆栈。在您识别出包含此项的线程以后,请注意 NTDLL!RtlEnterCriticalSection 所在行中 param1 的值。这个值就是传送到 EnterCriticalSection API 的有问题的那个对象。使用这个值,在命令窗口中输入以下命令:
!Critsec 100355A0
用上面的任意一种方法,都应该能够识别出锁定计数大于 0 的那个线程 ID。将此线程 ID 从十六进制转换为十进制。
现在请检查这个线程,并识别正在阻塞线程的那个方法。在 Debug 菜单中,单击 Threads。
在 Threads 对话框中,单击从以上步骤中找出的线程 ID,单击 Select。
在 View 菜单中,单击 Call Stack。这时显示此线程的堆栈。就像在上一个示例(访问冲突)中那样开始调试堆栈。
100% 的 CPU 问题
当一个线程占用了处理器的全部时间时,就会出现 100% 的 CPU 问题。一种可能的原因是,某个程序循环独占了 CPU 资源。性能监视器是解决 100% 的 CPU 使用问题的理想工具。首先需要创建一个性能监视器日志。
准备日志
要启动性能监视器,请执行以下步骤:
添加以下的计数器:
现在已经设置好所有的内容,可以开始记录数据了。用鼠标右键单击这个日志名,然后单击开始。确保日志捕获了处理器利用率猛增至 100% 的那段时间内的所有数据。启动 WinDBG 并连接到 inetinfo 进程。一旦处理器利用率回到正常水平,您就可以停止记录,方法是用鼠标右键单击该 Log Name ,然后单击 Stop 。
检查日志
现在已经创建了日志,我们可以开始仔细检查它来识别消耗了大部分 CPU 时间的进程,并进一步识别此进程内部的耗尽 CPU 时间的线程,以及它这样做的原因。
第一步是识别哪一个进程消耗了大部分的 CPU 时间。要打开日志文件,请单击控制台根目录下的 System Monitor 文件夹。在详细信息窗格中,用鼠标右键单击图表,然后单击 Properties。在 System Monitor Properties 中,单击 Source 选项卡。选择 Log File,通过浏览选择您刚创建的日志文件。
用鼠标右键单击图表,然后单击 Add Counters。在 Add Counters 对话框中,从 Performance Object列表中选择 Process。选择 Select counters from List,然后从此列表框中选择 %Processor Time。选择 All instances,单击 Add,然后单击 Close。
这将为我们提供所有处理器及其 CPU 使用情况的列表。现在您需要查清哪个进程引起了 100% 的 CPU 问题。
确保选中了 Highlight 工具按钮(有一个球形图标)。现在您可以滚动这些计数器,将除最接近 100% 标记的那个进程之外的所有进程全部删除。确保只选择了那些与您有关系的计数器。例如,如果您正在调试 100% 的 CPU 利用率的问题,您也许应该查找 inetinfo、其它的脱机处理的 COM 程序包/应用程序 (MTX.EXE/DLLHOST.EXE) 或者 IIS 应用程序。
一旦您在进程级将问题隔离出来,您就可以将注意力集中在该进程内部的那些在出现 100% 的 CPU 问题时仍然活动的线程上,并试着隔离引起问题的那个线程。为此,我们添加更多的计数器来显示该进程中所有线程的 CPU 使用情况。对于本例而言,就是为 inetinfo 进程中的所有线程添加计数器。用鼠标右键单击图表,然后单击 Add Counters。在 Add Counters 对话框中,从 Performance Object 列表中选择 Thread。选择 Select counters from list,然后从列表框中选择 %Processor Time。选择 Select instances from list,然后选择以 inetinfo/* 开头的所有实例。单击 Add,然后单击 Close。
这时,图表中显示在 inetinfo 进程中的所有线程的 CPU 使用情况。
现在可以滚动计数器,将除最接近 100% 标记的那个线程以外的所有线程全部删除。这样我们就准确地找到了引起问题的那个线程。
图 8. 显示关于 CPU 使用情况的计数器的性能监视器
我们只保留实例 ID 为 34 的线程,它占用了最长的 CPU 时间。现在必须找出这个线程的线程 ID,然后在 WinDBG 中找到这个线程并对它进行调试。
要找出线程 ID,请用鼠标右键单击图表,然后单击 Add Counters。在 Add Counters 对话框中,从 Performance Object 列表中选择 Thread。选择 Select counters from list,然后从此列表框中选择 ID Thread。选择 Select instances from list,然后选择实例 inetinfo/34。单击 Add,然后单击 Close。
这样我们就得到了引起问题的那个线程的 ID。选择 ID Thread 计数器。记录该计数器的最后一个值、最大值、最小值和平均值。通常这四个数是相同的。记录这个值(本例中为 764),并切换到 WinDBG 应用程序。
图 9. 显示 ID 线程计数器的性能对话框
在 WinDBG 的 View 菜单上,单击 Threads。这将显示在出现 100% 的 CPU 问题时在 inetinfo 进程内部运行的线程的一个列表。查找您在性能监视器中记录的线程 ID 号。如果在列表中找到此 ID 号,则单击它,然后单击 Select。单击 OK。
在 View 菜单上,单击 Call Stack。这时显示引起问题的那个线程的堆栈。该堆栈将有问题的方法隔离出来。一旦我们找到这个方法,我们就可以返回原代码,查看是哪行代码引起这个问题的。
通过使用 IIS Exception Monitor、WinDBG、User Mode Process Dump 工具和性能监视器,可以有效地解决分布式 Web 应用程序中的大多数常见问题。这些工具当然没有问题的所有回答和解决方案,但它们能够为您最终解决问题指出一个方向。
方法:对 Internet Information Services 中出现的高 CPU 利用率“挂起”进行调试(英文)
信息:对 Internet Server 产品中的异常进行调试(英文)
其它读物
Web 站点
知识库文章
Q229814, 将 IIS 配置为处理高利用率(英文)
Q150934, 如何为 NT 调试创建一个性能监视器日志(英文)
致谢
我非常感谢 Systems Integration Engineering 集团的技术主管 Aaron Barth。我认为 Aaron 是我的调试老师;他教会我如何进行调试,而且在我写这篇文章的过程中给予了我极大的帮助。
Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=12723