MongoDB爱好者
垂直技术交流平台

关于索引的常见问题

把我遇到过的跟索引相关的问题梳理一遍。

基础知识

索引的基础知识还是先过一遍,以免有人还不清楚。如果你还卡在这些问题的理解上面,需要反思一下自己基础知识是否学好了哟。

复合索引的顺序

MongoDB不会收集数据的统计信息,没有直方图之类的东西,所以创建复合索引时也不需要考虑数据倾斜的问题,用最基本的ESR原则来思考就可以了:

  • Equality – 等值条件
  • Sort – 排序条件
  • Range – 范围条件

顺便说句,虽然索引中字段的顺序很重要,但查询条件中只要是AND的关系时,条件出现的顺序是无关紧要的。我也不知道为什么会有不少人跟我争论这个……关于这点如果你现在脑海中有不同的意见,冷静,再好好想想。

区分度

有些字段的区分度很低,比如性别,它们适合放在复合索引里吗?至少从MongoDB的角度而言,区分度不是个大问题。如果频繁作为搜索条件出现的话,我甚至推荐把区分度低的字段放在前面。原因如下:

  • MongoDB是把复合索引的所有字段连接在一起作为索引键的,所以单个字段的区分度影响不大,所有字段在一起有区分度就可以了;
  • MongoDB的索引在任何时候都启用前缀压缩,这使得区分度低的字段在前面时,反而会因为前缀压缩而得到更大的收益。当然,能否放在前面还需要看你的查询条件是否能够命中索引前缀;

前缀压缩:当一个字符串在索引键出现过一次,再在其他键中出现时,就不需要再完整地存储下来了。例如,当出现过apple后再出现apple iphone则后者不需要存储整个字符串,只需要存储一个指针指向apple,再存储 iphone就可以了。在一些特定场景中,前缀压缩可以达到非常好的压缩效果。

索引前缀

当一个复合索引{a: 1, b: 1, c: 1}存在时,所有能够命中它的前缀的查询,都能够使用这个索引。换句话说,当前述索引存在时,以下所有就没有存在的必要了,直接删除即可:

  • {a: 1, b: 1}
  • {a: 1}

索引的问题

唯一索引

在分片集中,唯一索引生效的充分必要条件是:唯一索引键正好是片键。除此之外唯一索引都不能保证全局唯一。另外有少数版本出现过唯一索引失效的bug,请参考SERVER-58936

后台索引

这大概是遇到最多的问题之一了。在&lt4.2的版本中,MongoDB创建索引时索引分为前台和后台两种模式:

  • 前台索引:创建更快,B树更均衡,但占用库级锁
  • 后台索引:创建更慢,但不占用锁;

因为库级锁的原因,在4.2之前几乎没有机会使用前台索引,除非:

  • 系统还没上线;
  • 线上系统有足够的维护时间;
  • 正在使用滚动维护来构建索引;

滚动维护:将复制集的从节点单独拆出来,以独立模式重启然后进行维护的工作方式。最后再进行主从切换后完成原主节点的维护。

如果在线上环境直接使用前台索引,可想而知会发生什么问题。在MongoDB 2.x的时代,曾经有过一个bug,即使在主节点上使用了{background: true}来创建后台索引,在该操作复制到从节点时,{background: true}没有正确复制,导致从节点上创建索引变为前台索引,进而造成一些问题。严格来讲这其实也不算bug,如果你从最终一致性的角度来思考这个问题,其实这种做法是有它的道理的。
不管怎么说,已经是陈芝麻烂谷子的事情,但印象特别深刻。如果你还在使用2.x版本,与其了解这些无用的知识还不如早日升级吧。

幸运的是,从4.2开始MongoDB支持混合索引,综合了两者的长处,所以不再需要区分前后台索引。

杀死索引构建的时机

如果一个索引构建过程已经开始了,想要结束它该怎么办?首先,索引是一个消耗极大的动作,即使使用了后台模式,索引不可避免地还是需要遍历整个数据表,这会造成:

  • 大量冷数据需要从磁盘加载到内存中,它会:
    • 需要驱逐一部分热数据来腾出空间;
    • 显著增加磁盘读I/O;
  • 消耗可观的CPU运算资源来构建B树;
  • 索引的写入还会造成一定的写I/O增加;

