作为一名开发者,我最有成就感的一项工作就是优化:使我的产品 Kiln 中的某些部分运行得更快。因为即便你的产品有着最好的特性,或者最吸引人的界面,如果它的速度慢到令人无法忍受,那它依然是毫无价值的。去年,我的团队得到一个机会,去优化 Kiln 中运行最慢的一部分,并且最终提高了它的速度,远比之前快得多。
本文描述的就是一个名为 Elasticsearch 的优秀工具如何帮助我们将 Kiln 的速度提高 1000 倍的过程。
Kiln 是一个源代码管理工具,它提供 Mercurial 及 Git 存储库的寄宿服务,并且包括代码审查及一些其它的实用特性。我们在 2010 年早期发布了 Kiln,不可否认的是,v1 版本的功能非常基础,仅包括存储库管理、代码审查和包含权限管理的代码 push 和 pull。不过作为一个核心产品来说,我们知道它能为用户带来实际作用。但当我们自己使用Kiln 来开发Kiln 的时候,就开始注意到了某些不足。
我们所期望的功能中最重要的一项是搜索。在堆积如山的源代码中进行人工查找是近乎不可能的任务,而像grep 这样的工具又需要你在自己电脑中保留一份源代码的拷贝。因此当我们在首个版本发布后的第二年夏天进行2.0 版本的开发时,我们决定对提交(Commit)信息、文件名及文件内容的搜索功能将作为这一次更新的标志性特性之一。
SQL Server
在那段时间内,我们对一系列不同的搜索引擎进行了评估。在搜索提交信息这一点上,我们最终选择了使用我们已熟知的工具:Microsoft SQL Server。提交以一种易于查询的格式存储在数据库表中,因此我们只需打开 MSSQL 的全文搜索特性就能轻松实现查询。我们对文件名搜索也使用了类似的处理方式:将它们保存一张数据库表中,让 SQL Server 进行接下来的工作。
OpenGrok
查找代码本身就是一项巨大的挑战,它需要使用各种不同的工具。我们对各种代码搜索引擎进行了比较之后,将目光放在了 OpenGrok 上,这是一个优秀的工具,而且看起来符合我们的需求。OpenGrok 从某个文件夹下的代码开始查找,并使用 ctag 以解析代码(本身支持多种语言),并使用 Apache Lucene 建立索引。OpenGrok 不仅仅能为代码中的每个类、方法及变量建立索性,它还能够区分代码的定义与引用,因此你不仅能够搜索方法的定义,还能搜索到所有被引用到的地方。
我们发布 Kiln 2 时加入了搜索及其它许多特性,我们对所完成的功能十分满意,它允许你深入地探索你的代码,包括了代码历史,提交信息以及你的团队所提交的每一行代码。
但随着 Kiln 的发展,我们渐渐意识到它的搜索功能并没有和我们希望的那么好。说实话,即使在最好的运行环境下,搜索速度也低于我们的期望。而在峰值的时候,它基本上就完全不能用了。OpenGrok 虽然是令人印象深刻的工具,但它在应对几万个存储库和几个 TB 的代码时的负载能力就不太好了。索引更新失败的情况时有发生,并且实时索引代码需要将每个存储库的代码都做一个签出的备份,而不仅仅是历史记录,这就使存储的需求成倍地增长。SQL Server 全文搜索在我们的规模中也开始显得缓慢,并且对数据库服务器造成了极大的负担。另外,它还有着许多不足之处,限制了我们想加入Kiln 中的新功能。
在2012 年初期,我们发现是时候重新思考我们的搜索架构了。Kiln 的最初设计为每一个Kiln 帐户创建一个新的数据库。SQL Server 在一台服务器上维护数千个数据库的时候伸缩性表现不佳,因此我们决定以一种多租户(multi-tenant)的数据库应用方式重新设计Kiln 的架构,在这种方式下所有帐户的数据都保存在同一个数据库中。由于我们需要将每个帐户的独立数据库迁移至新的数据库中,我们正好有机会为我们的数据存储做出一些根本性的改变。这代表我们也可以开始改造我们的搜索引擎了。放弃已有的OpenGrok 和全文搜索固然是一个很大的遗憾,但随之而来是更大的优势。
Solr
再次回到画板前,我们重新思考在 2012 年内如何做出一个优秀的搜索功能来。我们希望把搜索做成 Kiln 最好的特性,打造成人们会热烈讨论的金字招牌。经过研究,我们瞄准了两个不同但看起来颇为相似的两个搜索引擎:Elasticsearch 和 Apache Solr,它们在底层都使用了 Lucene,这是目前最强大的开源搜索引擎了,在它的基础上各自提供了一个友好的用户界面,并隐藏了一些 Lucene 的复杂性。它们都提供了基于 JSON 的 API,各自具有不同的查询功能,并且发挥了 Lucene 的全部能量。那么哪一个更适合于 Kiln 呢?
通过对每一个工具大量的相关材料阅读及试用之后,Elastcicsearch 看起来占了上风:它易用、强大、伸缩性良好且速度飞快。运行 Elasticsearch 非常简单:如果你已经安装了 Java,只需要去下载它的最新版本就可以运行了。只需几分钟时间,我就能够开发出一个结构并存储测试数据了。虽然Elasticsearch 的文档中已经描述了一些可用的查询,但能够运行我自己的示例查询对于学习使用Elasticsearch 的最佳方式还是大有帮助的。最后一项测试是确保Elasticsearch 能够承受Kiln On Demand 的整个数据集,我们不打算为了这项测试采购新的服务器,因此用上了一点黑客的手段。Elasticsearch 在商业硬件上运行良好,它能够利用你的所有机器上所能提供的所有资源,然后建立一个独立的集群。于是我们发动了公司里几乎每一个开发者,让他们下载Elasticsearch 并加入某个办公室网络的集群。在一个下午的测试中,我们为Elasticsearch 加载并输出了几百个G 的数据。而它不仅没被压跨,甚至在大量写操作的压力下也能够在几毫秒之内返回结果。Solr 则没能达到我们的期望,它在大量写操作的情况下读取性能会严重降低,而ES 依然保持飞快。很显然Elasticsearch 正是我们的解决方案。
Elasticsearch
Elasticsearch 所保存的是“文档”,这是一种类似 JSON 对象的结构化数据类型。每个文档都是一系列的键与值,键是字符串类型,而值则可以支持多种数据类型,比如字符串、数字、日期及列表。我们首先把提交注释迁移至 Elasticsearch 中,以取代 SQL Server 的全文搜索。每个提交的文档保存三个键值对:提交日期、作者以及提交信息。查询被表达为多种不同的操作,这些操作会关注这些值中的一个或多个,并根据匹配程度为搜索结果评分。最基本的查询操作会查找字符串中的文本,不过其它的查询操作,例如内置的布尔值查询或日期范围查询则允许你编写复杂的查询,经过优化后能够在几毫秒内运行完毕。
当你需要在Elasticsearch 中加入一个文档时,你可以通过HTTP POST 的方式,将一个或多个文档作为JSON 对象提交。你在查询时同样使用JSON 格式,用简单的HTTP GET 方法就可以获得JSON 格式的结果。这种RESTful 的架构使得从命令行中直接对数据进行测试及验证变得更方便了。实际上,在调试及开发Elasticsearch 时,我最常用的工具就是cURL 了!
基于 Python 和.NET 的客户端类库可以很方便地将Elasticsearch 整合至我们现有的代码中。这些类库可以使用Python 及C#对象对JSON 进行包装,而不需要手动创建这些对象,以实现对新文档建卡索引或者获取查询结果之类的任务了。比方如,查询结果可以表示为强类型的C#对象,和其它部分的数据访问对象保持一致。
我们起初在确定提交文档的正确结构时绕了些弯路。很重要的一点是,Elasticsearch 不是一个关系型数据库,因为你从MSSQL 或其它关系型数据库所学到的知识未必适用。其中最重要的一个概念是反范式:Elasticsearch 不允许关联查询或子查询,因为你必须将你的数据反范式化。
举例来说:我们为某个提交的文档在它每次出现在某个存储库时都保存一条记录。如果你按照传统的关系型数据库来思考,这种做法无疑是大错特错的,但用在Elasticsearch 中却是最佳的做法。由于提交信息是紧邻着构成索引的元数据存放的,减少了所必须的数据读取次数,因此在某个存储库中搜索提交的信息非常之快。由于Lucene 在内部对索引进行了压缩,多个相似的文档不会增加额外的存储空间,因此索引的大小也不会如你所想的一般过快的增长。
此外,Elasticsearch 通常不允许你修改某个现有文档的域,你必须重建索引,包括那些你不打算修改的域。这一点需要我们改变一下固有思维,因为它和数据库是完全不同的。在SQL Server 中,在一个几百万行的表中修改某一行的某个字段值是非常快速的,因此我们已经习惯于这种保存数据的方式了。
当我们确立了数据格式后,就可以进行开发了。我们很乐于看到Elasticsearch 的创始人与首席开发者Shay Banon 与整个社区在Elasticsearch 的邮件列表中一直非常活跃。我们的疑问和顾虑会很快得到解答,在我们停滞不前时给予了我们巨大的帮助。我们也乐于贡献各种补丁包,这仅仅是我们能为整个社区和这个项目所能做的最低程度的回馈罢了。
关于提交信息的处理,Elasticsearch 的分析引擎所生成的结果非常好。人类语言所编写的文本在进行索引前,会先历经一个名为“词干提取”的过程,这表示“running”这个单词会以其最基本形式“run”进行索引。其它各种变形,如“runs”和“runner”也以同样方式进行索引。之后,对“run”、“runner”、“running”或其它各种变形的搜索都会经历相同的过程,它们都会查找“run”的索引入口。这样一来,你就可以使用某个搜索关键字各种变形来得到相同的结果,而不需要对每一种变形都进行索引了。这种方式不仅使搜索具有良好的表现力,并且减少了索引的大小,同时意味着你能够快速地获得结果,甚至在你不清楚你所查找的短语或关键字的准确名称时也能工作。
Elasticsearch 在数据中心方面同时表现出众。我们为服务器安装了 SSD 和大容量的 RAM,因为 Elasticsearch 读取索引的速度越快,它的整体表现就越好。由于运行的唯一先决条件就是 Java,因此我们很快就能上手并快速运行。如果你对 Elasticsearch 的可靠性还存有疑虑,那它绝对可以让你睡个安稳觉,因为它内置了分发功能。在一个运行 Elasticsearch 的服务器群集中,你可以指定让每个文档都分发到多台机器中,使某个结点的宕机不会造成任何停机时间。在集群中传输的数据会自动在各个结点间切换,因此扩展系统的可用性也非常简单。按我们的经验,在数据量增长 10 倍后搜索速度依然保持恒定。
我们现在运行在生产服务器集群中的结点只有两台,目前还没有横向扩展的必要,但我们很清楚:一旦需要进行扩展,我们能轻松地实现。实际上,这两台机器也不是我们最初使用的生产结点。当我们发现最初的两个结点性能不够强劲时,我们就安装了两台新服务器,并加入到集群中。接下来我们增加了分发结点的数量,于是 ES 自动将所有文档复制到现有的全部四台服务器上。之后我们将两台旧结点关闭,并将分发结点数量调回 2,搞掂!整个基础架构的升级没有造成任何停机时间。
我们在 2012 年中期发布了新的多租户架构,同时发布了基于 Elasticsearch 的提交搜索功能。和其它任何成功的基础架构变动一样,我们的目标之一是客户应该感觉不到发生了某些变化!但每个 Kiln 的用户都感觉到搜索变得像光速一样快。这种速度到底是什么概念呢?在旧的架构中,搜索的过期时间被严格限定为 3 秒,而最终超过这一时间而导致过期的搜索数量之多令人失望。即使是那些完成的查询,也往往要花费一两秒的时间。而同样的查询在 Elasticsearch 中不仅可以得到更好的结果,而且只需花费 5 毫秒。没错,Elasticsearch 的搜索足足快上了 1000 倍。由于它的速度之快,使我们决定加入了键入时即时搜索的功能,所以基本上返回结果的速度和你的思考一样快。
从 OpenGrok 迁移至 Elasticsearch
在看到了结果报告之后,我们立即着手以 Elasticsearch 代替 OpenGrok 实现代码搜索,因为我们哪怕做出一些牺牲也打算获得那种速度。过去几个月时间里,我们已经将几个 TB 的代码索引从 OpenGrok 中迁移至 Elasticsearch 了。使用 Elasticsearch 进行代码索引需要一些详细的计划。由于 OpenGrok 是一项专门为代码搜索所设计的工具,因此在我们转向一个更加通用的解决方案时,会不可避免地丧失某些特性所带来的好处,例如区分代码的定义和引用,或者基于某个文件的编程语言忽略某些关键字。但是如果搜索的速度慢到没人能用,那这些特性也失去意义了。
经过多次尝试之后,我们确定了某种数据结构,每个文件会根据它进行处理,以抽取出某些唯一的 token:包括每个关键字、类、方法以及注释等文件中使用到的东西。这个文件所对应的 ES 文档会包括其文件名以及这些 token。由于只对这些唯一的 token 进行索引,使得索引大小远远小于对整个未处理内容进行索引的尺寸(想象一下在一个平均大小的源代码文件中,“new”、“public”或“return”等关键字重复的次数)。这个索引还有另一项职责:当搜索文件名称时,我们只需搜索每个代码文档所保存的文件名,而忽略这些代码 token。
更小的索引意味着更快的搜索,不过在结果的显示上需要一些技巧。一旦 Elasticsearch 告诉我们某个文件符合这次查询,我们就会将结果传递至存储服务器,在这里读取文件的完整内容,并高亮显示所匹配的行。由于这些额外的复杂性,使我们在几个 TB 的数据中的平均搜索时间略小于 50 毫秒。但无论如何,这种速度才是我们想要的。
这段时间,我们的 Elasticserach 集群每天都有几百万新文档加入,并且承担了多种搜索的职责,包括提交、文件和代码,但其速度依然超过我们的想象。我们对我们的搜索引擎深感自豪,自豪到愿意在 Kiln 的每个页面的正中央放置一个搜索框。由于加入了这个快速的新搜索引擎,我们还在某些没有想到过的地方找到了它的用武之地。比方说,Kiln 能够显示某个存储库中两个提交的区别。在过去,我们需要你手动地输入每个提交的 ID,而现在我们只需显示一个搜索框,而你只需敲几下键盘就可以在存储库中的几万个提交中找到正确的内容了。Elasticsearch 为我们提供了如同 Google 一样快速的搜索能力,并包含了一个强大的查询语言,以及开源项目所具有的全部优势。
关于作者
Kevin Gessner是位于纽约的 Fog Creek Software 的一位开发者,他目前从事 Kiln 的研发工作,这是一个团队版本控制系统。在闲暇时,他喜欢动手烧烤,或者骑单车环绕布鲁克林区。
查看英文原文: How Fog Creek Software Made Kiln’s Search 1000x Faster with Elasticsearch
评论