如何排查线上w3wp.exe CPU高的问题,使用到了WinDbg、Visual studio来分析IIS进程池的.dmp文件

最近发现服务器上某个web站点老是CPU很高,该站点部署在IIS上,我IIS上有多个站点,每个站点一个进程池,每个进程池取名都是根据站点来取的,所以很容易看出哪个站点吃掉的CPU,该站点已运行十几年,是基于.net 4.8 framework 编写的web站点(十几年的老项目重构的话就不用提,新项目大家还是用.net core加个前端框架,前后端分离 ,真的很香)

发现CPU很高的时候,我第一反应是用windbg来分析进程池的转储文件(即创建.dmp文件,windbg是加载.dmp文件来分析的),因为以前没做过这个工作,真的花了很长时间踩坑(2天,所以这篇文章是我踩坑2天总结的)

先说第一个坑,我用的服务器系统是windows server2019 64位的操作系统,我的web站的IIS进程池->高级设置中开启了"启用32位应用程序"(因为某个读写excel的组件是基于32位的),所以我的进程池是运行在32位模式下的,但是我一开始并不知道,我是直接在操作系统的任务管理器中找到这个进程池的进程,然后右键点击"创建转储文件",但是各位千万要注意,这里有大坑,因为64位的操作系统的任务管理器这个是64位的(任务管理器的进程名是 Taskgmr.exe),,所以我是在用 64位的任务管理器,创建32位的进程池的转储文件,这里一定不能这么干,如果这么干,接下来你无论做何种努力,都将得到很多莫名其妙的反馈(windbg能加载,但是总有莫名其妙的问题,visual studio也能分析,也得不到正确的结果,我后面会说结果里面缺少了什么,总之你记住这个坑是我花了一天多时间才踩出来的。。。。。),那如何确认自己的进程池是64位还是32位呢,你可以打开服务器的任务管理器,然后点击"详细信息"这个标签,然后在任务管理器列表的标题上右键,会出现"隐藏列、选择列",你点击"选择列",然后拖动滚动条,往下拉找到"平台"勾选上(图1),任务管理器上就会多出一列平台,上面会显示"64位或32位",如下图2

图1

图2

从上图2可以发现,我PID=11720这个 w3wp.exe 它是32位的,我的任务管理器却是64位的,这样子创建出来的转储文件就有问题,正确创建转储文件的是用32位的任务管理器去创建32位进程的转储文件,64位系统里面其实自带了32位的任务管理器的,路径如下:C:\Windows\SysWOW64\Taskmgr.exe (64位的任务管理器的路径是:C:\Windows\System32\Taskmgr.exe) ,打开32位任务管理器之前,要先关闭之前已经打开的64位的管理器,打开32位管理器后也一样去勾选"平台"这个列(确保自己没有开错任务管理器),可以从下图3看出来,我PID=9056 这个进程池显示是32位,taskmgr.exe也是显示32位(和上图的进程池没对上是因为我随便选了一个截图而已,这只是为了证明任务管理器和进程池的位数一致了)

图3

创建进程池的转储.dmp文件的这个操作一定要注意!!!:使用和自己进程池位数一致的任务管理器来创建转储文件.dmp非常关键(32位的进程池用32位任务管理器创建转储文件,64位进程池用64位任务管理器创建转储文件,如何打开不同任务管理器上面有介绍)!!我大多数时间都浪费在了因为创建错了dmp文件导致分析结果一直没正确(windbg可以加载,visual studio 也可以加载(visual studio可以分析.dmp文件,而且非常好用!),两个工具都可以进行分析,就是没办法给出准确结果!(这是非常要命的,因为我根本没怀疑过这个.dmp文件会有问题),比如visual studio 死活不给出"线程CPU使用率摘要"这个关键数据,其他数据给了,让我误以为我的.dmp文件没有问题,所以掉进这个坑里面掉了这么久。。。。如果它干脆加载不了,我也会怀疑到是我的.dmp文件有问题)

你要在你CPU高的时候去用任务管理器手动创建转储文件,你也可以使用其他工具,在CPU超过某个阈值的时候自动创建.dmp文件(自己搜一下,我没有用工具不好介绍经验)

如果你已经按上面这个操作创建了.dmp文件,想快速知道CPU高在哪里的原因(毕竟现在你线上环境正出问题,哪有时间去想windbg怎么用,你现在关心的是问题出在哪,如果你是这样子想的,请拉到文章后面一点的位置,直接跳过windbg的使用问题,接下来的篇幅是说windbg的问题了,如果你只想先快速先解决问题,直接看文章结尾的visual studio分析.dmp文件的介绍)

==================================分割线=================使用WinDbg分析.dmp文件=============================================================================================

