KDB2 开发小结

November 12, 2007 – 9:22 pm

最近消失了好久,主要是考试吧,大三课程不多,但是都是学得累得很的那种。还有就是课程 Project ,最近这个就是很著名的 MiniSQL 了,经常都听学长们说,做一个 MiniSQL 下来确实会收获很多的。本来也是要认真做的,但是时间估计失误,在 6 号的时候才得知是 11 号截止,所以最后有些仓促了,不过最后还是做完了,已知的 Bug 都修正并且通过了压力测试,心里面也是很高兴的。这里写下一点总结吧,一是给大家分享一下,也是留给自己将来看的,我的 Blog 专门有一个分类就是 Bug Archive ,我主要就是想把自己平时实际开发中犯的错误和遇到的 Bug 都搜集起来,时而看看,希望能够不再遇到同样的问题吧。这次开发时间虽然很紧,但是其实主要开发时间和调试用的时间差不多也该对半分了,所以这篇小结也必须得放到这个分类里了。 :P

KDB2 就是 MiniSQL 了。其实同 MiniSQL 结缘也挺深的,大一上的 C 语言 Project 和大一下的 C 语言综合程序设计的 Project 都是做的 MiniSQL ,也都是同 moonykily 一起做的,其实大学挺多 Project 的,要找好的搭档也不太容易,有许多人就直接自己做了,或者组了一个队,仍然只有自己做,即使是几个很牛的人凑到一块儿,也不一定是好的组合。这一点还是觉得自己很幸运了,同 moonykily 几次合作都还算愉快,只是他总是不慌不忙的,有时候让人相当恼火。趁这个机会,我就把以前的东西也一块翻出来看一下吧,还是能找到许多比较有趣的东西的。

恩,越是去找越是能发现更多有趣的东西,不过也许我觉得很有趣,匆忙路过的人却没有心情去看,为了提高我 Blog 文章的质量,我还是把有趣的东西和有用的东西分成两篇文章吧。这里列举一下几次开发的教训吧,首先是第一次 MiniSQL ,以下是我当时写的总结文档里提到的问题:

  • 内存问题。这个也许是 C 之类的语言恒久不变的问题了,好像当时是因为一个 memcpy 将内存覆盖掉了,困扰了很久。因为内存整个坏掉了,调用堆栈看到的也全部都只是问号而已,又没有多少经验,在 GDB 下调试得异常郁闷。导致我后来对 memcpy 很敏感的样子。因为这样的 Bug 其实是防不胜防的,它就像感冒一样无处不在,发烧、头痛、喉咙痛、咳嗽、全身酸软无力……各种症状都是感冒引起的,内存问题也会引发一系列连锁反应,让问题隐藏得很深,而且最后改掉错误之后往往会发现其实是由一些很简单的问题造成的,甚至就只是一个拼写错误!
  • fopen 的 b 选项。熟悉 C 语言的应该都知道是什么了,就是那个让文件以二进制的方式打开的选项。我当然也是知道那个选项的,但是我似乎一直把它的存在当作是历史原因,因为在《The C Programming Language》上看到这样一句(P.178)In particular, our fopen does not recognize the “b” that signals binary access, since that is meaningless on UNIX systems, nor the “+” that permits both reading and writing. 于是就没有加那个选项。当时的情况是我在 Linux 机器上,而 moonykily 在 Windows 机器上,程序在我的机器上就能跑得很好,可是在他的机器上一运行就会疯狂出错:错误当然不会是那么明显地告诉你忘记加“b”选项了。我们为了这个 Bug 调试了大约三天,一次老师在上课的时候聊起他年轻时候的编程经历,就说起了这个选项,才让我们走出了困境。问题其实很简单了,在 Linux 上运行很好,因为它不区分文本和二进制格式,而在 Windows 下则被它做了一些诸如换行符转换之类的工作,结果造成了完全莫名其妙的错误。文件的内容用编辑器打开都是好好的,在程序里面读出来却又造成了错误,在经历了第一个问题之后不禁让人立马想到哪里又有内存覆盖了,可是却万万没有想到只是这么一个选项忘记了。
  • 设计。这个是大学的第一个 Project 了,当然是非常激动的,迫不及待地就开始写代码了,结果代码越写越乱,苦不堪言呢!现在,我觉得开发主要有三种方式:
    • 对于小程序,比如平时用的脚本之类的,立马动手开始写是不错的选择了,因为这样的程序通常是能用就行,太多顾虑可扩展性以及架构啊、OOP 啊、模式之类的乱七八糟的东西反而会陷入无边的深渊。
    • 对于中等大小的项目,需要事先经过谨慎地设计,尽量把握好各方面的问题,设计得不好,开发会变得越来越恶心的。
    • 对于大型甚至是超大型的项目,设计当然也是很重要的,但是最好不要把所有的时间都花在上面了,需要认识到一点:设计永远不可能的完美的。对应越大的项目这点越明显,各种不确定因素随时都有可能让你的设计毁于一旦,我们要做的不是在一开始把握住所有的方面,或者是去压制各种引起变化的因素,因为那是根本不可能做到的,这个时候需要从另一个方面来把握问题,引用那本著名的书的名字,就是:“拥抱变化”!极限编程、敏捷、重构这些东西现在也越来越得到重视了吧。
  • 版本控制。那个时候没有使用版本控制,也许也并不了很解这么一个东西吧,结果导致了很多问题。当时的情况是他使用 Windows + VS 2005 ,我则使用 Linux + GCC ,虽然我们的程序是 ANSI C 的,在哪里都能编译,开发过程却无限麻烦:
    • 代码同步。这个是最不好搞的,我写了一个脚本自动从他的 ftp 下载他更新过的源文件。可是有时候会不小心覆盖掉自己刚写的代码,或者是发现改过之后不如以前的版本好用,于是就手工做备份,时间一长备份的文件夹多了根本搞不清哪个是哪个了……
    • 代码转换。不止是换行符,因为在源代码里面使用了中文,还需要从他 Windows 的 GB2312 转化为我 Linux 里用的 utf-8 编码,幸好 Linux 下脚本非常方便,这些问题也好解决了。
    • Makefile 。他那边用 Visual Studio 管理工程,可是拿到我这边不能直接用,幸好项目不是很大,文件依赖关系也比较简单,我也是用脚本来处理这个问题的。总之当时的开发环境可以说是相当原始了。

