MongoDB 4.x——治理经验

MongoDB 4.x治理经验

1、强化约束

1.1、使用JSON Schema

一个有趣的问题是,开发者一方面在享受动态模式带来的灵活性的同时,另一方面也产生了一些担忧,一些不受约束的错误行为终将会造成损害,这该如何避免呢?

MongoDB并非完全忽视模式管理,相反,MongoDB 3.2版本已经开始支持模式的校验。在执行createCollection命令创建集合时可以指定一个验证程序(validator)​,用于实现对写入数据进行规则检查。到了3.6版本则引入了标准的JSON Schema。由此可见,实现MongoDB文档的强制约束并不难。

JSON Schema提供了一套通用的词法规则用于JSON元数据定义。元数据本身是基于JSON表示的,可以对字段、类型和结构等实现约束。下面是一个使用JSONSchema的例子。

  • $jsonSchema包含了元数据结构体,其中描述users的集合中,phone、age字段是必须提供 的。
  • properties定义了当前层级对象的属性定义,phone为字符串,age为1~9的整数。
  • validationAction=error表示当写入数据不符合规则时直接报错。validationAction可以设置为warn或者error,如果是warn级别,则MongoDB会允许违反规则的写入并记录日志。

尝试向users集合中写入不合法的数据,会得到如下错误:

validator的定义允许通过collMod命令进行修改。无论是新增validator还是修改已有的validator定义,都不会对已经存在的数据产生任何影响。validator只对新产生的插入、修改进行校验,这意味着违反约束的旧数据仍然存在。为了保证兼容性,应用代码需要做出妥协,或者对数据手动进行转换,但可能也会付出不小的代价。

由于在写入数据时多了校验的动作,在性能上会有些损失。但在一些特定的场景中,validator不失为一种避免出错的办法。应用在使用JSON Schema的同时,可考虑以下两个做法。

  • 使用工具来管理JSON Schema,避免编写错误。
  • 将JSON Schema纳入升级变更流程,保持数据与规则的一致性。

MongoDB默认的设定是不对JSON Schema进行约束,这意味着文档结构的变更可以不经过任何校验。当然,这或许已经成为一种常态,而且在某些条件下也成为数据库选型时所必须接受的一个缺点。然而,笔者认为,是否对JSON Schema强加约束属于管理层面的问题,任何一种选择都是权衡利弊之后的结果。

1.2、管理文档结构

在项目发展初期,集合文档的结构(DDL)管理往往不受重视,但这可能不是一个好的前兆。

有必要为MongoDB集合、索引的设计维护一份可信赖的资料文档。始终保持代码、数据库以及文档的一致,将有利于开发人员充分理解设计的意图。更重要的一点是,这有助于减少在项目演进时产生的一些技术债务。

如果认为文档的维护工作过于烦琐,则可以考虑一些自动化的手段。例如,当项目统一为ODM开发模式之后,利用代码扫描来辅助生成文档,如图所示。

2、使用Mongobee实现升级

2.1、模式演进

微服务模式下提倡快速迭代以应对变化,这可能会促进数据库模式的演进。不同于代码版本的管理,在数据库中维持多种Schema版本的数据并不容易,为了表达这种差异,一种做法是在集合文档中添加版本字段,应用代码根据集合文档中的版本提示来选择性处理。

然而,多版本的数据共存不应该成为常态,因为这样一来代码容易变得臃肿而产生一些"坏味道"​,测试兼容性的工作也变得复杂。另外,现有的ODM框架并不能很好地为此工作。更好的做法是停止堆叠版本,尽快推动数据的升级。假若这种变化无休无止,那么就应该重新考虑当前的服务设计是否合理了,可以考虑使用新的功能模块,甚至是微服务来实现新的需求。总之,始终保持一种数据版本,是最好的做法。

2.2、Mongobee介绍

Mongobee是一款支持MongoDB数据升级的变更管理框架,与Flyway、Liquibase这类SQL变更管理工具十分类似。Mongobee在理念上非常契合微服务的特点,传统的数据升级方式会将多个功能模块或服务的数据升级脚本进行集中式管理,这会打破微服务的自治性。而借助Mongobee框架则可以实现服务数据的自升级能力。

