复盘一个诡异的Bug
该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
接入方案
有如下两个接入数据的方案:
- 使用传统的数据转发程序解析和上传
- 采用我方经过多年沉淀和积累的空气数采软件DATS解析存储并上传
传统的数据转发程序功能简单,仅支持解析和上传,自身不存储数据,在某种意义上增加了维护难度。
而DATS不仅具备前者的功能,还有存储数据、云端配置备份等功能,安全性和稳定性都较好。不过也有个缺点,虽然两者共用一个上传模块,但是界面上没开放按照仪器配置上传通道的功能。
从大局出发选择后者,而前者将被逐步淘汰。
本次任务的模板就明确了:
- 开发两个驱动
- 开放按仪器配置上传通道的设置界面
由于所有数据都生成在一个文件中,每次读取完整的文件不仅降低了解析效率,还耗费大量内存,当文件超过一定体量时甚至可能引发内存溢出问题。故而选择分段读取方式,在程序首次启动后从文件头开始解析,并缓存文件末尾流的位置,下一次直接从该位置往后读取。此前写过类似代码,可以复制粘贴一下。
鉴于两个驱动的相似性,考虑代码的复用,我先实现了一个通用的解析逻辑TxtReader,在其基础上封装了两个驱动TSI_3321和TSI_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_3321和TSI_3034的数据文件,一眼就看出了猫腻。
程序中严格按照\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!!!
虽然上传模块实现了按仪器发送数据,可此前类似的需求通常使用另一个轻量级的数据转发软件来实现,空气数采软件并没有支持该功能,并不单单是界面不支持设置。
先前只看到了两个通道都有数据收发,没注意收发记录文件大小一模一样。如果仔细一点的话,兴许就不会耗费这么多功夫了。
尾声
最后的最后,调整数采主程序,兼容按仪器发送数据,这件事情才真正解决完毕。
知道真相后,回过头来想想,其实问题并不复杂,困难之处就在于多种巧合的碰撞,让原本很简单的问题复杂化了。
相关文章:

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

【uniapp】通用列表封装组件
uniapp页面一般都会有像以下的列表页面,封装通用组件,提高开发效率; (基于uView前端框架) 首先,通过设计图来分析一下页面展示和数据结构定义 w-table组件参数说明 参数说明类型可选值默认值toggle列表是…...

17 Linux 中断
一、Linux 中断简介 1. Linux 中断 API 函数 ① 中断号 每个中断都有一个中断号,通过中断号可以区分出不同的中断。在 Linux 内核中使用一个 int 变量表示中断号。 ② request_irq 函数 在 Linux 中想要使用某个中断是需要申请的,request_irq 函数就是…...

微信小程序真机调试连接状态一直在正常和未链接之间反复横跳?
背景:小程序真机调试的时候,发现真机的network不显示接口调用情况,控制台也没有输出内容。具体如下所示; 解决方法: 1、确保手机端连接的网络和微信开发者工具网络一致,比如用同一个WiFi 2、真机自动调试…...

最新Next 14快速上手基础部分
最新Next 14快速上手基础部分 最新的NEXT快速上手文档,2023.10.27 英文官网同步,版本Next14.0.0 本项目案例:GitHub地址,可以根据git回滚代码到对应知识,若有错误,欢迎指正! 一、介绍 1.什么是…...

【uniapp/uview】Collapse 折叠面板更改右侧小箭头图标
最终效果是这样的: 官方没有给出相关配置项,后来发现小箭头不是 uview 的图标,而是 unicode 编码,具体代码: // 箭头图标 ::v-deep .uicon-arrow-down[data-v-6e20bb40]:before {content: \1f783; }附一个查询其他 u…...

企业如何落地搭建商业智能BI系统
随着新一代信息化、数字化技术的应用,引发了新一轮的科技革命,现代化社会和数字化的联系越来越紧密,数据也变成继土地、劳动力、资本、技术之后的第五大生产要素,这一切都表明世界已经找准未来方向,前沿科技也与落地并…...
RedisTemplate连接密码设置教程
最近在一个项目中使用Redis保存Token时,出现连接Redis报错的情况 org.springframework.data.redis.RedisConnectionFailureException: Unable to connect to Redis; nested exception is io.lettuce.core.RedisConnectionException: Unable to connect to localhos…...

