July 2009


beta技术沙龙越办越有意思了,上次错过了阙宏宇的mod_cache(还有关于线程进程的讨论)就很可惜,这次关于Lucene的演讲,是无论如何不应该错过了。

到目前为止,全文检索已经完全不算高技术门槛了,记得以前看过一本书里面写:“今天,任何程序员,都可以很容易地构造一个全文检索应用”。是的,全文检索的基本原理大家都知道差不多了,剩下的只是实践。我见过纯粹自己开发的,具有AS(Advanced Search)、BS(Basic Search)、DI(Digest)等结构,“像模像样”的全文检索架构,不过应用更多的,却是在开源项目上完善、定制而来的,Apache的Lucene就是众多开源全文检索项目中,名气最大、资格最老、应用也最广泛的一个。本期beta技术沙龙,讲的就是大型网站中lucene的应用,主讲人是手机之家团队的唐福林(“手机之家”总是有些东东来共享,比如上次的DAL,这真是不错)。

众所周知,用Lucene构造一个“索引-查询”的应用是非常简单的,搭好环境,参照(修改)示范代码,很容易就可以成功。但是,要构造一个真正大规模、稳定、可靠的应用,就不说这么简单。程序的编写、模块的分布、架构的设计,都有许多费心思的讲究。按照PPT提供的数据,手机之家目前的Lucene应用,采用的是Lucene 2.4.1 + JDK 1.6(64 bit)的组合,运行在8 CPU, 32G内存的机器上,数据量超过3300万条,原始数据文件超过14G,每天需要支持超过35万次的查询,高峰时期QPS超过20。单看这些数据可能并没有大的亮点,但它的重建和更新都是自动化完成,而且两项任务可以同时运行,另一方面,在不影响服务可靠性的前提下,尽可能快地更新数据(如果两者发生冲突,则优先保证可用性,延迟更新),其中的工作量还是非常大的。

演讲的主要内容都PPT里,非常丰富,我就不再赘述了。要补充的是,这份PPT做得非常清楚,需求-目标-进度-设计-上线-测试-上线,整个流程非常清楚,给出的数据同样非常精当,我想,这也反映了手机之家团队的开发规范。

因为对Lucene的使用稍微有些经验,我在这里补充几句,权当狗尾续貂:

  1. 在大规模的应用中,Lucene更适合用于狭义的“搜索”,而不应当负责数据的存储。我们看看Lucene的源代码也可以知道,Document和Field的存储效率是不够好看的。手机之家的团队也发现了这一点,他们的办法是,用Lucene存放索引,用Memcache + Berkeley DB(Java Edition)负责存储。这样有两个好处,一是减小了Lucene的数据规模,提高了程序的效率;另一方面,这套系统也可以提供某些类似SQL的查询功能。实际上,Lucene Project自己似乎也注意到了这个问题,在Store中新增了一个db选项,其实也是利用的Berkeley DB。如果仅仅用Lucene存放索引,而不存放Document,并且合理配置,一台机器可以支持几十G甚至上百G的索引;如果需要用Lucene存放索引,最好在读取时使用FieldSelector,只读取需要的Field,如果使用恰当,性能会有10%左右的提升。
  2. 在大规模应用中,Cache是非常重要的。PPT中也提到,可以在程序提供服务之前,进行几次”预热“搜索,填充Searcher的Cache。据我们(银杏搜索)的经验,也可以在应用程序中,再提供针对Document的Cache,这样对性能有较大的改善(同一个JVM内部的Cache,速度更快一些)。Lucene自己似乎也注意到了这个问题,在2.4版本中提供了Cache,并提供了一个LRU Cache实现。不过据我们测试,在极端情况下,这个Cache可能会突破大小限制,一路膨胀最后吃光内存,甚至从网络上找的许多LRU Cache实现在极端条件下都有可能出现这样的问题(这也是我们百思不得其解的地方:反复检查程序的逻辑都没有问题),最终自己写了一个LRU Cache,并修改多次,目前来看是稳定的。
  3. 在编写Java服务程序的时候,记得设置退出的钩子函数(RunTime.getRunTime.addShutdownHook)是一个非常好的习惯。许多Java程序员都没有这种意识,或者有,也只是写一个finalize函数,结果程序非正常退出时,可能造成某些外部资源的状态不稳定。拿Lucene来说,之前的IndexWriter是默认autoCommit的,这样每添加一条记录,就提交一次,好处是如果中断,则之前添加的记录都是可用的,坏处则是,索引的速度非常低。在新版本中autoCommit默认为False,速度提升明显(我们测试的结果是,提高了大约8倍),但如果中途异常退出,则前功尽弃。如果我们添加了退出的钩子函数,捕获到退出信号则自动调用writer.close()方法,就可以避免这个问题。
  4. 目前的Lucene是兼容JDK 1.4的,它的binary版本也是JDK1.4编译的,如果对性能要求比较高,可以自行下载Lucene Source Code,用更新版本的JDK编译出.jar文件,据我测试,速度大约有30%的提升。
  5. 如果对并发的要求较高,可以考虑采用多IndexSearcher的技术,也就是在一个应用服务中,开启多个IndexReader(可以对同样的索引开启多个),每个IndexReader再生成一个IndexSearcher,将这些Searcher放在一个“池”里头,给搜索请求调用。这样可以大幅度提高并发的性能,代价是在写程序的时候就要考虑到这一点,进行相应的调整。