Mongobee基于Java代码来实现数据的变更管理,和Spring框架可以进行无缝集成。

2.2.1、关键概念
  • ChangeLog数据变更日志,通常对应一个变更业务模块,不同的ChangeLog可以使用order属性来指定执行顺序。
  • ChangeSet数据变更集,对应一组变更操作。一个ChangeLog内可以包含多个ChangeSet。

一个变更集具有"作者"​"变更集ID"​"执行顺序"属性,可以将变更集指定为仅执行一次或每次都执行。

2.2.2、实现原理

在应用启动时,Mongbee会扫描指定的包路径获得ChangeLog实例。在执行升级之前,Mongobee会获取一个分布式锁,这是为了避免多个微服务实例同时启动升级而产生冲突。

分布式锁采用数据库唯一性索引实现,Mongobee对同一个数据库(db)的升级流程保持互斥,因此在应用数据库中可以发现对应的锁记录集合(名称为mongobeelock)​。除此之外,dbchangelog这个集合记录了所有ChangeSet的执行记录。对于一般的变更集,只要存在执行成功的记录,那么第二次将不会执行,除非ChangeSet中指定了runAlways=true。

2.3、范例

在引入Spring-Data-Mongo的Web项目中添加依赖,代码如下:

Mongobee依赖于Spring、MongoDB Java Driver组件,由于这些可能与当前版本冲突,这里通过exclusion命令消除冲突的依赖。

2.3.1、配置文件

在application.properties中添加配置,代码如下:

说明:

  • mongbee.enabled,开启Mongobee模块。
  • mongbee.file,指定Mongobee预置数据文件(JSON格式)。
  • mongobee.uri,远程连接MongoDB的URI。
2.3.2、初始化实例

这里我们声明了MongobeeInit作为升级的入口类,其中对Mongobee类型的Bean对象进行相应的配置,包括数据库地址、数据库名称以及ChangeLog的扫描路径。Mongobee继承了InitializingBean,在初始化时会自动触发升级流程。

2.3.3、实现变更

在指定的扫描路径中,使用@ChangeLog注解来声明一个变更,代码如下:

  • prepareData方法使用@ChangeSet注解声明为一个变更集,其中id必须保证唯一,而order则决定了执行顺序。
  • 在变更集的操作方法中,会读取指定的JSON文件,将预置数据写入指定的集合。

在类路径的mongobee目录中创建data.json文件,构造需要预置的数据,代码如下:

这里将会向roles集合中预置写入几个常驻的系统角色。

2.3.4、执行结果

启动SpringBoot应用,输出日志如下:

日志中可以看到Mongobee已经完成了变更集的操作,此时查看roles集合,可确认是否产生了正确的记录。此外,查看数据库中的dbchangelog集合,也可以看到相应的变更记录,代码如下:

3、规范与自动化

传统的团队运作模式倾向于将工作分工最小化,例如:

  • 开发工程师负责开发Web应用层代码。
  • 数据库工程师负责数据表设计、SQL语句编写及调优。

然而,如今我们发现很难将代码开发和数据库开发完全分离开来,过于精细的分工并不利于高效的项目运作。关键的问题在于,只有在开发人员、数据库工程师同时对一份具体需求产生了同样的理解时,才可能实现无缝对接的合作。但达成一致的理解本身是困难的,尤其是在各方理解不一致的情况下进行开发,必然会产生各种各样的问题。

MongoDB在操作上比较简单,而且提供了大量应用友好的特性,这些降低了使用的门槛。因此,由开发人员同时进行代码编写和MongoDB设计的情况并不鲜见,这种很美好的错觉容易让团队疏于数据库设计开发方面的管理。

随着项目的演进,一些弊端也会逐渐暴露出来,例如:

  • 数据表设计混乱,文档中出现诸如xxxV1、xxxV2等难以理解的字段,后期维护成本太高。
  • 过度采用内聚设计,例如一个"超级表"包含了大量不相关的业务字段,导致单表上的操作性能低下且难以扩展。
  • 未提前考虑扩展,或分片键不合理,导致后期进行改造的成本非常高。

