现网GhsHis表有几百万数据,但是测试环境只有几万数据,想要模拟现网数据量进行测试。
叮嘱测试用js脚本往数据库插入,结果她还是调了接口进行插入。虽然测试环境MongoDB部署的还是分片集群,但是,还是把测试环境搞挂了。
关键时刻,还得开发上场。
有了测试同事的教训,为了研发环境的安全,我用js脚本先插入了一万条数据小试牛刀。结果呢,执行得很慢。考虑到还有主从复制,从
库查询压力也很大。
肯定不能一条一条插。那就批量插入。刚好MongoDB有支持批量插入的命令insertMany,于是试了一下,果真批量插入,速度不同凡
响,快的不是一星半点。
理论上可行,但实践起来还有很多细节要考虑。
要插入一百万数据,肯定不能一次性插入,我们一次插入一万,分一百次插入。写两层for循环轻松搞定。
插入的数据不能一模一样,比如创建时间和进入历史表的时间不能都一样,所以需要动态设置。
尝试插入了二十条数据,虽然动态设置了时间,但是发现最终所有数据都跟最后一条数据时间一毛一样。
我一个写Java的,为啥要让我写Js脚本?我感觉你这是在为难我胖虎。
钱难挣,屎难吃,工资也不是那么好拿的。于是,我又开始了面向百度编程。
很明显是由于对Js语法不了解导致的。百度了两行代码,试了一下可以。
var b = {};
Object.assign(b, a);
b.createDate = NumberLong(new Date().getTime() - Math.round(Math.random() * 10000) + 10000);
之前写的是
var b;
b.createDate = NumberLong(new Date().getTime() - Math.round(Math.random() * 10000) + 10000);
然后就是往数组里面插入数据,使用push方法即可。
统计一下执行脚本的耗时,java的sout使用的是加号拼接,Js使用的是逗号拼接。
var time = new Date().getTime();
print("执行耗时:",new Date().getTime()-time);
让时间具有随机性,调用Js数学类库函数。
NumberLong(new Date().getTime() - Math.round(Math.random() * 10000) + 10000);
最终脚本
var a = {
"customerId": "123456789",
"username": "[email protected]",
"pkgId": "66666666",
"state": "USE_END",
"price": 1.0,
"createDate": NumberLong(1666341382443),
"orgId": "963852741",
"deptId": "147258369",
"remark": "666",
"inHisTime": NumberLong(1666346588556)
};
var time = new Date().getTime();
for (j = 1; j <= 100; j++) {
var arr = [];
for (i = 1; i <= 10000; i++) {
var b = {};
Object.assign(b, a);
if (i % 3 == 0) {
b.state = "USE_END";
} else if (i % 3 == 1) {
b.state = "EXPIRE";
} else {
b.state = "TRANSFER";
}
b.createDate = NumberLong(new Date().getTime() - Math.round(Math.random() * 10000) + 10000);
b.inHisTime = NumberLong(b.createDate + Math.round(Math.random() * 10000) + 100);
arr.push(b);
}
db.GhsHis.insertMany(arr);
}
print("执行耗时:",new Date().getTime()-time);
今天是程序员节,祝大家节日快乐!!!
大功告成???
不,这才是万里长征第一步。革命尚未成功,同志仍需努力!!!
数据是构造好了,有了跟现网差不多的数据量级,但是现网问题的复现、测试还没开始呢!!!
故事背景:现网导出接口导出Excel数据出现了Id重复,几乎是必现。
测试环境不能复现,距离升级只有三天时间了。时间紧,任务重。
找业务人员要了当时导出的那份Excel,将Id列复制到D:\delete1.txt文件,准备用java代码分析一下。
代码思路,使用高速缓冲字符流一次读取一行,将读取到的Id放入List集合,然后遍历List集合,使用Set集合去重,拿到重复的Id以及下标。
public class IOReader {
public static void main(String[] args) throws IOException {
BufferedReader bis = null;
List<String> list = new ArrayList<>();
try {
bis = new BufferedReader(new InputStreamReader(new FileInputStream("D:\\delete1.txt")));
String str = null;
while ((str = bis.readLine()) != null){
list.add(str);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
bis.close();
}
System.out.println(list.size());
Set<String> set = new HashSet<>();
for(int i=0;i<list.size();i++){
if(set.contains(list.get(i))){
System.out.println("重复数据:"+list.get(i)+",下标"+i);
}else{
set.add(list.get(i));
}
}
}
}
通过代码分析发现,确实存在重复Id。因为Id是唯一的,出现重复有悖常理。因为有现表和历史表,表结构大差不差。导出又可以同时导出历史表和现表。查看代码发现,在将现表数据移入历史表的时候,先插入的历史表,然后删除的现表,这样在极端情况下,是可能出现导出重复Id的。
事情会这么简单吗?很明显不会。
随着代码的继续深入,我发现对于导出所有的情况,代码已经做了去重处理,并无明显Bug。
带着内心的疑问,我又找到了业务人员。仔细询问了他操作的细节。
操作流水号?操作Id?很明显,业务人员并不关注这些,只是丢给了我一个用户名。
我进入用户表一查,好家伙,一个用户名对应十几个用户。
我又问了他操作时所属的部门,这才将将初步定位了嫌疑人。
案发时间在昨天,还好时间不久,一切证据还未被抹除。
现网Kabana日志只展示七天,超过七天的就只能去磁盘看。需要申请一堆的权限,耗时又费力,大家一般都不愿意申请。
将时间定格在昨天,将嫌疑人锁死在刚才找到的Id。果真寻到了案发现场。
本着不放过一切蛛丝马迹的原则,我仔仔细细地查看了案发时的证据,但并未发现什么特别的有价值的证据。
导出Excel的过程是这样的,前台请求中台,中台请求后台。每次最多导出1000条,中台分次请求后台。
我看了一下每次导出的时间间隔竟然相差14秒,确实有点大。这算是案发现场唯一的收获了。
是中台设置的请求时间还是后台接口竟然如此慢?
我去测试环境试了一下,很快。那就是后台接口慢喽。
案件一下陷入了僵局,扑朔迷离的案情属实让人焦头乱额。
大脑飞速地转动着,思索着还有哪些未考虑到的场景。
我又去看了一下导出Excel,这算是直接证据了,要好好分析一下。
凭借着精湛的业务能力和三年多的工作经验,灵光一现,我机智地发现了出现重复的数据位于每一页开头的位置。
一个Idea浮现在了我的脑海。
我激动地翻找代码,想要佐证自己的想法。果真如我所想,初步定位到了问题所在。
知错,改错,验错。第一步总算是完成了。
改错也很简单,几秒钟搞定。知错几小时,改错几秒钟。
接力棒交接到了测试同事的手中,我总算可以松一口气了。
测试按我所说,未能成功复现问题。球呀,又到了我的手里。
作为全场最靓的仔,这点事肯定难不倒英明神武的我。
现网每一次操作时间间隔有14秒,有充足的时间来进行我们想要的操作。测试环境只有三秒,时间不等人,拼的是手速。
交代一下我定位出来的问题:我怀疑是排序字段选择不当造成导出Id重复。
啥?排序字段还能引起导出Id重复?我只是工作了三年的实习生,你别蒙我!
别急,听我细细道来。因为导出是既可以选择现表,也可以选择历史表,它们的排序规则都是一样的:创建时间逆序。问题就出在这里,现表使用创建时间没有问题,但是历史表就有问题了,创建时间早的不一定进入历史表的时间就早。
举个例子,导出的第二页,第1001条到第2000条数据。在正导出第二页的时候,有一个创建时间恰好位于第1001条到第2000条之间的数据被插入,那么根据创建时间逆序排序,原来第2000条数据就会被排到第2001条数据的位置,表现出来就是第2001条数据Id重复。
找出数据量在10000条以上的部门,然后选一个一万条左右的部门导出。
db.GhsHis.aggregate([{$group:{_id:"$deptId",total:{$sum:1}}},{$match:{total:{$gt:10000}}}],{allowDiskUse: true})
管道有100M内存限制。设置allowDiskUse:true,允许使用磁盘存储数据。
由于测试环境数据量用户量不足,一瞬间没有那么多数据失效然后进入历史表。如此苛刻的复现条件只能是手动来提供。
三秒的操作时间,理论上是来得及操作的。但是,可能不具有普适性。测试又复现失败了。
不知道测试同事心中此时作何感想。(叼毛,按你说的方法复现不了问题?)
为了维护我在同事心中的靓仔形象。啪,很快呀,我又写了一个脚本。
var a = {
"customerId": "123456789",
"username": "[email protected]",
"pkgId": "66666666",
"state": "USE_END",
"price": 1.0,
"createDate": NumberLong(1666341382443),
"orgId": "963852741",
"deptId": "147258369",
"remark": "666",
"inHisTime": NumberLong(1666346588556)
};
function sleep(number){
var now = new Date();
var exitTime = now.getTime() + number;
while (true) {
now = new Date();
if(now.getTime() > exitTime){
return;
}
}
}
var timeArr=[1641864542173,1641864263779,1637305936014,1637293032528,1636098374840,1624608384592,1621040814675,1617868030123,1617866906466];
timeArr.forEach(function(t){
var b = {};
Object.assign(b, a);
b.createDate = NumberLong(t);
sleep(200);
db.GhsHis.insert(b);
});
开发呀,你不讲武德,你跟我说用手操作,你竟然偷偷写脚本。
还不是为了操作的普适性,为了问题在测试环境必现,你以为我想写脚本呀?
说起来云淡风轻,实际上斩棘披荆。
老规矩:先解释一下代码思路。找出了九个时间点,这九个时间点都是位于第二页的。然后遍历,每隔200毫秒,以该时间点为创建时间,插入一条数据,代表的是此时有一条创建时间位于第二页的数据被插入历史表,以此来模拟现网操作。
Java线程睡眠一行代码就够了,Js竟然还得自己写,当然都是百度的了。
那这九个时间点是怎么找出来的?
db.GhsHis.find({"deptId" : "147258369","state":{$in:["EXPIRE","USE_END","TRANSFER"]}}).sort({"createDate":-1}).skip(1100)
skip那里改成1200,一直到1900,找出9个创建时间。
我将脚本给了测试,让他在第一页导出之后,第二页正在导出的时候,立刻执行该脚本。
结果呢,翻车了,还是没有复现。
大意呀,我没有改进入历史表的时间。因为我改了排序规则,新规则使用进入历史表的时间和创建时间两个字段来逆向排序。
b.inHisTime= NumberLong(new Date().getTime());
又把脚本给了测试,问题成功复现!靓仔的形象得到了强有力的维护!自己的形象要靠自己来维护。
然后,测试升级版本,开始验错,到了校验我改错的时候了。
结果又翻车了!!!每次都能复现问题,改了跟没改一样,我怕不是改了个毛线?
这下好了,靓仔彻底变叼毛了。
细细端详代码,思忖着究竟是哪里出了问题?原来是排序字段的排序方式有问题。
之前是按照创建时间逆向排序,我想都没想,就沿用了之前的方式,根据进入历史表的时间和创建时间两个字段来逆向排序。(当进入历史表时间相同,按照创建时间来逆向排序)
我怎能重蹈前任的覆辙呢?我跟他们又有什么区别?我改错又有什么意义呢?只是从一个坑爬到了另一个坑。
大意,还是大意呀。不仅丢了燕云十八州,连荆襄九郡都丢了。
痛定思痛,痛改前非。
还是时间太紧急了,搞得我很急躁,都不能冷静思考了,犯了如此低级的错误。总得给自己找个借口安慰一下英明神武明察秋毫的自己。
改成正向排序以后,问题果然解决了。问题不会复现了。Nice !
现网问题到这里已经解决了,不会再导出重复的Id了。
bug已经解决,但是优化永无止境。查询导出1000条耗费14秒,太慢了。
查询慢,第一反应肯定是没加索引。查看现网GhsHis表的索引。
db.GhsHis.getIndexes()
发现表里索引挺多的,但是,排序字段createDate竟然没加索引!
分析一下查询导出语句的执行计划,“stage” : “SORT”,证明排序没有使用索引,在内存中做了排序,且做了全表扫描。
db.GhsHis.find({"deptId" : "147258369","state":{$in:["EXPIRE","USE_END","TRANSFER"]}})
.sort({"createDate":-1}).skip(1000).limit(1000).explain("executionStats")
鉴于已经修改了排序规则,所以我给进入历史表时间和创建时间建了联合索引。{background:true}这句一定要加,否则会锁表。
db.GhsHis.ensureIndex({inHisTime:1,createDate:1},{background:true})
state字段与deptId字段数据区分度不高,暂时不加索引。
db.GhsHis.find({"deptId" : "147258369","state":{$in:["EXPIRE","USE_END","TRANSFER"]}}).sort({"inHisTime":1,"createDate":1}).skip(1000).limit(1000).explain("executionStats")
再次查看执行计划,“stage” : “IXSCAN”,说明使用了索引,只扫描了几千条数据,查询时间也只有几十毫秒了。
MongoDB的执行计划比起Mysql而言更加复杂难懂,后续有时间再做深入研究学习,今日尚且浅尝辄止!