当前位置: 首页 > 科技观察

抓到一个概率不到万分之一的bug,你遇到过吗?

时间:2023-03-19 17:28:37 科技观察

前言在开始这篇文章之前,我想说一句:如果一个系统暂时没有问题,那只是因为它的并发不够。上周查看系统日志的时候,发现了一条不一样的日志。日志中一半内容是正常的消息数据,另一半是0x00等空数据。虽然系统没有抛出任何异常,但是这些日志肯定是异常的。多年的经验告诉我,这其中一定有问题,在好奇心的驱使下,我终于发现了一个隐藏得很深的bug。有时很容易找到错误并解决它。困难的部分是如何找到错误并找出问题所在。下面就带大家分析一下这个Bug。奇怪的日志输出一个调用外部接口的基础类,打印出类似如下的日志:abcdabcdabcdabcdabcdabcdabcd<0x00><0x00><0x00><0x00><0x00>其中,前面的abcd是正常业务数据,后面莫名其妙多了很多<0x00>。那么,这个基础工具类到底有多基础呢?这个方法用的地方很多,一天调用几十万次,而上面的情况一天只出现几次。真是太巧了,碰巧被人看到了。看代码,初步推断是字节数组转String时,字节数组后半部分为空或者有一些数据无法转换。老代码分析这里先把业务代码脱敏写成demo给大家看:publicstaticvoidoldCode()throwsIOException{//外部系统通过HttpURLConnection读取返回的流InputStreamin=newByteArrayInputStream("abc".getBytes());//明确已知数据包长度(通过解析Header获得)intbodyLen=2048;byte[]body=newbyte[bodyLen];intrecvLen=0;while(recvLen的可能是字节数组的一部分为空或者数据错误导致的。上面的代码有一个明显的错误,你能看出来吗?根据代码原来的写法,推测出现这个错误的原因是用户不熟悉InputStream的read方法。这里的读者先自己阅读一下,看看上面代码的bug在哪里。接下来介绍一下InputStream的read方法。InputStream的读取方法InputStream是一个抽象类,代表所有代表字节输入流的类的超类。它提供了三种常用的read()方法:read(),无参数方法。此方法从输入流中读取下一个字节的数据。返回0到255范围内的int字节值。如果因为已到达流末尾而没有可用字节,则返回值-1。这个方法会处于阻塞状态,等待数据的到来,直到返回值为-1或者抛出异常。read(byteb[],intoff,intlen):从输入流中读取最多len个数据字节到字节数组中。尝试读取len个字节,但也可能读取少于此值。返回作为整数读取的实际字节数。read(byte[]b):从输入流中读取一定数量的字节,存入缓冲区数组b。返回作为整数读取的实际字节数。分析以上三种方法。其中,第一种方法,本质上后两种方法都是调用第一种方法实现的,但是直接使用第一种方法的缺点很明显,就是处理效率低,读一个字节一个字节一次。后两种方法都添加字节数组,用作缓冲区。第三种方法等同于以下列方式调用第二种方法:read(b,0,b.length)并且在错误代码中使用了第二种方法。Bug分析看了read方法的API说明,你发现BUG了吗?是的,写这段代码的人误解了read方法的返回值。recvLen=in.read(body,recvLen,bodyLen-recvLen);原来写代码的人可能在read方法的返回值中读到了参数off的新位置。这样,调用read方法后,得到填充的位置,然后将填充的位置减去总长度,然后继续读取后面的内容,继续填充。但实际上read方法的返回结果是:以整数形式返回实际读取的字节数,可能与off位置值相同,但不是off位置。下面分析一下while循环中的逻辑处理:while(recvLen。bug产生的原因经过上面的分析,我们已经找到了这个bug,也得到了这个bug的原因。首先,之所以没有大面积爆发这个bug,是因为大部分请求都是一次性读取流中的数据,直接循环结束。当第二个循环不进入时,bug就隐藏起来了。其次,出现bug的原因是用户不理解API的返回值,更重要的原因是read方法可能返回结果多次(粘包拆包现象)。bug修改找到原因,修改非常容易。改造一下demo:publicstaticvoidoldCode()throwsIOException{//外部系统通过HttpURLConnection读取返回的流InputStreamin=newByteArrayInputStream("abc".getBytes());//明确已知消息长度(通过解析Header获得)intbodyLen="abc".getBytes().length;System.out.println(bodyLen);byte[]body=newbyte[6];intrecvLen=0;while(recvLen