第二次开发是在大一下学期,时间隔得比较近吧(其实似乎也不近了,从文件修改时间上来看似乎至少有六个月),总结了一些问题,重新设计了一番(例如,这次加上了页面缓存,上一个版本的 B+ 树操作是在没有任何 buffer 的情况下直接在文件里做的 -,-bb),便开工了。开工的时候异常兴奋,因为前面那个版本并不是很满意,想到能把那些毛病都去掉,做一个强大的系统出来,就非常兴奋。由于上一个版本写得太恶心了,我们代码全部重写。上次遇到的一些问题也得到了解决,例如我们使用 Subversion 来做了版本控制,用 qmake 来自动生成 Makefile ,并且都是在类 Unix 的环境下开发的,即使是在 Windows 下也是使用了 Cygwin 进行构建。最后的开发中有些什么具体问题已经记不清楚了,文档里也没有详细说,只是有一点还有挺深的印象:大概是太兴奋了,有些急于求成。本来是酝酿了很久的,设计得也比较清楚,采用了自底向上的方法,分了几个层次,可是总是想着做更多的东西,结果底层的东西没有经过什么测试就接着往上写,最后是写起来也不爽,因为心里对自己要用到的下面层次的东西也没底,跑起来就更不爽了,各个层次的未经过测试的 bug 混杂在一起,完全乱套了!

第三次就是这次了。时隔这么久,再做 MiniSQL 。本来上次做了以后说再也不要看到 MiniSQL 了,结果 C++ 的 Project 也没有做 MiniSQL ,这次却是我选了数据库系统设计这门课,必须要做 MiniSQL ,最后还是拉了 moonykily 来帮忙,说服他也选了这个课。

