找bug记(2)


发表于 2011-09-10   |   分类于  未分类    |  


    这篇blog迟到了很久,本来是想写另一个跟网络相关bug的查找过程,偷偷懒,写下最近印象比较深刻的bug。这个bug是我的同事水寒最终定位到的。
    前几个月同事报告称有一个线上MQ集群会同一时间抛出ArrayIndexOutOfBoundsException这个异常,也就是数组越界。查看源码,除去一些无关紧要的细节大概是这样子:
<!–

Code highlighting produced by Actipro CodeHighlighter (freeware)

–> public   class  ConnectionSelector{
     private  AtomicInteger sets= new  AtomicInteger(0);

    public   void  selectConnection(List<Connection> connList){
           if (connList== null ){
                 return   null ;
           }
           final   int  size = connList.size();
             if  (size == 0) {
                 return   null ;
            }
            return connList.get(sets.incrementAndGet() % size);
}

   }

    很显然,这里的本意是实现一个轮询的连接选择器,返回一个选中的连接。使用AtomicInteger递增并对链表大小取模,返回结果索引位置的连接。异常抛出的位置就是我代码中标红的位置。

    显然,这里有两种可能,一种情况下是说在执行那一行代码的时候,connList的大小缩小了(也就是说连接可能被其他线程移出),那么导致取模的结果越界。另一种可能是取模的结果本身确实超过了列表范围。

    第一种情况是完全可能的,因为服务器的连接可能随时断开或者重连,但是这种情况相对非常少见,因此我们这里并没有对这个选择过程做同步,主要是从性能的角度出发,偶尔的失败可以接受。很遗憾的是,我被我的思维惯性误导了,从来没有怀疑过第二种情况,总是认为是不是真的连接恰巧断开导致这个异常,但是却无法解释这个异常发生后就一直错误下去,无法自行恢复。
    为什么说思维惯性误导呢?这里的问题其实是负数取模的问题,对一个负数进行取模,结果会是正数还是负数?答案是结果因语言而异。
    我很早以前在使用Ruby的时候做过测试,负数取模结果为正数,例如在irb里尝试下:
<!–

Code highlighting produced by Actipro CodeHighlighter (freeware)

–> >> -1000%3
=> 2
>> -2001%4
=> 3

    这个印象持续至今,在clojure里结果也是这样子:
<!–

Code highlighting produced by Actipro CodeHighlighter (freeware)

–> Clojure 1.2.1
user=> (mod -1000 3)
2
user=> (mod -2001 4)
3

    可以再试试python:
<!–

Code highlighting produced by Actipro CodeHighlighter (freeware)

–> Python 2.7.1 (r271:86832, Jun 16 2011, 16:59:05) 
[GCC 4.2.1 (Based on Apple Inc. build 5658) (LLVM build 2335.15.00)] on darwin
Type “help“, “copyright“, “credits“ or “license“  for  more information.
>>> -10000%3
2
>>> -2001%4
3

    这三种语言的结果完全一致,结果都为正数。这个惯性思维延续到java却不成立了,可惜我根本没做测试,让我们试下:
<!–

Code highlighting produced by Actipro CodeHighlighter (freeware)

–>     public   static   void  main( final  String[] args) {
        System.out.println(-1000 % 3);
        System.out.println(-2001 % 4);
    }

打印结果为:
<!–

Code highlighting produced by Actipro CodeHighlighter (freeware)

–> -1
-1

    果然,在java里负数取模的结果为负数,而不是我习惯性地认为是正数。因此最终的定位到的原因就是sets这个变量递增超过Integer.MAX_VALUE后越界变成负数了,取模的结果为负数,导致抛出数组越界的异常,这也解释了为什么同一个集群都在同一时间出问题,因为这个集群内的机器启动时间相邻并且调用这个方法次数相对平均。修正问题很简单,加个Math.abs就好。

    这个问题更详细的讨论后来我找到 这篇博客 ,作者讨论几种语言和计算器的这个问题的结果,给出了一些结论。不过我觉的这个结论可能也不是那么可靠,特别是对c/c++来说,很大程度上应该还是依赖于实现,最可靠的办法还是强制结果为正。

    这个bug的几个教训:
