MongoDB 基础系列四:数据建模 Data Model 注意事项

前言

此篇博文是 Mongdb 基础系列之一;主要介绍 MongoDB 的数据建模相关内容;

当进行数据建模的时候,处理全盘考虑读写相关的操作以外,需要考虑本文所描述的这些因素;

本文为作者的原创作品,转载需注明出处;

Document Growth

正如 Embedded 建模模型中的坏处中所提到的那样,document 可以通过新增字段或者 push elements into array field 来迅速增加 document 的大小;

如果使用 MMAPv1 存储引擎,当某个 document 的存储大小已经超出了为它预设的大小值,MongoDB 将会对该 document 进行 relocate (我的疑问,是整体移动还是部分移动?);然后,通过 Power of 2 Sized Allocations 最大限度的减少了这种 relocate 的情况发生;

然后,如果 document 的增加速度远超预期,并且很快就突破了 Power of 2 Sized Allocations 的限定范文,那么应当考虑使用 References 的建模方式,将与之关联的 related entities 定义到不同的 documents 中去,而当前的 denormolized 的数据建模方式就不太适用了;

不过,你可以通过 pre-allocation 的策略来避免 document 的增长;参考 Pre-Aggregated Reports Use Case 的例子看看是如何通过 pre-allocation 的策略来避免 document 增长的;

Atomicity

In MongoDB, operations are atomic at the document level. No single write operation can change more than one document. Operations that modify more than a single document in a collection still operate on one document at a time. [1] Ensure that your application stores all fields with atomic dependency requirements in the same document. If the application can tolerate non-atomic updates for two pieces of data, you can store these data in separate documents.

在 MongoDB 中,写入操作的原子性是在 document 级别上的;一个写入操作不可能同时改变多个 documents;即便是当一个写入操作作用在一个 collection 上,其底层仍然是一个一个 document 来进行处理的;如果应用可以容忍写入操作的非原子性要求,那么可以将 document 相关联的 entities 分散在多个 documents 中,既是使用 normolized 的建模方式;

A data model that embeds related data in a single document facilitates these kinds of atomic operations. For data models that store references between related pieces of data, the application must issue separate read and write operations to retrieve and modify these related pieces of data.

如果使用 Reference 的建模方式,那么当要去获取相关的 related entities 的时候,就必须发起单独的读和写的查询操作去获得相关的 related data;相关用例可以参考 Model Data for Atomic Operations

Sharding

MongoDB 通过 sharding 来提供水平扩展;Sharing 允许用户对一个 database 中的 collection 通过 partition 操作将该 collection 中的 documents 分散到一系列的 mongod 实例当中;

MongoDB 使用 sharding key 在分布式场景中来分发数据,以及在多个 sharded collection 中进行数据传递;选择合适的 sharding key 不仅可以显著的提升性能,并且可以保证查询之间的隔离性,以及提升写入的性能;

查看 ShardingShard Keys获取更多的信息;

Indexes

使用 Indexes 来提升普通查询的查询性能;索引通常需要建立在,经常出现在查询中的字段,需要对查询结果进行排序的所有查询操作上;MongoDB 会自动在 _id 字段上创建索引;

当你创建索引的时候,需要考虑到如下的因素,

  • 每一个索引至少需要 8K 的存储空间;
  • 索引会影响写入的性能;如果某些 Collections 的写入频率高于读取的频率,那么索引将会严重的影响到写入的性能,毕竟每次写入都需要额外的开销去维护索引;
  • 相反,如果某些 Collections 的读取频率远远高于写入的频率,那么索引将会显著提升查询的性能;
  • 当索引启动,每一个 index 将同时暂用磁盘和内存的空间;而且这个占用的空间大小是显著的,所以,当决定采用 index 的时候,需要考虑磁盘和内存的空间规划以免超过系统负载;

参考 Indexing StrategiesAnalyze Query Performance 获取更多信息,同时参考 MongoDB 的 database profiler 可以帮助你鉴定一些无效的查询;

Large Number of Collections

