一:背景

1. 讲故事

前段时间有位朋友微信找到我,说他的程序使用 hsl 库之后,采集 plc 时内存溢出,让我帮忙看一下怎么回事,哈哈,貌似是分析之旅中的第二次和 hsl 打交道,既然找到我,那就上 windbg 说话吧。

二:WinDbg 分析

1. 为什么会内存溢出

简单观察程序的提交内存之后,发现内存的 Stack 区非常大,随用 !t 看了下到底有多少个线程,截图如下:

不看不知道,一看吓一跳,这个程序居然有近 1.5w 的线程,虽然我见过大世面(3w+线程),但还是心有余悸,随用 ~*e !clrstack 看了下各个线程都在做什么,经过一顿观察,发现线程都卡在 Interactivelock.Enter 锁上,截图如下:

从代码中看,理论上存在 Interactivelock.Enter()Interactivelock.Leave() 因为各种异常导致锁的不成对进而造成锁污染的情况,看起来是 hsl 代码不严谨造成的什么 bug,观察了下版本也不是最新的,而且最新版的锁这块也修改了逻辑,就让朋友升级下 hsl 再观察看看,看样子这个问题应该轻松搞定了,不过我保守的说了下,如果还是遇到大量的线程,可以随时联系我。

2. 真的搞定的吗

过了一天这位朋友又找到我,说把 hsl 升级到最新版本之后还是出现了大量线程,让我再看一下,继续用 ~*e !clrstack 观察各个线程栈,发现还是卡在 pipeSocket.PipeLockEnter() 这里,这就很迷了,代码如下:


