复盘一个诡异的Bug

news2024/11/23 7:52:49

该Bug的诡异之处在于这是一个由多种因素综合碰撞之后形成的综合体。纵观整个排查过程,一度被错误的目标误导,花费大量功夫后才找到问题点所在,成熟的组件在没有确凿证据之前不能随意怀疑其稳定性。

前言

此前在接入两台粒径谱仪(TSI3321、TSI3034)数据时,碰到了一个非常诡异的情况,它能导致数据停止解析。

这两台粒径谱仪的数据文件是由两套看似相同的软件实时导出,格式一致,不同在于列的数量和数据频次。

  • TSI3321数据结构
19426	11/07/23	20:22:50	Raw Counts	5194	173	152	133	100	78	58	40	41	20	30	20	20	21	16	25	17	22	23	15	19	15	16	12	12	6	9	4	3	4	1	0	0	0	0	0	0	0	0	0	0	0	0	0	0	0	0	0	0	0	0	0	3243	7	0	19	1013.9	4.97	3.99	0	0	0	0	0	75	55.7	2.93	2.976	31.1	28.5	204	0000 0000 0000 0000	0.695616	0.994068	0.856124	0.542469	1.64629	18.8966(#/cm3)
  • TSI3034数据结构
2740	11/07/23	16:59:18		0	0	0	0	0	7.1886	3.5483	19.002	22.113	37.142	57.044	101.7	102.44	97.561	148.99	142.91	147.79	131.06	178.67	150.45	126.46	146.46	139.83	101.2	135.73	133.63	111.5	145	108.56	84.471	106.15	107.23	93.794	71.517	69.307	79.288	92.092	58.615	88.356	75.774	45.489	37.047	35.692	40.959	23.661	31.461	29.858	24.924	21.685	6.1227	16.117	22.555	0	0	10	486.97	1.2		OK	56.8122	81.9485	61.9724	37.8552	2.0539	117.442

接入方案

有如下两个接入数据的方案:

  1. 使用传统的数据转发程序解析和上传
  2. 采用我方经过多年沉淀和积累的空气数采软件DATS解析存储并上传

传统的数据转发程序功能简单,仅支持解析和上传,自身不存储数据,在某种意义上增加了维护难度。

DATS不仅具备前者的功能,还有存储数据、云端配置备份等功能,安全性和稳定性都较好。不过也有个缺点,虽然两者共用一个上传模块,但是界面上没开放按照仪器配置上传通道的功能。

从大局出发选择后者,而前者将被逐步淘汰。

本次任务的模板就明确了:

  1. 开发两个驱动
  2. 开放按仪器配置上传通道的设置界面

由于所有数据都生成在一个文件中,每次读取完整的文件不仅降低了解析效率,还耗费大量内存,当文件超过一定体量时甚至可能引发内存溢出问题。故而选择分段读取方式,在程序首次启动后从文件头开始解析,并缓存文件末尾流的位置,下一次直接从该位置往后读取。此前写过类似代码,可以复制粘贴一下。

鉴于两个驱动的相似性,考虑代码的复用,我先实现了一个通用的解析逻辑TxtReader,在其基础上封装了两个驱动TSI_3321TSI_3034,再按仪器独立配置MN号上传。

开发、调试顺风顺水,按照这个节奏,不出两天就能交差。

偏偏在最后的节骨眼上爆出了问题!

当我把时间调回去重新解析后,两台相同解析逻辑的仪器驱动,一台正常解析完成,另一台则卡住了,就出现在我的开发环境中。

发现问题

这是个偶然发现但必然出现的问题。

由于开发调试过程中不可避免要经常回退最新解析时间,在调试TSI_3034这台仪器时,一个偶然的机会发现时间回退后启动程序每次只解析到两条就停止了。

举个例子,回调时间到2023-10-12 14:05:00,那么14:05这条数据能成解析并上传,下一个时间点,也就是14:10分的数据在解析完成刷新界面后就死了,并且没有写入发送队列中。

如果把上传模块生成的发送队列里的数据清空就能解析到文件末尾,而另一台3321的驱动则没有这个问题,正常得不能再正常了。

第一次排查

面对这个从没出现过的问题,有如下可能导致问题出现的环节:

  • 驱动解析
  • 存储模块
  • 界面刷新
  • 上传模块

好在可以打断点调试,停掉TSI_3321,只启用TSI_3034一台仪器,在解析到一个时间点的数据并抛给主程序的地方打了断点,单步调试,最终把问题点锁定在上传模块内的一个函数中。

这是另外一个同事开发的模块,先把数据放到队列中,为了保证多线程操作,大量使用了锁。

public void SendHisData(DateTime wdt, FactorTypes wDataType, List<UploadFactorValue> wItemValue, bool wIsReSend, DateTime? beginTime = null, DateTime? endTime = null)
{
    ……
    lock (m_LockNewSendList)
    {
        m_NewSendList.Add(new SendList()
        {
            Dt = wdt,
            DataType = (FactorTypes)LocIndex,
            ServerID = _UploadPara.ServerID,
            Guid = context.Guid,
            PNO = context.PNO,
            Context = context.Context, //预留QN和MN的位置,没有CRC信息
            NeedResponse = NeedResponse,
            IsTmp = false,
            Qn = ""
        });
    }
    ……
}

在另一个线程中扫描这个队列

private void ScanSendList(bool isTmp, int readCnt)
{
    lock (m_LockSendUtil)
    {
        List<SendList> info = m_SendListUtil.GetUploadSendListByServerID(_UploadPara.ServerID, isTmp, readCnt);
        if (info.Count > 0)
        {
            SendList info1 = null;
            for (int i = 0; i < info.Count; i++)
            {
                if (isTmp)   //缓存的数据
                {
                    AddSendList(info[i].Dt, info[i].DataType, info[i].Guid, info[i].Context, false, info[i].NeedResponse, info[i].IsTmp);
                }
                else
                {
                    info1 = m_SendList.Where(a => a.Guid == info[i].Guid).FirstOrDefault();
                    if (info1 == null)    //不在待发送列表里
                    {
                        info1 = m_ReturnList.Where(a => a.Guid == info[i].Guid).FirstOrDefault();
                        if (info1 == null) //不在待应答列表里
                        {
                            if (m_SendListUtil.DelOneSendListByGuid(info[i].Guid))   //删除旧的ID,生成新的ID,顺序往后移
                            {
                                lock (m_LockNewSendList)
                                {
                                    m_NewSendList.Add(new SendList()
                                    {
                                        Dt = info[i].Dt,
                                        DataType = info[i].DataType,
                                        ServerID = _UploadPara.ServerID,
                                        Guid = info[i].Guid,
                                        PNO = 1,
                                        Context = info[i].Context,
                                        NeedResponse = info[i].NeedResponse,
                                        IsTmp = info[i].IsTmp,
                                        Qn = ""
                                    });
                                }
                            }
                        }
                    }
                }
            }
        }
        m_SendListUtil.DelDaysSendList(_UploadPara.ServerID, _UploadPara.ValidDay);   //缓存的不能删除
    }
}

程序就是在调用SendHisData时发生死锁,既然找到了源头,问题似乎就能解决了吧。

很遗憾,开发上传模块的同事排查逻辑后并未得出死锁的理由,也是,如果代码真存在问题,怎么可能现场那么多点位,尤其是数据量比它多的点位都运行好好的。

于是想到把配置发了他一份,在他电脑上跑一下看看,果不其然,在他电脑上没有死锁,两个文本均能同时正常解析。

这时我怀疑可能是自身电脑问题了,一个多星期没关过机,调试时明显感觉到卡顿。临近下班,一时半会儿找不出问题根源,干脆直接装到现场看看效果。

第二次排查

第二天上班,远程到现场机器一看,TSI_3034数据停了,意料之中的事。就这样观察了近半个月每次都是刚启动软件能正常解析,过了一会儿3034的数据就停了,由于我坚持认为问题出在上传模块,并曾当着他的面复现了,所以第二次排查主要是那个同事经手。

当局者迷,旁观者清,调试别人的代码可能更容易找出问题吧。他一眼就看出TxtReader中用于缓存拼包的变量定义成了临时变量,并没有起到作用。

看来问题还是出在这次新开发的驱动内部。

using (var fs = new FileStream(_file.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
    if (auto)
    {
        fs.Seek(_filePoint, SeekOrigin.Current);
    }

    string buffer = null;
    byte[] byteData = new byte[10000];
    int length;
    while ((length = fs.Read(byteData, 0, 10000)) > 0)
    {
        if (auto)
        {
            _filePoint += length;
        }

        string text = Encoding.ASCII.GetString(byteData);
        byteData = new byte[10000];
        text = buffer + text;
        if (string.IsNullOrWhiteSpace(text))
        {
            return;
        }

        // 找出最后一个\r\n,将剩余的字符串缓存起来
        int endIndex = text.LastIndexOf("\r\n");
        if (endIndex > 0)
        {
            buffer = text.Substring(endIndex, text.Length - endIndex);
            text = text.Substring(0, endIndex + 1);
        }
        else
        {
            buffer = null;
        }
        ……
    }
}

修改过后,放到现场一跑,问题依然存在。

他电脑上复现不了死锁现象,只能通过记录大量日志分析根源。联想到之前我电脑可以上百分百复现死锁,当即跑了一下,奇怪的是这回死锁问题怎么都不出现。可能当初电脑的确卡,不仅仅解析,入库也慢地难以忍受,甚至引发了.NET运行时的某个Bug,重启电脑后恢复正常。

到了这一步,问题又回到了起点,为什么同一个类的不同实例,一个运行正常,另一个出现了死锁呢,这怎么都解释不通。

这才想起来,有个东西自始至终都被忽略了——数据源。两份文件表面上看似格式相同,有没有可能存在细节上的差别?

同事听后觉得有道理,与其陷在僵局之中,不如换一个方向排查。用十六进制视图分别打开TSI_3321TSI_3034的数据文件,一眼就看出了猫腻。
TSI3321数据十六进制视图
TSI3034数据十六进制视图

程序中严格按照\r\n分割不同时间点的数据,这在3321的数据文件中是合理的,而到了3034中,每一行结尾却是\t\r\n。没错,多出来一个字符\t,偏偏这个字符又是用于分割列,导致分割后列的数量与预期不一致,跳出了解析逻辑。

string[] rows = text.Split(new string[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries);
for (int rowIndex = 0; rowIndex < rows.Length; rowIndex++)
{
    string row = rows[rowIndex].ToLower();
    string[] dataArr = row.Split(new char[] { '\t' }, StringSplitOptions.RemoveEmptyEntries);

    // 解析数据时间
    if (dataArr.Length != _dataRowColumnCount || "Sample #".Equals(dataArr[0], StringComparison.OrdinalIgnoreCase))
    {
        continue;
    }

    ……
}

排查到这里,似乎柳暗花明,调整程序后更新到现场,问题依旧……

第三次排查

究竟哪里还有问题?

同事再一次查看代码,花费一番功夫后,终于找出问题所在。这是第二次排查时,把临时变量改为全局变量后带出来的新Bug。

对于3034来说,每条数据都是以09 0D 0A结尾,以它为例,上面代码中计算的endIndex=1,SubString(0, endIndex+1)=09 0D,跟缓存变量拼接后再分割永远都会多出一列,当然不会解析了。

下面的伪代码能清楚地说明问题

假设本次读取到的字节如下:

byte[] byteData = new byte[xx 0x09 0x0D 0x0A yy 0x09 0x0D 0x0A]

转换为字符串,计算得到endIndex=6

string text = Encoding.ASCII.GetString(byteData);
int endIndex = text.LastIndexOf("\r\n");

去掉尾部的\r\n后本次要解析的目标文本为和缓存文本分别如下

buffer = text.Substring(endIndex, text.Length - endIndex);  // buffer=0D 0A
text = text.Substring(0, endIndex + 1);  // text=xx 09 0D 0A yy 09 0D

按照\r\n分割得到rows

string[] rows = text.Split(new string[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries);

包含两个元素

[
    {xx 0x09},
    {yy 0x09 0x0D}
]

再按照\t分割后取得列

string[] dataArr = row.Split(new char[] { '\t' }, StringSplitOptions.RemoveEmptyEntries);

rows[0]能正常解析,rows[1]由于多出了一个0x0D跳出了解析逻辑

rows[0] = [xx];
rows[1] = [yy 0x0D]

等到下一个扫描周期只读取到一个时间点的数据,跟buffer拼接之后永远多出一个0x0D,所以之后的数据也就再也不会被解析了。

而程序刚启动时每次只读取10000字节,大概率不会碰巧读到行尾,因此能解析到最新数据,而第二个扫描周期每次只能读取一行数据了,因为多出的0x0D而无法解析。

到此为止,这个由驱动引发的Bug才真正地排查完毕。

还有个意外

你以为事情到此就结束了吗?

NO!!!

虽然上传模块实现了按仪器发送数据,可此前类似的需求通常使用另一个轻量级的数据转发软件来实现,空气数采软件并没有支持该功能,并不单单是界面不支持设置

先前只看到了两个通道都有数据收发,没注意收发记录文件大小一模一样。如果仔细一点的话,兴许就不会耗费这么多功夫了。

尾声

最后的最后,调整数采主程序,兼容按仪器发送数据,这件事情才真正解决完毕。

知道真相后,回过头来想想,其实问题并不复杂,困难之处就在于多种巧合的碰撞,让原本很简单的问题复杂化了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1184422.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

tingpng 批量压缩工具

无聊的时候写的 自用 还行 https://ttkeji.lanzoul.com/iPCfY1e5wwwh

虹科示波器 | 汽车免拆检测 | 2017款路虎发现车行驶中发动机抖动且加速无力

一、故障现象 一辆2017款路虎发现车&#xff0c;搭载3.0L发动机&#xff0c;累计行驶里程约为3.8万km。车主反映&#xff0c;车辆在行驶过程中突然出现发动机抖动且加速无力的现象&#xff0c;于是请求拖车救援。 二、故障诊断 拖车到店后首先试车&#xff0c;发动机怠速轻微抖…

盈科视控荣获创响中国大赛第四名

近日&#xff0c;随着2023“创响中国”安徽省创新创业大赛省内赛区复赛的举办完成&#xff0c;60个项目从6个专项组中脱颖而出。 盈科视控凭借其【IC 载板及先进 PCB 智慧工厂服务商】参赛项目&#xff0c;荣获大赛第四名。 本次大赛由安徽省发改委、安徽省财政厅、合肥市人民…

图形学及图形学进展

有段时间没有来CSDN了&#xff0c;最近一直忙于工作&#xff0c;最近图形学方面&#xff0c;特别是重建图形学方面有了比较大的进展&#xff0c;然后NeRF-SLAM向也有不少进展&#xff0c;但由于ChatGPT风光无限&#xff0c;光芒都没有发出来&#xff0c;后续还是继续创作&#…

【已解决】ChatGPT报错“Unable to load history Retry“等问题

解决ChatGPT历史加载错误&#xff1a;“Unable to load history Retry”问题指南 引言 在使用ChatGPT时&#xff0c;您可能遇到过一个常见的错误提示&#xff1a;“Unable to load history Retry”。这可能会阻止您查看以前的对话历史。本文将为您提供一个详细的教程&#xf…

nodejs express vue 酒店预订系统源码

开发环境及工具&#xff1a; nodejs&#xff0c;vscode&#xff08;webstorm&#xff09;&#xff0c;大于mysql5.5 技术说明&#xff1a; nodejs express vue elementui 功能介绍&#xff1a; 用户端&#xff1a; 用户登录注册 首页显示轮播图&#xff0c;客房分类&…

2010年5月27日Go生态洞察:I/O中Go的热门问答

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…

【JS】判断字符串是否为 url 的方法

文章目录 用法解析 用法解析 当你传递一个字符串给 URL 构造函数时: 如果字符串是一个有效的 URL&#xff0c;它将返回一个新的 URL 对象。否则&#xff0c;它将返回一个错误。 const url new URL("https://www.baidu.com/"); console.log(url);函数封装&#xf…

Final Cut Pro X 10.6.10 跨行小白都能看懂的安装教程

Final Cut Pro X又名FCPX,是MAC上非常不错的视频非线性剪辑软件,它剪辑速度超凡,具有先进的调色功能、HDR 视频支持&#xff0c;以及 ProRes RAW&#xff0c;让剪辑、音轨、图形特效、整片输出&#xff0c;支持主流的摄像机格式,是专业视频剪辑领域的王者工具。 获取路径地址 最…

C语言每日一题(25)链表的中间结点

力扣 876. 链表的中间结点 题目描述 给你单链表的头结点 head &#xff0c;请你找出并返回链表的中间结点。 如果有两个中间结点&#xff0c;则返回第二个中间结点。 思路分析 快慢指针法 用一慢一快指针遍历整个链表&#xff0c;每次遍历&#xff0c;快指针都会比慢指针多…

4141B/4141E/4141F信号源分析仪

4141B/E/F 信号源 频率范围&#xff1a;10MHz~7/26.5/40GHz 4141系列信号源分析仪采用双通道互相关技术&#xff0c;具备优异的相位噪声、幅度噪声和基带噪声测量能力&#xff0c;同时具备瞬态测量、频谱监测、频率功率测量等多种测量功能&#xff0c;具有频率覆盖范围宽、动态…

JavaScript文档操作

文档对象模型(document object model&#xff0c;DOM)是W3C制定的一套技术规范&#xff0c;用来描述JavaScript脚本与HTML文档进行交互的Web标准。DOM规定了一系列标准接口&#xff0c;允许开发人员通过标准方式访问文档结构、操作网页内容、控制样式和行为等。 1、节点 节点…

SpringBoot的bean属性校验

1.导入坐标 <!-- 导入JSR303规范--> <dependency><groupId>javax.validation</groupId><artifactId>validation-api</artifactId> </dependency> 2.Validated 说明&#xff1a;开启对当前bean的属性注入校验 package com.…

跨链知识指南

跨链知识指南 什么是跨链 跨链就是能够让两个不同的链产生某种关联的技术&#xff0c;或者说能把链A的东西搬到链B&#xff0c;跨链是一个复杂的过程&#xff0c;需要链对链外的信息的获取与验证&#xff0c;需要节点有单独的验证能力等等 什么是跨链桥&#xff1f; 跨链桥…

viple进阶3:打印不同形状的三角形

&#xff08;1&#xff09;题目&#xff1a;打印实心的三角形&#xff08;正三角&#xff09; 第一步&#xff1a;观察图形。首行是1颗星&#xff0c;其余的每一行都比上一行多1颗星&#xff1b;其次&#xff0c;每一行的星号数和行数值相等&#xff0c;第一行有1颗星&#xff…

国风数字人:数字时代的传统戏剧文化代言人

国风数字人不是简单搬运中国元素&#xff0c;而是创新优秀传统文化&#xff0c;结合现代元素&#xff0c;富含艺术性、趣味性、科技感&#xff0c;利用数字人的形式将国风文化“活”起来。 数字人翎Ling登上国风少年创演节目&#xff0c;演绎梅派京剧经典《天女散花》&#xff…

机器视觉的试卷批改系统 - opencv python 视觉识别 计算机竞赛

文章目录 0 简介1 项目背景2 项目目的3 系统设计3.1 目标对象3.2 系统架构3.3 软件设计方案 4 图像预处理4.1 灰度二值化4.2 形态学处理4.3 算式提取4.4 倾斜校正4.5 字符分割 5 字符识别5.1 支持向量机原理5.2 基于SVM的字符识别5.3 SVM算法实现 6 算法测试7 系统实现8 最后 0…

Camera Raw 16 v16.0.0

Camera Raw 16是一款允许摄影师处理原始图像文件的软件PS增效工具。原始图像文件是未经相机内部软件处理的数码照片&#xff0c;因此包含相机传感器捕获的所有信息。Camera Raw 为摄影师提供了一种在将原始文件转换为更广泛兼容的格式&#xff08;如 JPEG 或 TIFF&#xff09;之…

安卓手持机 条码扫描终端 物流仓储盘点机

HT520条码扫描手持机提供各种硬解扫描头选配 霍尼&#xff1a;HS7,4603,6603 斑马&#xff1a;4710,4750 新大陆&#xff1a;N1,CM60 可以快速、精准采集各种一/二维码、破损码、弯折码、屏幕码等光学图形条码。可选NFC读写功能&#xff0c;可以读各类卡证&#xff0c;会员卡…

【Python3】【力扣题】232. 用栈实现队列

【力扣题】题目描述&#xff1a; 栈&#xff1a;线性集合。后进先出。 队列&#xff1a;线性集合。先进先出。 【Python3】代码&#xff1a; 解题思路&#xff1a;两个栈&#xff0c;一个入队的栈&#xff0c;一个出队的栈。出栈时&#xff0c;若出队的栈为空&#xff0c;才将…