MongoDB生存指南

所有优秀的初创企业要么迅速死亡,要么规模扩大。 我们将对这样的启动进行建模,首先是关于功能,然后是性能。 我们将使用流行的NoSQL数据存储解决方案MongoDB来提高性能。 MongoDB易于入门,而且许多问题都有开箱即用的解决方案。 但是,当负荷增加时,一头耙子发出来,直到今天为止还没有人警告过您!

图片

建模由负责后端基础结构的Sergey Zagursky (特别是Joom中的 MongoDB)执行。 在MMORPG Skyforge开发的服务器端也可以看到它。 正如谢尔盖(Sergei)所描述的那样,他是“有自己的额头和耙子的专业锥果饲养者”。 在显微镜下,这是一个使用累积策略来管理技术债务的项目。 在HighLoad ++的此文本版本的报告中,我们将按时间顺序从问题发生到使用MongoDB解决方案。

最初的困难


我们正在建模一个充满麻烦的初创公司。 人生的第一阶段-功能是在我们的启动阶段启动的,出乎意料的是,用户来了。 我们的小型MongoDB服务器具有我们从未梦想过的负载。 但是我们在云端,我们是一家创业公司! 我们会做最简单的事情:查看请求-哦,在这里,我们减去了每个用户的整个校正值,在这里我们将建立索引,在此处添加硬件,并在此处进行缓存。
一切-我们继续前进!

如果可以通过这种简单的方法解决问题,则应以这种方式解决。

但是,成功启动的未来之路是水平缩放时刻的缓慢而痛苦的延迟。 我将尝试提供有关如何在此期间生存,扩大规模而不是踩在耙子上的建议。

记录慢


这是您可能会遇到的问题之一。 如果遇到她该怎么办,而上述方法无济于事? 答: 默认情况 ,MongoDB中的 持久性 保证模式 。 用三个词可以这样看:

  • 我们来到第一行说:“写!”。
  • 记录了主副本。
  • 之后,从她那里读取了二级副本,他们说是一级副本:“我们录制了!”

在大多数辅助副本执行此操作时,该请求被视为已完成,并且控制权返回给应用程序中的驱动程序。 这样的保证使我们可以确保,当控制权返回给应用程序时,即使MongoDB崩溃了,持久性也不会消失,除非绝对可怕的灾难。

幸运的是,MongoDB是一个这样的数据库,它使您可以减少每个单独请求的持久性保证。

对于重要的要求,我们可以默认保留最大耐用性保证,对于某些要求,我们可以降低它们。

要求课程


我们可以删除的第一层保证不是等待大多数副本对记录的确认 。 这样可以节省延迟,但不会增加带宽。 但是有时需要延迟,特别是在集群有点过载并且辅助副本无法按我们期望的那样快速运行时。

{w:1, j:true} 

如果我们使用这样的保证来写记录,那么当我们在应用程序中获得控制权时,我们将不再知道在发生某种事故后该记录是否仍然有效。 但通常来说,她还活着。

下一个保证也将影响带宽和延迟,将禁用日志记录确认 。 无论如何都会写日记帐分录。 杂志是基本机制之一。 如果我们关闭了写入确认,那么我们就不会做两件事: 在日志上 执行 fsync 并且不等待其结束通过简单地更改保证的持久性,可以节省大量磁盘资源并获得吞吐量多次增长

 {w:1, j:false} 

最严格的耐用性保证是禁用任何确认 。 我们只会收到确认请求已到达主副本的确认。 这将节省等待时间,并且不会以任何方式增加吞吐量。

 {w:0, j:false} —   . 

我们还将收到其他各种信息,例如,由于与唯一键冲突而导致录制失败。

这适用于什么操作?


我将告诉您有关Joom设置的应用程序。 除了来自用户的负担(其中没有耐用性方面的让步)之外,还有一种负载可以描述为后台批处理负载:更新,重新计算等级,收集分析数据。

这些后台操作可能要花费数小时,但经过精心设计,如果后台发生中断(例如后端崩溃),它们将不会丢失所有工作的结果,而是从最近的时间恢复。 减少持久性保证对于此类任务很有用,尤其是因为日志中的fsync像任何其他操作一样,也会增加读取的延迟。

读取比例


