项目背景
数据质量项目,实时消费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. 开启读写分离模式,减少主库压力。
我们大部分数据库存储引擎在资源限制都是一个绕不开的问题,限制也会比较麻烦,一般都会借助第三方中间件之类的工具来完成,所以我们在考虑限制资源之前,可以从业务特征出发,结合数据库的底层原理,做好适合的模型设计,把限制变成充分利用。