Contents
  1. 1. 一、测试内容
  2. 2. 二、测试方法及数据准备
  3. 3. 三、测试环境
  4. 4. 四、Jmeter设置
  5. 5. 五、测试数据
    1. 5.1. 1 插入数据压测
    2. 5.2. 2 查询数据压测
    3. 5.3. 3 更新数据压测
    4. 5.4. 4 删除数据压测
    5. 5.5. 5 CRUD混合测试
  6. 6. 六、总结
  7. 7. 附录
    1. 7.1. 压测过程使用的代码

一、测试内容

本次测试目的是衡量MongoDB单机环境的CRUD性能,包括单独及混合CRUD测试。

  • C 创建数据,insert操作
  • R 读数据,find操作
  • U 更新数据,update操作
  • D 删除数据,remove操作

二、测试方法及数据准备

本次测试采用开源测试工具Jmeter 3.0进行测试,模拟大用户并发对MongoDB发起请求,达到压力测试的目的。
每单元测试进行5次,观察数据结果的稳定性。
测试数据使用地市结构,city及pop数据为程序随机,模型如下:

1
2
3
4
5
6
7
{
"_id" : ObjectId("5791df26e4b0a6cf3fa35e14"),
"city" : "Zhongshan",
"loc" : { "x" : 203, "y" : 102 },
"pop" : 524800,
"state" : "GD"
}

三、测试环境

MongoDB服务器:wx-dev-com,2核cpu,4g内存,MongoDB version v3.2.7
Jmeter服务器:wx-dev-test02,2核cpu,4g内存
带宽:金山云内网带宽

MongoDB配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# mongod.conf
# for documentation of all options, see:
# http://docs.mongodb.org/manual/reference/configuration-options/
# Where and how to store data.
storage:
dbPath: /var/lib/mongodb
journal:
enabled: true
# where to write logging data.
systemLog:
destination: file
logAppend: true
path: /var/log/mongodb/mongod.log
# network interfaces
net:
port: 27017
bindIp: 127.0.0.1

四、Jmeter设置

测试url:内部链接
Jmeter线程1000,单线程循环200次
测试脚本:内部脚本
参数化文件:内部数据

名词定义(时间的单位均为ms):
Samples – 本次场景中一共完成了多少个线程
Average – 平均响应时间
Median – 统计意义上面的响应时间的中值
90% Line – 所有线程中90%的线程的响应时间都小于xx
Min – 最小响应时间
Max – 最大响应时间
Error – 出错率
Troughput – 吞吐量
KB/sec – 以流量做衡量的吞吐量

五、测试数据

1 插入数据压测

从测试结果看,除了样本1,其他样本的QPS基本在8000以上,可以看到MongoDB的插入能力很不错。
CPU使用率在65%左右,IO操作在压测过后有一些峰值出现,大概是30M/s,应该是批量数据同步到磁盘。

样本 # Samples Average Median 90% Line 95% Line 99% Line Min Max Error % Throughput KB/sec
1 100000 4 2 10 18 46 0 348 0.00% 7509.2 1078
2 100000 22 9 69 99 154 0 372 0.00% 8058.7 1156.9
3 100000 17 9 47 67 107 0 219 0.00% 8392.8 1204.8
4 100000 12 8 31 44 80 0 182 0.00% 8332.6 1196.2
5 100000 16 9 45 66 105 0 229 0.00% 8530.2 1224.6

2 查询数据压测

单独查询数据的性能要比插入数据略好,QPS有3次样本突破9200,最低也有8100。
服务器CPU消耗比插入数据低接近50%,
IO写只有插入操作的1/4左右,IOPS只有插入1/10左右,
磁盘读基本没有,可见数据应该是cache在内存了。
每次查询结果集大概是6w数据。

样本 # Samples Average Median 90% Line 95% Line 99% Line Min Max Error % Throughput KB/sec
1 100000 1 1 2 3 12 0 83 0.00% 8446.7 49.5
2 100000 1 1 2 3 15 0 117 0.00% 8122.8 47.6
3 100000 1 1 2 4 15 0 100 0.00% 9398.5 55.1
4 100000 1 1 3 5 18 0 142 0.00% 9280.7 54.4
5 100000 1 1 3 6 22 0 166 0.00% 9495.8 55.6

3 更新数据压测