P.S. 据我观察,国内公司内部的项目,一般取的名字都中规中矩,以’er’结尾的比较多,多是Indexer, Crawler, Layer之类。好像很少有外国那种“天马行空”的奇特名字,譬如Hadoop(这是一个“没来由”的名字)、Lucene(这是个少见的姓)。国内我接触过不多,以前抓虾有个重要的DB叫tudui(“土堆”),目前银杏有个项目叫LaserTank,都是跟实际用途毫不相关的,印象反而深刻。

我第一次吃香蕉的时候,顺手把蕉把(也就是互相连结的那一头)一揪,就开始剥皮。父亲看了说:怎么能这样剥香蕉呢,一定要从另外一头开始剥的。在父母面前,小孩子当然不敢当面顶撞,而且,我并没有尝试过从另一头开始剥。可是比较两种方法之后,我觉得还是自己的方法更好,于是去找父亲理论。父亲又试了试,说:“真怪,从这一头来剥,果然是要省事一些,可是我们从小到大,都是从那头开始剥皮的呀。”

这件事过去已经有二十多年了,我却时常想起它,开始只是告诉自己“不要迷信权威”,到后来,我逐渐发现,在生活中,一些看似简单的事情,似乎有“想当然(甚至都不用‘想’)”的做法,可是这做法未必好,许多时候,我们甚至需要意识到“我这是在想当然”、“想当然其实并不是好的办法”,于是或者另辟蹊径别出心裁,或者放下身段重新学习,结果要么遇见别样洞天,要么由此再上层楼;这样的事情,在我的生活中,一次又一次地被验证。

最近一次的经验,来自读书。
我最开始读书,主要看重的就是故事情节,哪几个人,发生了一回怎样的事情,了解了这些,就差不多够了。那时候,除了情节,学校语文课顶多教教“拿个小本子,把优美词句、名人名言记录下来”。这种做法,我从来没有尝试过,所以它有什么效果,我也不得而知。“读书这样简单的事情,还有什么道理可言吗?”许多年里,这就是我“想当然”的观点。
印象里,第一次启蒙来自在大学学到姚斯的“接受美学理论”:审美经验原来分为三个层次!原来普通的“愉悦”只停留在最表面的层次!这可以算“歪打正着”吧,姚斯说的本是对于文艺作品的接受,可是我却想到了自己读书的方法原来那么“原始”,并一下子明白了,为什么同样读一本书,不同的人能谈出来的内容完全不一样。
原来,这道理就和剥香蕉一样,即便是“读书”这样“再简单不过”、“人人都会”的事情,居然也可以有门道可言,居然也有规矩可讲。在这些门道和规矩面前,我这个“读”了许多年书的人,似乎根本“没有上道”!于是我开始有意识地学习和锻炼读书的方法:记住作者的名字,了解作品的背景,寻找文字的特点,并且,要针对不同的文本,选择不同的方法和侧重点……这样下来,成效大增(去年有位朋友与我谈及读书,最后问我:你这样的阅读境界,是怎样来的呢?)
读书这样简单的事情,自己探索和反思都可以收到这样的成果,那么已经成型的“门道”和“规矩”,岂不是帮助更大?可是,这些“门道”和“规矩”,究竟在哪里呢,是不是只能自己反省、参悟?这个问题我一直不很清楚,直到最近接触了《如何阅读一本书》才想到,所谓“门道”和“规矩”,大概就存在于这样的书里头罢。

