Olery成立于将近5年前。从Ruby代理开发的单一产品(OleryReputation)开始,随着时间的推移,我们开发了一系列不同的产品和应用程序。今天,我们的产品不仅包括(Olery)Reputation,还有OleryFeedback、HotelReviewDataAPI、widgets,它们可以嵌入到网站和不久的将来的更多产品/服务中。我们已经大大增加了应用程序的数量。今天,我们部署了超过25个不同的应用程序(全部使用Ruby),其中一些是Web应用程序(Rails或Sinatra),但大多数是守护进程。我们最引以为豪的是我们迄今为止所取得的成就,但在这一切背后总有一样东西闪耀:底层数据库。从Olery成立之日起,我们就安装了使用MySQL存储核心数据(用户、合同等)和MongoDB存储评论和类似数据(即在数据丢失时可以轻松恢复的数据)的数据库。一开始,这个安装效果很好,但是,随着公司的发展,我们开始遇到各种问题,尤其是MongoDB。这些问题有些是由应用程序与数据库交互的方式引起的,有些是由数据库本身引起的。例如,在某个时刻,我们需要从MongoDB中删除一百万个文档,稍后再将这些数据重新插入到MongoDB中。这样的处理方式使得整个数据库几乎被锁定几个小时,自然服务性能会降低。直到对数据库执行修复(即在MongoDB上执行repairDatabase命令)后,它才会被解锁。完成修复可能需要几个小时,具体取决于数据库的大小。在另一个实例中,我们注意到我们的应用程序性能缓慢,并设法追踪到MongoDB集群。但是,经过进一步检查,我们无法找到问题的真正原因。不管我们怎么安装,用什么工具,输入什么命令,都找不到原因。直到我们更换了集群的主节点,性能才恢复正常。这只是两个例子,我们遇到过很多这样的情况。问题的核心在于,不仅数据库在运行,而且无论何时我们查看它,都没有绝对指示导致问题的原因。无模式问题此外,我们面临的核心问题是mongoDB的关键特征之一:缺乏模式。缺少模式可能听起来很有趣,并且在某些情况下是有益的。然而,对于无模式存储引擎的许多使用,它会导致模式之间的一些内部问题。这些模式不是由您的存储引擎定义的,而是由您的应用程序的行为及其可能的需求定义的。例如:您可能有一个页面存储您的应用程序需要的字符串类型的标题字段集合。这里的模式非常符合情况,尽管它没有明确定义。但是如果这个数据随着时间的推移而改变,特别是如果原始数据没有迁移到新的数据结构,这就会成为一个问题(在一些无模式存储引擎上这是相当有问题的)。例如,您可能有这样的Ruby代码:post_slug=post.title.downcase.gsub(/\W+/,'-')这样对于每个具有“title”字段并返回字符串的文档,它将正常工作。但是,对于使用不同字段名称(例如:post_title)或根本没有标题字段的文档,它将无法正常工作。要处理这种情况,您需要将代码调整为以下内容:ifpost.titlepost_slug=post.title.downcase.gsub(/\W+/,'-')else#...end在您的模型中定义模式。例如Mongoid,一种流行的MongoDBODMforRuby,可以让你做到这一点。但是,在使用这些工具定义模式时,您可能想知道为什么它们不在数据库内部定义模式。事实上,这样做解决了另一个问题:可重用性。如果您只有一个应用程序,那么在代码中定义模式并不是什么大问题。但是,如果您有很多应用程序,这很快就会变得麻烦。无模式存储引擎希望通过消除对模式的约束来简化您的工作。但现实情况是,确保数据一致性的责任落在了用户自己身上。有时无模式引擎可以工作,但我敢打赌,它往往适得其反。#p#良好的数据库需求Olery有更多特殊需求后,迫使我寻求更好的数据库来解决问题。对于系统,尤其是数据库,我们非常重视以下方面:一致性数据和系统行为的可视化正确性和清晰性可扩展的一致性很重要,因为它有助于我们对系统设定明确的期望。如果数据总是以相同的方式存储,那么系统就可以很容易地使用这些数据。如果在数据库层面要求表的某一列必须存在,那么在应用层面就不需要检查这列数据是否存在。即使数据库处于高压状态,也必须保证每个操作的完整性。没有什么比简单地插入数据,几分钟后才找到它更令人沮丧的了。可见性包括两件事:系统本身以及从中获取数据的难易程度。如果系统出错,应该很容易调试。反过来,用户应该很容易找到他们想要查询的数据。正确性意味着系统的行为符合我们的预期。如果字段定义为数值,则任何人都不能向其中插入文本。MySQL因这个而臭名昭著,一旦你这样做,你将得到虚假的结果。可扩展性不仅与性能有关,还与财务方面以及系统响应不断变化的需求的能力有关。如果没有大量的资本成本或减慢系统所依赖的开发周期,系统很难表现得很好。考虑到上述要求不再使用MongoDB,我们开始寻找一个数据库来替代MongoDB。上面提到的特性通常是一组核心的传统RDBM特性,因此我们锁定了两个候选者:MySQL和PostgreSQL。本来MySQL是第一个候选的,因为我们的一些关键数据已经存储在里面了。然而,MySQL也存在一些问题。例如,当您将一个字段定义为int(11)时,您可以轻松地将文本数据插入该字段,因为MySQL会尝试转换它。下面是一些示例:mysql>createtableexample(`number`int(11)notnull);QueryOK,0rowsaffected(0.08sec)mysql>insertintoexample(number)values(10);QueryOK,1rowaffected(0.08sec)mysql>insertintoexample(number)值('wat');QueryOK,1rowaffected,1warning(0.10sec)mysql>insertintoexample(number)values('whatisthis10nonsense');QueryOK,1rowaffected,1warning(0.14sec)mysql>insertintoexample(number)values('10a');QueryOK,1rowaffected,1warning(0.09sec)mysql>select*fromexample;+------+|number|+------+|10||0||0||10|+------+4rowsinset(0.00sec)值得注意的是,MySQL会在这些情况下发出警告。然而,这些只是警告,它们通常(如果不是总是)被忽略。另外,MySQL还有一个问题,就是任何修改表的操作(例如:增加列)都会导致表被锁住,此时无法进行读写操作。这意味着使用此类表的任何操作都必须等待修改完成才能继续。对于包含大量数据的表,这可能需要几个小时才能完成,很可能会导致应用程序停机。这导致一些公司(如SoundCloud)不得不开发自己的工具(如lhm)来解决这个问题。知道了以上问题,我们开始研究PostgreSQL。PostgreSQL可以解决很多MySQL不能解决的问题。例如,您不能将文本数据插入PostgreSQL中的数字字段:olery_development=#createtableexample(numberintnotnull);CREATETABLEolery_development=#insertintoexample(number)values(10);INSERT01olery_development=#insertintoexample(number)values('wat');ERROR:invalidinputsyntaxforinteger:"wat"LINE1:insertintoexample(number)values('wat');^olery_development=#insertintoexample(number)values('whatisthis10nonsense');错误:invalidinputsyntaxforinteger:"whatisthis10nonsense"LINE1:insertintoexample(numberhatis10'...^olery_development=#insertintoexample(number)values('10a');ERROR:invalidinputsyntaxforinteger:"10a"LINE1:insertintoexample(number)values('10a');PostgreSQL也有很多方法不需要每一个操作重写表的能力具有所有锁。例如,添加一个没有默认值但可以设置为空的列可以在不锁定整个表的情况下快速完成。还有其他各种有趣的功能,例如在PostgreSQL中可以:trigram-基于索引检索、全文检索、支持JSON查询、支持查询/存储键值对、支持发布/订阅等。最重要的是PostgreSQL可以在性能、可靠性、正确性和一致性。#p#迁移到PostgreSQL最后,为了在各个项目之间取得平衡,我们决定使用PostgreSQL。然而,将整个平台从MongoDB迁移到一个截然不同的数据库并不是一件容易的事。为了使迁移更容易,我们将该过程分为3个步骤:设置PostgreSQL数据库并迁移一小部分数据。更新所有依赖MongoDB的应用程序以及任何需要的重构,以替换那些依赖PostgreSQL的应用程序。将生产数据迁移到新数据库,然后部署新平台。部分数据迁移在考虑将所有数据迁移到新数据库之前,我们迁移了一小部分数据用于测试目的。如果只迁移一小部分数据,会很麻烦,那么数据库迁移就没有意义了。虽然有现成的工具,但是有些数据(比如列重命名,数据类型不一致)需要转换,我们针对这些数据开发了一些工具。这些工具中的大多数都是用Ruby编写的一次性脚本,用于删除一些注释、清理数据编码、更正主键出现的顺序等等。虽然刚开始测试时出现了一些数据问题,但没有出现阻碍迁移的大问题。例如,一些用户提交的数据没有完全格式化,并且在重新编码之前无法导入到新数据库中。异常一个有趣的变化是,以前评论数据存储评论所用语言的名称(例如“荷兰语”、“英语”等),现在语言的编码发生了变化,因为我们的新语义分析系统使用的是语言代码,而不是语言名称。更新应用到目前为止,最耗时的是更新应用,尤其是那些严重依赖MongoDB聚合框架的应用。扔掉少数需要数周才能测试的遗留Rails应用程序。更新应用程序的过程大致如下:将MongoDB驱动/配置模块的代码替换为PostgreSQL相关代码运行测试修复bug反复运行测试,直到所有测试通过。对于非Rails应用程序,我们推荐Sequel。对于Rails应用程序,我们建议There'snowaytogetridofActiveRecord(至少现在还没有)。Sequel是一个非常好的数据库工具集,它支持我们想要使用的大部分(如果不是全部)PostgreSQL功能。与ActiveRecord相比,其基于DSL的查询功能要强大得多,尽管可能需要更长的时间。例如,假设您要统计有多少用户使用某种语言,并计算每种语言的比例(相对于整个集合)。纯SQL查询如下所示:SELECTlocale,count(*)ASamount,(count(*)/sum(count(*))OVER())*100.0ASpercentageFROMusersGROUPBYlocaleORDERBYpercentageDESC;在我们的示例中,将产生以下输出(使用PostgreSQL命令行界面时):locale|amount|percentage--------+--------+---------------------------en|2779|85.193133047210300429000nl|386|11.833231146535867566000it|40|1.226241569589209074000de|25|0.766400980993255671000ru|17|0.521152667075413857000|7|0.214592274678111588000fr|4|0.122624156958920907000JA|1|0.03065603923973027000AR-AE-AE|1|0.030656039239730227000ENG|1|1|1|0.0306565603923923923923973027000ZH-CN|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1。:star=Sequel.lit('*')User.select(:locale).select_append{count(star).as(:amount)}.select_append{((count(star)/sum(count(star)).over)*100.0).as(:percentage)}.group(:locale).order(Sequel.desc(:percentage))如果你不喜欢使用"Sequel.lit("*")",你可以还使用以下语法:User.select(:locale).select_append{count(users.*).as(:amount)}.select_append{((count(users.*)/sum(count(users.*)).over)*100.0).as(:percentage)}.group(:locale).order(Sequel.desc(:percentage))虽然这可能有点冗长,但上面的两个查询都使它们在没有字符串连接的情况下更容易重用。将来我们也可能将我们的Rails应用程序迁移到Sequel,但是考虑到Rails与ActiveRecord的耦合如此紧密,所以我们不完全确定是否值得花费时间和精力。迁移生产数据最后我们来到迁移生产数据的过程。通常有两种方法可以做到这一点:关闭整个平台,直到所有数据都迁移完毕。在迁移数据的同时保持系统运行第一个选项有一个明显的缺点:停机时间。第二种选择不需要停机但难以处理。比如这个场景,当你迁移数据的时候,你要考虑所有将要添加的数据,否则你会丢失数据。幸运的是,Olery有一个独特的解决方案,我们数据库的绝大多数写操作都是相当有规律的,频繁变化的数据(比如用户通讯录信息)只占总数据量的一小部分,相比我们查数据以及迁移它们所需的时间相当少。这部分的基本工作流程是:迁移用户、联系人等关键数据,基本上是我们无论如何都无法弥补损失的数据迁移不太重要的数据(比如我们可以抓取并重新计算的数据等)测试在一组独立的服务器中一切正常运行将产品环境切换到新的服务器上,然后迁移步骤1中的数据,以确保在平均故障间隔时间内创建的数据不会丢失。第2步是迄今为止最耗时的,大约需要24小时。相比之下,步骤1和5中提到的数据迁移只用了45分钟。#p#结论我们迁移了大约一个月,直到我们对它感到非常满意。除了迄今为止的这些积极影响外,它还导致应用程序在各种情况下的性能显着提高。例如,我们的酒店评论数据API(在Sinatra上运行)的交互延迟比迁移前低得多:迁移从1月21日开始,峰值表明应用程序性能硬重启(导致处理过程中交互时间略有放缓)).21日之后的平均互动时间大约是这个时间的一半。在另一个我们称之为“评论持久化”(译者注:即存储评论)的过程中,我们发现了巨大的性能提升。守护进程的目标很简单:保存评论数据(评论内容、评论分数等)。当我们最终对迁移工作进行了很多大的更改时,结果令人鼓舞:fetcher也更快了:fetcher的性能提升不如注释存储过程那么大,因为fetcher只使用数据库来检查是否存在注释(比较快的操作),所以这个结果不是很意外。最后,我们来到程序中用来调度爬虫进程的进程(简称“调度器”):因为调度器只是以固定的频率运行,所以这张图可能有点难以理解,但不管怎么说,迁移之后,平均处理时间有明显下降。最后,我们对现在的结果非常满意,我们一定不会错过MongoDB。它的性能非常好,它的处理方案相较于其他数据库黯然失色,查询数据的过程与MongoDB相比太令人满意了(尤其是对于非开发人员)。尽管我们还有一项服务(OleryFeedback)仍在使用MongoDB(尽管运行在一个单独的、相对较小的集群上),但我们打算在未来将其移植到PostgreSQL。原文链接:http://www.oschina.net/translate/goodbye-mongodb-hello-postgresql
