# 规范
# 数据库设计规范
- 数据库不应该创建过多的表
在wiretiger引擎中,每个集合都需要创建多个文件夹来保存元数据、数据及索引,磁盘上过多的小文件会导致性能下降。
建议单个数据库的表个数控制在100个以内,整个数据库实例的表数量控制在2000个以内。 - 数据库最好以db开头,不能包含除_以外的特殊字符,所有字母全部小写,数据库名不超过64个字符。
- 集合名最好以t_开头,不能包含除_以外的特殊字符,集合名不超过120个字符。
# 索引规范
- 创建索引时要带上background参数,特别是生产环境
MongoDB 4.2及之前版本,createIndex()命令默认是foreground模式,这种模式下创建索引会阻塞数据库的所有操作,会造成业务中。示例: db.users.createIndex({user_id:1}, {unique:true, background:true}) - 排序字段需要创建索引,避免业务大量在内存中排序造成数据库OOM(Out Of Memory)
- MongoDB 4.2及之前的版本,一条查询默认只允许使用32MB内存进行排序,如果超出会提示Sort operation used more than the maximum 33554432 bytes of RAM错误,通过执行db.adminCommand({setParameter : 1,"internalQueryExecMaxBlockingSortBytes" : 104857600})调整内存。
- 生产环境禁止调整,这样会增大数据库OOM的概率。建索引时就应该考滤加入排序字段。
- MongoDB 4.4版本虽然提供了磁盘排序的选项来避免排序消耗大量内存,但建议最好使用索引排序。
- 一个表不宜创建过多的索引 MongoDB插入每条数据的时候同时需要写索引。索引越多,写入数据时就要花费更多的代价。单个表索引建议不超过10个。
- 最好定期清理无用的索引
索引在写操作时会带来额外的资源消耗,所以需要尽量精简索引。
MongoDB 4.4版本之后,可使用hidden index先隐藏掉无用的索引,隐藏后业务确认正常后再删除索引。 - 按照最左匹配原则,如果单字段索引已经被复合索引包含,应该删除,因为额外的索引会造成写操作时的性能浪费。
- 尽量避免$ne/$nin等操作,和其他数据库(如MySQL)一样,不等于及not in类的操作无法有效利用索引,应该避免使用。
- 考滤使用区分度大的字段建立索引
如果索引字段区分度较小,查询扫描的行数依然会比较多,查询效率较低,对数据库负载影响较大。
# 数据库操作规范
# 禁止类
- 操作命令应该使用explain()确认执行计划
上线执行操作命令前需要用explain()确认执行计划是否符合预期,否则上线可能会引起故障。 - 生产环境禁止关闭鉴权
关闭鉴权会将数据库暴露给所有人,特别是数据库服务器开通了外网。 - admin,local库不能存储业务数据
admin库读写时会加db锁,影响性能;local库只会保存到本地,不会复制到从节点,如果发生主人切换会丢失数据。 - 禁止执行db.dropDatabase()命令后再创建同名的db
- MongoDB 4.0及之前的版本,官方文档要求删除db并创建同名db后,业务读写数据前需要在所有mongos节点上执行重启或flushRouterConfig命令。
- MongoDB 4.2及之后的版本,需要对所有的mongos和mongod节点重启或执行flushRouterConfig命令。
所以禁止在业务代码中直接执行db.dropDatabase()命令后再创建同名的db。
- 高并发高性能场景,禁止过度使用in和or
in 或者or 条件语句在数据库底层需要转换成多次查询,过多的in和or操作在高并发高性能场景,会严重影响请求的响应时延及数据库负载。 - 高并发高性能场景,禁止将复杂的运算操作交给数据库进行
MongoDB 提供了强大的计算能力(如 MapReduce 等),这些特性对开发人员非常友好,极大减轻了业务逻辑。但是这些运算不可避免是需要资源的,如果将复杂运算下沉到数据库层,高并发场景势必会给数据库造成极大的负担,数据库一旦故障会造成整个系统雪崩。
建议在高并发高性能的场景下,数据库操作保持简单,复杂的运算交给服务器并适当在数据库前端增加缓存。 - 线上业务禁止直接进行批量数据 remove
remove 命令到数据库后会先查询符合删除条件记录的_id,之后一条条按照_id 进行删除,并记录到 oplog 中(删除每条记录都会写一条 oplog)。
当满足 remove 条件的数据较多时对数据库压力较大,且极容易引起主从延迟突然增大。
线上业务建议直接用 drop 集合或用脚本一条条删除并控制删除速度。或尽量使用 ttl 索引。 - 业务禁止自定义 _id 字段
_id 是 MongoDB 内部的默认主键,默认这是一个自增的序列。如果自定义_id 并且业务无法保证_id 递增,每次插入数据后,_id 索引不可避免需要对 B 树索引进行调整,这将对数据库带来额外的负担。 - 副本集直连 mongod 节点的场景,使用禁止只在连接串中配置单个 IP;分片集群集群禁止只连接单个 mongos 地址(除非 mongos 和应用服务器部署在一起)
线上业务如果只连接副本集主节点,一旦数据库发生 HA 会造成写入中断;如果只连接单个 mongos,这个 mongos 故障后会造成业务中断。 - 线上业务禁止设置 Write Concern j:false
Write Concern 默认一般为 j:true,表示服务端会写入 journal log 完成后再向 client 端返回。一般请勿设置 j:false,否则进程突然故障重启后,可能会造成数据丢失。 - update 语句中禁止不带条件的更新
业务代码要确保query参数不能传{},否则会造成全表数据更新。推荐保持 multi 为默认值(false)。 - 禁止更新数组内部分元素时,将数组全部拿出来更新后再写回去
推荐使用arrayFileters (opens new window)仅对需要的元素进行修改。
# 建议类
- 建议局部读写而不是全读全写
查询语句中应尽量使用 $projection 运算符投影出需要的字段;在 update 命令中如果只是修改某个字段,建议使用 $set,请勿将文档全部读出来修改后再全量写进去。 - 线上环境慎重使用 db.collection.renameCollection() 命令
renameCollection() 在4.0及之前的版本会阻塞 db 的所有操作;在4.2及其之后版本会阻塞当前表及目标表的操作。而且 renameCollection() 执行期间会造成游标失效、changeStream 失效及带 --oplog 命令的 mongodump 失败等问题。线上环境禁止高峰期直接操作。 - 建议核心业务配置 WriteConcern 为 {w: “majority”} 参数
默认情况下,一般驱动的 WriteConcern 配置为 {w:1},即在主节点写入完成后认为请求成功。如果机器突然发生故障并且写入的数据还未复制到从节点,这样的配置会导致数据丢失。
因此对于线上的核心业务,建议配置 {w: “majority”},这样的配置会等数据同步大多数节点后再返回客户端。当然可靠性和性能不能兼顾,选择了 {w: “majority”} 配置后请求的延迟也会相应的增加。
# 不建议类
- 除非必要,不要在高性能场景大量使用多文档事务
MongoDB 4.0 及之后的版本,MongoDB 提供了多文档事务。但是多文档事务只是 MongoDB 数据库能力的补充,在高并发高性能场景下,大规模使用多文档事务需要进行充分的压测。
一般来说,多文档事务提交前需要在内存中保留快照,这可能消耗大量的 cache 从而导致性能下降。 - 不建议使用短连接
MongoDB 的认证逻辑是一个比较复杂的运算过程,而且默认 MongoDB 会为每个连接创建一个线程。大量短连接会对数据库产生较大的负担,特别是没有 mongos 的副本集集群。建议使用长连接,详细参考 mongodb url 中 Connection Pool 参数。
# 分片集群设计规范
- 如果使用 _id 字段作为片键,禁止使用范围分片
id 默认是一个递增的序列,随着数据量的增加会一直增大。如果_id 作为片键并使用范围分片,集群随着数据的插入不断的进行 balance。 - 分片集群禁止直连 mongod 节点写数据
分片集群应该通过 mongos 写数据,直接通过 mongod 写入的数据无路由信息,会导致访问不到。 - 线上环境禁止长时间关闭 balancer 和 autoSplit 配置
关闭 balance 会导致片之间数据不均衡,关闭 autoSplit 可能会产生 jumbo chunk。 - 分片表尽量避免不带片键的查询
分片表不带片键进行查询,需要扫描所有分片后在mongos聚合结果,比较消耗性能。 - 线上环境务必设置balancer窗口,避免balance对业务造成影响
balance过程会明显对数据库造成较大的压力,应该设置在业务低峰期进行。 - 应该使用区分度较大的字段作为片键,最理想的情况是使用唯一主键作为片键
比如用户表里有性别和姓名这两个字段,一般情况使用性别作为片键的区分度比姓名作为片键的区分度低,因为理论上性别相同的数据会有一半。 - 如果使用hash分片,建议进行预分配,特别是表比较大且经常需要大量插入数据
shardCollection()命令默认每个分片只会创建2个chunk,随着数据越来越大,MongoDB需要不断地balance和splitChunk,频繁地数据块拆分会给数据库带来较大压力。 - 没有按片键顺序扫描的强需求,不建议使用range分片,推荐hash分片
range分片容易引起不均衡和数据热点,而且因为无法预分片所以随着数据的写入balance不可避免,因此不建议使用,除非有特殊的近片键范围查询需求。 - 分片集群中不建议使用非分片表
MongoDB的分片集群如果未执行shardCollection命令,默认数据只存储在主分片上。大量未分片的表会造成分片间的数据量不平衡。集群长时间运行下去,可能会造成某片数据量特别多,运维在这种情况下不得不使用movePrimary手动进行数据拆迁,从而增加运维复杂度。