下一个问题是读取带宽不足 。 回想一下,在我们的集群中,不仅有主副本,还可以从中读取辅助副本。 来吧

您可以阅读,但有细微差别。 稍微过时的数据将来自辅助副本-大约需要0.5-1秒。 在大多数情况下,这是正常现象,但是辅助副本的行为与主要副本的行为不同。

在辅助数据库上,存在使用oplog的过程,该过程不在主副本上。 这个过程不是为低延迟而设计的,只是MongoDB开发人员没有为此而烦恼。 在某些情况下,从主要到次要使用oplog的过程可能会导致最多10 s的延迟。

辅助副本不适合用户查询-用户体验迅速进入了垃圾箱。

在没有阴影的群集上,这些峰值不太明显,但仍然存在。 分片群集受苦是因为oplog特别受到删除的影响,而删除是平衡器工作的一部分 。 平衡器在短时间内可靠,高雅地删除了成千上万的文档。

连接数


下一个要考虑的因素是MongoDB实例上的连接数限制 。 默认情况下, 除了操作系统资源外,没有任何限制-您可以在允许的情况下进行连接。

但是,并发请求越多,它们的运行速度就越慢。 性能会非线性下降 。 因此,如果高峰请求到达我们,服务80%总比不服务100%好。 连接数必须直接限制到MongoDB。

但是有一些错误会因此而引起麻烦。 特别是, MongoDB端连接池对于用户和服务集群内连接都是通用的 。 如果应用程序“占用”了该池中的所有连接,则群集中的完整性可能会受到侵犯。

我们在重建索引时了解了这一点,并且由于需要从索引中删除唯一性,因此该过程经历了几个阶段。 在MongoDB中,您不能在索引旁边构建相同的索引,但是没有唯一性。 因此,我们想要:

  • 建立没有唯一性的相似索引
  • 以唯一性删除索引;
  • 建立一个没有唯一性而不是远程的索引;
  • 删除临时。

当临时索引仍在辅助索引上完成时,我们开始删除唯一索引。 此时,二级MongoDB宣布了锁定。 一些元数据被阻止,并且大多数记录都停止了:它们挂在连接池中 ,等待它们确认记录已通过。 由于已捕获全局日志,因此在辅助数据库上的所有读取也都停止了。

处于如此有趣状态的集群也失去了连接。 有时会出现这种情况,并且当两个评论相互关联时,他们试图以无法进行的状态做出选择,因为它们具有全局锁定。

故事的寓意:必须监视连接数。

有一个著名的MongoDB耙,该耙仍然经常受到攻击,因此我决定走一小段路。

不要丢失文件


如果您通过索引将请求发送到MongoDB,则在完全意外的情况下,该请求可能不会返回所有满足条件的文档 。 这是由于以下事实:当我们转到索引的开头时,末尾的文档将移动到我们通过的那些文档的开头。 这完全是由于索引的可变性 。 为了可靠地进行迭代,请在非稳定字段上使用索引 ,这样就不会有困难。
MongoDB对于使用哪些索引有其自己的看法。 解决方案很简单- 在$ hint的帮助下,我们强制MongoDB使用我们指定的索引

集合大小


我们的创业公司正在开发中,有很多数据,但是我不想添加磁盘-我们在上个月已经添加了三次。 让我们看看数据中存储了什么,看看文档的大小。 如何了解集合中的哪些地方可以减小大小? 根据两个参数。

  • 特定文件 的大小与其长度一起发挥: Object.bsonsize() ;
  • 根据 集合中 文档 的平均 大小db.c.stats().avgObjectSize

如何影响文件的大小?


我对此问题有非特定的答案。 首先, 长字段名会增加文档的大小。 在每个文档中,所有字段名都被复制,因此,如果文档的字段名很长,则必须将名称的大小添加到每个文档的大小中。 如果您的集合在几个字段上包含大量小文档,则使用以下短名称命名字段:“ A”,“ B”,“ CD”-最多两个字母。 在磁盘上,这可以通过压缩来抵消 ,但是所有内容都按原样存储在缓存中。