《如何阅读一本书》
,就是一本关于“读书”的书(是的,它就好像一口咬住自己尾巴的蛇)。这本初版六十多年来不断重印、再版的书,就是要教会读者读书的方法。大致来说,整本书分为三个部分:第一部分可以视作纵向探究,主要向读者介绍阅读层次的概念,由下而上依次介绍了基础阅读(也就是初步的,毫无结构的阅读)和检视阅读(带有结构意识,能够抓住主题的阅读)。第二部分则着重讲解阅读的第三层次,也就是分析阅读,即如何“通透”——紧紧抓住这本书,做到“我注六经”,读到这本书“真正属于自己”为止。有过亲身经历的人都知道,这并非易事,大学里的一些课程,老师会带领学生,把大量的精力倾注到少数的经典上——可惜,许多人即便花了大量的时间精力,因为没有掌握合理的阅读方法和技巧,也无法达到“通透”的境界;我想,作者之所以愿意花三分之一的篇幅来讲分析阅读,原因也部分在于分析阅读是如此重要,而它本身又需要大量的技巧和锻炼。全书的第三部分则可以视为横向解剖,在这一部分,作者分门别类地讲解了各类书籍的阅读方法:实用型的书籍要如何阅读,想象文学要如何阅读,历史、科学、数学、哲学类书籍,又应当怎样阅读……虽然我们未必要完全遵照其中的方法去阅读各类文本,但是相比自己从头开始积累,书中提到的各种办法,无疑是具有相当价值的,不容错过。
另外,这本书也有一些总览性建议,我觉得很受用,譬如阅读书籍时,要仔细观察目录,要读一读作者的前言,要仔细想想每一部分的标题……这些都是我之前读书不太重视,其实又非常重要的技巧(当然,这也可能与我国特色的前言、序言有关,不信,看看《中华人民共和国宪法》),以后应当努力改进。

也许有人会说:读书就是读书,要那么多条条框框干什么,太累了。
没错,一千个读者就有一千个汉姆雷特,极端地说,作品的意义完全存在于“接受”的过程之中。可是,我又分明看到,同样的一本书,同样的时间,有人还在刀耕火种,也有人读书却已经开上了联合收割机——虽说都是“接受”,可是接受的深度和广度,却有如此的差别。造成这种差别的,恐怕并非悟性,而是清晰的思维,和长期有意识的锻炼。

朋友,你的香蕉是怎样剥的?你的书,又是怎样读的呢?

出版两年之后,《精通正则表达式》马上要第四次重印了,这个消息很是让我兴奋。

我读大学的时候,有幸接触到侯捷老师的许多文章,尤其是他谈关于选择技术书籍的言论,感觉受益匪浅——正是从此,我深刻认识到,“学习”的宾语不应该是“教材”,而是“知识”。认识到这一点,就豁然开朗了;当然,也无比真切地知道了好的书籍是多么重要。

另一方面,我也深信,总的来说,知识的价值是在传播中实现的。我经历过“有了新的收获自己保密,一人独享”,也经历过众人把自己的心得拿出来分享、彼此协作的环境,两厢对比,后者提供的满足感远远超越前者。因此,有更多的人迅速学会“卑之无甚高论”的正则表达式,不需要重走我自己当初学习的弯路,对我来说,也是一种不小的满足(相比之下,帮人写各种表达式所得到的满足,实在是“很小很小”)。

在这里还要感谢博文视点的编辑许莹,她细心地把目前勘误列表列出的所有错误都做了订正。因为《精通》一书中存在的错误,始终是我的一块心病。

另外,要兑现我年初的计划,今年要写一本关于正则表达式的书,正好在这里征集大家的意见:你们是期望它更加“下里巴人”,包括Word, EditPlus等等常用软件的应用例子,以应付更广泛的工作呢;还是希望更加“阳春白雪”,与狭义的“IT行业”(也就是开发)靠的更近呢? 或者有什么别的想法,还请不吝赐教。