MongoDB大数据量模型设计最佳实践

2021-08-06 10:57:14 浏览数 (1)

项目背景

数据质量项目,实时消费kafka数据,并经过流式计算后,需要展示并处理有问题的数据。

数据需求

(1) 数据的schema不固定,并且需要存储在一张表里,并且需要通过这个不固定的字段查询

(2) 需要存储时序数据,数据量较大。每天预估大概6000W条数据写入。

架构图:

基于MongoDB的解决方案

1. MongoDB天生的json处理能力,不需要固定的字段。并在4.2版本推出 Wildcard index 天生支持对不固定子集的查询(索引)。

*这个可以对子集中,不固定的集合自动创建索引,可以有效解决不固定字段的查询性能问题。

创建方式:db.data.createIndex({"unique_key_value.$**:1"})

对unique_key_value这个字段所有的子集建立通配符索引。

2. 使用MongoDB数据桶的方式来存储,数据容器里面的数据做时序,容器里面的数据加一个timestamp来标识。

原始数据结构4个文档(mysql中的4行数据):

原始数据模型结构

代码语言:javascript复制
代码语言:javascript复制
{
    _id: ObjectId(),
    deviceid: 1,
    date: ISODate("2019-11-10"),
    samples : [
       { info: 10, time: 1573833152},
       
   ]
}
代码语言:javascript复制
代码语言:javascript复制
代码语言:javascript复制
{
    _id: ObjectId(),
    deviceid: 1,
    date: ISODate("2019-11-10"),
    samples : [
       { info: 15, time : 1573833153},
 
   ]
}
代码语言:javascript复制
代码语言:javascript复制
代码语言:javascript复制
{
    _id: ObjectId(),
    deviceid: 1,
    date: ISODate("2019-11-10"),
    samples : [
       { info: 14, time: 1573833154},
 
   ]
}
代码语言:javascript复制
代码语言:javascript复制
代码语言:javascript复制
{
    _id: ObjectId(),
    deviceid: 1,
    date: ISODate("2019-11-10"),
    samples : [
       { info: 20, time : 1573833155},
 
   ]
}
代码语言:javascript复制

数据桶模型结构(由4个文档合并成了1个文档):

数据桶模型结构

代码语言:javascript复制
代码语言:javascript复制
{
    _id: ObjectId(),
    deviceid: 1,
    date: ISODate("2019-11-10"),
    first: 1573833152,
    last: 1573833155,
    samples : [
       { info: 10, time: 1573833152},
       { info: 15, time : 1573833153},
       { info: 14, time: 1573833154},
       { info: 20, time : 1573833155}
   ]
}
代码语言:javascript复制

数据模型设计思路:

业务需求,3W个设备,都要查询,但只需要查询最近2天的数据,那就按照时间维度来拆分,每个文档中保持1000条实时数据,每天大概6W个文档。这样,每次业务全量查询,只需要查询6W个文档即可。占用内存参考下面统计的数据大小。

这样不仅解决了时序的需求,同时也降低了冗余数据的存储,可以节约一大笔内存和磁盘开销。

但每个数据桶中存放多少条时序数据呢?

MongoDB本身一个文档大小限制为16M,这里考虑到,我们的设备会比较多,2W个设备。可能每个用户只需要查询其中几十或者几百个设备,所以,我们设计成上面数据桶的方式。

字段解释:

id —文档的ID(MongoDB的ObjectId) deviceId —查询的设备ID date—样品的日期;我们可以将其存储在此处以简化聚合 first —在存储桶中读取的最旧数据的时间戳 last —存储桶中读取的最新数据的时间戳 samples —数据容器

经过实际,测试每个文档对应一个设备id,每个文档中存放1000条设备记录查看这个文档大小:

评估实际场景当个文档大小

