服务器之家:专注于服务器技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - ASP.NET教程 - 记一次 .NET 某风控管理系统内存泄漏分析

记一次 .NET 某风控管理系统内存泄漏分析

2021-11-02 23:29一线码农聊技术 ASP.NET教程

从源码逻辑看,我猜测朋友将 GetConfig 方法标记成 static 后就以为是单例化了,再次调用不会重复 new AppConfig(settingfile),所以问题就出在这里。

记一次 .NET 某风控管理系统内存泄漏分析

一、背景

1. 讲故事

上个月中旬,星球里的一位朋友在微信找我,说他的程序跑着跑着内存会不断的缓慢增长并无法释放,寻求如何解决 ?

记一次 .NET 某风控管理系统内存泄漏分析

得,看样子星球还得好好弄!!!不管怎么说,先上 windbg 说话。

二、Windbg 分析

1. 经验推理

从朋友的截图看,有大量的 8216 字节的 byte[],这表示什么呢?追随本系列的朋友应该知道,有一篇 某三甲医院 的内存暴涨的dump中,也同样有此 size= (8216-24=8192) 的 byte[] 数组, 他的问题是 Oracle 中读取某大字段时sdk里的 OraBuf 出了问题,换句话说,这肯定又是底层或者第三方库中的池对象搞出来的东西,接下来从 托管堆 看起。

2. 查看托管堆

  1. 0:000>!dumpheap-stat
  2. Statistics:
  3. 00007ffe107248f048370715478624System.Threading.PreAllocatedOverlapped
  4. 00007ffe1079c16048374415479808System.Threading.ThreadPoolBoundHandle
  5. 00007ffe1079cff848370123217648System.Threading._IOCompletionCallback
  6. 00007ffe106e7a9048370423217792Microsoft.Win32.SafeHandles.SafeFileHandle
  7. 00007ffe1079b08848370330956992System.IO.FileSystemWatcher+AsyncReadState
  8. 00007ffe1079ceb048370734826904System.Threading.OverlappedData
  9. 00007ffe1079ccb048370734826904System.Threading.ThreadPoolBoundHandleOverlapped
  10. 0000016c646510802456521473128080Free
  11. 00007ffe105abf304881723977571092System.Byte[]

扫完托管堆,卧槽 ,byte[] 没吸引到我,反而被 System.IO.FileSystemWatcher+AsyncReadState 吸引到了,毕竟被 System.IO.FileSystemWatcher 折腾多次了,它已经深深打入了我的脑海。。。毕竟让程序卡死,让句柄爆高的都是它。。。这一回八成又是它惹的祸,看样子还是有很多程序员栽在这里哈。

为做到严谨,我还是从最大的 System.Byte[] 入手,按size对它进行分组再按totalsize降序,丑陋的脚本我就不发了,直接上脚本的输出结果。

  1. !dumpheap-mt00007ffe105abf30
  2. size=8216,count=483703,totalsize=3790M
  3. size=8232,count=302,totalsize=2M
  4. size=65560,count=6,totalsize=0M
  5. size=131096,count=2,totalsize=0M
  6. size=4120,count=11,totalsize=0M
  7. size=56,count=301,totalsize=0M
  8. size=88,count=186,totalsize=0M
  9. size=848,count=16,totalsize=0M
  10. size=152,count=85,totalsize=0M
  11. size=46,count=242,totalsize=0M
  12. size=279,count=38,totalsize=0M
  13. !dumpheap-mt00007ffe105abf30-min0n8216-max0n8216-short
  14. 0000016c664277f0
  15. 0000016c66432a48
  16. 0000016c6648ef88
  17. 0000016c6649daa8
  18. 0000016c6649fb00
  19. 0000016c664a8b90
  20. ...

从输出结果看,size=8216 的 byte[] 有 48w 个,然后脚本也列出了一些 8216 大小的 address 地址,接下来用 !gcroot 看下这些地址的引用。

  1. 0:000>!gcroot0000016c664277f0
  2. HandleTable:
  3. 0000016C65FC28C0(asyncpinnedhandle)
  4. ->0000016C6628DEB0System.Threading.OverlappedData
  5. ->0000016C664277F0System.Byte[]
  6. Found1uniqueroots(run'!gcroot-all'toseeallroots).
  7. 0:000>!gcroot0000016c667c80d0
  8. HandleTable:
  9. 0000016C65FB7920(asyncpinnedhandle)
  10. ->0000016C663260F8System.Threading.OverlappedData
  11. ->0000016C667C80D0System.Byte[]

从输出中可以看到这些 byte[] 都是 async pinned,也就是当异步IO回来的时候需要给 byte[] 填充的存储空间,接下来我们看看如何通过 OverlappedData 找到源码中定义为 8192 大小的 byte[] 地方。