3.1、开发规范

  1. 命名原则。数据库、集合命名需要简单易懂,数据库名使用小写字符,集合名称使用统一命名风格,可以统一大小写或使用驼峰式命名。数据库名和集合名称均不能超过64个字符。
  2. 集合设计。对少量数据的包含关系,使用嵌套模式有利于读性能和保证原子性的写入。对于复杂的关联关系,以及后期可能发生演进变化的情况,建议使用引用模式。
  3. 文档设计。避免使用大文档,MongoDB的文档最大不能超过16MB。如果使用了内嵌的数组对象或子文档,应该保证内嵌数据不会无限制地增长。在文档结构上,尽可能减少字段名的长度,MongoDB会保存文档中的字段名,因此字段名称会影响整个集合的大小以及内存的需求。一般建议将字段名称控制在32个字符以内。
  4. 索引设计。在必要时使用索引加速查询。避免建立过多的索引,单个集合建议不超过10个索引。MongoDB对集合的写入操作很可能也会触发索引的写入,从而触发更多的I/O操作。无效的索引会导致内存空间的浪费,因此有必要对索引进行审视,及时清理不使用或不合理的索引。遵循索引优化原则,如覆盖索引、优先前缀匹配等,使用explain命令分析索引性能。
  5. 分片设计。对可能出现快速增长或读写压力较大的业务表考虑分片。分片键的设计满足均衡分布的目标,业务上尽量避免广播查询。应尽早确定分片策略,最好在集合达到256GB之前就进行分片。如果集合中存在唯一性索引,则应该确保该索引覆盖分片键,避免冲突。为了降低风险,单个分片的数据集合大小建议不超过2TB。
  6. 升级设计。应用上需支持对旧版本数据的兼容性,在添加唯一性约束索引之前,对数据表进行检查并及时清理冗余的数据。新增、修改数据库对象等操作需要经过评审,并保持对数据字典进行更新。
  7. 考虑数据老化问题,要及时清理无效、过期的数据,优先考虑为系统日志、历史数据表添加合理的老化策略。
  8. 数据持久性方面,非关键业务使用默认的WriteConcern:1(更高性能写入);对于关键业务类,使用WriteConcern:majority保证持久性(性能下降)。如果业务上严格不允许脏读,则使用ReadConcern:majority选项。
  9. 使用update、findAndModify对数据进行修改时,如果设置了upsert:true,则必须使用唯一性索引避免产生重复数据。
  10. 业务上尽量避免短连接,使用官方最新驱动的连接池实现,控制客户端连接池的大小,最大值建议不超过200。
  11. 对大量数据写入使用Bulk Write批量化API,建议使用无序批次更新。
  12. 优先使用单文档事务保证原子性,如果需要使用多文档事务,则必须保证事务尽可能小,一个事务的执行时间最长不能超过60s。
  13. 在条件允许的情况下,利用读写分离降低主节点压力。对于一些统计分析类的查询操作,可优先从节点上执行。
  14. 考虑业务数据的隔离,例如将配置数据、历史数据存放到不同的数据库中。微服务之间使用单独的数据库,尽量避免跨库访问。
  15. 维护数据字典文档并保持更新,提前按不同的业务进行数据容量的规划。

3.2、实现自动化

DevOps的目标理念是敏捷、持续地交付。其中一个关键的原则是,在代码开发、测试、发布等一系列过程中建立持续反馈的机制。

对于数据库在设计或开发上的问题,越是尽早发现,越是能降低后期修复所产生的成本消耗。因此,我们除了建立规范、遵循最佳实践,还可以利用自动化设施来建立快速反馈机制。

一个典型的持续构建流水线如图所示。

在打造MongoDB的质量管理系统时,我们通常关心的问题主要如下。

  • 数据表设计的合理性,数据库对象的命名是否符合规范,是否存在索引超量、重复索引(两个索引出现覆盖)的嫌疑。
  • 数据库操作是否存在性能风险:一些存在"坏味道"的SQL,如全表扫描;内存排序,无法利用索引的排序问题;不推荐使用$or查询;索引命中不全;分页条件不合理(limit、skip);低效的操作符(如nin/not)。
  • 数据库Schema是否发生重大变更,变更是否合理。