代码语言:javascript复制
rs01:SECONDARY>
rs01:SECONDARY>
rs01:SECONDARY>
rs01:SECONDARY>
rs01:SECONDARY> rs.slaveOk()
rs01:SECONDARY>
rs01:SECONDARY> use dq;
switched to db dq
rs01:SECONDARY>
rs01:SECONDARY> var doc = db.meta_ts_detail.find({ "_id" : ObjectId("60e660d02a0b6833efee50e6")})
rs01:SECONDARY>
rs01:SECONDARY> print(Object.bsonsize(doc))
79222
rs01:SECONDARY>
rs01:SECONDARY>
代码语言:javascript复制
可以看到,单个文档的大小为80K。

下面是研发按照实际生产环境数据模型压测结果:

MongoDB每秒upsert数量

代码语言:javascript复制
代码语言:javascript复制
insert query update delete getmore command dirty  used flushes vsize   res qrw  arw net_in net_out conn  set repl                time
    *0    *0   4398     *0     611   796|0  5.0% 72.7%       0 7.20G 3.98G 0|0  2|0  2.98m   15.5m  118 rs01  PRI Jul 16 02:54:17.526
    *0    *0   3295     *0     512   663|0  4.3% 72.4%       0 7.20G 3.98G 0|0  1|1  2.32m   15.6m  118 rs01  PRI Jul 16 02:54:18.516
    *0     1   4215     *0     741   941|0  4.5% 72.6%       0 7.20G 3.98G 0|0  1|1  3.08m   16.6m  118 rs01  PRI Jul 16 02:54:19.517
    *0    *0   4475     *0     602   732|0  4.8% 72.8%       0 7.20G 3.98G 0|0  1|0  2.95m   17.9m  118 rs01  PRI Jul 16 02:54:20.516
    *0    *0   4452     *0     640   795|0  5.0% 73.0%       0 7.20G 3.98G 0|0  1|0  3.02m   16.4m  118 rs01  PRI Jul 16 02:54:21.516
    *0     1   3408     *0     522   685|0  5.1% 73.1%       0 7.20G 3.98G 0|0  1|0  2.41m   11.7m  118 rs01  PRI Jul 16 02:54:22.517
    *0    *0     *0     *0       0     3|0  4.5% 72.9%       0 7.20G 3.98G 0|0  1|0   683b   39.1k  118 rs01  PRI Jul 16 02:54:23.516
    *0     2    339     *0      59    85|0  4.5% 72.9%       0 7.20G 3.98G 0|0  1|0   251k   1.52m  118 rs01  PRI Jul 16 02:54:24.516
    *0    *0   4163     *0     560   719|0  4.3% 73.1%       0 7.20G 3.97G 0|1  1|0  2.78m   18.1m  118 rs01  PRI Jul 16 02:54:25.518
    *0    *0   4484     *0     652   786|0  4.5% 73.3%       0 7.20G 3.97G 0|0  1|3  3.03m   18.3m  118 rs01  PRI Jul 16 02:54:26.516