在某些情况下,为了更好的建模,你可能会将相关的信息存放在不同的 collections 当中而不是将这些信息放入一个 collection 当中;比如,我们有这样一个例子,我们需要针对不同的环境和应用存储不同的日志,我们的 logs collection 包含如下的 document 格式内容:

1
2
{ log: "dev", ts: ..., info: ... }
{ log: "debug", ts: ..., info: ...}

然后,在建模的过程中,你希望根据不同的日志类型( dev、debug …)将日志分别以 document 存储在不同的 collections 当中;所以我们可以创建注入 logs_dev、logs_debug 这样的 collections 来分别存储不同日志类型的日志数据;

通常而言,当有大量的 collections 并不会影响性能反而会有很好的性能表现;多个不同的 collections 对高通量的大批量的查询操作是至关重要的;

当考虑使用大量的 collections 来建模的时候,需要考虑如下的行为,

  • 每一个 collection 将会有一些 K bytes 的额外开销;
  • 每一个 index,包括 _id 字段的开销,都至少需要 8 K 的数据空间;
  • 针对每一个 database,MongoDB 都提供了一个唯一的 namespace file( 通常命名为 <database>.ns) 用来存储该 database 的所有元数据,然后,每一个 index 和 collection 都在该 namespace file 中有自己的记录;namespace file 是有大小限制的,如果使用 MMAPv1 引擎,namespace file 的文件大小不能超过 2047M,然而默认情况下,namespace 文件的最大值被设置为 16M,可以通过 nsize 操作来更改;如果使用的是 WiredTiger 存储引擎,将不会受此约束;
  • 当 MongoDB 使用 MMAPv1 存储引擎的时候,namespace file 的大小是受限的,所以,你可能很想知道当前 namespace 的大小占用是多少,可以通过如下的指令来获取,

    1
    db.system.namespaces.count()

    不过 namespace 文件的大小依赖于 <database>.ns 文件中的设置,通常该默认值是 16M;如果想要更改该值的大小,根据不同的情况将由如下的不同的操作

Collection Contains Large Number of Small Documents

当你有大量的小的 documents 的时候,为了性能的,你应当考虑使用 Embedded 的建模方式;并且,试图将这些分散的,大量的小的 documents 通过 “rolling-up” 的方式,将它们重构为一个 document 的 array;这样可以非常有效的提升访问的效率;

Storage Optimization for Small Documents

如果你有大量小的 Documents 文档,通常一个文档中可以只有两三个 fields;那么可以考虑如下因素来优化存储,提升存储效率;

  • 显式的使用 _id 字段
    因为 MongoDB 会自动的为每一个 documents 生成一个 12 bytes 的 ObjectId 作为 _id 字段值;并且 MongoDB 会自动的对 _id 进行索引,所以,该 _id 字段所消耗的存储空间是显著的;这个对有大量的 Small Documents 的存储开销是巨大的,所以,一般建议自定义 _id 值,比如定义一个只占用 4 bytes 的字段值;不过,在自定义 _id 的时候,需要注意的是,必须保证其全局唯一;
  • 使用更短的字段名
    MongoDB 将会存储每一个 document 中的字段名,虽然单独每一个 document 的字段名所占用的空间并不大,但是,如果有海量的 small documents,那么这个空间就不可忽略了;举个例子如何来使用更短的字段名,

    1
    { last_name : "Smith", best_score: 3.9 }

    可以简写为、

    1
    { lname : "Smith", score : 3.9 }
  • Embed document
    Collection Contains Large Number of Small Documents 中所描述的那样,可以将大量的,相似的 documents 通过 “rolling up” 的方式形成一个 array,将其赋值给某个 Document;

Data Lifecycle Management

建模的时候,需要考虑数据的生命周期;Collections 的 Time to Live or TTL 的特性允许 collections 中的 documents 在某段时间以后自动过期;比如,你只期望某些数据在数据库中只存储一段特定的时间,可以考虑使用 TTL;额外的,如果你只希望使用最近新增的 documents,考虑使用 Capped Collections

Reference

https://docs.mongodb.com/manual/core/data-model-operations/