基于SSM的二手车交易网站的设计与实现
末尾获取源码 开发语言:Java Java开发工具:JDK1.8 后端框架:SSM 前端:Vue 数据库:MySQL5.7和Navicat管理工具结合 服务器:Tomcat8.5 开发软件:IDEA / Eclipse 是否Maven项目:是 目录…...

温故知新:探究Android UI 绘制刷新流程
一、说明: 基于之前的了解知道ui的绘制最终会走到Android的ViewRootImpl中scheduleTraversals进行发送接收vsync信号绘制,在ViewRootImpl中还会进行主线程检测,也就是我们所谓子线程更新ui会抛出异常。 像我们常用的刷新ui,inval…...
设计模式-命令模式(Command)
设计模式-命令模式(Command) 一、命令模式概述1.1 什么是命令模式1.2 简单实现命令模式1.3 使用命令模式的注意事项 二、命令模式的用途三、命令模式实现方式3.1 使用匿名内部类实现命令模式3.2 使用Lambda表达式实现命令模式3.3 使用Java内置的函数式接…...
linux批量解压zip
方法一 1,创建unzip.sh #!/bin/bashwhile read line do unzip $linedone < filelist.txt #!/bin/bashwhile read line dounzip "$line" >& log & done < filelist.txt3. 在终端中执行以下命令 $ chmod x unzip.sh $ ./unzip.sh 这…...
HBase导出建表语句
HBase导出建表语句 HBase是一个面向大数据的分布式列存数据库,它以Hadoop作为底层存储和计算平台。在HBase中,数据以表的形式存储,每个表由行和列组成。本文将介绍如何使用HBase导出建表语句,并提供相应的代码示例。 HBase建表语…...