以下是使用windbg来分析问题(如果你现在火烧眉毛了就别去看这个段落,往下拉到是用visual studio 分析.dmp文件去看)

因为写这篇博文的时候,真的非常忙,我不写博文又怕自己忘记了这个经过,下次自己又要踩坑,所以简单的写一下,用windbg也踩了不少坑,为了节省时间(现在真的很忙),简单来说一下:

1、确保你用的windbg的版本足够新(因为新的版本会自动从微软的符号服务器下载和这个dmp文件匹配的相关dll) ,我一共安装了3个版本的windbg,从老版本到新版本分别是:6.12.0002(最老)、10.0.26100.1、最新版:1.2407.24003.0(Debugger client version),10.0.27668.1000(Debugger engine version)

我发现好像老版本的分 64位和32位的windbg,但是最新版的,我看安装目录的文件,是两者兼容的,因为有人说得用32位的windbg分析32位的.dmp文件,64位的分析64位的.dmp文件,实际上我用最新版的,是可以加载64位的.dmp文件并且进行分析的,我用最新版的windbg尝试加载了32位的.dmp文件后,我从任务管理器发现它 Debug Engine Host Process这个进程显示的是32位,然后任务管理器上转到文件位置是 C:\Program Files\WindowsApps\Microsoft.WinDbg_1.2407.24003.0_x64__8wekyb3d8bbwe\x86 这个文件夹下的exe ,看它调用的是x86文件夹下的exe文件,说明它应该是正确识别了.dmp文件类型,然后使用了32位的exe打开了.dmp文件,然后我又尝试用它加载了一个64位的.dmp文件,发现任务管理器里面多了一个 Debug Engine Host Process ,没有显示32位,并且任务管理器上显示它加载的是文件路径是是 C:\Program Files\WindowsApps\Microsoft.WinDbg_1.2407.24003.0_x64__8wekyb3d8bbwe\amd64 ,明显是根据.dmp文件加载了不同类型的分析工具了。任务管理器截图如下:

结论:你别纠结32位还是64位的windbg,安装最新的版本,它会自动识别.dmp文件

附:最新版windbg安装教程,首先微软的地址 https://learn.microsoft.com/zh-cn/windows-hardware/drivers/debugger/ ,点击"下载WinDbg",然后会下载到一个"windbg.appinstaller" ,双击这个文件,如果你能一切顺利安装完成,那就可以了,如果你安装不了提示"无法打开次应用包"的话,则用记事本打开 "windbg.appinstaller" ,里面有一个 Uri="https://windbg.download.prss.microsoft.com/dbazure/prod/1-2407-24003-0/windbg.msixbundle" ,复制这个Uri进行下载安装,我就是这么安装上的,不知道你可行不可行。

最新版的截图如下:

经过以上,你应该已经安装好了最新版的WinDbg了(不要再用旧版的WinDbg了,珍爱生命吧,我折腾了3个版本,最新版的是最好用的(能自动下载分析你dmp文件所需的东西、4k显示器分辨率显示适配的,老版本不适配、能自动识别.dmp文件是32位还是64位,自动加载正确的引擎分析你的.dmp文件,这三个理由足够你用最新版了))

安装好了以后,windows左下角搜索"windbg",然后右键管理员权限运行(这是我的习惯,这种工具我喜欢管理员权限运行,怕权限问题各种未知错误,实际上是否需要管理员权限运行我不确定),再点击"文件->open Source file",然后选择你刚刚的那个.dmp文件。

正常来说,应该会自动下载相关需要的.dll文件了,如果没有的话,你可以执行一下:.reload /f (这个过程很久,有时候都得十几分钟,如果你有梯子挂个梯子会快很多)

