一:背景
1. 讲故事
前段时间有位朋友加我微信,来了就要进我的训练营,并且附带着纠结了他几个月的一个疑难杂症,让我帮忙看下怎么回事,问题描述截图如下:

由于这个定时任务是 furion 写的,刚好这位学员是VIP客户,找了小僧大佬,大佬需要最小化的问题代码,由于不能本地复现,也就没下文了,毕竟也是触发了 13w 次之后才出现的问题,确实比较难搞,截图如下:

像这种带着问题进训练营的朋友还是蛮多的,对这类需求我也是严格,谨慎,认真的对待,毕竟是骡子是马,得要拉出来溜溜。
二:为什么会任务延迟
1. 初步分析
经过和学员的沟通和截图确认,是一个叫 M71EnterPortService
的服务出现的延迟,这种问题相对来说比较简单,可能任务卡死在某个地方,通过 ~*e !clrstack
观察下各个线程栈上是否有 M71EnterPortService
字样就能知道,截图如下:

从卦中看,尼玛,居然没有 M71EnterPortService
关键词,这说明任务压根就没执行?难度是任务被意外退出了吗? 但朋友截图出来的面板信息还是蛮全的,而且底层框架对这些容错性应该还是非常强的,所以个人推论,大概率不应该是任务退出,说实话,有点进入迷雾了。。。
2. 走出迷雾
要想走出迷雾,需要回头看下 M71EnterPortService
类的调度方法,源码方法参考如下:
C#
[JobDetail("job_M71xxx", Description = "M7-1xxx作业", GroupName = "default", Concurrent = false)]
[Period(500L, TriggerId = "trigger_M71xxx", Description = "M7-1xxx作业")]
public class M71EnterPortService : IJob
{
public static string processCode = "M7-1A";
public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
{
_ = string.Empty;
try
{
plcProcessPara = await xxxService.CheckProcess(processCode);
if (!plcProcessPara.IsProcess)
{
return;
}
...
if (string.IsNullOrWhiteSpace(rFIDReaderResultModel.RfidUid))
{
PLCxxxDriver.WritePlcxxxData($"PLC/{processCode}/xxxIPC", "2");
await Task.Delay(2000);
}
await Task.CompletedTask;
}
catch (Exception ex)
{
xxxService.xxxWarn(produceRecord, plcProcessPara, ex);
}
await Task.CompletedTask;
}
}
从源码看,这是一个纯异步的写法,看到这个纯异步我就想到了新版的sos提供了一个 !dumpasync
命令,专门观察状态机链的,输出如下:
C#
0:000> !dumpasync
...
STACK 9
0000025fc3a02f78 00007ff90f3f75e8 (-1) xxx.ComProcessService+<CheckProcess>d__1 @ 7ff90fdf02b0
0000025fc3a03008 00007ff90f3f82c0 (0) xxx.M71EnterPortService+<ExecuteAsync>d__3 @ 7ff90fe15a80
0000025fc3a03090 00007ff90f3f8a60 (0) Furion.Schedule.ScheduleHostedService+<>c__DisplayClass23_3+<<BackgroundProcessing>b__3>d @ 7ff90fdf0000
0000025fc3a030f8 00007ff90f3f8f58 (0) Furion.FriendlyException.Retry+<InvokeAsync>d__1 @ 7ff90fdef840
0000025fc3a03198 00007ff90f3f93d0 (1) Furion.Schedule.ScheduleHostedService+<>c__DisplayClass23_2+<<BackgroundProcessing>b__2>d @ 7ff90fded340
...
我去,真的卡在 M71EnterPortService
下的 CheckProcess
中,接下来拿 CheckProcess
在所有线程栈上再次搜索,本以为有惊喜,同样毛都没有,我去。。。截图如下。

