本文为2020年MongoDB应用案例与解决方案征集活动优秀应用案例:MongoDB在七牛云的应用,作者李鑫。
本文基于MongoDB 3.4/3.6版本描述,主要剖析在万亿级文档/百万级chunks场景下,路由管理模块对数据库操作的影响以及源码级优化思路。
现状
MongoDB的路由组织
MongoDB Sharding的数据组织形式为逻辑上是Chunk,将全局key分散到多个Chunk上,数据的迁移、平衡的单元就是一个Chunk。物理上是Shard(Mongod进程)。一个Shard就是一个Mongod进程,每个Shard存放多个Chunk数据。MongoDBSharding通过一个角色为Configsvr的副本集管理整个Sharding集群。通过Mongos代理,将client请求拆分,转发到指定Shard。
Chunks信息(查看config库的chunks collection):
Lastmod:第一部分是major version,一次movechunk命令操作(Chunk从一个Shard迁移到另一个Shard)会加1;第二个数字是Minor Version,一次split命令操作会加1。
Min:该Chunk管理的最小值,闭区间
Max:该Chunk管理的最大值,开区间
MongoDB的路由使用
下面以mongos为例,描述路由信息的管理模块。
核心代码实现一. 路由模块的内存结构
对于每一个collection,mongos会有一个全局变量ChunkManager(chunk_manager.h中),用来存放该collection的路由信息用图形描述数据结构,格式如下图:
ChunkMap:key为每一个Chunk的max值,value为Chunk的详细信息,包括:min,max,version(lastmod),lastmodEpoch(collection创建时生成的,在collection删除前不会变化),Shard等信息
ShardVersionMap:key为Shard名称,value为version,记录每个Shard的当前version
ChunkRangeMap:可以忽略,是个优化,不影响逻辑。
路由模块的使用
Mongos对于每一个client链接,第一次对collection进行操作时,会获取全局的ChunkManager镜像,保存ChunkManager镜像为缓存,记做cm,后续这个链接再访问collection的时候,直接使用cm进行路由信息的查询。
根据client发来的请求,将请求条件通过cm的ChunkMap查询到到这个请求需要转发给哪些Shard,进而进行转发。
MongoDB的路由变更
MongoSharding的balance通过movechunk命令,将一个chunk的数据从一个Shard迁移到另一个Shard,数据迁移完成后,修改Chunk的Lastmod。但是mongos作为无状态的proxy,是不会知道chunk的迁移过程和状态的,所以,mongos中的路由信息相对shard/configsvr进程是有延迟的,mongos感知路由变化的流程如下图:
从上面的流程中可以看到:
由于以上3条,当Chunks数量比较大的时候,拷贝和遍历都会耗时较久,同时以为必须等待路由刷新,才能访问到正确的shard,请求必然需要阻塞等待路由刷新。根据我们的使用情况,100w的Chunks,整个路由刷新过程大概在1s左右,也就是发生了movechunk,mongos层至少就会有秒级别的卡顿。同时,mongod发生movechunk,也要经过一次路由刷新,刷完后才能识别出mongos的version旧了,所以这里耗时也基本是是1s。于是,业务侧观察就会发现,在movechunk发生时,经常会有批量的请求超时。下图我们仅截取了mongos中路由刷新耗时情况:
优化思路
从上面的分析可以看出,卡顿阻塞主要是因为ChunkMap拷贝和遍历构建ShardVersiorMap造成的,加快这个速度,就是解决MongoDB Sharding路由刷新造成的卡顿的根本方向。
一. 官方的优化
Mongo3.4/3.6版本ChunkMap使用BSONObjIndexedMap,这个数据结构的性能不够好。既然BSONObjIndexedMap速度不够快,那么提供更快的数据结构,就可以优化这种情况。官方从3.7.1版本使用std::map替代BSONObjIndexedMap,提供更高的查询性能,来优化这个问题。根据实际测试,std::map的查询性能比BSONObjIndexedMap高出20%-30%,在一定程度上缓解这个问题。
但是对于百万级的chunks来说,耗时在1s+,优化的20%的耗时不解决根本问题。同时随着集群持续扩大,chunks数量继续增加,这里的优化杯水车薪。
二.加锁修改
BSONObjIndexedMap的拷贝和遍历,本质上是因为mongo中路由信息使用了copy and update的思路,这种方式的好处是普通的访问都是只读,不需要加锁;但是如果通过加锁的方式,将长尾的大耗时平均到每次请求,降低波动,也是可以接受的。这个方式我们尝试实现了,但是发现了非常大的缺陷:
还有一些其他问题,这里不在细说,总之,这个方案增加了风险且对性能有巨大影响。
三.多级索引
拷贝ChunkMap和构建ShardVersionMap的速度提高至少要有2个量级的优化才比较有意义,但是没有数据结构能够达到这种需求,那么就考虑减少每次拷贝的数量。
将ChunkMap拆分成多个小的ChunkMap。路由刷新时,找到特定的小的ChunkMap,将小ChunkMap做拷贝+修改,就可以控制每次拷贝的耗时。修改后的结构图如下:
新增TopIndexMap,做一级索引,结构为:map<string,shard_ptr<ChunkMap>>.其中TopIndexMap的key是value中存放的的ChunkMap的最大值(开区间)。查找时先在TopIndexMap中找到指定的ChunkMap,然后再在ChunkMap中找到chunk信息。
路由刷新时,创建一个新的“文档路由对象”记做new。将old的TopIndexMap拷贝到new(TopIndexMap规模可控在千级别),因为TopIndexMap的value都是存放的shard_ptr,所以整个拷贝过程就是TopIndexMap中的key+value的拷贝,不会对shard_ ptr内容拷贝。然后在根据变更的chunk,找到指定的需要修改的chunkMap,将old中的chunkMap拷贝一份,修改,再替换回去。这样没有变更的chunkMap没有拷贝操作。
优化后的效果:
Refresh的耗时控制在10ms级别。
后续
多级路由方案缺陷
对chunk进行一次movechunk操作,会涉及到2个变更。首先是接收端收到了一个新的chunk,major version会+1;发送端的管理的chunks也发生了变化,所以会在发送端源shard当中选择一个chunk(max最大的),将其majorversion +1,虽然这个chunk并没有发生迁移,但是对应到config.chunks表中,就会有2条记录变化。多级路由到方案是根据变化的chunks信息,来对ChunkManager中的ChunkMap和shardVersion进行变更,但是有一个场景:将一个shard中的唯一一个chunk迁移走,发送端源shard没有chunk了,在config.chunks表中就不会有一个chunks的major +1的操作,这种情况下,一次movechunk,就只有接收端shard有一个major变更。这就意味着,从变更的chunk信息来看,源shard没有发生变更,所以不会修改ShardVersionMap中的源shard的version。就导致如果这时有访问刚刚被迁移的chunk,mongos端和源shard端的version就一致了(2个都没有变化),但是这个chunk的数据应该在接收端,就产生了路由错误。
原来的方案是每次路由Refresh都会重建,多级路由方案是根据变更修改。所以,要解决这个问题,应该在一个shard的所有chunk都迁移走后,在config.chunks中增加一个虚拟的操作,让路由Refresh时能够触发将这个shard从ShardVersionMap中删除。因为我们的场景不需要缩容,所以这个工作还没有做。
作者介绍:
李鑫,七牛云技术专家。现负责七牛云元数据系统、万亿级文档数据库MongoDB开发维护。曾就职滴滴出行,蘑菇街,海康威视。多年分布式数据库领域设计及开发经验。曾参多个 NoSQL/NewSQL/时序数据库设计及开发工作。
为了挖掘MongoDB更多应用场景和案例实践,向用户和行业输送有启发、应用价值的思路和经验,MongoDB中文社区携手上海锦木和Tapdata于2020年12月开展MongoDB优秀解决方案暨应用案例征集活动。我们从创新性和应用价值的维度进行评选,评出本次案例征集活动最佳创新案例和优秀应用案例。
2020最佳创新案例获得者:徐靖
2020优秀应用案例获得者:李鑫 张爱强 王勇 马天艺
应用案例来自:圆通速递,七牛云,平安科技,京东,以及不便分享的神秘伙伴
更多案例请期待后期分享!
本文分享自 Mongoing中文社区 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!