第二个提示是,有时可以将一些基数较低的字段放在集合的名称中 。 例如,这样的字段可以是语言。 如果我们有一个包含俄语,英语,法语翻译的集合,并且包含有关所存储语言的信息的字段,则可以将该字段的值放在集合的名称中。 因此,我们将减少文档的大小,减少索引的数量和大小 -节省大量资金! 这并非总是可以做到的,因为如果将集合分为不同的集合,则文档中有时会有些索引不起作用。

关于文档大小的最后提示- 使用_id字段 。 如果您的数据具有自然唯一键,则将其直接放在id_field中。 即使键是复合键,也请使用复合ID。 它是完美索引的。 只有一个小小的耙子-如果您的编组有时更改了字段的顺序,则具有相同字段值但不同顺序的id在MongoDB中的唯一索引方面将被视为不同的id。 在某些情况下,这可能会在Go中发生。

索引大小


索引存储其中包含的字段的副本 。 索引的大小由被索引的数据组成。 如果我们试图为大型字段建立索引,请准备好使索引变大。

第二时刻使索引大大膨胀:索引中的数组字段乘以该索引中文档的其他字段 。 注意文档中的大型数组:不要对数组进行其他索引,或者按照索引中的字段列出的顺序进行操作。

字段的顺序很重要尤其是如果索引字段之一是数组时 。 如果这些字段的基数不同,并且在一个字段中,可能值的数量与另一字段中的可能值的数量非常不同,则可以通过增加基数来构建它们。 如果交换具有不同基数的字段,则可以轻松保存索引大小的50%。 字段的排列可以使大小更显着减小。

有时,当字段包含一个较大的值时,我们不需要或多或少地比较该值,而是进行清晰的相等比较。 然后, 具有大量内容的字段上 的索引可以替换为此字段上的哈希上的索引 。 哈希的副本将存储在索引中,而不是这些字段的副本。

删除文件


我已经提到删除文档是一项不愉快的操作, 最好不要删除。 在设计数据模式时,请尝试考虑最大程度地减少单个数据的删除或删除整个集合。 它们可以与整个收藏夹一起删除。 删除集合是一种廉价的操作,而删除数千个单个文档则是一项困难的操作。

如果仍然需要删除大量文档,请确保进行限制操作 ,否则大量删除文档会影响读取的延迟,并且会令人不快。 这对于辅助节点上的延迟特别不利。

制作一些“笔”来节流是值得的-第一次很难恢复水平。 我们经历了很多次,以至于第三次,第四次都在节流。 首先,考虑将其拧紧的可能性。

如果您删除大型集合的30%以上,则将实时文档传输到相邻集合 ,然后整体删除旧集合。 显然存在细微差别,因为负载已从旧集合切换到新集合,但会尽可能转移。

删除文档的另一种方法是TTL索引,它是索引包含Mongo时间戳记的字段的索引,该时间戳记包含文档死亡的日期。 这时候,MongoDB将自动删除此文档。

TTL索引很方便,但是在实现中没有任何限制。 MongoDB不在乎如何删除这些删除。 如果您尝试同时删除一百万个文档,则几分钟后,您将拥有一个无法操作的群集,该群集只能处理删除操作,仅此而已。 为了防止这种情况的发生,请添加一些随机性业务逻辑和延迟允许的特殊影响范围内尽可能分散TTL 。 如果您出于自然的业务逻辑原因而将删除集中在某个时间点,则必须涂抹TTL。

分片


我们试图推迟这一刻,但现在已经到了-我们仍然必须水平扩展。 对于MongoDB,这是分片。

如果您怀疑需要分片,则不需要它。

分片通过各种方式使开发人员和开发人员的生活变得复杂。 在公司中,我们称其为分片税。 当我们对一个集合进行分片时, 该集合特定性能会下降 :MongoDB需要一个单独的索引来进行分片,并且必须将其他参数传递给请求,以便可以更高效地执行它。

一些分片的东西就是行不通的。 例如,将查询与skip一起使用是一个坏主意,尤其是在您有很多文档的情况下。 您输入命令:“跳过100,000个文档。”

MongoDB这样想:“第一,第二,第三……十分之一,让我们走得更远。 我们会将其退还给用户。”

在非共享集合中,MongoDB将在自身内部的某个位置执行操作。 在mongos中 ,她确实像分片一样真正读取了所有100,000个文档并将其发送到分片代理中,而mongos已经支持了它,将以某种方式过滤掉并丢弃前100,000个。

