记一次某制造业ERP系统 CPU打爆事故分析
一:背景
1.讲故事
前些天有位朋友微信找到我,说他的程序出现了CPU阶段性爆高,过了一会就下去了,咨询下这个爆高阶段程序内部到底发生了什么? 画个图大概是下面这样,你懂的。
按经验来说,这种情况一般是程序在做 CPU 密集型运算
,所以让朋友在 CPU 高的时候间隔 5~10s
抓两个 dump 下来,然后就是用 WinDbg 分析。
二:WinDbg 分析
1. CPU 真的爆高吗
耳听为虚,眼见为实,我们用 !tp
观察下当前的CPU情况。
0:000> !tp
CPU utilization: 100%
Worker Thread: Total: 16 Running: 2 Idle: 14 MaxLimit: 32767 MinLimit: 2
Work Request in Queue: 0
--------------------------------------
Number of Timers: 2
--------------------------------------
Completion Port Thread:Total: 2 Free: 2 MaxFree: 4 CurrentLimit: 2 MaxLimit: 1000 MinLimit: 2
果不其然,CPU直接打满,接下来就是看看当前有几个CPU逻辑核,这么不够扛。。。
0:000> !cpuid
CP F/M/S Manufacturer MHz
0 6,106,6 <unavailable> 2700
1 6,106,6 <unavailable> 2700
我去,一个生产环境居然只有两个核。。。果然这大环境下公司活着都不够滋润。
2. 到底是谁引发的
既然是阶段性爆高,最简单粗暴的就是看下各个线程栈,使用 ~*e !clrstack
命令即可,因为只有两核,所以理论上两个线程就可以把 CPU 干趴下,扫了一下线程栈,果然有对号入座的,输出信息如下:
0:000> ~*e !clrstack
OS Thread Id: 0x146c (42)
Child SP IP Call Site
00000089abcfca18 00007ffc4baffdb4 [InlinedCallFrame: 00000089abcfca18] System.Drawing.SafeNativeMethods+Gdip.IntGdipDisposeImage(System.Runtime.InteropServices.HandleRef)
00000089abcfca18 00007ffbdd4a7a48 [InlinedCallFrame: 00000089abcfca18] System.Drawing.SafeNativeMethods+Gdip.IntGdipDisposeImage(System.Runtime.InteropServices.HandleRef)
00000089abcfc9f0 00007ffbdd4a7a48 DomainNeutralILStubClass.IL_STUB_PInvoke(System.Runtime.InteropServices.HandleRef)
00000089abcfcaa0 00007ffbdd52ad0a System.Drawing.SafeNativeMethods+Gdip.GdipDisposeImage(System.Runtime.InteropServices.HandleRef)
00000089abcfcae0 00007ffbdd52ac3f System.Drawing.Image.Dispose(Boolean)
00000089abcfcb30 00007ffbdd556b5a System.Drawing.Image.Dispose()
00000089abcfcb60 00007ffbe39397c7 NPOI.SS.Util.SheetUtil.GetCellWidth(NPOI.SS.UserModel.ICell, Int32, NPOI.SS.UserModel.DataFormatter, Boolean)
00000089abcfcc00 00007ffbe3939654 NPOI.SS.Util.SheetUtil.GetCellWidth(NPOI.SS.UserModel.ICell, Int32, NPOI.SS.UserModel.DataFormatter, Boolean)
00000089abcfcd30 00007ffbe39382e1 NPOI.SS.Util.SheetUtil.GetColumnWidth(NPOI.SS.UserModel.ISheet, Int32, Boolean)
00000089abcfcdc0 00007ffbe39380bc NPOI.XSSF.UserModel.XSSFSheet.AutoSizeColumn(Int32, Boolean)
...
OS Thread Id: 0x1c8c (46)
Child SP IP Call Site
00000089ad43dba8 00007ffc4baffdb4 [InlinedCallFrame: 00000089ad43dba8] System.Drawing.SafeNativeMethods+Gdip.IntGdipDisposeImage(System.Runtime.InteropServices.HandleRef)
00000089ad43dba8 00007ffbdd4a7a48 [InlinedCallFrame: 00000089ad43dba8] System.Drawing.SafeNativeMethods+Gdip.IntGdipDisposeImage(System.Runtime.InteropServices.HandleRef)
00000089ad43db80 00007ffbdd4a7a48 DomainNeutralILStubClass.IL_STUB_PInvoke(System.Runtime.InteropServices.HandleRef)
00000089ad43dc30 00007ffbdd52ad0a System.Drawing.SafeNativeMethods+Gdip.GdipDisposeImage(System.Runtime.InteropServices.HandleRef)
00000089ad43dc70 00007ffbdd52ac3f System.Drawing.Image.Dispose(Boolean)
00000089ad43dcc0 00007ffbdd556b5a System.Drawing.Image.Dispose()
00000089ad43dcf0 00007ffbe39397c7 NPOI.SS.Util.SheetUtil.GetCellWidth(NPOI.SS.UserModel.ICell, Int32, NPOI.SS.UserModel.DataFormatter, Boolean)
00000089ad43dd90 00007ffbe3939654 NPOI.SS.Util.SheetUtil.GetCellWidth(NPOI.SS.UserModel.ICell, Int32, NPOI.SS.UserModel.DataFormatter, Boolean)
00000089ad43dec0 00007ffbe39382e1 NPOI.SS.Util.SheetUtil.GetColumnWidth(NPOI.SS.UserModel.ISheet, Int32, Boolean)
00000089ad43df50 00007ffbe39380bc NPOI.XSSF.UserModel.XSSFSheet.AutoSizeColumn(Int32, Boolean)
...
00000089ad43e460 00007ffbe115b193 System.Web.Mvc.ControllerActionInvoker.InvokeActionMethod(System.Web.Mvc.ControllerContext, System.Web.Mvc.ActionDescriptor, System.Collections.Generic.IDictionary`2<System.String,System.Object>)
...
00000089abcfd310 00007ffbe115b147 System.Web.Mvc.Async.AsyncControllerActionInvoker+c.b__9_0(System.IAsyncResult, ActionInvocation)
...
有些朋友要问了,你是怎么确定就是这两个线程呢? 其实有两个方法可以验证。
- 使用 !whttp 看http请求
既然是 web 请求,自然就可以拿到里面的 HttpContext
,这里面记录着当前请求的运行时间,这个信息非常重要,截图如下:
从图中可以看到,有两个 xxxx/Export
请求运行时间非常高,一个是 4min30s
,一个是 50s
,刚好落在了 42
和 46
号线程上。
- 借助第二个 dump 文件
这就是为什么要抓二个dump的原因了,因为另一个dump会给我们相当有价值的对比信息,同样使用 !whttp
验证。
接下来我们就要调研为什么这两个线程会运行这么久?
3. 为什么会运行这么久
既然是 Export 导出文件,第一时间就应该想到是不是和数据量有关?通过线程栈上的方法,发现是一个List
集合,接下来用 !dso
命令找出来看看。
0:042> !dso
OS Thread Id: 0x146c (42)
RSP/REG Object Name
00000089ABCFCAC8 0000020683b7c128 System.Drawing.Bitmap
00000089ABCFCAF8 0000020683b7c158 System.Drawing.Graphics
00000089ABCFCB10 0000020683b7c128 System.Drawing.Bitmap
00000089ABCFCB30 0000020683b7c128 System.Drawing.Bitmap
00000089ABCFCB40 0000020683b7c4d0 NPOI.XSSF.UserModel.XSSFCellStyle
00000089ABCFCB50 0000020683b7c198 NPOI.XSSF.UserModel.XSSFRichTextString
00000089ABCFCB68 0000020683b7c198 NPOI.XSSF.UserModel.XSSFRichTextString
00000089ABCFCBC0 0000020683b7c198 NPOI.XSSF.UserModel.XSSFRichTextString
00000089ABCFCBC8 0000020683b7c2e8 System.String[]
00000089ABCFCBD0 0000020683b7c360 System.Drawing.Font
00000089ABCFCDE8 0000020666501240 System.Collections.Generic.List`1[[System.Collections.Generic.List`1[[System.Object, mscorlib]], mscorlib]]
...
0:042> !do 0000020666501240
Name: System.Collections.Generic.List`1[[System.Collections.Generic.List`1[[System.Object, mscorlib]], mscorlib]]
MethodTable: 00007ffbde342440
EEClass: 00007ffc36fc2af8
Size: 40(0x28) bytes
File: C:\Windows\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
Fields:
MT Field Offset Type VT Attr Value Name
00007ffc36e4e250 40018a0 8 System.__Canon[] 0 instance 00000207658592d8 _items
00007ffc36e385a0 40018a1 18 System.Int32 1 instance 44906 _size
00007ffc36e385a0 40018a2 1c System.Int32 1 instance 44906 _version
00007ffc36e35dd8 40018a3 10 System.Object 0 instance 0000000000000000 _syncRoot
00007ffc36e4e250 40018a4 0 System.__Canon[] 0 shared static _emptyArray
>> Domain:Value dynamic statics NYI 0000020563eec3c0:NotInit dynamic statics NYI 0000020795f5b9a0:NotInit <<
可以清楚的看到,这个list高达 4.5w
,这个量级说多也不多,说少也不少,言外之意就是代码写的也不好不到哪里去。
4. 用户代码要承担责任吗
要判断用户代码是不是很烂,除了白盒看代码,也可以黑盒观察这几个线程栈,可以发现两个dump 显示的栈信息都和 AutoSizeColumn
方法有关。
00000089abcfcae0 00007ffbdd52ac3f System.Drawing.Image.Dispose(Boolean)
00000089abcfcb30 00007ffbdd556b5a System.Drawing.Image.Dispose()
00000089abcfcb60 00007ffbe39397c7 NPOI.SS.Util.SheetUtil.GetCellWidth(NPOI.SS.UserModel.ICell, Int32, NPOI.SS.UserModel.DataFormatter, Boolean)
00000089abcfcc00 00007ffbe3939654 NPOI.SS.Util.SheetUtil.GetCellWidth(NPOI.SS.UserModel.ICell, Int32, NPOI.SS.UserModel.DataFormatter, Boolean)
00000089abcfcd30 00007ffbe39382e1 NPOI.SS.Util.SheetUtil.GetColumnWidth(NPOI.SS.UserModel.ISheet, Int32, Boolean)
00000089abcfcdc0 00007ffbe39380bc NPOI.XSSF.UserModel.XSSFSheet.AutoSizeColumn(Int32, Boolean)
从名字看是 NOPI 提供的自动调整列宽
的方法,那是不是这个方法的单次性能很慢呢?要寻找答案,只能求助百度啦。。。
图一
图二
到这里我们基本就搞清楚了,导致 reqeust 高达 5min + 的诱因大概有三个。
数据量大
AutoSizeColumn 速度慢
代码上的其他因素
跟朋友沟通后,朋友说这块请求中的 AutoSizeColumn
方法忘了改掉。
三:总结
这个 Dump 分析起来其实非常简单,思路也比较明朗,重点还是提醒一下大家慎用 NPOI 的 AutoSizeColumn
方法,弄不好就得出个生产事故!
记一次某制造业ERP系统 CPU打爆事故分析的更多相关文章
- 再记一次 应用服务器 CPU 暴高事故分析
一:背景 1. 前言 大概有2个月没写博客了,不是不想写哈
- 记一次 .NET 某HIS系统后端服务 内存泄漏分析
一:背景 1. 讲故事 前天那位 his 老哥又来找我了,上次因为CPU爆高的问题我给解决了,看样子对我挺信任的,这次另一个程序又遇到内存泄漏,希望我帮忙诊断下. 其实这位老哥技术还是很不错的,他既然 ...
- 记一次 .NET医疗布草API程序 内存暴涨分析
一:背景 1. 讲故事 我在年前写过一篇关于CPU爆高的分析文章 再记一次 应用服务器 CPU 暴高事故分析 ,当时是给同济做项目升级,看过那篇文章的朋友应该知道,最后的结论是运维人员错误的将 IIS ...
- 解析大型.NET ERP系统 业务逻辑设计与实现
根据近几年的制造业软件开发经验,以我开发人员的理解角度,简要说明功能(Feature)是如何设计与实现的,供参考. 因架构的不同,技术实现上会有所差异,我的经验仅限定于Windows Form程序. ...
- 解析大型.NET ERP系统 权限模块设计与实现
权限模块是ERP系统的核心模块之一,完善的权限控制机制给系统增色不少.总结我接触过的权限模块,以享读者. 1 权限的简明定义 ERP权限管理用一句简单的话来说就是:谁 能否 做 那些 事. 文句 含义 ...
- 解析大型.NET ERP系统 高质量.NET代码设计模式
1 缓存 Cache 系统中大量的用到缓存设计模式,对系统登入之后不变的数据进行缓存,不从数据库中直接读取.耗费一些内存,相比从SQL Server中再次读取数据要划算得多.缓存的基本设计模式参考下面 ...
- 为什么我会认为SAP是世界上最好用最牛逼的ERP系统,没有之一?
为什么我认为SAP是世界上最好用最牛逼的ERP系统,没有之一?玩过QAD.Tiptop.用友等产品,深深觉得SAP是贵的有道理! 一套好的ERP系统,不仅能够最大程度承接适配企业的管理和业务流程,在技 ...
- 解析大型.NET ERP系统 20条数据库设计规范
数据库设计规范是个技术含量相对低的话题,只需要对标准和规范的坚持即可做到.当系统越来越庞大,严格控制数据库的设计人员,并且有一份规范书供执行参考.在程序框架中,也有一份强制性的约定,当不遵守规范时报错 ...
- 面向企业客户的制造业CRM系统的不成熟思考
CRM就是客户关系管理(Customer Relationship Management),一直一知半解,最近有涉及这方面的需求,所以稍作研究,并思考一些相关问题. CRM是什么? CRM具体如何定义 ...
随机推荐
- 上穷碧落下凡尘:Win10系统下基于Docker配置Elasticsearch7配合Python3进行全文检索交互
原文转载自「刘悦的技术博客」https://v3u.cn/a_id_166 基于文档式的全文检索引擎大家都不陌生,之前一篇文章:使用Redisearch实现的全文检索功能服务,曾经使用Rediseac ...
- PROFIBUS,PROFINET,Ethernet三者的区别
PROFIBUS,PROFINET,Ethernet三者的区别 简单一点说,PROFINET=PROFIbus+etherNET,把Profibus的主从结构移植到以太网上,所以profinet会有C ...
- Redis 14 发布订阅
参考源 https://www.bilibili.com/video/BV1S54y1R7SB?spm_id_from=333.999.0.0 版本 本文章基于 Redis 6.2.6 概述 Redi ...
- thinkphp 5 及一下或php项目里实现模糊查询
想在thinkPHP或者PHP项目实现模糊查询怎么实现呢? 今天在网上搜了一下用 mysql里的 like 就可以实现 怎么用呢? 看代码: 错误用法: where('title','like',$s ...
- python自动化测试-列表、元组、字典学习笔记
1.列表 格式: L = [1,2,3,5] M = [7,8,9] print(type(L)) -> :list 列表增加元素: print(L.append(10)) -&g ...
- iommu分析之---DMA remap框架实现
本文主要介绍iommu的框架.基于4.19.204内核 IOMMU核心框架是管理IOMMU设备的一个通过框架,IOMMU设备通过实现特定的回调函数并将自身注册到IOMMU核心框架中,以此通过IOMMU ...
- OpenCV CMake VSCode Windows 平台下运行配置及其解决方案
前言 最近在搞 计算机图形学相关的东西,有个 demo 用到了 opencv,找了 google 一圈,发现国内都没有比较好的配置和解决的办法,要不就是几年前的教程,最近正好踩坑完,其中经历了自己编译 ...
- [HNOI2016]最小公倍数 (可回退并查集,回滚莫队)
题面 题目链接 题目描述 给定一张 N N N 个顶点 M M M 条边的无向图(顶点编号为 1 , 2 , - , n 1,2,\ldots,n 1,2,-,n),每条边上带有权值.所有权值都可以分 ...
- 最短路径算法-迪杰斯特拉(Dijkstra)算法在c#中的实现和生产应用
迪杰斯特拉(Dijkstra)算法是典型最短路径算法,用于计算一个节点到其他节点的最短路径. 它的主要特点是以起始点为中心向外层层扩展(广度优先遍历思想),直到扩展到终点为止 贪心算法(Greedy ...
- Jenkins JNLP方式启动 Agent
Jenkins Server配置 如果你是通过Nginx代理了jenkins,那么需要调整下Nginx的配置 map $http_upgrade $connection_upgrade { defau ...