更新数据结果出乎意料的差,Jmeter线程循环降低到10次。
QPS只有100左右,与插入查询比相差几百倍。
IO资源消耗较高,IOPS达到160左右,写操作峰值1500K/s。
怀疑是mongodb的锁机制导致,mongodb只有库级粒度锁。
结果存疑,待验证。

样本 # Samples Average Median 90% Line 95% Line 99% Line Min Max Error % Throughput KB/sec
1 10000 8192 8754 12116 12655 13525 11 24492 0.00% 104.9 0.3
2 10000 8622 9067 13659 14660 16542 10 29413 0.00% 99.8 0.3
3 10000 8173 8694 12379 13082 14103 8 27516 0.00% 104.8 0.3
4 10000 8338 9054 12167 12738 13570 10 25689 0.00% 103.5 0.3
5 10000 8123 8437 13196 13857 15043 8 29844 0.00% 104.7 0.3

4 删除数据压测

QPS一路走低,比更新数据性能好一点。
但是使用1000并发进程压测会出现报错,改成并发500进程。
CPU使用率达到了90%以上,已经出现瓶颈了。
IO写峰值有6000KB/s,IOPS 200左右,磁盘压力不高。

样本 # Samples Average Median 90% Line 95% Line 99% Line Min Max Error % Throughput KB/sec
1 5000 992 828 1955 2424 3258 5 3625 0.00% 229 0.7
2 5000 1326 1277 2578 3277 3722 10 3992 0.00% 212.3 0.6
3 5000 1403 1264 3075 3400 3723 9 4085 0.00% 196.1 0.6
4 5000 1780 1589 3521 4603 5081 12 5448 0.00% 166.5 0.5
5 5000 2179 1989 4021 5410 6648 19 7273 0.00% 146.3 0.4

5 CRUD混合测试

使用之前单项压测的脚本进行混合压测,Jmeter并发500线程,循环10次。
因为4个操作并发线程一样,从结果看是遵循木桶原理,单项测试QPS是80左右,总体QPS在323左右。
从资源使用率看,瓶颈是CPU,使用率90%,应该是删除数据导致的,与上面单项压测结果一样。
IOPS出现了一个峰值,但是大部分时间磁盘写入不算频繁,平均在1.2MB/s左右。

Label # Samples Average Median 90% Line 95% Line 99% Line Min Max Error % Throughput KB/sec
Thread Group:Insert 5000 359 181 1238 1417 1704 0 2430 0.00% 81.1 11.6
Thread Group:Find 5000 323 189 1072 1338 1693 0 2415 0.00% 81.1 .5
Thread Group:Update 5000 2636 2578 4812 5226 6168 10 8164 0.00% 81.2 .0
Thread Group:Remove 5000 1457 1387 2891 3121 3953 3 4488 0.00% 81.1 .2
统计 20000 1194 510 3250 4163 5325 0 8164 0.00% 323.7 12.3

六、总结

1.MongoDB强项是数据插入及查询,并发性能非常好,如果压测客户端能多几个的话应该成绩还能提升。
2.对更新及删除数据的成绩不理想,估计和锁机制有关系,有待进一步验证。
3.更新及删除操作超过每秒100次就可以考虑进行分库。

CRUD操作QPS汇总表:

指标 插入 查询 更新 删除 95% Line 99% Line Min Max Error % Throughput KB/sec
CPU 60% 40% 5% 90% 1417 1704 0 2430 0.00% 81.1 11.6
IOPS 200 60 170 220 1338 1693 0 2415 0.00% 81.1 .5
QPS 8300 9000 100 180 5226 6168 10 8164 0.00% 81.2 .0
Thread Group:Remove 5000 1457 1387 2891 3121 3953 3 4488 0.00% 81.1 .2
统计 20000 1194 510 3250 4163 5325 0 8164 0.00% 323.7 12.3

附录

压测过程使用的代码

