记一次 .NET某工控任务调度系统 卡死分析

一:背景

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
              ...

要想理解上面的字段,需要大家对状态机内部的有一些了解,比如:

  1. 5__xxx 表示 await 的返回值。
  2. 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命令的使用,我相信这个问题你很难搞定。