搭建网页滤重系统的工作总结
趁最近工作业务不多,写篇博客关于 【搭建一个基于 网页相似度来 滤重的系统服务】 的相关工作。当作整理学习,温故知新吧。
项目旧况
我接手时的线上代码主要有如下特点:
-
旧代码的滤重效果和预期的不一致,而旧代码的作者已经离职,所以旧代码等于一个无人可以解释的黑盒。 只知道有问题,但是出了问题没人知道如何解决。
-
性能低下得无法容忍,由于线上出现问题没人知道如何修复,解决的办法只有重启。重启有事需要对旧数据进行重跑,因为程序运行效率低下,重跑耗时巨大,但是又无可奈何。
-
严重依赖数据库,几乎到处是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.
在云计算时代,软件基本上都转服务化了,也就是功能以服务接口的形式提供。我个人非常喜欢服务化,服务化可以让功能和模块变得更清晰,而且可以跨语言的调用, protobuf
和 thrift
都是非常优秀的 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。。