insert query update delete getmore command dirty  used flushes vsize   res qrw  arw net_in net_out conn  set repl                time
    *0    *0   4618     *0     677   873|0  4.9% 73.5%       0 7.20G 3.97G 0|0  1|1  3.17m   16.6m  118 rs01  PRI Jul 16 02:54:27.516
    *0     1   2934     *0     473   628|0  5.1% 73.7%       0 7.20G 3.97G 0|0  1|0  2.10m   10.7m  118 rs01  PRI Jul 16 02:54:28.571
    *0    *0   4673     *0     658   843|0  4.8% 73.9%       0 7.20G 3.98G 0|0  1|3  3.17m   14.1m  118 rs01  PRI Jul 16 02:54:29.516
    *0    *0   4679     *0     568   708|0  4.5% 74.1%       0 7.20G 4.00G 0|0  1|2  3.03m   15.1m  118 rs01  PRI Jul 16 02:54:30.521
    *0    *0   4245     *0     494   642|0  3.7% 74.1%       0 7.20G 4.00G 0|0  1|1  2.74m   15.0m  118 rs01  PRI Jul 16 02:54:31.517
    *0    *0   3339     *0     395   490|0  3.3% 74.2%       0 7.20G 4.02G 0|0  1|0  2.13m   13.2m  118 rs01  PRI Jul 16 02:54:32.517
    *0    *0     *0     *0       0     2|0  3.3% 74.2%       0 7.20G 4.02G 0|0  1|0   681b   39.0k  118 rs01  PRI Jul 16 02:54:33.518
    *0    *0    201     *0      32    40|0  3.3% 74.2%       0 7.20G 4.02G 0|0  1|0   141k    828k  118 rs01  PRI Jul 16 02:54:34.518
    *0    *0   4173     *0     710   985|0  3.6% 74.4%       0 7.20G 4.02G 0|0  1|2  3.09m   17.9m  118 rs01  PRI Jul 16 02:54:35.517
    *0    *0   4320     *0     643   834|0  3.8% 74.6%       0 7.20G 4.02G 0|0  2|0  2.99m   16.8m  118 rs01  PRI Jul 16 02:54:36.516
insert query update delete getmore command dirty  used flushes vsize   res qrw  arw net_in net_out conn  set repl                time
    *0    *0   4495     *0     629   794|0  4.0% 74.8%       0 7.20G 4.02G 0|0  1|1  3.03m   16.4m  118 rs01  PRI Jul 16 02:54:37.518
    *0    *0   3543     *0     577   764|0  4.0% 74.9%       0 7.20G 4.02G 0|0  1|0  2.54m   13.0m  118 rs01  PRI Jul 16 02:54:38.516
    *0    *0   4252     *0     671   873|0  4.2% 75.0%       0 7.20G 4.02G 0|1  1|4  3.01m   13.4m  118 rs01  PRI Jul 16 02:54:39.516
    *0    *0   4654     *0     670   865|0  4.3% 75.2%       0 7.20G 4.02G 0|0  1|5  3.18m   15.1m  118 rs01  PRI Jul 16 02:54:40.517
    *0    *0   4522     *0     696   913|0  4.5% 75.4%       0 7.20G 4.02G 0|0  1|2  3.18m   17.4m  118 rs01  PRI Jul 16 02:54:41.516
    *0    *0   3276     *0     530   714|0  4.7% 75.5%       0 7.20G 4.02G 0|0  1|0  2.36m   13.1m  118 rs01  PRI Jul 16 02:54:42.516
    *0    *0     *0     *0       0     2|0  4.7% 75.5%       0 7.20G 4.02G 0|0  1|0   682b   39.0k  118 rs01  PRI Jul 16 02:54:43.517
    *0    *0    146     *0      14    21|0  4.0% 75.5%       1 7.20G 4.02G 0|1  1|1  90.2k    528k  118 rs01  PRI Jul 16 02:54:44.546
    *0    *0   4110     *0     676   892|0  3.6% 75.3%       0 7.20G 4.02G 0|0  2|2  2.96m   16.6m  118 rs01  PRI Jul 16 02:54:45.516
    *0    *0   4181     *0     407   512|0  3.1% 75.5%       0 7.20G 4.02G 0|0  1|3  2.54m   15.8m  118 rs01  PRI Jul 16 02:54:46.517
