前言
我们线上有一套MongoDB应用,架构为分片架构,后端有两个节点,在整体QPS不高的情况下发现其分片节点之间的流量显得有点点异常的高,当然高与不高主要是和别的应用做对比得出的,这对于一个经验丰富的DBA来说,其实很容易猜想到肯定是应用在做DML的时候带上了很多无关紧要的数据,拉高整体的带宽流量。
分析过程
- 是否应用层的DML语句没有走分片键,导致所有的语句下发到后端都要发往所有的分片节点?
NO,应用所有DML都会走_id字段,并且_id字段已经做了hashed分片。类似如下

2. 分析表结构
字段很简单,只有简短的几个字段,唯一可疑的就是children字段,里面是一个很大的数组,长达587个元素,查看其他的表基本都是类似这种,其中包含有一个很大的数组,

查看数组的具体内容,发现里面格式基本一致,类似如下:

和研发沟通了解到,即使用户只修改了其中一个元素,在更新的时候也会依然带上一整个数组去更新,类似如下,其他表的处理方式基本都是这样,聊到这里原因基本上就能定位了,难怪会流量会比较高,每次都带了一些无关数据,流量不高才奇怪呢。
db.test_component.updateOne(
{_id: 1234567},
{$set: {
children: [
{"a":1, "b":1, "c":1, "d":1},
{"a":2, "b":2, "c":2, "d":2},
............
{"a":3, "b":3, "c":3, "d":3}
]
}
}
);
这个原理其实类似MySQL下row格式的binlog,当binlog_row_image为full模式的时候,即使我只是修改了表中的其中一个字段将1修改为2,那么binlog也会把一整条记录都写到binlog里面,这就是MySQL里面有一个耳熟能详的优化原则的由来:将经常更新的字段尽量放一张表,不经常更新的字段尽量放另外一张表。
研发继续反问到:不管啥数据库, 一个字段假如存的是数组的话,只能全部更新的吧?
答案方式否定的
演示
构建数组
db.t3.insertMany([
{
_id: 1,
title: "MongoDB Learning",
authors: [
{ name: "Alice", age: 30 },
{ name: "Bob", age: 35 },
{ name: "Charlie", age: 40 }
]
},
{
_id: 2,
title: "Oracle Learning",
authors: [
{ name: "Alice", age: 15 },
{ name: "Tom", age: 20 },
{ name: "Mary", age: 25 }
]
}
])
更新语句, 将数组内Alice的age修改28岁
db.t3.updateMany(
{_id:1, "authors.name": "Alice"}, /* 查询条件中必须含有array */
{$set: {
"authors.$.age": 28
}
}
);
当然也可以增加多个字段
db.t3.updateOne(
{_id:1, "authors.name": "Alice"}, /* 查询条件中必须含有array */
{$set: {
"authors.$.age": 38,
"authors.$.address": "Shanghai City"
}
}
);
为数组文档增加字段($[])
功能:在查询条件匹配的情况下,$[]会修改指定数组字段中的所有元素。 $[]是修改整个数组,如下便是数组里面所有文档都增加一个address为Shanghai City的键值对。
db.t3.updateOne(
{
_id:1
},
{
$set: { "authors.$[].address" : "Shanghai City"}
}
)
MongoDB的为数组提供了很多的操作符,方便我们在各种情况下的数据操作,上面两个方法也只是冰山一角,比如$PUSH, $EACH等,更有待我们去探索。
最后
其实这个也属于结构设计不合理的一个案例,数组的其实更适合存储数据内容少的数据,每个元素的数据量都很小,数组的优点是能根据索引下标能快速的访问到数据,但是本案例中数组确并不是根据数组下表来访问数据的,因此也可以用内嵌文档对象的方式来存储数据类似如下:
{
"_id" : NumberLong("165421313025"),
"_t" : "TaskComponent",
"Children" : {
ConId10001: {
"_t" : "Info",
"_id" : NumberLong("201666817577281"),
"Status" : 2,
"Progress" : 1
},
.........
ConfId1000x: {
"_t" : "Info",
"_id" : NumberLong("2016668148577281"),
"Status" : 2,
"Progress" : 1
}
},
MainTaskVersion: 1,
MainTaskId: 1,
DayRefreshTime: xxxx
}
倘若这样设计的话其实能够将每次更新时所带上的数据量减少到最低,甚至可以只更新里面很少的一个字段值,
db.test_component.updateOne(
{_id: 1234567},
{$set: {
"Children.ConfId10002.Status": 2
}
}
);
这样就能大大的减少应用流量带宽。