MongoDB应用质量管理在理念上和SQL审核系统非常类似,但由于MongoDB是基于动态Schema的模式,我们无法通过数据库获得准确的表设计(DDL)​。

一种可行的思路是基于代码扫描的方式,首先我们在项目上统一使用SpringData框架进行持久层代码开发,实体类和MongoDB集合保持一一映射(ODM)​。有了这个前提,我们便可以通过扫描实体类源代码来获得当前的表设计信息。接下来,就可以对Schema进行规范扫描以完成质量检查,实现版本变更的对比。

在自动化功能测试阶段,开启MongoDB的Profiler以获得业务操作的SQL语句信息,也可以利用MongoDB JavaDriver提供的CommandListener来抓取SQL。在获得SQL语句之后,将其逐一进行explain分析以获得执行计划信息,最终对这些计划进行评估来分析潜在的风险。

另外一个值得关注的细节是,在整个自动化过程中必须保持对问题、风险进行反馈,例如对于重大的变更风险或一些问题,SQL自动进行邮件推送。

对于部署到生产环境的应用,必须十分重视来自线上数据库运维的优化反馈。然而,这里所提及的质量管理仍 然属于研发阶段,根据DevOps原则,尽早发现并反馈问题是实现高效率产品运作的一个关键。

4、运维管理

4.1、容量规划

容量规划的目的是评估系统在保证业务正常发展时所需要付出的资源成本,通过进行合理的规划来解答如下的问题:

  • MongoDB节点采用什么样的服务器规格(内存、CPU)?
  • 副本集是否已经能满足需求,是否需要分片,需要多少分片?
  • 需要配置多大的磁盘,对磁盘的IOPS要求是多少?

对于MongoDB来说,准确的评估系统容量并不是一个简单的任务,你需要了解业务应用的表是如何设计的,关键流程对数据库的访问诉求,以及未来一段时间系统需要承载的访问量和数据量等,综合多方面的需求进行考量并最终商定结果。

一般来说,在容量规划中可参考下面几个原则。

4.1.1、保证充足的内存可用

在理想的情况下,内存应该能装下整个工作集。

工作集应该同时包含频繁使用的文档(热数据)和索引。那么哪一些是频繁使用的文档呢?不同的业务场景差异是很大的。例如,对于内容社区来说,由于历史的帖子很少被访问,此时热数据可估算为最近3天发布的帖子。在物联网系统场景,几乎所有设备都是在线的,因此热数据应包含全部的设备快照信息。

从MongoDB数据读取的流程中可以理解这种差异,当仅通过内存读取的时候所用的开销是最小的(如图所示,跳过第2、4步骤)​。

WiredTiger的内部缓存默认占用一半的内存,可以在运行过程中观察"非脏页"的淘汰以及页面读入缓存的行为指标,可以辅助判定InternalCache是否可容纳工作集。从理论上讲,索引在InternalCache和外部缓存中大小相当(均使用了前缀压缩)​,而考虑启用压缩的情况下,外部缓存通常能装下更多的文档数据。因此,为保证足够快速地读数据,应保证热数据和其相应的索引小于可用的内存大小。

4.1.2、评估IOPS需求

一般来说,吞吐量大小对IOPS有一定的影响,由于MongoDB大多使用随机访问,因此对于连续请求来说,磁盘的I/O合并优化效果十分有限。可用使用简化的模型来评估IOPS的需求,这里假设内存可满足工作集的条件,indexCount是平均每个操作所涉及的索引数量。IOPS需求的计算公式如下:

js 复制代码
insert操作产生的IOPS=insert.ops×(1+indexCount)
js 复制代码
delete操作产生的IOPS=delete.ops×(1+indexCount)

update操作产生的IOPS=update.ops×(2+indexCount)

将所得到的各项指标IOPS进行累加,就可以推断出总的IOPS需求。注意这里并没有提到find操作,主要考虑到查询操作都能通过内存返回。假设内存中只有索引,那么find则应该对应一次针对文档的磁盘读取操作。