如果你了解 FileSystemWatcher ,反向查找链大概是这样的 OverlappedData -> ThreadPoolBoundHandleOverlapped -> System.IO.FileSystemWatcher+AsyncReadState -> Buffer[], 这中间涉及到 ThreadPool 和 SafeHandle 的绑定。

  1. 0:000>!do0000016C663260F8
  2. Name:System.Threading.OverlappedData
  3. MethodTable:00007ffe1079ceb0
  4. EEClass:00007ffe107ac8d0
  5. Size:72(0x48)bytes
  6. File:C:\ProgramFiles\dotnet\shared\Microsoft.NETCore.App\5.0.10\System.Private.CoreLib.dll
  7. Fields:
  8. MTFieldOffsetTypeVTAttrValueName
  9. 00007ffe106e3c0840009ce8System.IAsyncResult0instance0000000000000000_asyncResult
  10. 00007ffe104a0c6840009cf10System.Object0instance0000016c66326140_callback
  11. 00007ffe1079cb6040009d018...eading.Overlapped0instance0000016c663260b0_overlapped
  12. 00007ffe104a0c6840009d120System.Object0instance0000016c667c80d0_userObject
  13. 00007ffe104af50840009d228PTR0instance00000171728f66e0_pNativeOverlapped
  14. 00007ffe104aee6040009d330System.IntPtr1instance0000000000000000_eventHandle
  15. 00007ffe104ab25840009d438System.Int321instance0_offsetLow
  16. 00007ffe104ab25840009d53cSystem.Int321instance0_offsetHigh
  17. 0:000>!do0000016c663260b0
  18. Name:System.Threading.ThreadPoolBoundHandleOverlapped
  19. MethodTable:00007ffe1079ccb0
  20. EEClass:00007ffe107ac858
  21. Size:72(0x48)bytes
  22. File:C:\ProgramFiles\dotnet\shared\Microsoft.NETCore.App\5.0.10\System.Private.CoreLib.dll
  23. Fields:
  24. MTFieldOffsetTypeVTAttrValueName
  25. 00007ffe1079ceb040009d68...ng.OverlappedData0instance0000016c663260f8_overlappedData
  26. 00007ffe1079b81840009c010...ompletionCallback0instance0000016f661ab8a0_userCallback
  27. 00007ffe104a0c6840009c118System.Object0instance0000016c667ca0e8_userState
  28. 00007ffe107248f040009c220...locatedOverlapped0instance0000016c66326090_preAllocated
  29. 00007ffe104af50840009c330PTR0instance00000171728f66e0_nativeOverlapped
  30. 00007ffe1079c16040009c428...adPoolBoundHandle0instance0000000000000000_boundHandle
  31. 00007ffe104a723840009c538System.Boolean1instance0_completed
  32. 00007ffe1079b81840009bf738...ompletionCallback0static0000016f661ab990s_completionCallback
  33. 0:000>!do0000016c667ca0e8
  34. Name:System.IO.FileSystemWatcher+AsyncReadState
  35. MethodTable:00007ffe1079b088
  36. EEClass:00007ffe107a9dc0
  37. Size:64(0x40)bytes
  38. File:C:\ProgramFiles\dotnet\shared\Microsoft.NETCore.App\5.0.10\System.IO.FileSystem.Watcher.dll
  39. Fields:
  40. MTFieldOffsetTypeVTAttrValueName
  41. 00007ffe104ab258400002b30System.Int321instance1k__BackingField
  42. 00007ffe105abf30400002c8System.Byte[]0instance0000016c667c80d0k__BackingField
  43. 00007ffe106e7a90400002d10...es.SafeFileHandle0instance0000016c66326028k__BackingField
  44. 00007ffe1079c160400002e18...adPoolBoundHandle0instance0000016c66326058k__BackingField
  45. 00007ffe107248f0400002f20...locatedOverlapped0instance0000016c66326090k__BackingField
  46. 00007ffe1079b8c8400003028...eSystem.Watcher]]0instance0000016c66326078k__BackingField

上面的 k__BackingField 就是当初丢给 OverlappedData 作为 异步IO 读写的缓冲,然后看下 System.IO.FileSystemWatcher+AsyncReadState 的源码。

记一次 .NET 某风控管理系统内存泄漏分析

有了这些原理之后,接下来就可以问朋友是否有对 appsettings 设置了 reloadonchange=true 的情况,朋友找了下代码,写法大概如下:

  1. publicobjectGetxxxFlag()
  2. {
  3. stringvalue=AppConfig.GetConfig("appsettings.json").GetValue("xxxx","0");
  4. returnnew
  5. {
  6. state=200,
  7. data=value
  8. };
  9. }
  10. publicclassAppConfig
  11. {
  12. publicstaticAppConfigGetConfig(stringsettingfile="appsettings.json")
  13. {
  14. returnnewAppConfig(settingfile);
  15. }
  16. }
  17. publicclassAppConfig
  18. {
  19. privateAppConfig(stringsettingfile)
  20. {
  21. _config=newConfigurationBuilder().AddJsonFile(settingfile,optional:true,reloadOnChange:true).Build();
  22. _settingfile=settingfile;
  23. }
  24. }

从源码逻辑看,我猜测朋友将 GetConfig 方法标记成 static 后就以为是单例化了,再次调用不会重复 new AppConfig(settingfile),所以问题就出在这里。

不过有意思的是,前面二篇的 FileSystemWatcher 都会造成程序卡死,那这一篇为啥没有呢?恰好他没有在程序根目录中放日志文件,不然的话。。。,可万万没想到逃过了卡死却没逃过一个 watcher 默认 8byte 空间的灵魂拷问。。。

三、总结

总的来说,设置 reloadOnChange: true 一定要慎重, 可能它会造成你的程序卡死,句柄泄漏,内存泄漏 等等!!!

原文链接:https://mp.weixin.qq.com/s/k0fBF772iE8whAGDd08hsw

延伸 · 阅读

精彩推荐