Linux环境配置(云服务器)
目录 1.第一步:购买云服务器 2.第二步:下载Xshell 7 3.第三步:打开Xshell,登录云服务器 4.第四步:更加便捷的云服务器登录方式 1.第一步:购买云服务器 (推荐:阿里云、华为云、腾…...

【性能测试】Linux下Docker安装与docker-compose管理容器(超细整理)
目录:导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结(尾部小惊喜) 前言 1、Linux下Docker…...

陪玩2.0升级版源码/价值18500元的最新商业版游戏陪玩语音聊天系统源码
陪玩2.0升级版源码,价值18500元的最新商业版游戏陪玩语音聊天系统源码。 修复部分逻辑以及bug 修复bug:店员拒单后,退款会退到店员账号里而不是用户账户里。 修复bug:客户在盲盒下单后,马上取消了订单,但…...

读程序员的制胜技笔记08_死磕优化(上)
1. 过早的优化是万恶之源 1.1. 著名的计算机科学家高德纳(Donald Knuth)的一句名言 1.2. 原话是:“对于约97%的微小优化点,我们应该忽略它们:过早的优化是万恶之源。而对于剩下的关键的3%,我们则不能放弃优化的机会。” 2. 过早…...

【gltf-pipeline】安装gltf-pipeline 进行文件格式转换
问题 想使用gltf-pipeline进行gltf和glb格式转换。简单记录一下安装过程。 解决 1、安装Node.js Node.js下载路径:https://nodejs.org/en 建议默认设置安装。 添加系统环境变量: 测试安装是否成功: 在cmd.exe中运行: no…...
Android OpenGL ES踩坑记录
因为项目中的一个自定义绘图控件性能不行,改用OpenGL实现,也是第一次使用OpenGL,由于只是绘制2D图形,参考官方以及网上的教程,实现起来还是比较顺畅的,开发时只用了两个手机测试,运行良好&#…...
Vue3 项目完整配置
目录 一、配置简述二、创建项目1、使用包管理工具 pnpm2、新增目录 三、配置 ESLint1、添加代码2、修改 VSCode 配置 四、husky 工具配置五、暂存区 eslint 校验六、axios 配置1、安装创建2、测试 七、导入 Element Plus八、Pinia 持久化实现九、其他导入 .scss 文件需要安装 s…...
conda相比python好处
Conda 作为 Python 的环境和包管理工具,相比原生 Python 生态(如 pip 虚拟环境)有许多独特优势,尤其在多项目管理、依赖处理和跨平台兼容性等方面表现更优。以下是 Conda 的核心好处: 一、一站式环境管理:…...

无法与IP建立连接,未能下载VSCode服务器
如题,在远程连接服务器的时候突然遇到了这个提示。 查阅了一圈,发现是VSCode版本自动更新惹的祸!!! 在VSCode的帮助->关于这里发现前几天VSCode自动更新了,我的版本号变成了1.100.3 才导致了远程连接出…...

汽车生产虚拟实训中的技能提升与生产优化
在制造业蓬勃发展的大背景下,虚拟教学实训宛如一颗璀璨的新星,正发挥着不可或缺且日益凸显的关键作用,源源不断地为企业的稳健前行与创新发展注入磅礴强大的动力。就以汽车制造企业这一极具代表性的行业主体为例,汽车生产线上各类…...

【快手拥抱开源】通过快手团队开源的 KwaiCoder-AutoThink-preview 解锁大语言模型的潜力
引言: 在人工智能快速发展的浪潮中,快手Kwaipilot团队推出的 KwaiCoder-AutoThink-preview 具有里程碑意义——这是首个公开的AutoThink大语言模型(LLM)。该模型代表着该领域的重大突破,通过独特方式融合思考与非思考…...
生成 Git SSH 证书
🔑 1. 生成 SSH 密钥对 在终端(Windows 使用 Git Bash,Mac/Linux 使用 Terminal)执行命令: ssh-keygen -t rsa -b 4096 -C "your_emailexample.com" 参数说明: -t rsa&#x…...

分布式增量爬虫实现方案
之前我们在讨论的是分布式爬虫如何实现增量爬取。增量爬虫的目标是只爬取新产生或发生变化的页面,避免重复抓取,以节省资源和时间。 在分布式环境下,增量爬虫的实现需要考虑多个爬虫节点之间的协调和去重。 另一种思路:将增量判…...
Redis的发布订阅模式与专业的 MQ(如 Kafka, RabbitMQ)相比,优缺点是什么?适用于哪些场景?
Redis 的发布订阅(Pub/Sub)模式与专业的 MQ(Message Queue)如 Kafka、RabbitMQ 进行比较,核心的权衡点在于:简单与速度 vs. 可靠与功能。 下面我们详细展开对比。 Redis Pub/Sub 的核心特点 它是一个发后…...

R语言速释制剂QBD解决方案之三
本文是《Quality by Design for ANDAs: An Example for Immediate-Release Dosage Forms》第一个处方的R语言解决方案。 第一个处方研究评估原料药粒径分布、MCC/Lactose比例、崩解剂用量对制剂CQAs的影响。 第二处方研究用于理解颗粒外加硬脂酸镁和滑石粉对片剂质量和可生产…...

无人机侦测与反制技术的进展与应用
国家电网无人机侦测与反制技术的进展与应用 引言 随着无人机(无人驾驶飞行器,UAV)技术的快速发展,其在商业、娱乐和军事领域的广泛应用带来了新的安全挑战。特别是对于关键基础设施如电力系统,无人机的“黑飞”&…...

Chromium 136 编译指南 Windows篇:depot_tools 配置与源码获取(二)
引言 工欲善其事,必先利其器。在完成了 Visual Studio 2022 和 Windows SDK 的安装后,我们即将接触到 Chromium 开发生态中最核心的工具——depot_tools。这个由 Google 精心打造的工具集,就像是连接开发者与 Chromium 庞大代码库的智能桥梁…...