4.1.3、存储空间

评估每个业务表的大小,在模拟数据集中使用db.collection.stats命令来评估在未来需要多少存储空间。

关注每个集合的指标:

  • dataCount,集合文档总数。
  • indexSize,集合索引大小。
  • dataSize,集合压缩前的文档数据大小。
  • avgObjectSize,平均文档大小,avgObjectSize=dataSize/dataCount。
  • storageSize,磁盘文件占用,对应于集合压缩后的大小。
  • compressRatio,文档压缩率,compressRatio=storageSize/dataSize。

最终,数据库对磁盘的需求大小计算为diskSize=storageSize+indexSize。

4.1.4、吞吐量

考虑每个分片能承受的吞吐量大小,具体的指标可参考基准测试结果。假设在最接近当前业务的基准模型中,每个分片不能超过3万TPS的访问量(保证响应时延不超过水位线)​,业务系统的API要求承担1万TPS访问,每次API调用需要产生大约7次数据库操作,那么系统至少应该配置3个分片。

总的来说,容量规划是一项重要且富有挑战的任务,在项目的演进过程中需要做到:

  • 提前规划,最好在设计阶段就进行容量规划,为业务数据的增长提前做出判断。在条件允许的情况下,进行充分的性能测试。
  • 定期监控,对线上运行的数据库系统进行监控,识别潜在的资源瓶颈并提前做好扩容准备。
  • 复盘分析,根据线上的业务增长情况审视容量评估原则,及时做出调整。

4.2、监控时关注哪些指标

为生产环境中的MongoDB数据库实现监控,下面介绍一些常用的指标。

4.2.1、容量

通过db.stats命令可获得每个数据库的存储空间信息(见表)​。

  • 数据库的cacheSize值要求可容纳索引,否则会影响性能。
  • 对磁盘空间的需求约等于storageSize(WiredTiger压缩后的数据集大小)和indexSize的总和,考虑水位线设定在80%左右。
4.2.2、资源用量

通过db.serverStatus命令获得完整的数据库状态指标信息。

(1)连接数(见表)

  • 如果连接数产生未知的波动,则可能会使应用程序产生业务失败。目前所有的MongoDB驱动程序都使用了连接池机制,如果客户端连接变得非常多,则很可能意味着请求数增长迅速,此时应该尽快考虑扩展。Driver的连接池配置若不合理也可能导致连接数过高。可以为连接数设置低、中、高不同的级别阈值,比如在峰值的50%时产生一个普通告警,当超过峰值的2倍时产生严重告警。数据库通过设定maxIncomingConnections可以限定单进程可接入的连接数,默认为65536。

(2)并发队列(见表)

  • WiredTiger引擎使用ticket计票方式用于管理并发的线程。ticket数一般对应了同时进行的读写操作。当剩余可用的ticket为0时,新的读写请求会被阻塞(进入阻塞队列),通常最大的可用ticket数量由wiredTigerConcurrentReadTransactions、wiredTigerConcurrentWriteTransactions参数确定,这两个值默认为128。一般情况下不建议调整,对于过大的并发数可能会导致CPU资源耗尽,在负载需求过大时建议添加分片。

(3)内存、缓存使用(见表)

  • WiredTiger会同时使用文件系统缓存以及存储引擎的缓存(默认内存的一半)。memory.resident是指MongoDB占用的物理内存,一些Schema设计不合理、不必要的冗余索引等情况都可能导致占用过多的内存。
  • 脏缓存指的是缓存中已经被修改,但还没有刷新到磁盘的数据。脏数据比例逐渐增多,当达到20%以上时,则意味着缓存淘汰压力很大,此时业务请求时延会相应增加。通常如果写压力过大,磁盘写性能存在不足,则可能会出现脏数据比例持续较高的情况,可以通过提升磁盘性能或进行水平扩展优化。
  • 对于读场景较多的业务,最好预留充足的缓存空间。如果读入缓存页(pages-read-into-cache)或未修改淘汰页(unmodified pagesevicted)频繁变动,则意味着工作集超过了缓存大小,需要考虑增大内存,或水平扩展。工作集太大通常也会伴随较高的磁盘读压力,而在操作系统层面会观测到可用内存减少。
  • checkpoint、TTL定时器在一定程度上会产生积压式的写。如果磁盘能力较差,则会出现I/O用率的尖峰;如果出现业务时延抖动,则可以考虑设置更小的触发间隔以达到平滑写入。