insert query update delete getmore command dirty  used flushes vsize   res qrw  arw net_in net_out conn  set repl                time
    *0    *0   4448     *0     482   614|0  2.5% 75.7%       0 7.20G 4.02G 0|0  2|2  2.78m   16.6m  118 rs01  PRI Jul 16 02:54:47.517
    *0    *0   3883     *0     451   597|0  1.2% 75.4%       0 7.20G 4.02G 0|0  1|0  2.50m   15.4m  118 rs01  PRI Jul 16 02:54:48.518
    *0    *0   3995     *0     525   679|0  1.4% 75.5%       0 7.20G 4.02G 0|0  1|1  2.65m   12.7m  118 rs01  PRI Jul 16 02:54:49.518
    *0    *0   4415     *0     649   811|0  1.7% 75.7%       0 7.20G 4.02G 0|0  1|2  3.01m   15.4m  118 rs01  PRI Jul 16 02:54:50.516
    *0    *0   4804     *0     631   779|0  2.0% 75.5%       0 7.20G 4.02G 0|0  1|1  3.16m   18.0m  118 rs01  PRI Jul 16 02:54:51.516
    *0    *0   3573     *0     582   766|0  2.1% 75.6%       0 7.20G 4.02G 0|0  1|0  2.56m   12.9m  118 rs01  PRI Jul 16 02:54:52.517
    *0    *0     *0     *0       0     3|0  2.1% 75.6%       0 7.20G 4.02G 0|0  1|0   683b   39.1k  118 rs01  PRI Jul 16 02:54:53.516
    *0    *0     *0     *0       0     2|0  2.1% 75.6%       0 7.20G 4.02G 0|0  1|0   682b   39.1k  118 rs01  PRI Jul 16 02:54:54.516
    *0    *0   4032     *0     719   976|0  2.3% 75.8%       0 7.20G 4.02G 0|1  1|0  3.02m   16.7m  118 rs01  PRI Jul 16 02:54:55.519

一台2C 4G 的机器,为什么upsert能达到4000/s呢?这个得益于上面的模型设计,将每次从10个文档批量更新转换成了一次从一个文档中更新10次,这样的好处就是,磁盘和内存中数据交换的数量减少了10倍,大大节省了磁盘io的开销。

说到这里,有同学会问,既然MongoDB是内存数据库,而且,性能如此出众,那么如何在有限的内存中,处理庞大的数据呢?

下面我和大家介绍下MongoDB的eviction,MongoDB是如何将数据淘汰出内存,确保内存的数据的热点:

当cache里面的“脏页”达到一定比例或cache使用量达到一定比例时就会触发相应的evict page线程来将pages(包含干净的pages和脏pages)按一定的算法(LRU队列)淘汰出去,以便腾挪出内存空间,保障后面新的插入或修改等操作。

触发page eviction条件由如下几种参数控制,如下表所示:

参数名称

默认配置值

含义

eviction_target

80%

当Cache的使用量达到80%时触发work thread淘汰page

eviction_trigger

90%

当Cache的使用量达到90%时触发application thread和work thread淘汰page

eviction_dirty_target

5%

当“脏数据”所占Cache比例达到5%时触发work thread淘汰page

eviction_dirty_trigger

20%

当“脏数据”所占Cache比例达到20%时触发application thread和work thread淘汰page

第一种情况:当cache的使用量占比达到参数eviction_ target设定值时(默认为80%),会触发后台线程执行page eviction;

如果使用量继续增长达到eviction_trigger参数设定值时(默认为90%),应用线程支撑的读写操作等请求将被阻塞,应用线程也参与到页面的淘汰中,加速淘汰内存中pages。

第二种情况:当cache里面的“脏数据”达到参数eviction_dirty_target设定值时(默认为5%),会触发后台线程执行page eviction;

如果“脏数据”继续增长达到参数eviction_dirty_trigger设定值(默认为20%),同时会触发应用线程来执行page eviction。

下图可以看到,内存使用置换在合理范围之内:

扩展处理:

1. 后期可以根据热加载的数据量评估,如果内存压力过大,可以扩容内存,或者做分片处理。

2. 开启读写分离模式,减少主库压力。

我们大部分数据库存储引擎在资源限制都是一个绕不开的问题,限制也会比较麻烦,一般都会借助第三方中间件之类的工具来完成,所以我们在考虑限制资源之前,可以从业务特征出发,结合数据库的底层原理,做好适合的模型设计,把限制变成充分利用。

0 人点赞