OS Thread Id: 0x1144 (21)
Child SP IP Call Site
...
000000A1AFF3DE90 00007ffa9cac6e05 System.Net.Sockets.SocketPal.Connect(System.Net.Sockets.SafeSocketHandle, Byte[], Int32) [/_/src/System.Net.Sockets/src/System/Net/Sockets/SocketPal.Windows.cs @ 118]
000000A1AFF3DEE0 00007ffa9cac6c52 System.Net.Sockets.Socket.DoConnect(System.Net.EndPoint, System.Net.Internals.SocketAddress) [/_/src/System.Net.Sockets/src/System/Net/Sockets/Socket.cs @ 4415]
000000A1AFF3DF30 00007ffa9cac6a63 System.Net.Sockets.Socket.Connect(System.Net.EndPoint) [/_/src/System.Net.Sockets/src/System/Net/Sockets/Socket.cs @ 810]
000000A1AFF3DF80 00007ffa9b7bc75a HslCommunication.Core.NetSupport.CreateSocketAndConnect(System.Net.IPEndPoint, Int32, System.Net.IPEndPoint)
000000A1AFF3DFF0 00007ffa9cac8768 HslCommunication.Core.Net.NetworkBase.CreateSocketAndConnect(System.Net.IPEndPoint, Int32, System.Net.IPEndPoint)
000000A1AFF3E030 00007ffa9cac84ba HslCommunication.Core.Net.NetworkDoubleBase.CreateSocketAndInitialication()
000000A1AFF3E070 00007ffa9cac83b8 HslCommunication.Core.Net.NetworkDoubleBase.ConnectServer()
000000A1AFF3E0B0 00007ffa9c697f8b HslCommunication.Core.Net.NetworkDoubleBase.GetAvailableSocket()
000000A1AFF3E0F0 00007ffa9c697545 HslCommunication.Core.Net.NetworkDoubleBase.ReadFromCoreServer(Byte[], Boolean, Boolean)
000000A1AFF3E160 00007ffa9c6a2779 HslCommunication.Profinet.Siemens.SiemensS7Net.ReadS7AddressData(HslCommunication.Core.Address.S7AddressData[])
000000A1AFF3E1A0 00007ffa9bfedef5 HslCommunication.Profinet.Siemens.SiemensS7Net.Read(System.String, UInt16)
... 0:021> !dso
OS Thread Id: 0x1144 (21)
RSP/REG Object Name
000000A1AFF3E058 00000280c8ca33d8 HslCommunication.Profinet.Siemens.SiemensS7Net
000000A1AFF3E0C0 00000281c9150e58 Microsoft.Win32.SafeHandles.SafeWaitHandle
... 0:021> !gcroot 00000281c9150e58
Thread 1144:
-> 00000280C8CA37E8 HslCommunication.Core.SimpleHybirdLock
-> 00000280C8CA3860 System.Lazy`1[[System.Threading.AutoResetEvent, System.Private.CoreLib]]
-> 00000281C9150E40 System.Threading.AutoResetEvent
-> 00000281C9150E58 Microsoft.Win32.SafeHandles.SafeWaitHandle 0:021> !do 00000280C8CA37E8
Name: HslCommunication.Core.SimpleHybirdLock
Fields:
MT Field Offset Type VT Attr Value Name
00007ffa998a71d0 4000162 14 System.Boolean 1 instance 0 disposedValue
00007ffa998ab1f0 4000163 10 System.Int32 1 instance 614 m_waiters
00007ffa9bcea5e0 4000164 8 ...Private.CoreLib]] 0 instance 00000280c8ca3860 m_waiterLock
00007ffa998ac570 4000165 2e8 System.Int64 1 static 859 simpleHybirdLockCount
00007ffa998ac570 4000166 2f0 System.Int64 1 static 857 simpleHybirdLockWaitCount

从上面的 m_waiters=614 来看,当前有 614 个线程在等待,这里要稍微吐槽一下,建议封装 SimpleHybirdLock 的时候最好记录下当前谁在持有这个锁,不然找起来太难了。。。

经过一顿摸索发现是 21号 线程正在持有 SimpleHybirdLock,正在调用 GetAvailableSocket 方法出不来,截图如下:

3. 为什么获取不到 Socket

既然有 600+ 线程在等待,大概率在获取可用 Socket 上出了什么问题,有了这个思路我们用 !dso 去找下 Socket 的 IP 地址是什么,看看dump中有没有什么异常。


0:021> !dso
OS Thread Id: 0x1144 (21)
RSP/REG Object Name
000000A1AFF3E350 00000281c8ac61a8 System.Object[] (System.Object[])
000000A1AFF3EA38 00000281c9c80608 System.String 172.16.3.208
....

提取到 IP 地址之后,接下来到用 !strings 到 dump 中搜一下可有这个 ip 相关的信息,果不其然,发现有大量的 IP 超时,截图如下:

到这里我们大概就知道了,原来是程序跑着跑着,由于网络等各方面的问题导致 IP 不可访问,进而引发程序系统性崩盘。

4. hsl 真的很无辜吗

这里没有任何针对性,只是从技术上进行一下探讨,先上一下 hsl 对这块的处理,简化后如下:



public OperateResult<byte[]> ReadFromCoreServer(byte[] send, bool hasResponseData, bool usePackAndUnpack = true)
{
OperateResult<byte[]> operateResult = new OperateResult<byte[]>();
OperateResult<Socket> operateResult2 = null;
pipeSocket.PipeLockEnter();
try
{
operateResult2 = GetAvailableSocket();
if (!operateResult2.IsSuccess)
{
pipeSocket.IsSocketError = true;
pipeSocket.PipeLockLeave();
operateResult.CopyErrorFromOther(operateResult2);
return operateResult;
}
ExtraAfterReadFromCoreServer(operateResult3);
pipeSocket.PipeLockLeave();
}
catch
{
pipeSocket.PipeLockLeave();
throw;
}
if (!isPersistentConn)
{
operateResult2?.Content?.Close();
}
return operateResult;
} internal static OperateResult<Socket> CreateSocketAndConnect(IPEndPoint endPoint, int timeOut, IPEndPoint local = null)
{
int num = 0;
while (true)
{
num++;
Socket socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
HslTimeOut hslTimeOut = HslTimeOut.HandleTimeOutCheck(socket, timeOut);
try
{
if (local != null)
{
socket.Bind(local);
}
socket.Connect(endPoint);
hslTimeOut.IsSuccessful = true;
return OperateResult.CreateSuccessResult(socket);
}
catch (Exception ex)
{
socket?.Close();
hslTimeOut.IsSuccessful = true;
if (hslTimeOut.GetConsumeTime() < TimeSpan.FromMilliseconds(500.0) && num < 2)
{
Thread.Sleep(100);
continue;
}
if (hslTimeOut.IsTimeout)
{
return new OperateResult<Socket>(string.Format(StringResources.Language.ConnectTimeout, endPoint, timeOut) + " ms");
}
return new OperateResult<Socket>($"Socket Connect {endPoint} Exception -> " + ex.Message);
}
}
}

从代码中可以看到,hsl 通过 catch 捕获到了异常,但并没有强制 throw 让用户自己做决断,而是吞到了 OperateResult 返回类中,用户层因为偷懒又没有判断这种异常状态导致了此问题的发生。 从逻辑看 Socket 是一个非常基础的功能,所以我觉得强制抛出更合理一点,逼迫用户可以更早的强制介入。

5. 为什么会有那么多线程

其实还留了一个问题没有解答,那就是为什么会产生那么多的线程,很显然这是一个 hsl 强吞异常导致的副作用,上层没有判断 OperateResult 的异常码,以为一切都 ok,继续它的周期性调度,被迫生成更多的线程池线程去赴死,危机重重,那具体是怎么调度的呢?可以观察各个线程的创建时间即可。


0:021> ~*e .printf "tid=%x\n",@$tid ; .ttime
...
Created: Thu Mar 9 09:02:05.541 2023 (UTC + 8:00)
Kernel: 0 days 0:00:00.062
User: 0 days 0:00:00.125
tid=38e8
Created: Thu Mar 9 09:02:10.540 2023 (UTC + 8:00)
Kernel: 0 days 0:00:00.015
User: 0 days 0:00:00.000
tid=2d64
Created: Thu Mar 9 09:02:15.540 2023 (UTC + 8:00)
Kernel: 0 days 0:00:00.015
User: 0 days 0:00:00.015
tid=3aa4
Created: Thu Mar 9 09:02:20.540 2023 (UTC + 8:00)
Kernel: 0 days 0:00:00.015
User: 0 days 0:00:00.000
tid=41ec
Created: Thu Mar 9 09:02:25.540 2023 (UTC + 8:00)
Kernel: 0 days 0:00:00.203
User: 0 days 0:00:00.218
...

从各个线程的创建时间来看,大概是 5s 采集一次。

三:总结

这次事故主要是由于在设备采集的过程中 IP 出了问题 导致的线程数暴涨引发的系统性崩溃,个人觉得朋友和hsl都有一定的责任,一个不检查错误码,一个强吞异常。

记一次 .NET 某传感器采集系统 线程爆高分析的更多相关文章

  1. 记一次 .NET 车联网云端服务 CPU爆高分析

    一:背景 1. 讲故事 前几天有位朋友wx求助,它的程序CPU经常飙满,没找到原因,希望帮忙看一下. 这些天连续接到几个cpu爆高的dump,都看烦了,希望后面再来几个其他方面的dump,从沟通上看, ...

  2. 记一次 .NET 某资讯论坛 CPU爆高分析

    大概有11天没发文了,真的不是因为懒,本想前几天抽空写,不知道为啥最近求助的朋友比较多,一天都能拿到2-3个求助dump,晚上回来就是一顿分析,有点意思的是大多朋友自己都分析了几遍或者公司多年的牛皮藓 ...

  3. 记一次 .NET 差旅管理后台 CPU 爆高分析

    一:背景 1. 讲故事 前段时间有位朋友在微信上找到我,说他的 web 系统 cpu 运行一段时候后就爆高了,让我帮忙看一下是怎么回事,那就看吧,声明一下,我看 dump 是免费的,主要是锤炼自己技术 ...

  4. 记一次 .NET 某电子病历 CPU 爆高分析

    一:背景 1.讲故事 前段时间有位朋友微信找到我,说他的程序出现了 CPU 爆高,帮忙看下程序到底出了什么情况?图就不上了,我们直接进入主题. 二:WinDbg 分析 1. CPU 真的爆高吗? 要确 ...

  5. 记一次 .NET 某安全生产信息系统 CPU爆高分析

    一:背景 1.讲故事 今天是的第四天,头终于不巨疼了,写文章已经没什么问题,赶紧爬起来写. 这个月初有位朋友找到我,说他的程序出现了CPU爆高,让我帮忙看下怎么回事,简单分析了下有两点比较有意思. 这 ...

  6. 记一次 .NET 某电商交易平台Web站 CPU爆高分析

    一:背景 1. 讲故事 已经连续写了几篇关于内存暴涨的真实案例,有点麻木了,这篇换个口味,分享一个 CPU爆高 的案例,前段时间有位朋友在 wx 上找到我,说他的一个老项目经常收到 CPU > ...

  7. Java线程问题分析定位

    Java线程问题分析定位 分析步骤: 1.使用top命令查看系统资源占用情况,发现Java进程占用大量CPU资源,PID为11572: 2.显示进程详细列表命令:ps -mp 11572 -o THR ...

  8. MTK6589下传感器框架结构和代码分析以及传感器的参数指标

    MTK6589下传感器框架结构和代码分析以及传感器的参数指标 作者:韩炜彬  中国当代著名嵌入式研究专家 一.      模块框架 1)配置 路径:Alps/mediatek/config/$(pro ...

  9. Android系统--输入系统(七)Reader_Dispatcher线程启动分析

    Android系统--输入系统(七)Reader_Dispatcher线程启动分析 1. Reader/Dispatcher的引入 对于输入系统来说,将会创建两个线程: Reader线程(读取事件) ...

  10. Android系统--输入系统(十三)Dispatcher线程情景分析_Reader线程传递事件

    Android系统--输入系统(十三)Dispatcher线程情景分析_Reader线程传递事件 1. 输入按键 我们知道Android系统的按键分为三类:(1)Global Key;(2)Syste ...

随机推荐

  1. vue后台管理系统

    1. 项目概述: 根据不同的应用场景,电商系统一般都提供了 PC 端.移动 APP.移动 Web.微信小程序等多种终端访问方式. 2. 电商后台管理系统的功能 电商后台管理系统用于管理用户账号.商品分 ...

  2. sql文件导入错误,There was an error while executing a query. The query and the error message has been logged at: C:\U

    x.sql转储文件导入异常. 之前成功导入过,再次导入异常. 1修改my.ini文件,2修改sql文件编码.都不适用. 解决方法:新建数据库,再次导入成功.注意编码.

  3. pads:数据格式不正确,网络必须包含一个以上管脚

    1,如果已经有pcb封装,则在pads logic软件里面-元件编辑器-重新做封装,在--编辑电参数--里面匹配对应pcb封装, 2,点击-工具--,--从库中更新--,更新一下,之后导入pcb la ...

  4. elelment中el-cascader怎样自定义显示的lable 与value

    1.后端返回的数据类型 2.页面代码 3.重点在于  :props="{ value: 'id',label: 'className',children: 'childNode'}" ...

  5. 三大常用集群leader选举+哨兵模式原理

    一,Zookeeper集群的leader选举 Zookeeper的选举机制两个触发条件:集群启动阶段和集群运行阶段leader挂机(这2种场景下选举的流程基本一致) 1,Zookeeper集群中的fo ...

  6. centeros忘记root登录密码

    转载自:https://www.cnblogs.com/dongml/p/10333819.html 很多时候我们都会忘记Linux root 用户的口令,下面就教大家如果忘记root口令怎么办 第1 ...

  7. redis每天生成自增流水号(001、002...)

    原理:利用redis的RedisAtomicLong类实现该功能:让其每天第一次放置一个新的自增的值(一天过期)然后和每天的日期相加就可以了例子: 20180901 + 001 ;当天就是 20180 ...

  8. C语言初级阶段5——函数2

    C语言初级阶段5--函数2 址传递 1.地址:在定义变量,数组,函数等等,系统会自动给分配他们的内存区域(地址),把这个数据放到这个地址上面. 2.&:&a 得到a的地址编号 3.*: ...

  9. 十大经典排序之桶排序(C++实现)

    桶排序 桶排序是计数排序的升级版.它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定 思路: 根据数据规模,初始化合理桶数 将数列中的数据按照桶的规模进行映射,尽量保证数据被均匀的分布到桶 ...

  10. json for python学习笔记

    1.json作用 存储数据与数据传输 2.python中的json可以在代码中用字符串表示,字符串内部类似于字典 如: json1 = '{"name":"Bob&quo ...