4.2.3、吞吐量

** (1)访问类指标(见表)**

  • opcounters是当前请求操作的计数器,检查不同类型操作的增速用于判断当前的访问吞吐量。可以按读写操作来设定不同的阈值,如insert、update、delete总和不超过2万TPS,query、getmore总和不超过2万TPS,具体根据服务器资源来定。
  • 通过合理地监控读写请求,可以快速发现潜在的负载瓶颈,并在问题发生前采取措施进行扩容。activeClients表明当前正在进行中的读写,而currentQueue指标可用于确认请求是否处理足够快(是否存在阻塞)。

(2)游标(见表)

  • MongoDB会为每个查询启用一个游标(cursor),并指向一个查询结果集。客户端可通过游标进行数据操作。在业务量稳定的情况下,如果打开的游标数产生持续增长,则往往意味着查询操作太慢。这可能是索引不当,或者大数据集的查询导致的问题。
  • 当一个连接异常断开时,游标可能没有关闭,此时数据库会自动延长其超时时间。如果在后续的10分钟内(cursor.timeOut)没有活动,则被销毁。如果应用未及时关闭游标,则会导致大量的游标积压,这会消耗较多的内存。此外,应该尽量避免noTimeout的游标对象,否则可能产生资源泄露风险。
4.2.4、副本集

使用rs.status、db.getReplicationInfo命令用于检查副本集的相关指标,见表。

  • 复制延迟(replication lag)描述了备节点与主节点之间的差距。该值越小表明情况越佳。如果使用读写分离方案,该值则体现了数据获取的延迟情况。如果延迟过长,在产生主备节点倒换时可能会导致更多的数据丢失(被回滚)。
  • 复制窗口是oplog集合中最新和最老的记录之间的时间间隔。通常如果备节点停止后,在oplog窗口期内还未能恢复运行,那么备节点将无法继续同步,此时只能通过初始化同步恢复。oplog窗口时长与当时的负载是相关的。由于oplog集合大小固定,当写负载较高时,oplog很快会被填满,于是oplog窗口会变小,此时可以考虑增大oplog的大小。建议在oplog窗口达到正常峰值大小的75%及以下值时发出告警。
  • 复制净值是复制窗口与复制延迟的差值。如果复制净值迅速减小,直到到达负值时,则意味着复制延迟已经超过了oplog窗口。此时oplog中的写操作在备节点完成复制前会被覆盖掉,接下来你只能进行初始化同步操作,这将会花费大量的时间。
相关推荐
黄华SJ520it2 小时前
门店预约系统开发:提升服务行业数字化转型
前端·数据库·系统开发
万事可爱^2 小时前
Claude 新发布的 Opus 5,系统提示语删了 80%,半价还能逼近 Fable 5
android·服务器·数据库·人工智能·claude
早点睡啊Y3 小时前
深入学LangChain官方文档:Observability 与 Studio——先看清 Agent 到底做了什么
java·数据库·langchain
@insist1233 小时前
信息系统管理工程师-数据层知识点详解:存储、数据库与信息安全
数据库·软考·软件水平考试·信息系统管理工程师·软考信管
她说可以呀3 小时前
Redis集群
数据库·redis·缓存
Lorin 洛林3 小时前
为什么大模型总会“胡说八道“?一文彻底搞懂 RAG 知识库原理!
数据库
其实防守也摸鱼3 小时前
GitHub开源项目破圈方法论:从技术自嗨到生态共赢
服务器·数据库·学习·开源·github·命令行·linux系统
羑悻的小杀马特3 小时前
AI判不了设备异常?缺的不是算法,是这层数据底座
数据库·人工智能·ai
网安墨雨3 小时前
MySQL数据库 SQL语句详解
自动化测试·软件测试·数据库·python·sql·mysql