经过以上步骤,按道理现在该需要的Dll你应该就有了,可以执行一下命令: .cordll -ve -u -l (这个命令可以让WinDbg重新卸载和加载一下核心dll,并显示加载的情况。微软WinDbg使用文档如下: https://learn.microsoft.com/zh-cn/windows-hardware/drivers/debugger/getting-started-with-windbg ,相关元命令微软说明地址:https://learn.microsoft.com/zh-cn/windows-hardware/drivers/debuggercmds/meta-commands)

执行 .cordll -ve -u -l 这个命令结果如下图:

复制代码
0:000> .cordll -ve -u -l
CLRDLL: C:\Windows\Microsoft.NET\Framework\v4.0.30319\mscordacwks.dll:4.8.9261.00 f:8
doesn't match desired version 4.8.4739.00 f:8
CLRDLL: Loaded DLL C:\ProgramData\Dbg\sym\mscordacwks_x86_x86_4.8.4739.00.dll\665A9716849000\mscordacwks_x86_x86_4.8.4739.00.dll
Automatically loaded SOS Extension
CLR DLL status: Loaded DLL C:\ProgramData\Dbg\sym\mscordacwks_x86_x86_4.8.4739.00.dll\665A9716849000\mscordacwks_x86_x86_4.8.4739.00.dll

从图中可以看出来,一开始WinDbg是直接从 C:\Windows\Microsoft.NET\Framework\v4.0.30319\mscordacwks.dll 这个地址加载 mscordacwks.dll(下称本机dll)的 ,但是版本不对,因为本机dll的版本是4.8.9261(可以找到这个dll,右键属性看详细信息),然后WinDbg又开始从 C:\ProgramData\Dbg\sym\mscordacwks_x86_x86_4.8.4739.00.dll\665A9716849000\mscordacwks_x86_x86_4.8.4739.00.dll 这个路径加载,最后加载成功了。这个文件就是最新版的WinDbg自动从微软的符号服务器下载的。

DLL版本不对的原因:我本地开发机安装的.net framework是 4.8.9261 ,而我在服务器创建的.dmp文件当时创建的时候它的进程的运行环境是4.8.4739.00 ,所以会不匹配。就是因为:服务器上是一个版本,本地用于分析的是另外一个版本的.net版本。当初这个版本不对,我用老的WinDbg从网上各种搜文章,说是从服务器上复制下来放到本地,然后用 .cordll -lp D:\你的DLL路径 ,各种弄,死活不能正确加载,这个坑也是掉进去很久,下载了最新版后这种问题都不是问题了,所以一直强调用最新版,老版的需要研究太多命令,搞错了容易得出错误结论(主要还是因为我不熟这个,熟的话老版本用起来估计也顺手的,但是我相信你此刻能看到这里,估计也是WinDbg的新手,和我一样老老实实用最新版的,节省不少折腾环境的时间)

服务器上具体用的什么版本,可以在WinDbg里面执行!analyze -v 命令,等它执行完毕后有一个:CLR.Version 就是服务器上的版本号。命令执行完 后部分结果如下(结果内容太多,我只复制了关键的一个,你在你的WinDbg里面Ctrl+F搜一下这个"CLR.Version"这个字符串可以看到)

复制代码
0:000> !analyze -v
复制代码
*******************************************************************************
*                                                                             *
*                        Exception Analysis                                   *
*                                                                             *
*******************************************************************************


KEY_VALUES_STRING: 1
复制代码
    Key  : CLR.Version
    Value: 4.8.4739.0

重点:当初我用了6.12版本的老版本windbg,然后从网上搜了好多介绍文章,然后根据文章从服务器复制sos.dll clr.dll mscordacwks.dll来我本地,然后学着网上说的指定这三个Dll的path(我觉得理论这样子没错),
实际上折腾了是真的久,死活都不行,提示类似下面这样子

0:000> .cordll -I clr -lp E:\IISW3WP\core86server
CLR DLL status: No load attempts

但是用了最新版的,都是自动根据.dmp文件下载需要的dll到特定的目录,然后加载它自己下载的这个dll,也不需要自己去指定目录,这才像微软出品的工具,傻瓜式操作,我当初是查遍了各种文章,根据文章里面说的各种命令去尝试,从没有加载成功过,所以,珍爱生命,

用最新版的WinDbg!用最新版的WinDbg!用最新版的WinDbg!用最新版的WinDbg

执行了 .cordll -ve -u -l 命令,确保你看到的结果和我上面写的那样子的提示,这个时候应该就可以执行相关命令了

第一个命令:!runaway (该命令微软官方解释:https://learn.microsoft.com/zh-cn/windows-hardware/drivers/debuggercmds/-runaway ),不出意外将会得到如下结果:

然后可以看到列出了很多线程,比如我选中44号线程,则可以用命令 ~44s 进入到这个线程 (输入完命令按 Enter,下面就会变为0:044),然后再执行 !clrstack 就可以看到更详细的信息了,可以看出来

也可以用命令列出所有的线程堆信息,命令是:~*e !clrstack

然后会列出一堆的信息,从信息里面大致就可以看出来可能是哪个页面出现问题。比如上面的我就可以看出来是 ordersend_orderoutlistfishy_aspx页面出问题,但是具体什么问题还得具体分析。

但是,我发现Visual studio 分析这个.dmp文件那是相当优秀,结果里面直接给我标出来哪一行,我直接看这行代码就怀疑上了问题,不想折腾WinDbg的强烈建议直接用Visual Studio分析试试,因为这个页面显示的内容非常非常非常多,然后页面上有个控件 label,然后用label.Text+=xxxx,拼了非常非常非常多的内容,所以我直接怀疑上了这里,然后改为StringBuilder,减少了四分之三的时间(从技术上解决,从2分钟减少到30秒),然后后来又从业务上继续去沟通了一下使用这个页面的同事,直接把功能改掉了(总结起来就是历史遗留问题,这个页面的这个数据压根没人看,要看这个页面的人,压根不需要这么多数据,只显示极少一部分的数据就可以了)

但是我现在是用WinDbg有的问题如下,需要WinDbg的大佬帮我解答一下:

1、如何像Visual Studio那样显示具体哪一行代码呢?我已经把我的webform根目录下所有的pdb文件复制到我本地,然后用 E:\IISW3WP\pdb ,然后用 .sympath+ E:\IISW3WP\pdb 将这个目录添加到我的符号目录,并且用

.reload /f 重新强制加载了。

2、我用 !runaway 命令显示出线程,然后这个24号线程是时间最长的,然后我用命令 ~24s 进入该线程,然后用 !clrstack命令想看相关信息,结果发现报错,内容如下面的,,为什么获取不到呢?请看到这个文章,知道问题在哪,或者有什么建议的麻烦给我留言,我想学一下这个WinDbg,我已经搜来搜去没有找到解决办法了。chatgpt也没回答对。按我的理解Visual studio可以显示,WinDbg肯定也可以,我怀疑是我的符号设置问题+Source Link问题,到底该如何设置,才能和Visual studio 这样子明确指出哪个行代码问题?我用WinDbg只找到了哪个页面问题,想继续找问题我还得去仔仔细细看这个页面代码,打日志才能知道。

0:024> !clrstack

OS Thread Id: 0x6d40 (24)

Child SP IP Call Site

GetFrameContext failed: 1

00000000 00000000

==================================分割线=================使用Visual Studio分析.dmp文件===========================================================================================

以下是使用visual studio 分析.dmp文件的经过,如果你现在火烧眉毛,先别去研究windbg,直接打开visual studio 工具吧!

我不知道你的问题,能不能直接用visual studio分析.dmp文件找到问题(我相信是可以的),反正我的是直接用visual studio找到了问题,非常直观,也不要倒腾windbg一堆的命令和环境,真的是宇宙第一编辑器(别扛这个观点,杠就是你对了),直接打开visual studio(我用的是visual studio2022的版本),打开visual studio 然后点击"文件->打开文件",选择刚刚这个.dmp文件,如图4,点击右侧操作标签下的"运行诊断分析" 这个按钮,

点击"运行诊断分析后"如图5 ,全部勾选上那几个选项,点击"分析"按钮,会出现一个等待的对话框,等它执行完毕,就可以看到结果了,如图6

图6

然后鼠标点击这个"线程CPU使用率摘要",右侧滚动条往下滚动一点,就可以看到具体哪些线程有问题,如图7

坑点:如果你分析的结果里面没有"线程CPU使用率摘要"这个分析结果,并且你确定这个.dmp文件一定吃了你很多CPU,那么你很有可能是.dmp文件的问题,因为我用64位管理器创建32位进程池创建出来的转储文件.dmp,visual studio 死活分析不出来这个"线程CPU使用率摘要"

然后根据上面的代码行数仔细看了一下这个页面的代码,好家伙,这是一个我们web站点处理有问题订单的页面,当初开发的时候因为客服的工作模式就是在这个页面看所有发货超时的订单,然后通知发货员去及时发货,现在因为超时的订单太多太多了(每年高峰期都这个样),这个页面需要一口气展示非常非常多的内容,这些内容都是用一个 label.Text 控件输出的,然后写代码的时候拼接内容是 label.Text+= xxxxx ,所以这个页面是个完美的计算密集型线程,随便几个发货员访问下,直接GG了。假设这个页面仅仅只是多访问几次数据库,或者查询数据比较久这种IO型的计算,那迟迟没响应,也不至于杀伤力这么大(顶多就是访问这个页面的人多等一下),关键在于它丫的瓶颈是拼接字符串。。。。。

后续优化就简单了,问了一下客服,根据客服目前工作的逻辑,直接不展示全部超时的(客服不关注超时的了,每个发货员自己关注自己的,发货员自己问题订单不多,然后label.Text+=这个写法也很二逼,改为了StringBuilder对象来拼接了。

至此,问题解决,这个问题耗费了我差不多两天的时间,最大的坑就是前面说的创建.dmp文件的问题,如果一开始创建对了.dmp文件,然后直接用Visual Studio来分析,那估计发现问题的时间只要1小时了。不过,程序员在解决问题的时候,踩坑也是一种学习,踩的坑够多,积累的经验越多,下次遇到问题解决的越快,对于踩这个坑,我一点也不懊恼。