这就有点无语了,接下来我们观察下状态机地址0x0000025fc3a02f78 中的内部字段,从内部字段的赋值情况观察代码执行流,输出如下:
C#
0:000> !dumpasync --address 0x0000025fc3a02f78 --fields
STACK 1
0000025fc3a02f78 00007ff90f3f75e8 (-1) xxx.ComProcessService+<CheckProcess>d__1 @ 7ff90fdf02b0
Address MT Type Value Name
0000025fc3a02fe0 00007ff90c3f7408 System.Int32 -1 <>1__state
0000025fc3a02fe8 00007ff90d872010 ...y.PLCPara.xxx> 0000025fc3a02ff0 <>t__builder
0000025f80683200 00007ff90c44bf40 System.String "M7-1A" processCode
0000025fc3a028e0 00007ff90d822c00 ...ty.PLCPara.xxx 0000025fc3a028e0 <result>5__2
0000025fc3a02978 00007ff90c4f5b00 System.IDisposable 0000025fc3a02978 <>7__wrap2
0000025faf0ac600 00007ff90d8228d0 ...tity.PLCPara.xxx 0000025faf0ac600 <processMark>5__4
0000025fc3a0aaf8 00007ff90d824358 ...ntity.PLCPara.xxx 0000025fc3a0aaf8 <plcPara>5__5
0000025fc3a02ff0 00007ff90d872118 ...tity.PLCPara.xxx> 0000025fc3a02ff8 <>u__1
0000025fc3a02ff8 00007ff90c98b920 ....CompilerServices.TaskAwaiter 0000025fc3a03000 <>u__2
0000025fc3a03008 00007ff90f3f82c0 (0) xxx.M71EnterPortService+<ExecuteAsync>d__3 @ 7ff90fe15a80
Address MT Type Value Name
0000025fc3a03058 00007ff90c3f7408 System.Int32 0 <>1__state
0000025fc3a03060 00007ff90c98b540 ...rvices.AsyncTaskMethodBuilder 0000025fc3a03068 <>t__builder
0000000000000000 00007ff90d82ec98 ....DBModel.xxx null <produceRecord>5__2
0000000000000000 00007ff90d822c00 ...ty.PLCPara.xxx null <plcProcessPara>5__3
0000025fc3a03068 00007ff90d878de0 ...y.PLCPara.xxx> 0000025fc3a03070 <>u__1
0000025fc3a03070 00007ff90d879368 ...DBModel.xxx> 0000025fc3a03078 <>u__2
0000025fc3a03078 00007ff90d878eb8 ...Entity.xxx> 0000025fc3a03080 <>u__3
0000025fc3a03080 00007ff90c98b920 ....CompilerServices.TaskAwaiter 0000025fc3a03088 <>u__4
0000025fc3a03090 00007ff90f3f8a60 (0) Furion.Schedule.ScheduleHostedService+<>c__DisplayClass23_3+<<BackgroundProcessing>b__3>d @ 7ff90fdf0000
Address MT Type Value Name
0000025fc3a030d8 00007ff90c3f7408 System.Int32 0 <>1__state
0000025fc3a030e0 00007ff90c98b540 ...rvices.AsyncTaskMethodBuilder 0000025fc3a030e8 <>t__builder
0000025fc3a02560 00007ff90cbbc378 ...Service+<>c__DisplayClass23_3 0000025fc3a02560 <>4__this
0000025fc3a030e8 00007ff90c98b920 ....CompilerServices.TaskAwaiter 0000025fc3a030f0 <>u__1
0000025fc3a030f8 00007ff90f3f8f58 (0) Furion.FriendlyException.Retry+<InvokeAsync>d__1 @ 7ff90fdef840
Address MT Type Value Name
0000025fc3a03168 00007ff90c3f7408 System.Int32 0 <>1__state
0000025fc3a03180 00007ff90c98b540 ...rvices.AsyncTaskMethodBuilder 0000025fc3a03188 <>t__builder
0000025fc3a02860 00007ff90dd040f8 ...<System.Threading.Tasks.Task> 0000025fc3a02860 action
0000025fc3a0316c 00007ff90c3f7408 System.Int32 0 numRetries
0000025fc3a03178 00007ff90c3f2f78 System.Boolean true finalThrow
0000000000000000 00007ff90f237fc8 ... System.Threading.Tasks.Task> null fallbackPolicy
0000000000000000 00007ff90c528d80 System.Type[] null exceptionTypes
0000000000000000 00007ff90cbd2f28 ...on.Retry+<>c__DisplayClass1_0 null <>8__1
0000025fc3a028a0 00007ff90f233868 ...n<System.Int32, System.Int32> 0000025fc3a028a0 retryAction
0000025fc3a03170 00007ff90c3f7408 System.Int32 1000 retryTimeout
0000025fc3a03174 00007ff90c3f7408 System.Int32 0 <totalNumRetries>5__2
0000025fc3a03188 00007ff90c98b920 ....CompilerServices.TaskAwaiter 0000025fc3a03190 <>u__1
0000000000000000 00007ff90c334730 System.Object null <>7__wrap2
...
要想理解上面的字段,需要大家对状态机内部的有一些了解,比如:
5__xxx
表示 await 的返回值。1__state
表示当一个方法中有多个await 时,这个字段会阶段性的记录当前是第几个await。
结合 5__xxx
赋值情况 和 processMark
的数据标记情况,推测出是卡死在 SavePlcPara
中,截图如下:

下钻找到了 SavePlcPara 之后,继续回头从 ~*e !clrstack
中找结果,终于水滴石穿,真有一个线程在 SavePlcPara 方法中,截图如下:

从卦中看,没找到M71EnterPortService关键词应该是被栈inline了,根据调用栈,可以发现是查询时序数据库 TDengine
时卡住导致的雪崩,TDengine
虽然我没用过,但听说是一个好东西,放一下描述给大家。
TDengine 是一款 开源、高性能、云原生 的时序数据库(Time Series Database, TSDB), 它专为物联网、车联网、工业互联网、金融、IT 运维等场景优化设计。同时它还带有内建的缓存、流式计算、数据订阅等系统功能,能大幅减少系统设计的复杂度,降低研发和运营成本,是一款极简的时序数据处理平台。
最后就是让朋友重点观察下 TDengine.Driver.Impl.NativeMethods.NativeMethods.QueryWithReqid 方法,可以用排除法观察。
三:总结
这次任务延迟事故在分析过程中还是有相当大的迷惑性,如果你缺乏对状态机的理解以及不知!dumpasync
命令的使用,我相信这个问题你很难搞定。