分片无疑会使代码变得更加复杂-您将不得不将分片密钥拖动到许多地方。 这并不总是很方便,也不总是可能。 一些查询将进行广播或多播,这也不会增加可伸缩性。 选择一个可以使分片更加准确的密钥。

在分片集合中, count操作中断 。 她开始返回的数字比现实中的多-她可以撒谎2次。 原因在于平衡过程,即将文档从一个分片倒入另一个分片中。 当文档倒在相邻的分片上,但尚未在原始分片上删除时,无论如何, count它们进行count 。 MongoDB开发人员不会将其称为错误-这是一个功能。 我不知道他们是否会解决。

改组后的集群很难管理 。 Devops将停止问候您,因为删除备份的过程从根本上变得更加复杂。 分片时,对基础设施自动化的需求会像火灾警报一样闪烁,这是您之前所无法做到的。

分片在MongoDB中的工作方式


有一个集合,我们希望以某种方式将其分散在碎片中。 为此, MongoDB使用分片键将集合划分为多个块 ,并尝试在分片键空间中将它们分成相等的片段。 接下来是平衡器,平衡器根据集群中的碎片努力地安排这些块 。 而且,平衡器不在乎这些块的重量以及其中有多少文件,因为平衡是逐段进行的。

分片密钥


您还决定分片吗? 好吧,第一个问题是如何选择分片密钥。 好的密钥具有几个参数: 高基数不稳定和非常适合频繁的请求

分片键的自然选择是主键-id字段。 如果id字段适合分片,则最好直接在其上分片。 这是一个很好的选择-他具有良好的基数,他不是很稳定,但是他如何适应频繁的请求是您的业务特点。 根据您的情况。

我将举一个失败的分片密钥的例子。 我已经提到翻译的集合-翻译。 它具有存储语言的语言字段。 例如,该集合支持100种语言,而我们使用分片语言。 这很不好-基数,可能值的数量只有100个,很小。 但这不是最坏的情况-基数足以满足这些目的。 更糟糕的是,一旦我们改用该语言,我们立即发现说英语的用户比其他用户多3倍。 不幸的是,英语所在的分片请求的数量是其他所有请求的总和的三倍。

因此,应该记住,有时分片键自然会趋向于负载分配不均。

平衡


当我们的需求已经成熟时,我们便开始分片-我们的MongoDB集群崩溃,磁盘崩溃,处理器-力所能及。 去哪里 无处可寻,我们英勇地洗了系列的脚跟。 我们分片,发射,突然发现平衡不是免费的

平衡需要经历几个阶段。 平衡器从要转移的位置和位置选择块和碎片。 进一步的工作分为两个阶段:首先,将文档从源复制到目标,然后删除复制的文档。

我们的分片已超载,其中包含所有集合,但是操作的第一部分对他来说很容易。 但是第二个步骤-取下-非常不愉快,因为它将在肩blade骨上留下碎片,并且已经承受了负荷。

如果我们平衡了很多块(例如数千块),那么在默认设置下,所有这些块都将首先被复制,然后进入卸妆器并开始批量删除它们,这使问题更加复杂。 此时,该过程不再受影响,您只需要可悲地观察正在发生的事情即可。

因此,如果要分片过载的群集,则需要进行计划,因为平衡需要时间。 建议不要在黄金时间使用此时间,而应在低负载期间使用。 平衡器-断开的备件。 您可以在手动模式下接近主要平衡,在黄金时段关闭平衡器,然后在负载减少后再打开平衡器以增加负载。

如果云的功能仍然允许您垂直扩展,最好事先改善分片源,以稍稍减少所有这些特殊效果。

分片必须仔细准备。

HighLoad ++ Siberia 2019将于6月24日至25日在新西伯利亚举行。 HighLoad ++ Siberia为来自西伯利亚的开发人员提供了一个机会,可以收听报告,谈论高负荷的话题并跳入“每个人都有自己的房子”的环境,而无需飞越莫斯科或圣彼得堡三千多公里。 在80份申请中,计划委员会批准了25份,我们在邮件列表中告知了计划的所有其他更改,报告的公告和其他新闻。 订阅以保持最新状态。

Source: https://habr.com/ru/post/zh-CN454748/


All Articles