以下代码都是Jmeter的JSR223 sampler使用的代码,并非标准java代码。
插入数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import com.mongodb.DB;
import org.apache.jmeter.protocol.mongodb.config.MongoDBHolder;
import com.mongodb.BasicDBObject;
import com.mongodb.DBCollection;
import com.mongodb.WriteConcern;
import com.mongodb.WriteResult;
// 随机选择city和pop数据
def citys = ["Zhongshan","Zhuhai","Shenzhen","Guangzhou","Meizhou","Qingyuan","Shaoguang","Zhaoqing"];
def pops = ["528400","528401","528402","528403","528404","528405","528406","528407"];
def citynum = new Random().nextInt(citys.size);
// Get DB
DB db = MongoDBHolder.getDBFromSource("dbjmeter", "dbjmeter");
// Get collection to insert
DBCollection coll = db.getCollection("zips");
BasicDBObject doc = new BasicDBObject("city", citys[citynum]).
append("loc", new BasicDBObject("x", 203).append("y", 102)).
append("pop", pops[citynum]).
append("state", "GD");
// Insert object
WriteResult wr = coll.insert(doc, WriteConcern.ACKNOWLEDGED);
// Set response data
SampleResult.setResponseData(""+wr.toString(),"UTF-8");

查询数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import com.mongodb.DB;
import org.apache.jmeter.protocol.mongodb.config.MongoDBHolder;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
import com.mongodb.DBCollection;
import com.mongodb.DBCursor;
DB db = MongoDBHolder.getDBFromSource("dbjmeter", "dbjmeter");
DBCollection coll = db.getCollection("zips");
// 统计collection总数
int size = coll.count();
BasicDBObject searchQuery = new BasicDBObject("city", "Zhongshan");
// 查找Zhongshan的数据,压力测试不写结果显示代码
DBCursor cursor = coll.find(searchQuery);
SampleResult.setResponseData(""+size,"UTF-8");

更新数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import com.mongodb.DB;
import org.apache.jmeter.protocol.mongodb.config.MongoDBHolder;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
import com.mongodb.DBCollection;
import com.mongodb.DBCursor;
import org.bson.types.ObjectId;
import com.mongodb.WriteConcern;
import com.mongodb.WriteResult;
import java.util.Random;
import java.util.Date;
DB db = MongoDBHolder.getDBFromSource("dbjmeter", "dbjmeter");
DBCollection coll = db.getCollection("zips");
// 组装一个后三位随机的 _id
String[] hex = new String[]{"0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"};
Random r = new Random();
String hexid = "5798822de4b0915e330a7" + hex[r.nextInt(16)]+ hex[r.nextInt(16)]+ hex[r.nextInt(16)];
int xx = r.nextInt(1000);
int yy = r.nextInt(1000);
// 组装_id、loc等数据
BasicDBObject qid = new BasicDBObject("_id", new ObjectId(hexid));
BasicDBObject newLoc = new BasicDBObject("loc", new BasicDBObject("x",xx).append("y",yy));
BasicDBObject updateObj = new BasicDBObject("$set", newLoc);
// 使用随机_id为条件,更新loc字段
WriteResult wr = coll.update(qid, updateObj, false, false);
// 使用findAndModity方法测试,性能没变化
//WriteResult wr = coll.findAndModify(qid, updateObj);
// Set response data
SampleResult.setResponseData(""+wr.toString(),"UTF-8");

删除数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import com.mongodb.DB;
import org.apache.jmeter.protocol.mongodb.config.MongoDBHolder;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
import com.mongodb.DBCollection;
import com.mongodb.WriteConcern;
import com.mongodb.WriteResult;
DB db = MongoDBHolder.getDBFromSource("dbjmeter", "dbjmeter");
DBCollection coll = db.getCollection("zips");
BasicDBObject searchQuery = new BasicDBObject("city", "Zhongshan");
// 查找一条数据,返回_id
BasicDBObject qid = coll.findOne(searchQuery, new BasicDBObject("_id", "1"));
// 根据_id删除doc
WriteResult wr = coll.remove(qid);
// Set response data
SampleResult.setResponseData(""+wr.toString(),"UTF-8");
Contents
  1. 1. 一、测试内容
  2. 2. 二、测试方法及数据准备
  3. 3. 三、测试环境
  4. 4. 四、Jmeter设置
  5. 5. 五、测试数据
    1. 5.1. 1 插入数据压测
    2. 5.2. 2 查询数据压测
    3. 5.3. 3 更新数据压测
    4. 5.4. 4 删除数据压测
    5. 5.5. 5 CRUD混合测试
  6. 6. 六、总结
  7. 7. 附录
    1. 7.1. 压测过程使用的代码