当前位置: 首页 > 科技观察

为什么软件公司放弃MongoDB?

时间:2023-03-13 13:27:46 科技观察

Olery成立于2010年,总部位于阿姆斯特丹。这家初创公司为酒店业提供声誉管理和媒体监控工具,帮助酒店将在线评论和社交媒体反馈转化为可操作的商业情报分析。Olery开始使用MySQL来存储核心数据(用户、合同等),使用MongoDB来存储评论等(即在数据丢失时可以轻松恢复的数据)。一开始这个安装效果很好,但是随着公司的发展,开始遇到各种问题,尤其是MongoDB的问题。这些问题有些是由应用程序与数据库交互的方式引起的,有些是由数据库本身引起的。例如,在某个时刻,Olery需要从MongoDB中删除一百万个文档,稍后再将数据重新插入到MongoDB中。这样的处理方式使得整个数据库几乎被锁定几个小时,自然服务性能会降低。直到对数据库执行修复(即在MongoDB上执行repairDatabase命令)后,它才会被解锁。完成修复可能需要几个小时,具体取决于数据库的大小。在另一个实例中,Olery注意到应用程序性能缓慢,并设法追踪到MongoDB集群。但是,经过进一步检查,无法找到问题的真正原因。不管你怎么安装,用什么工具,输入什么命令,都找不到原因。直到Olery更换了集群的主节点,性能才恢复正常。这只是两个例子,Olery遇到过很多这样的例子。问题的核心在于,不仅仅是数据库在运行,而且无论何时查看它,都没有绝对指示导致问题的原因。无schema的问题另外,Olery面临的核心问题是mongoDB的重要特性之一:无schema。缺少模式可能听起来很有趣,并且在某些情况下是有益的。然而,对于无模式存储引擎的许多使用,它会导致模式之间的一些内部问题。这些模式不是由存储引擎定义的,而是由应用程序的行为及其可能的需求定义的。例如:您可能有一个页面存储您的应用程序需要的字符串类型的标题字段集合。这里的模式非常符合情况,尽管它没有明确定义。但是如果这个数据随着时间的推移而改变,特别是如果原始数据没有迁移到新的数据结构,这就会成为一个问题(在一些无模式存储引擎上这是相当有问题的)。例如,您可能有这样的Ruby代码:post_slug=post.title.downcase.gsub(/\W+/,'-')这样对于每个具有“title”字段并返回字符串的文档,它将正常工作。但是,对于使用不同字段名称(例如:post_title)或根本没有标题字段的文档,它将无法正常工作。要处理这种情况,您需要将代码调整为以下内容:ifpost.titlepost_slug=post.title.downcase.gsub(/\W+/,'-')else#...end在您的模型中定义模式。例如Mongoid,一种流行的MongoDBODMforRuby,可以让你做到这一点。但是,在使用这些工具定义模式时,您可能想知道为什么它们不在数据库内部定义模式。事实上,这样做解决了另一个问题:可重用性。如果您只有一个应用程序,那么在代码中定义模式并不是什么大问题。但是,如果您有很多应用程序,这很快就会变得麻烦。无模式存储引擎希望通过消除对模式的约束来简化您的工作。但现实情况是,确保数据一致性的责任落在了用户自己身上。有时无模式引擎可以工作,但我敢打赌,它往往适得其反。好的数据库的要求有更多的特殊要求,迫使Olery寻求更好的数据库来解决问题。对于系统,尤其是数据库,Olery非常重视以下方面:一致性数据和系统行为的可视化正确性和清晰度可扩展的一致性很重要,因为它有助于Olery为系统设定明确的期望。如果数据总是以相同的方式存储,那么系统就可以很容易地使用这些数据。如果在数据库层面要求表的某一列必须存在,那么在应用层面就不需要检查这列数据是否存在。即使数据库处于高压状态,也必须保证每个操作的完整性。没有什么比简单地插入数据,几分钟后才找到它更令人沮丧的了。可见性包括两件事:系统本身以及从中获取数据的难易程度。如果系统出错,应该很容易调试。反过来,用户应该很容易找到他们想要查询的数据。正确性意味着系统的行为符合Olery的预期。如果字段定义为数值,则任何人都不能向其中插入文本。MySQL因这个而臭名昭著,一旦你这样做,你将得到虚假的结果。可扩展性不仅与性能有关,还与财务方面以及系统处理不断变化的需求的能力有关。如果没有大量的资本成本或减慢系统所依赖的开发周期,系统很难表现得很好。#p#AbandoningMongoDB牢记以上要求,Olery开始寻找一个数据库来替代MongoDB。上面提到的特性通常是一组核心的传统RDBM特性,因此Olery锁定了两个候选者:MySQL和PostgreSQL。本来MySQL是第一个候选的,因为Olery的一些关键数据已经存储在里面了。然而,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)来解决这个问题。了解了以上问题后,Olery开始对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中还有其他各种有趣的功能,例如:基于三元组的索引和检索、全文检索、支持JSON查询、支持查询/存储键值对、支持发布/订阅等。最重要的是PostgreSQL可以在性能、可靠性、正确性和一致性之间进行权衡。#p#迁移到PostgreSQL最后,为了在各个项目之间取得平衡,Olery决定使用PostgreSQL。然而,将整个平台从MongoDB迁移到一个截然不同的数据库并不是一件容易的事。为了简化迁移,Olery将该过程分为3个步骤:设置PostgreSQL数据库并迁移一小部分数据。更新所有依赖MongoDB的应用程序以及任何需要的重构,以替换那些依赖PostgreSQL的应用程序。将生产数据迁移到新数据库,然后部署新平台。部分数据迁移在考虑将所有数据迁移到新数据库之前,Olery迁移了一小部分数据用于测试目的。如果只迁移一小部分数据,会很麻烦,那么数据库迁移就没有意义了。虽然有现成的工具可以使用,但有些数据(例如,列重命名,数据类型不一致)需要转换,Olery针对这些数据开发了一些工具。这些工具中的大多数都是用Ruby编写的一次性脚本,用于删除一些注释、清理数据编码、更正主键出现的顺序等等。虽然刚开始测试时出现了一些数据问题,但没有出现阻碍迁移的大问题。例如,一些用户提交的数据没有完全格式化,并且在重新编码之前无法导入到新数据库中。Exception一个有趣的变化是,以前评论数据存储的是评论所用语言的名称(如“荷兰语”、“英语”等),现在语言的编码发生了变化,因为Olery的新语义分析系统使用的是语言代码,而不是语言名称。更新应用到目前为止,最耗时的是更新应用,尤其是那些严重依赖MongoDB聚合框架的应用。扔掉少数需要数周才能测试的遗留Rails应用程序。更新应用程序的过程大致如下:将MongoDB驱动/配置模块的代码替换为PostgreSQL相关代码运行测试修复bug反复运行测试,直到所有测试通过。对于非Rails应用程序,Olery建议使用Sequel。对于Rails应用程序,Olery没有办法摆脱ActiveRecord(至少现在还没有)。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))虽然这可能有点冗长,但上面的两个查询都使它们在没有字符串连接的情况下更容易重用。将来也可能将Olery的Rails应用程序迁移到Sequel,但鉴于Rails与ActiveRecord的耦合如此紧密,因此Olery不完全确定是否值得花费时间和精力。#p#Migratingproductiondata最后,Olery谈到了迁移生产数据的过程。通常有两种方法可以做到这一点:关闭整个平台,直到所有数据都迁移完毕。在迁移数据时保持系统运行。第一种选择有一个明显的缺点:停机时间。第二种选择不需要停机但难以处理。比如这个场景,当你迁移数据的时候,你要考虑所有将要添加的数据,否则你会丢失数据。幸运的是,Olery有一个独特的解决方案。Olery的数据库的大部分写操作都是相当有规律的,频繁变化的数据(比如用户通讯录信息)只占总数据量的一小部分。与Olery检查数据相比,迁移它们花费的时间要少得多。这部分的基本流程是:迁移关键数据,例如用户、合同和无论如何都不能丢失的数据。迁移不太重要的数据(我们可以重新收集、重新计算等的数据)测试一切是否已完成并在一组单独的服务器上运行。将生产环境转换为新服务器。重新迁移第一步中的数据,保证迁移过程中产生的数据不丢失。第二步耗时最长,大约需要24小时。另一方面,迁移步骤1和5中提到的数据只用了45分钟。结论Olery迁移完成,大约一个月过去了,直到我对它非常满意为止。除了迄今为止的这些积极影响外,它还导致应用程序在各种情况下的性能显着提高。例如,Olery的酒店评论数据API(在Sinatra上运行)的交互延迟比迁移前低得多:迁移从1月21日开始,峰值表明应用程序性能硬重启(导致交互时间略有放缓加工)。21日之后的平均互动时间大约是这个时间的一半。在Olery称为“评论持久化”(译者注:即存储评论)的另一个过程中,Olery发现了巨大的性能提升。守护进程的目标很简单:保存评论数据(评论内容、评论分数等)。当最终对迁移工作进行了很多大的更改时,结果令人鼓舞:fetcher也更快:fetcher的性能增益不如评论存储过程大,因为fetcher仅使用数据库来检查评论是否存在(一个比较快的操作),所以这个结果不是很意外。最后,我们来到程序中用来调度爬虫进程的进程(简称“调度器”):因为调度器只是以固定的频率运行,所以这张图可能有点难以理解,但不管怎么说,迁移之后,平均处理时间有明显下降。原文链接:www.searchdatabase.com.cn/showcontent_88259.htm