所有这些原因都暗示着,索引应该是一个慎之又慎的动作,它

  • 不应该在业务高峰期进行;
  • 要么别开始,一旦开始就不应该轻易去停止它(消耗的资源已经消耗了,结束即意味着浪费);

但是,如果就是想要结束掉索引创建的进程,你需要先了解索引创建是怎样工作的(以下描述适用于&lte4.2):

  1. 在主节点上创建索引(前台或后台);
  2. 索引完成时createIndex指令被写入oplog中;
  3. 从节点开始以同样的方式创建索引;
  4. 索引完成时createIndex指令写入从节点的oplog中;

细心的朋友可能已经发现问题了,凡是进入oplog的东西,是一定会保证在主节点和从节点上是一致的。那么问题来了,想杀掉一个索引构建进程,就只能在createIndex进入oplog之前。换句话说,只有在主节点的索引构建还没完成时,才可以终止索引。在这之后,由于要保证数据一致性,从节点必须完成该索引的创建,杀死索引创建进程只会让它从头开始而已
那有人又要想了,既然不能直接杀死从节点上的索引构建,那我就在主节点上再下一条dropIndex指令,我们就当一切没有发生过好不好?我只想说,你想得美。从因果一致性的角度考虑,dropIndex必须发生在createIndex之后,所以这并不能帮你越过创建索引的步骤去直接删除它。说人话就是,该等的还得等。并且,dropIndex会被正在执行的createIndex阻塞。后者在{background: true}加持下虽然不会占用库级锁了,但前者会。结果就是,处在排队中的dropIndex阻塞了整个从节点的所有操作。对于使用读写分离的用户来说,这个结果是灾难性的。更不幸的是这个时候除了等以外没有别的办法,非常被动。还有更坏的消息吗?有的!等了也是白等。因为索引一旦建完就会马上被删除。所以你经历了一大段宕机时间,最后什么也没得到,赔了夫人又折兵。

总结一下就是,对索引的操作要慎之又慎,不要随便乱来

CommitQuorum

为了解决上述问题,从4.4开始对索引创建动作作出了大刀阔斧的改进。新的索引创建流程会同时在主从节点上创建索引,当足够多的节点完成索引时,主节点才会把索引标记为可用。多少节点算“足够多”呢?它就是由commitQuorum来控制的(具体参考文档)。默认情况下它是所有有投票权的节点。而shell在执行命令时也会等到commitQuorum满足之后才会结束阻塞(注意这里是shell阻塞,不影响服务器)。看出问题了吗?有仲裁节点,或者当前存在离线的节点时,你需要配置好合适的commitQuorum才能正确执行创建索引指令,否则createIndex将永远无法结束。那么当你无意中设置了过大的commitQuorum时该怎么办?不用担心,还有setIndexCommitQuorum可以帮你修改错误的设置。
一同改进的还有dropIndex的行为。当一个索引正在创建中时,现在你可以直接在主节点上发起dropIndex来停止正在进行的索引构建工作,而不会造成前一章所说的问题。动手前请先阅读文档Aborts In-Progress Index Builds哦。但同样的,我更希望你在开始前就考虑好,而不是事后补救。

PS:除了解决上述问题以外,这个改进还带来一个额外的好处,那就是可以使创建索引花费的总时间降到原来的一半,因为原来是先主后从,现在是主从一起上。

结束语

说到底,解决问题的最好办法是防患于未然。与其问“从10楼掉下来怎么能不摔死”,不如问“怎么可以避免从10楼掉下来”。虽然后者需要长时间的经验积累,但还是不推荐尝试前者。我这么说好像很可笑,但换到技术问题上很多人就是转不过这个弯来。祝大家没有宕机时间,不用半夜加班吧!

参考链接

赞(14)
未经允许不得转载:MongoDB中文社区 » 关于索引的常见问题

评论 1

评论前必须登录!

 

  1. #1

    学习了 🙏

    sunhaolin1年前 (2023-03-01)