1、首先是第一次出现的时候没有引起足够重视,重启解决问题后没有深究。有句玩笑话:99%的程序问题都可以通过重启解决。但是事实上问题仍然存在,该发生的终究还会发生。不管你信不信,它就是发生了,这是一个奇迹。
2、注意大脑的思维惯性,经验主义和教条主义都不可取。最近在读一本好书《 暗时间 》,大脑误导我们的手段可是多种多样。
3、最后就是这个负数取模的结果因语言而异,不要依赖于特定实现。
下载方式:https://pan.quark.cn/s/a4b39357ea24 布线问题(分支限界算法)是计算机科学和电子工程领域中一个广为人知的议题,它主要探讨如何在印刷电路板上定位两个节点间最短的连接路径。 在这一议题中,电路板被构建为一个包含 n×m 个方格的矩阵,每个方格能够被界定为可通行或不可通行,其核心任务是定位从初始点到最终点的最短路径。 分支限界算法是处理布线问题的一种常用策略。 该算法与回溯法有相似之处,但存在差异,分支限界法仅需获取满足约束条件的一个最优路径,并按照广度优先或最小成本优先的原则来探索解间树。 树 T 被构建为子集树或排列树,在探索过程中,每个节点仅被赋予一次成为扩展节点的机会,且会一次性生成其全部子节点。 针对布线问题的解决,队列式分支限界法可以被采用。 从起始位置 a 出发,将其设定为首个扩展节点,并将与该扩展节点相邻且可通行的方格加入至活跃节点队列中,将这些方格标为 1,即从起始方格 a 到这些方格的距离为 1。 随后,从活跃节点队列中提取队首节点作为下一个扩展节点,并将与当前扩展节点相邻且未标的方格标2,随后将这些方格存入活跃节点队列。 这一过程将持续进行,直至算法探测到目标方格 b 或活跃节点队列为。 在实现上述算法时,必须定义一个类 Position 来表征电路板上方格的位置,其成员 row 和 col 分别指示方格所在的行和列。 在方格位置上,布线能够沿右、下、左、上四个方向展开。 这四个方向的移动分别被为 0、1、2、3。 下述表格中,offset[i].row 和 offset[i].col(i=0,1,2,3)分别提供了沿这四个方向前进 1 步相对于当前方格的相对位移。 在 Java 编程语言中,可以使用二维数组...
<think>嗯,用户想了解如何高效地到软件中的Bug,需要给出调试技巧、方法和故障排查流程。首先,我得回忆一下常见的调试策略和相关引用内容。根据系统指令,要引用提供的资料,比如引用[3]和[4]提到了调试技巧和策略,还有工具使用。 首先,应该分步骤回答,结构要清晰。可能需要先讲基础调试技巧,比如断点、单步执行、日志录,这些都是基本的。然后介绍进阶方法,比如内存检查、静态分析工具。接下来是故障排查流程,分步骤说明,比如复现问题、定位原因、修复验证等。最后可以提一些注意事项,比如保持冷静、录过程、预防措施。 要确保引用正确,比如引用[3]里提到的查看内存和变量,还有assert的使用,这些可以放在基础技巧里。引用[4]提到的断点和观察点调试,属于策略部分。另外,用户提供的引用[1]和[2]可能不太相关,但引用[1]提到工具可以简化任务,可能需要提及调试工具如GDB或IDE内置的调试器。 还要注意生成相关问题,根据用户提供的示例,问题应该围绕调试技巧、工具、流程优化等方面。例如,如何利用断点调试,哪些工具推荐,如何避免常见错误等。 现在需要检查是否符合系统级指令:使用正确的中文,数学公式用$...$,独立公式用$$...$$,但这里可能不需要数学公式。回答结构是否清晰,有没有分步骤,引用是否自然添加在末尾。比如在提到断点调试时,可以引用[4],在提到代码规范时引用[3]。 可能遗漏的地方:故障排查流程是否需要更详细?比如分步骤说明,每个步骤的具体操作。还有,是否覆盖了用户可能遇到的常见问题,比如偶现Bug的处理方法,或者如何利用版本控制辅助调试。 需要确保每个部分都有对应的引用支持,同时语言要自然,避免太过技术化,让用户容易理解。最后生成的相关问题要相关且有价值,帮助用户进一步学习。</think>### 软件调试技巧与Bug方法 #### 一、基础调试技巧 1. **断点调试与单步执行** 使用IDE(如Visual Studio、PyCharm)的断点功能,逐行执行代码并观察变量值的变化。例如,在VS中可通过`F9`设置断点,`F10`单步跳过,`F11`进入函数内部[^4]。 ```python # 示例:观察循环变量 for i in range(10): print(i) # 在此行设置断点,检查i的值是否符合预期 ``` 2. **日志录与输出调试** 在关键位置添加日志输出,录程序状态。例如: ```python def calculate_sum(a, b): print(f"[DEBUG] a={a}, b={b}") # 输出中间值 return a + b ``` 3. **内存与变量检查** 对于指针错误或内存泄漏(如C/C++),使用工具(Valgrind)或IDE的内存查看功能,确认变量地址和值是否异常[^3]。 --- #### 二、进阶调试方法 1. **静态代码分析工具** 使用工具如`PyLint`(Python)、`ESLint`(JavaScript)扫描代码,发现潜在逻辑错误或编码规范问题[^1]。 2. **单元测试与断言** 编写测试用例覆盖边界条件,结合`assert`语句快速定位问题: ```python def divide(a, b): assert b != 0, "除数不能为0" # 触发断言提示 return a / b ``` 3. **观察点(Watchpoint)** 在特定变量被修改时触发中断,适用于追踪意外修改的全局变量[^4]。 --- #### 三、故障排查流程 1. **复现问题** - 明确Bug出现的条件(如输入数据、环境配置)。 - 尝试最小化复现步骤,剔除无关代码[^3]。 2. **定位问题根源** - 从错误堆栈或日志中提取关键信息。 - 使用“二分法”注释代码块,缩小问题范围。 3. **修复与验证** - 修改后通过单元测试和回归测试确认修复。 - 录修复方案,避免重复问题。 --- #### 四、注意事项 1. **保持冷静与系统性** 避免盲目修改代码,优先通过调试工具收集证据[^3]。 2. **录调试过程** 使用文档或注释Bug特征和解决路径,便于团队协作。 3. **预防性编码** 遵循代码规范(如变量命名清晰、模块化设计),减少低级错误。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值