搭建网页滤重系统的工作总结

趁最近工作业务不多,写篇博客关于 【搭建一个基于 网页相似度来 滤重的系统服务】 的相关工作。当作整理学习,温故知新吧。

项目旧况

我接手时的线上代码主要有如下特点:

  1. 旧代码的滤重效果和预期的不一致,而旧代码的作者已经离职,所以旧代码等于一个无人可以解释的黑盒。 只知道有问题,但是出了问题没人知道如何解决。
  2. 性能低下得无法容忍,由于线上出现问题没人知道如何修复,解决的办法只有重启。重启有事需要对旧数据进行重跑,因为程序运行效率低下,重跑耗时巨大,但是又无可奈何。
  3. 严重依赖数据库,几乎到处是select和insert,搞得整份代码支离破碎。甚至连数据库的表都没有好好设计,该建索引的地方没有建索引,不该建索引的地方滥建索引,等等十分拙劣的工程设计。

按我的经验判断这样处处是坑的代码,与其重构,不如我直接重写,然后我就哐当哐当开始重写了。

系统框架

项目开发语言选用 C++,功能主要分为五个模块:

  • 算法
  • 存储
  • 索引
  • 服务
  • 测试

1. 去重的算法有不少,在本系统中同时支持两种算法, shingle算法和 simhash算法。前者是旧代码中本来就在使用的。 后者是本人调研出来的,google出品的,资料丰富,好评如潮。

对于 simhash和[shingle]算法的评测效果可以发现,总体效果差不多,不过对于内容篇幅较小的文章, simhash的算法效果明显优于 shingle算法。 而且 simhash算法速度远快于 shingle算法(至少3倍以上吧)。在此值得一提的是, simhash是针对中文处理的的 simhash,具体原理请见 SimhashBlog

2. 对于滤重系统来说,数据的存储和索引和cache系统非常类似,不停的有新数据进来,所以也要不停的删除过期的数据,才能保证内存使用量稳定在一个可控的量级上面。

所以数据的存储采用 vector包装而成的 容量固定的 循环队列作为核心数据结构,在此称为 BoundedQueue。 容量固定是因为滤重系统也是个有时效性的系统,我们需要将过时的信息删除掉,所以使用 queue是天经地义的事情。

不过在此需要注意的是,在 c++的 stl里面的 queue的底层实现是用 deque这种双向数组,已经很大程度上提高了 push和 pop的效率。 但是毕竟在 queue里的 push和 pop都会直接或者间接的导致内存的申请和释放。

而用 BoundedQueue底层就是一个固定大小的vector,每次push或者pop只是循环的移动head和tail指针,无需内存分配。所以在此,使用 BoundedQueue的好处就显而易见了。

后来得知其实在boost里面有这种循环队列的结构,在 circular_buffer.hpp里面,不过其实这玩意也就那么回事,很简单,自己写也不难。

3. 有比较就会有查找,有查找就要有索引。

当我们用算法把一大段一大段的文档计算成一个特征值时,这个特征值在内存中就代表了该文档。而滤重就需要对特征值进行对比,去掉那些 特征值相似的旧数据。所以我们需要对当前 BoundedQueue队列中的所有数据在 push进来的时候,进行索引的建立,然后当数据 pop出去的时候,进行索引的删除。

在本项目中,数据只需要约保留最新的100W条数据。而且特征值占用空间很小,所以直接使用c++里的 unordered_map作为索引的数据结构(不选用 map因为 unoredered_map的查找和插入效率远快于红黑树实现的 map)。

本项目索引的设计是参考了 Mysql数据库里 InnoDB索引的设计,在 InnoDB中,数据必须有个主索引 primary_key,其他普通索引都是指向这个主索引,所以数据位置统一在主索引里面存储即可。

4. 在云计算时代,软件基本上都转服务化了,也就是功能以服务接口的形式提供。我个人非常喜欢服务化,服务化可以让功能和模块变得更清晰,而且可以跨语言的调用, protobufthrift都是非常优秀的 rpc解决方案。

在本项目中,使用的是 thrift来搭建服务。但是也因此发现了 thrift的一些恨铁不成钢的地方,详情见博文 ThriftBlog

5. 测试主要是 单元测试和 性能测试。单元测试使用 google的单元测试框架 gtest。 不得不说我非常喜欢gtest来写单元测试。

稍微有点工程经验的人都明白单元测试的重要性。在工作的时候,当业务需求比较紧急的时候,我们开发也会尽量用最快的方法去开发,从而能迅速能上线。 但是上线不是一个项目完成的标志。正如有人说的, 如果一个项目上线的时候是完美的,那说明这个项目上线上得太晚了。 互联网项目讲究 持续集成和 快速迭代,而这一切的坚实基础就是较为完备的单元测试。

如果没有单元测试,重构时会发生的事情就是,你重构的时间用了一个月,但是重构产生的新bug需要你用半年时间来修复。

性能测试就更是必需品了,值得一提的是,重写之后性能提高至少 十倍

总结一下重写之后主要改进的几个点:

  • 模块职责清晰,代码规范,性能提升至少 10倍
  • 无需依赖数据库。还是那句话。 没有依赖,就没有伤害。
  • 丰富的单元测试才能保证项目可以持续集成,快速迭代。
  • 运行稳定,再也没有那些乱七八糟的未知bug了。

感想

1. 有人说 写代码write less不是重点,关键是read easy。 。 读书的时候我不能很好的理解这句话,觉得代码优化就是尽提高代码的复用,从来达到 write less 的目的。 在公司干活了才能深刻体会到,把一个系统服务写得 read easy 是一件更需要功底和经验的事。

记得当初给同事讲解整个项目架构的时候,同事听完说表扬说代码逻辑很清晰。 为了让代码read easy,确实多花了我不少工作时间,但是看来是值得的。

2. 写程序时的 开源心态:我一直尽可能的把每个项目都当成开源项目来写(哪怕由于公司的原因无法开源)。

它的好处就是:当有些功能模块【既可以写的很丑陋难懂但是很快就能写完,又可以写的清晰易懂但是需要废点脑筋】的时候,你会变得尽可能选择后者。 因为开源的最大好处是会让作者对脏乱臭的代码有 羞耻感,比 codereview的效果甚至都好。

3. 幸亏当时果断重写旧项目,否则我到现在估计还在修旧代码的bug。。

Tagged: , ,

Comments are closed.