记一次MongoDB分片集群分片不均衡和索引缺失的优化处理

1,774 阅读6分钟

本文正在参加「技术专题19期 漫谈数据库技术」活动

记一次MongoDB分片集群分片不均衡和索引缺失的优化处理

问题现象

项目中使用MongoDB(4.0版本)用来存储一些非关系型数据,近期需要做环境割接打算从线下自己服务器中搬到腾讯云中并且使用云服务,项目本次需要调整几个地方为了提高平台的扩展性和可靠性,但是因为一些历史原因遇到一下几个问题(线下环境MongoDB采用ReplicaSet线上计划换成分片集群的方式):

  • MongoDB服务迁移后腾讯云后,遇到MongoDB分片集群实例中不同分片实例间CPU使用率不均匀问题(其中一个分片CPU使用率0%,另一个CPU使用率一直100%<后来发现是因为慢查询过多导致的>)
  • 腾讯云MongoDB分片实例持续长时间慢查询 mongodb优化-cpu使用率持续100%.png mongodb优化前-慢查询全天持续1.png 通过图片以看到慢查询一直持续,并且CPU使用率持续100%
  • 还有一个问题就是一共两个分片,其中只有一个分片实例有数据,另一个分片无数据存储不均匀; mongodb分片0-使用率0.png mongodb分片1-使用率35.png 可以看到两个实例中一个存储是0

原因分析

  • 看到一直慢查询一直持续,猜测可能CPU使用率持续100%和慢查询有关,那么先优化慢查询先将慢查询的问题解决掉,看看CPU是否可以正常
  • 对于分片中存储不均匀的问题,初步猜测可能因为和MongoDB在线下是replicaset,线上是分片集群那么数据存储没有设置分片键,那么MongoDB无法进行数据分片分配,因此出现数据分配不均匀的问题,要解决这个问题需要设置分片键,那么分片键该如果设置;
  • 当前业务中没有进行分片键设置,并且代码中也没有考虑过分片键的场景,设置分片键之后对于系统会不会产生什么影响; 根据问题表象来看很有可能是以上原因造成的,那么分别来一个一个问题来验证解决

处理方式

慢查询处理

通过分析慢查询日志发现,平台有很多慢查询,再根据集合的索引进行分析发现索引创建不合理并且有很多集合缺失索引,因为集合太多打算写一个批量创建索引的脚本来创建索引,这里需要注意的是创建索要指定{backgroup:true}否则会锁库,添加了backgroup参数后创建索引操作会在后台进行不会锁库。

  • 创建单列索引
use cms;
//创建索引
db.getCollection("cms.ins").createIndex({"_account":1},{background:true});
//查看索引
db.getCollection("cms.ins").getIndexes();
  • 批量创建联合索引
//基于_account_corp_accessId_region组成的联合索引(cms.*中排除_account,_corp_accessId的表和其他中的表)
"cms.*","diag.platform.component","orch.serviceDir"

//基于_account,_corp_accessId组成的联合索引
"cms.alert",cms.cdn","cms.monitor.month.avg","cms.project","cms.zdiskoffering","cms.zinstanceoffering","sws.monitor.alert.history","sws.monitor.alert.strategy.corp"

//其他暂不添加联合索引
"type","job","flavor","region","site.monitor"

var cmsColls = db.getCollectionNames();
var acaCmsColls = ["cms.alert","cms.cdn","cms.monitor.month.avg","cms.project","cms.zdiskoffering","cms.zinstanceoffering"];
var unhandlerCmsColls = ["type","job","flavor","region","site.monitor"];
cmsColls.forEach(function(item){
	var cmsReg = RegExp("cms.*");
	if(cmsReg.test(item) && !unhandlerCmsColls.includes(item)){
		if(acaCmsColls.includes(item)){
			db.getCollection(item).createIndex({"_account":1,"_corp":1,"_accessId":1},{"name":"_account_corp_accessId_ASC",background:true});
			print(item + "是sws开头创建_account_corp_accessId")
		}else{
			db.getCollection(item).createIndex({"_account":1,"_corp":1,"_accessId":1,"_region":1},{"name":"_account_corp_accessId_region_ASC",background:true});
			print(item + "是cms开头创建_account_corp_accessId_region")
		}	
	}else if(item == "sws.monitor.alert.history" || item == "sws.monitor.alert.strategy.corp"){
		db.getCollection(item).createIndex({"_account":1,"_corp":1,"_accessId":1},{"name":"_account_corp_accessId_ASC",background:true});
		print(item + "是sws开头创建_account_corp_accessId")
	}
});