其实前面两次都遇到的问题就是程序的变动和调试等,后来也了解了许多软件开发的方法,就是关于迭代、重构以及单元测试之类的,都觉得很有道理,也许能比较有效地解决那些问题,便也想真正实践一下。甚至一开始准备做结对编程尝试的,因为我们两个住同一个寝室,也有这个条件,平时一起调试程序以及偶尔一起讨论一个问题
深有感触,两个人一起处理一个问题比分别处理效率和效果都要好。

可惜这次是计算失误,本身就是一个短学期,我虽然是比较早开始写的,但是也是断断续续零星地写一些,而 moonykily 按照他的一贯风格建好了项目然后就不管了。直到 11 月 6 日的时候,偶然翻起笔记本,才发现里面赫然写着“due date:11 月 10 日”!原先是以为一个短学期时间是太短了,也许验收会在考试之后,没想到突然就只剩下几天时间了,而且正好马上就是考试周,还要复习考试,而且现在和大一那会儿不同,晚上 11 点还要熄灯断电,想加班都不行。结果立马分工,别说结对编程了,几乎就是在没有整体设计、甚至连项目需求都没有完全明确的情况下开始了。

总之 KDB2 就这样做出来的,10 号早上还起床调好了一个重大 Bug ,拿去验收了。翻开 Subversion 的日志,许多问题就历历在目了:

  • bit_test 的问题。我写了一个辅助函数,让我能方便地测试某个位是否是打开的,例如:bit_test(flag, Kolumn::FL_UNIQUE) ,可是我把这个辅助函数写成了这样:

    inline bool bit_test(DWORD x, DWORD flag)
    {
        return (x & flag) == flag;
    }

    一般情况下没有问题,可是这样就不行了:

    if (bit_test(column.flag, Kolumn::FL_UNIQUE | Kolumn::FL_PRIMARY)
    {
        //...
    }

    这个 bug 导致了非常莫名其妙的问题,不过由于调试也多少积累了些经验,很容易就跟踪到它了,真是个低级错误啊!

  • 删除 STL 容器里的一些元素。容器里放了一些用于管理页面映射的对象,某个表被 drop 掉之后,与它相关的所有页面都要被强制卸载。一开始我使用了一个比较笨的办法,直接用 iterator 来枚举容器,遇到合适的就 erase 掉。可是后来出错了,应该是执行了 erase 操作之后原来那个 iterator 不再有效了吧,要维持有效性确实比较麻烦。于是我就换了个方法,写了个 Functor ,用了 STL 里面的 remove_if 。我印象中 remove_if 是把要被 remove 的元素交换到容器的末尾,最后再调用一次容器的区段 erase 操作就好了。这个“印象”可把我害惨了!

    好像是在最后一天的时候吧,原来运行得好好的 drop table 操作突然会出错了,而且出错的地方很诡异,Windows 报告 Access is denied (Windows 说废话的功力真是特别深厚 -.-b) ,然后紧接着会有各种各样的错误出现,最后甚至 Visual Studio 也挂掉了,弹一个框问是不是要发送错误报告。当时的沮丧程度是可想而知的了,第二天就要截止了,原来正常的地方却突然冒出这么一个奇怪的 Bug ,根本等不及静下心去跟踪,大概最容易的办法就是通过 Subversion 回滚到一个一个正常的版本,然后对比差异。可是更令人沮丧的是,回滚到了很久以前的一个版本才终于正常了,就是说这个问题竟然一直都没有被发现,而在期间的改动实在是太多了,检查了可疑的地方都没有发现问题。最后没有办法,强制冷静下来,去跟踪程序。其实主要还是时间太紧了,有些毛躁了,强制镇定下来最终还是找到了问题的所在。

    回到 STL 的 remove_if ,问题正是出在这里。按照我的理解,假如你有一个数组 [9, 7, 2, 3, 7, 5, 8] ,现在调用 remove_if(a, a+len, greater6);并假设 len 是数组长度,greater6 会在大于 6 的情况下返回 true 。得到的结果应该是 [2, 3, 5, 9, 7, 7, 8] 。但是结果并不是我想象的那样,实际的结果是 [2, 3, 5, 3, 7, 5, 8] ,前三个是留下的,需要被 remove 的元素是直接被覆盖掉了而不是被交换到了后面,并且此时后面的元素是未定义的。而我按照我的假设对被交换到后面的元素做了一些处理,我原意是要处理 9, 7, 7, 8 ,而此时却错误地动到了 3, 7, 5, 8 ,这导致有些工作未做,于是 Windows 报错:Access is denied 。在程序退出的时候是要处理剩下的 2, 3, 5 的,可是前面却处理过 3 和 5 了,其中有一项操作是 delete ,于是双重 delete 把堆破坏掉了,就引发大灾难了。其实在这个问题上我从一开始就犯了致命错误:不该把指针放到容器里面去!最后是在 11 月 9 日 23 时的时候才把这个问题给解决掉。

  • 其他还有一些各种各样的小问题,其实仔细回想,会发现花了很多时间在调试上,最后找到问题所在的时候其实只是一些很低级的错误,比如这次我发现我有好多地方都是忘记加一了,还有 moonykily 也跟我说起他有一个地方忘记加逗号了,结果两个相邻的字符串常量被编译器自动连接在一起,原本是两个元素的,现在少了一个,内容还不一样了。其实说起调试,本身系统地讲解调试技术的书,我好像至今就看到过一本《Why Program Fails》不错,而且只凭看书总是不够的,经验真的很重要,比如这次发现输出结果中第一列的数据被有规律地破坏掉了,我一看那些数据,立马就猜出了出问题的地方,我想这在以前肯定是要调好久的,而经验的获取又是一个相当漫长的过程,并且经验也不是万能的了,如果是遇到多线程甚至是分布式程序,调试可以说是难上加难了。这方面我觉得做好各种测试工作能够有效减轻调试负担,问题毕竟是越早发现影响也就越小了,就越方便处理了。可惜这次也是诸多原因,比如 C++ 语言本身没有方便的库和工具支持来做测试,时间又异常的紧张,最后能做完都是要谢天谢地了。真的希望像 TDD 之类的东西能够快速推广开来。不过程序员的门槛越来越低的话,我们的饭碗就危险了啊,哈哈! :P
  • 设计模式。设计模式我也看过一些,说得土一点就是在某些语言限制下,要做一些事情很别扭,然后就有人总结出了相对较好的办法来解决了。这次 KDB2 使用 C++ 来开发,方便了许多,可是方便的时候方便,复杂的时候却也异常复杂,缺少一个分号让编译器给你报 800+ 给 error 出来也是常事。再加上设计得相当凌乱(或者说根本没有做任何设计 -,-bb),导致了相当多的问题,代码耦合度太高,甚至有几次出现了头文件互相依赖造成无法编译的问题。总之也不能完全把责任推给时间问题,还是自己功力不深厚,回头要好好看看设计模式,要再弄出头文件循环依赖的问题就太丑了。

虽说时间仓促,但是这次的成果仍然是三次中最满意的一次。毕竟是要有所长进的嘛,要是写出来的东西还不如大一时候的作品,大学岂不是白上了?特别是最后通过了压力测试的时候,真是高兴啊!也是第一次在 deadline 之前把所有已知的 bug 都解决了(以前的课程 Project 多少最后都有一些已知 Bug 被偷偷隐藏起来了 -,-bb),也是特别的高兴,没有白费这几天的努力了。晚上要断电,可是我有笔记本,所以还得加班,我不是那种夜猫子类型的,这几天连续都是晚上一两点才睡觉,竟然有些受不了,有天在写程序的时候甚至感觉到好像是飘起来了——反正就是头晕目眩了。唉!不过干这行的似乎总是会需要调程序调到很晚的,但愿我不要因此折寿才好,哈哈! :D

  1. 3 Responses to “KDB2 开发小结”

  2. 嗯,深刻地体会到设计模式和单元测试的重要……
    还有,vs挺好用的……除了编辑器不爽,嗯。

    By moonykily on Nov 12, 2007

  3. 真不错,话说当时写MiniSQL真是让人无比的怀念,在翠柏1-526通宵,白天起来去上软工+睡觉,下午回来继续写,继续通宵…= =
    momo~好好加油

    By shawn on Nov 12, 2007

  4. ^_^ 谢谢鼠 mm ~

    By pluskid on Nov 13, 2007

Post a Comment