一些 MongoDB 的坑

由于过去的历史原因,我们使用的默认 DB 是 MongoDB 数据库。MongoDB 数据库本身在支持非格式化的数据存储方面有比较大的优势,也不需要额外做很多的 Schema Migration,在我们项目初期,数据存储结构变动频繁时帮助非常大。

但是,随着我们的业务不断增长,我们也遇到了一些问题,这篇文章总结了一些我们在过去的过程中出现的一些问题或者失误,帮助大家在实践过程中进行规避。

集合使用不当问题

在使用 MongoDB 过程中,我们创建了大量的集合。这种情况在 WiredTriger 引擎下创建大量零散文件。这个在使用过程中暂时未对我们造成实际的业务较大影响,但是从实际使用而言,对整体数据库性能、后续主从复制集群同步,都是有一定影响的。在实际应用过程中应该避免:拥有过多 DB、一个 DB 下有过多 Collection,这样都会导致部分指令的性能大幅度下滑。同时,过多的 DB 也会导致主从同步失败 (listDatabse 超时导致失败,从库退出)。

数据库索引建立

MongoDB Collection 索引建立功能支持前台创建模式与后台创建模式两种。默认模式为前台创建模式。这种模式下会发生:DB 被锁,主库所有读写被禁止、从库无法访问。从另一方面来说,会导致业务系统的数据库访问受到影响。虽然前台创建模式效率更高,但是如果是线上操作,并且有一定数量级,创建索引时需要添加 {background:true} 让创建索引操作转换为后台操作,尽管时间较长,但是对业务影响较小。同时,该操作也应该在业务量小时进行。

数据库主从切换

虽然 MongoDB 的 Replica Set 功能可以方便的进行自动主从切换。但是实际使用过程中,我们也会遇到一些需要手工进行主从库切换的情况。比如,进行进行版本升级修复 BUG 或者安全漏洞。但是如果这个时候强行进行 Primary 的重启,则可能会出现未同步至 Secondary 的数据丢失的情况(血泪教训)。这种情况下,重启集群的方式是,优先进行 Secondary 的更新操作,在 Secondary 更新完成后,对 Primary 进行 stepDown 操作,等待主节点降级成为 Secondary 节点,之后进行操作。这样的好处是不会丢失数据。但是如果主从数据差异较大时,有可能会造成降级失败,此时可以重复执行 stepDown 直至成功。还有一个值得注意的是,即便 Secondary 可以停机维护,但是仍旧有可能丢数据,这个与 oplog 大小有关,我们放在后面说。

慢查询问题

慢查询的记录可以通过 Profiling 进行记录,这部分资料比较多,可以通过 db.setProfilingLevel() 进行管理。同时,主动分析也可以通过 explain 进行预分析。这一部分资料比较多,我这里就不再啰嗦了。但是还有一个问题是,部分慢查询操作不会随着客户端断开中断执行,需要通过 db.currentOp()db.killOp() 功能干掉长时间执行、浪费资源的操作。

主从同步延迟问题

主从同步问题是比较常见的延迟问题了,主要问题都是出在 oplog 上。毕竟主从同步机制都是通过此实现的。oplog 是固定大小的集合,如果满了,就会自动删除老的数据。

这里可以看到,此处查询 oplog 操作记录到从库同步,已经执行了 194 毫秒。

上面我们提到,当 Secondary 停机维护时也可能出现问题,就是因为,当 oplog 不足时,那么就有可能会导致数据同步失败。因此一般数据库停机维护操作,也应该放在业务量小的时候进行。不过 oplog 是可以调整的,可以参考 官方文档 进行调整。注意,这需要重启数据库来执行。

主从同步延迟一般出现的情况都是数据出现大量插入和修改的情况。在 Secondary 进行 oplog 重放时,开销会大于 Primary 进行操作的开销。这种情况会产生大量的 oplog(每条修改记录一条,非操作),记录越多,同步速度越慢。同时,某些操作也会大幅度提高单条记录的同步成本,比如 $inc$push 等等会让同步更慢。如果你对数据库中较大量数据进行 $set 操作,同样也会造成数据延迟。我们的一位工程师就因为这样曾经造成了分钟级别的延迟,教训也是非常惨痛的。这种操作比较频繁时,我们可以通过调整 replWriterThreadCount 参数进行 Secondary 重放线程数量调整。在 MongoDB 中默认为 16,这个可以根据情况进行具体调整。不过该参数为 MongoDB 启动参数,调整该参数会要求重启 MongoDB。另外一个值得注意的是这种操作会加大内存开销,如果内存紧张时也需要注意。