重庆网站制作一般多少钱,单页面网站有哪些,wordpress调用阅读量,建筑公司取名参考该Bug的诡异之处在于这是一个由多种因素综合碰撞之后形成的综合体。纵观整个排查过程#xff0c;一度被错误的目标误导#xff0c;花费大量功夫后才找到问题点所在#xff0c;成熟的组件在没有确凿证据之前不能随意怀疑其稳定性。 前言
此前在接入两台粒径谱仪#xff08;… 该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, ListUploadFactorValue 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){ListSendList 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结尾以它为例上面代码中计算的endIndex1SubString(0, endIndex1)09 0D跟缓存变量拼接后再分割永远都会多出一列当然不会解析了。
下面的伪代码能清楚地说明问题
假设本次读取到的字节如下
byte[] byteData new byte[xx 0x09 0x0D 0x0A yy 0x09 0x0D 0x0A]转换为字符串计算得到endIndex6
string text Encoding.ASCII.GetString(byteData);
int endIndex text.LastIndexOf(\r\n);去掉尾部的\r\n后本次要解析的目标文本为和缓存文本分别如下
buffer text.Substring(endIndex, text.Length - endIndex); // buffer0D 0A
text text.Substring(0, endIndex 1); // textxx 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
虽然上传模块实现了按仪器发送数据可此前类似的需求通常使用另一个轻量级的数据转发软件来实现空气数采软件并没有支持该功能并不单单是界面不支持设置。
先前只看到了两个通道都有数据收发没注意收发记录文件大小一模一样。如果仔细一点的话兴许就不会耗费这么多功夫了。
尾声
最后的最后调整数采主程序兼容按仪器发送数据这件事情才真正解决完毕。
知道真相后回过头来想想其实问题并不复杂困难之处就在于多种巧合的碰撞让原本很简单的问题复杂化了。