索引创建完成后,观察一段时间后发现持续慢查询的告警消失了,并且资源CPU使用率也下降了,到这里解决了慢查询导致的CPU使用率100%的问题,效果如下图所示。 mongodb优化-cpu使用率-优化后.png mongodb优化前-慢查询-优化后.png 注意:另外在我遇到的问题中还有一点就是操作mongodb的有些定时任务代码执行周期有些过于频繁,在结合业务分析后发现,原本小时级别就可以支持的业务写到分钟级别,同时因为慢查询和较多的任务,导致持续长时间的慢查询(加索引后平均cpu使用率降低到30%左右,减少定时周期后cpu使用率降低到20%左右)

MongoDB replicaset模式转分片实例引起的存储不均匀问题处理

根据业务来看为了满足查询的需要,我将大多数集合选择按照范围划分分片,在这里整理了几点设置和使用分片键的一些事项

  • 设置分片键的字段必须要有以该字段开始的索引,以该字段创建的索引否则会遇到
mongos> db.adminCommand( { shardCollection: "cms.ins", key: { _account: 1 } } );
{
	"ok" : 0,
	"errmsg" : "Please create an index that starts with the proposed shard key before sharding the collection",
	"code" : 72,
	"codeName" : "InvalidOptions",
	"operationTime" : Timestamp(1650360549, 12),
	"$clusterTime" : {
		"clusterTime" : Timestamp(1650360549, 12),
		"signature" : {
			"hash" : BinData(0,"WAv1ZgnAUiAHwIt14/rkOe8Ue6k="),
			"keyId" : NumberLong("7075687118097350685")
		}
	}
}
mongos> db.getCollection("cms.ins").getShardDistribution();
Collection cms.ins is not sharded.
  • 分片键字段不可以存在值为null的记录,需要清理掉无效的数据或者补全数据db.getCollection("cms.ins").remove({_account : null});
  • 分片键一经设置后,分片键字段的值不可以修改(很重要切记)
mongos> db.getCollection("cms.ins").update({"_srn":"srn:tcs:cvm:ap-beijing:111:instance/ins-2uxjhl0h","_account":111},{"$set":{"_region":"ap-beijing1-分片键更新","instanceId":"ins-111-分片键更新","_account":112}});
WriteResult({
	"nMatched" : 0,
	"nUpserted" : 0,
	"nModified" : 0,
	"writeError" : {
		"code" : 66,
		"errmsg" : "Performing an update on the path '_account' would modify the immutable field '_account'"
	}
})
mongos>
  • 在将集合设置为分片集合后,更新记录的时候,必须要在跟新条件中指定主键或者分片键字段,否则会更新失败(很关键要切记)
    • 因为我们早期没有考虑到分片键,因此代码中很多更新条件中缺少分片键;
    • 另一点就是有的代码中更新条件中也包含了分片键; 这两点让我们很是头疼,需要改代码早期没考虑到现在要处理了
mongos> db.getCollection("cms.ins").update({"_srn":"srn:tcs:cvm:ap-beijing:111:instance/ins-2uxjhl0h"},{"$set":{"_region":"ap-beijing1-分片键更新","instanceId":"ins-111-分片键更新"}});
WriteResult({
	"nMatched" : 0,
	"nUpserted" : 0,
	"nModified" : 0,
	"writeError" : {
		"code" : 61,
		"errmsg" : "A single update on a sharded collection must contain an exact match on _id (and have the collection default collation) or contain the shard key (and have the simple collation). Update request: { q: { _srn: \"srn:tcs:cvm:ap-beijing:111:instance/ins-2uxjhl0h\" }, u: { $set: { _region: \"ap-beijing1-分片键更新\", instanceId: \"ins-111-分片键更新\" } }, multi: false, upsert: false }, shard key pattern: { _account: 1.0 }"
	}
})
  • 设置分片键的相关命令
//1.enable数据库分片特性
db.adminCommand({ enablesharding:smartdb});
//2.采用范围的方式设置_account字段作为集合cms.ins的分片键字段,‘1’表示使用范围的方式创建分片键(如果想要指定hash的方式使用‘hashed’)
db.adminCommand({ shardCollection: "cms.ins", key: {_account: 1}});
db.adminCommand({ shardCollection: "cms.ins", key: {_account: "hashed"}});
//3.查看集合分片信息
db.getCollection("cms.ins").getShardDistribution();

集合设置好分片后,观察一段时间后发现两个分片中都有数据了,到这里数据不分片存储的问题也解决了。 mongodb分片0-使用率0.png mongodb分片0-使用率25.png

总结

其实本次问题分析下来,问题主要原因还是以下几点:

  • MongoDB使用不规范,该有的索引没有;
  • 其他一些事情就早期考虑不完善,MongoDB架构没有合理规划;
  • 代码中的定时采用合理符合业务场景的定时,减少不必要的资源浪费;
  • 代码中的更新字段做到合理; 整体下来看其实很多问题都是平时只要稍加注意都能避免的,因此还是要在平时的开发中严格把握质量。

在使用MongoDB分片键的时候,要注意分片键字段无法更新,另外更新分片集合的记录时候必须要指定主键和分片键,不能仅仅是业务主键了